/*
* 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.klint.checks;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.detector.api.*;
import com.google.common.collect.Maps;
import com.intellij.psi.PsiMethod;
import org.jetbrains.uast.*;
import org.jetbrains.uast.util.UastExpressionUtils;
import org.jetbrains.uast.visitor.AbstractUastVisitor;
import org.jetbrains.uast.visitor.UastVisitor;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static com.android.SdkConstants.RESOURCE_CLZ_ID;
/**
* Detector looking for cut & paste issues
*/
public class CutPasteDetector extends Detector implements Detector.UastScanner {
/** 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 PsiMethod mLastMethod;
private Map<String, UCallExpression> mIds;
private Map<String, String> mLhs;
private Map<String, String> mCallOperands;
/** Constructs a new {@link CutPasteDetector} check */
public CutPasteDetector() {
}
// ---- Implements UastScanner ----
@Override
public List<String> getApplicableMethodNames() {
return Collections.singletonList("findViewById"); //$NON-NLS-1$
}
@Override
public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UCallExpression call, @NonNull UMethod calledMethod) {
String lhs = getLhs(call);
if (lhs == null) {
return;
}
UMethod method = UastUtils.getParentOfType(call, UMethod.class, false);
if (method == null) {
return; // prevent doing the same work for multiple findViewById calls in same method
} else if (method != mLastMethod) {
mIds = Maps.newHashMap();
mLhs = Maps.newHashMap();
mCallOperands = Maps.newHashMap();
mLastMethod = method;
}
String callOperand = call.getReceiver() != null
? call.getReceiver().asSourceString() : "";
List<UExpression> arguments = call.getValueArguments();
if (arguments.isEmpty()) {
return;
}
UExpression first = arguments.get(0);
if (first instanceof UReferenceExpression) {
UReferenceExpression psiReferenceExpression = (UReferenceExpression) first;
String id = psiReferenceExpression.getResolvedName();
UElement operand = (first instanceof UQualifiedReferenceExpression)
? ((UQualifiedReferenceExpression) first).getReceiver()
: null;
if (operand instanceof UReferenceExpression) {
UReferenceExpression type = (UReferenceExpression) operand;
if (RESOURCE_CLZ_ID.equals(type.getResolvedName())) {
if (mIds.containsKey(id)) {
if (lhs.equals(mLhs.get(id))) {
return;
}
if (!callOperand.equals(mCallOperands.get(id))) {
return;
}
UCallExpression earlierCall = mIds.get(id);
if (!isReachableFrom(method, earlierCall, call)) {
return;
}
Location location = context.getUastLocation(call);
Location secondary = context.getUastLocation(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.asSourceString()));
} else {
mIds.put(id, call);
mLhs.put(id, lhs);
mCallOperands.put(id, callOperand);
}
}
}
}
}
@Nullable
private static String getLhs(@NonNull UCallExpression call) {
UElement parent = call.getUastParent();
while (parent != null && !(parent instanceof UBlockExpression)) {
if (parent instanceof ULocalVariable) {
return ((ULocalVariable) parent).getName();
} else if (UastExpressionUtils.isAssignment(parent)) {
UExpression left = ((UBinaryExpression) parent).getLeftOperand();
if (left instanceof UReferenceExpression) {
return left.asSourceString();
} else if (left instanceof UArrayAccessExpression) {
UArrayAccessExpression aa = (UArrayAccessExpression) left;
return aa.getReceiver().asSourceString();
}
}
parent = parent.getUastParent();
}
return null;
}
static boolean isReachableFrom(
@NonNull UMethod method,
@NonNull UElement from,
@NonNull UElement to) {
ReachabilityVisitor visitor = new ReachabilityVisitor(from, to);
method.accept(visitor);
return visitor.isReachable();
}
private static class ReachabilityVisitor extends AbstractUastVisitor {
private final UElement mFrom;
private final UElement mTarget;
private boolean mIsFromReached;
private boolean mIsTargetReachable;
private boolean mIsFinished;
private UExpression mBreakedExpression;
private UExpression mContinuedExpression;
ReachabilityVisitor(UElement from, UElement target) {
mFrom = from;
mTarget = target;
}
@Override
public boolean visitElement(UElement node) {
if (mIsFinished || mBreakedExpression != null || mContinuedExpression != null) {
return true;
}
if (node.equals(mFrom)) {
mIsFromReached = true;
}
if (node.equals(mTarget)) {
mIsFinished = true;
if (mIsFromReached) {
mIsTargetReachable = true;
}
return true;
}
if (mIsFromReached) {
if (node instanceof UReturnExpression) {
mIsFinished = true;
} else if (node instanceof UBreakExpression) {
mBreakedExpression = getBreakedExpression((UBreakExpression) node);
} else if (node instanceof UContinueExpression) {
UExpression expression = getContinuedExpression((UContinueExpression) node);
if (expression != null && UastUtils.isChildOf(mTarget, expression, false)) {
mIsTargetReachable = true;
mIsFinished = true;
} else {
mContinuedExpression = expression;
}
} else if (UastUtils.isChildOf(mTarget, node, false)) {
mIsTargetReachable = true;
mIsFinished = true;
}
return true;
} else {
if (node instanceof UIfExpression) {
UIfExpression ifExpression = (UIfExpression) node;
ifExpression.getCondition().accept(this);
boolean isFromReached = mIsFromReached;
UExpression thenExpression = ifExpression.getThenExpression();
if (thenExpression != null) {
thenExpression.accept(this);
}
UExpression elseExpression = ifExpression.getElseExpression();
if (elseExpression != null && isFromReached == mIsFromReached) {
elseExpression.accept(this);
}
return true;
} else if (node instanceof ULoopExpression) {
visitLoopExpressionHeader(node);
boolean isFromReached = mIsFromReached;
((ULoopExpression) node).getBody().accept(this);
if (isFromReached != mIsFromReached
&& UastUtils.isChildOf(mTarget, node, false)) {
mIsTargetReachable = true;
mIsFinished = true;
}
return true;
}
}
return false;
}
@Override
public void afterVisitElement(UElement node) {
if (node.equals(mBreakedExpression)) {
mBreakedExpression = null;
} else if (node.equals(mContinuedExpression)) {
mContinuedExpression = null;
}
}
private void visitLoopExpressionHeader(UElement node) {
if (node instanceof UWhileExpression) {
((UWhileExpression) node).getCondition().accept(this);
} else if (node instanceof UDoWhileExpression) {
((UDoWhileExpression) node).getCondition().accept(this);
} else if (node instanceof UForExpression) {
UForExpression forExpression = (UForExpression) node;
if (forExpression.getDeclaration() != null) {
forExpression.getDeclaration().accept(this);
}
if (forExpression.getCondition() != null) {
forExpression.getCondition().accept(this);
}
if (forExpression.getUpdate() != null) {
forExpression.getUpdate().accept(this);
}
} else if (node instanceof UForEachExpression) {
UForEachExpression forEachExpression = (UForEachExpression) node;
forEachExpression.getForIdentifier().accept(this);
forEachExpression.getIteratedValue().accept(this);
}
}
private static UExpression getBreakedExpression(UBreakExpression node) {
UElement parent = node.getUastParent();
String label = node.getLabel();
while (parent != null) {
if (label != null) {
if (parent instanceof ULabeledExpression) {
ULabeledExpression labeledExpression = (ULabeledExpression) parent;
if (labeledExpression.getLabel().equals(label)) {
return labeledExpression.getExpression();
}
}
} else {
if (parent instanceof ULoopExpression || parent instanceof USwitchExpression) {
return (UExpression) parent;
}
}
parent = parent.getUastParent();
}
return null;
}
private static UExpression getContinuedExpression(UContinueExpression node) {
UElement parent = node.getUastParent();
String label = node.getLabel();
while (parent != null) {
if (label != null) {
if (parent instanceof ULabeledExpression) {
ULabeledExpression labeledExpression = (ULabeledExpression) parent;
if (labeledExpression.getLabel().equals(label)) {
return labeledExpression.getExpression();
}
}
} else {
if (parent instanceof ULoopExpression) {
return (UExpression) parent;
}
}
parent = parent.getUastParent();
}
return null;
}
public boolean isReachable() {
return mIsTargetReachable;
}
}
}