| /* |
| * Copyright (C) 2012 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.tools.lint.checks; |
| |
| import static com.android.SdkConstants.RESOURCE_CLZ_ID; |
| |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.tools.lint.detector.api.Category; |
| import com.android.tools.lint.detector.api.Context; |
| import com.android.tools.lint.detector.api.Detector; |
| import com.android.tools.lint.detector.api.Implementation; |
| import com.android.tools.lint.detector.api.Issue; |
| import com.android.tools.lint.detector.api.JavaContext; |
| import com.android.tools.lint.detector.api.Location; |
| import com.android.tools.lint.detector.api.Scope; |
| import com.android.tools.lint.detector.api.Severity; |
| import com.google.common.collect.Maps; |
| |
| import java.io.File; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Map; |
| |
| import lombok.ast.ArrayAccess; |
| import lombok.ast.AstVisitor; |
| import lombok.ast.BinaryExpression; |
| import lombok.ast.Cast; |
| import lombok.ast.Expression; |
| import lombok.ast.ForwardingAstVisitor; |
| import lombok.ast.If; |
| import lombok.ast.MethodInvocation; |
| import lombok.ast.Node; |
| import lombok.ast.Select; |
| import lombok.ast.Statement; |
| import lombok.ast.VariableDefinitionEntry; |
| import lombok.ast.VariableReference; |
| |
| /** |
| * Detector looking for cut & paste issues |
| */ |
| public class CutPasteDetector extends Detector implements Detector.JavaScanner { |
| /** The main issue discovered by this detector */ |
| public static final Issue ISSUE = Issue.create( |
| "CutPasteId", //$NON-NLS-1$ |
| "Likely cut & paste mistakes", |
| |
| "This lint check looks for cases where you have cut & pasted calls to " + |
| "`findViewById` but have forgotten to update the R.id field. It's possible " + |
| "that your code is simply (redundantly) looking up the field repeatedly, " + |
| "but lint cannot distinguish that from a case where you for example want to " + |
| "initialize fields `prev` and `next` and you cut & pasted `findViewById(R.id.prev)` " + |
| "and forgot to update the second initialization to `R.id.next`.", |
| |
| Category.CORRECTNESS, |
| 6, |
| Severity.WARNING, |
| new Implementation( |
| CutPasteDetector.class, |
| Scope.JAVA_FILE_SCOPE)); |
| |
| private Node mLastMethod; |
| private Map<String, MethodInvocation> mIds; |
| private Map<String, String> mLhs; |
| private Map<String, String> mCallOperands; |
| |
| /** Constructs a new {@link CutPasteDetector} check */ |
| public CutPasteDetector() { |
| } |
| |
| @Override |
| public boolean appliesTo(@NonNull Context context, @NonNull File file) { |
| return true; |
| } |
| |
| // ---- Implements JavaScanner ---- |
| |
| @Override |
| public List<String> getApplicableMethodNames() { |
| return Collections.singletonList("findViewById"); //$NON-NLS-1$ |
| } |
| |
| @Override |
| public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor, |
| @NonNull MethodInvocation call) { |
| String lhs = getLhs(call); |
| if (lhs == null) { |
| return; |
| } |
| |
| Node method = JavaContext.findSurroundingMethod(call); |
| if (method == null) { |
| return; |
| } else if (method != mLastMethod) { |
| mIds = Maps.newHashMap(); |
| mLhs = Maps.newHashMap(); |
| mCallOperands = Maps.newHashMap(); |
| mLastMethod = method; |
| } |
| |
| String callOperand = call.astOperand() != null ? call.astOperand().toString() : ""; |
| |
| Expression first = call.astArguments().first(); |
| if (first instanceof Select) { |
| Select select = (Select) first; |
| String id = select.astIdentifier().astValue(); |
| Expression operand = select.astOperand(); |
| if (operand instanceof Select) { |
| Select type = (Select) operand; |
| if (type.astIdentifier().astValue().equals(RESOURCE_CLZ_ID)) { |
| if (mIds.containsKey(id)) { |
| if (lhs.equals(mLhs.get(id))) { |
| return; |
| } |
| if (!callOperand.equals(mCallOperands.get(id))) { |
| return; |
| } |
| MethodInvocation earlierCall = mIds.get(id); |
| if (!isReachableFrom(method, earlierCall, call)) { |
| return; |
| } |
| Location location = context.getLocation(call); |
| Location secondary = context.getLocation(earlierCall); |
| secondary.setMessage("First usage here"); |
| location.setSecondary(secondary); |
| context.report(ISSUE, call, location, String.format( |
| "The id `%1$s` has already been looked up in this method; possible " + |
| "cut & paste error?", first.toString())); |
| } else { |
| mIds.put(id, call); |
| mLhs.put(id, lhs); |
| mCallOperands.put(id, callOperand); |
| } |
| } |
| } |
| } |
| } |
| |
| @Nullable |
| private static String getLhs(@NonNull MethodInvocation call) { |
| Node parent = call.getParent(); |
| if (parent instanceof Cast) { |
| parent = parent.getParent(); |
| } |
| |
| if (parent instanceof VariableDefinitionEntry) { |
| VariableDefinitionEntry vde = (VariableDefinitionEntry) parent; |
| return vde.astName().astValue(); |
| } else if (parent instanceof BinaryExpression) { |
| BinaryExpression be = (BinaryExpression) parent; |
| Expression left = be.astLeft(); |
| if (left instanceof VariableReference || left instanceof Select) { |
| return be.astLeft().toString(); |
| } else if (left instanceof ArrayAccess) { |
| ArrayAccess aa = (ArrayAccess) left; |
| return aa.astOperand().toString(); |
| } |
| } |
| |
| return null; |
| } |
| |
| private static boolean isReachableFrom( |
| @NonNull Node method, |
| @NonNull MethodInvocation from, |
| @NonNull MethodInvocation to) { |
| ReachableVisitor visitor = new ReachableVisitor(from, to); |
| method.accept(visitor); |
| |
| return visitor.isReachable(); |
| } |
| |
| private static class ReachableVisitor extends ForwardingAstVisitor { |
| @NonNull private final MethodInvocation mFrom; |
| @NonNull private final MethodInvocation mTo; |
| private boolean mReachable; |
| private boolean mSeenEnd; |
| |
| public ReachableVisitor(@NonNull MethodInvocation from, @NonNull MethodInvocation to) { |
| mFrom = from; |
| mTo = to; |
| } |
| |
| boolean isReachable() { |
| return mReachable; |
| } |
| |
| @Override |
| public boolean visitMethodInvocation(MethodInvocation node) { |
| if (node == mFrom) { |
| mReachable = true; |
| } else if (node == mTo) { |
| mSeenEnd = true; |
| |
| } |
| return super.visitMethodInvocation(node); |
| } |
| |
| @Override |
| public boolean visitIf(If node) { |
| Expression condition = node.astCondition(); |
| Statement body = node.astStatement(); |
| Statement elseBody = node.astElseStatement(); |
| if (condition != null) { |
| condition.accept(this); |
| } |
| if (body != null) { |
| boolean wasReachable = mReachable; |
| body.accept(this); |
| mReachable = wasReachable; |
| } |
| if (elseBody != null) { |
| boolean wasReachable = mReachable; |
| elseBody.accept(this); |
| mReachable = wasReachable; |
| } |
| |
| endVisit(node); |
| |
| return false; |
| } |
| |
| @Override |
| public boolean visitNode(Node node) { |
| return mSeenEnd; |
| } |
| } |
| } |