blob: 2310d230b7dd1ee6f9c08e05ea175187dde717f2 [file] [log] [blame]
/*
* 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.tools.lint.client.api.JavaParser.ResolvedMethod;
import static com.android.tools.lint.client.api.JavaParser.ResolvedNode;
import static com.android.tools.lint.client.api.JavaParser.TypeDescriptor;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.client.api.JavaParser;
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.LintUtils;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import java.io.File;
import java.util.Collections;
import java.util.List;
import lombok.ast.Assert;
import lombok.ast.AstVisitor;
import lombok.ast.Block;
import lombok.ast.Case;
import lombok.ast.ClassDeclaration;
import lombok.ast.ConstructorDeclaration;
import lombok.ast.DoWhile;
import lombok.ast.Expression;
import lombok.ast.ExpressionStatement;
import lombok.ast.For;
import lombok.ast.ForwardingAstVisitor;
import lombok.ast.If;
import lombok.ast.MethodDeclaration;
import lombok.ast.MethodInvocation;
import lombok.ast.Node;
import lombok.ast.NormalTypeBody;
import lombok.ast.Return;
import lombok.ast.Statement;
import lombok.ast.VariableDeclaration;
import lombok.ast.VariableDefinition;
import lombok.ast.VariableReference;
import lombok.ast.While;
/**
* Detector looking for SharedPreferences.edit() calls without a corresponding
* commit() or apply() call
*/
public class SharedPrefsDetector extends Detector implements Detector.JavaScanner {
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"CommitPrefEdits", //$NON-NLS-1$
"Missing `commit()` on `SharedPreference` editor",
"After calling `edit()` on a `SharedPreference`, you must call `commit()` " +
"or `apply()` on the editor to save the results.",
Category.CORRECTNESS,
6,
Severity.WARNING,
new Implementation(
SharedPrefsDetector.class,
Scope.JAVA_FILE_SCOPE));
public static final String ANDROID_CONTENT_SHARED_PREFERENCES =
"android.content.SharedPreferences"; //$NON-NLS-1$
private static final String ANDROID_CONTENT_SHARED_PREFERENCES_EDITOR =
"android.content.SharedPreferences.Editor"; //$NON-NLS-1$
/** Constructs a new {@link SharedPrefsDetector} check */
public SharedPrefsDetector() {
}
@Override
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
return true;
}
// ---- Implements JavaScanner ----
@Override
public List<String> getApplicableMethodNames() {
return Collections.singletonList("edit"); //$NON-NLS-1$
}
@Nullable
private static NormalTypeBody findSurroundingTypeBody(Node scope) {
while (scope != null) {
Class<? extends Node> type = scope.getClass();
// The Lombok AST uses a flat hierarchy of node type implementation classes
// so no need to do instanceof stuff here.
if (type == NormalTypeBody.class) {
return (NormalTypeBody) scope;
}
scope = scope.getParent();
}
return null;
}
@Override
public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor,
@NonNull MethodInvocation node) {
assert node.astName().astValue().equals("edit");
boolean verifiedType = false;
ResolvedNode resolve = context.resolve(node);
if (resolve instanceof ResolvedMethod) {
ResolvedMethod method = (ResolvedMethod) resolve;
TypeDescriptor returnType = method.getReturnType();
if (returnType == null ||
!returnType.matchesName(ANDROID_CONTENT_SHARED_PREFERENCES_EDITOR)) {
return;
}
verifiedType = true;
}
Expression operand = node.astOperand();
if (operand == null) {
return;
}
// Looking for the specific pattern where you assign the edit() result
// to a local variable; this means we won't recognize some other usages
// of the API (e.g. assigning it to a previously declared variable) but
// is needed until we have type attribution in the AST itself.
Node parent = node.getParent();
VariableDefinition definition = getLhs(parent);
boolean allowCommitBeforeTarget;
if (definition == null) {
if (operand instanceof VariableReference) {
if (!verifiedType) {
NormalTypeBody body = findSurroundingTypeBody(parent);
if (body == null) {
return;
}
String variableName = ((VariableReference) operand).astIdentifier().astValue();
String type = getFieldType(body, variableName);
if (type == null || !type.equals("SharedPreferences")) { //$NON-NLS-1$
return;
}
}
allowCommitBeforeTarget = true;
} else {
return;
}
} else {
if (!verifiedType) {
String type = definition.astTypeReference().toString();
if (!type.endsWith("SharedPreferences.Editor")) { //$NON-NLS-1$
if (!type.equals("Editor") || //$NON-NLS-1$
!LintUtils.isImported(context.getCompilationUnit(),
ANDROID_CONTENT_SHARED_PREFERENCES_EDITOR)) {
return;
}
}
}
allowCommitBeforeTarget = false;
}
Node method = JavaContext.findSurroundingMethod(parent);
if (method == null) {
return;
}
CommitFinder finder = new CommitFinder(context, node, allowCommitBeforeTarget);
method.accept(finder);
if (!finder.isCommitCalled()) {
context.report(ISSUE, method, context.getLocation(node),
"`SharedPreferences.edit()` without a corresponding `commit()` or `apply()` call");
}
}
@Nullable
private static String getFieldType(@NonNull NormalTypeBody cls, @NonNull String name) {
List<Node> children = cls.getChildren();
for (Node child : children) {
if (child.getClass() == VariableDeclaration.class) {
VariableDeclaration declaration = (VariableDeclaration) child;
VariableDefinition definition = declaration.astDefinition();
return definition.astTypeReference().toString();
}
}
return null;
}
@Nullable
private static VariableDefinition getLhs(@NonNull Node node) {
while (node != null) {
Class<? extends Node> type = node.getClass();
// The Lombok AST uses a flat hierarchy of node type implementation classes
// so no need to do instanceof stuff here.
if (type == MethodDeclaration.class || type == ConstructorDeclaration.class) {
return null;
}
if (type == VariableDefinition.class) {
return (VariableDefinition) node;
}
node = node.getParent();
}
return null;
}
private static class CommitFinder extends ForwardingAstVisitor {
/** The target edit call */
private final MethodInvocation mTarget;
/** whether it allows the commit call to be seen before the target node */
private final boolean mAllowCommitBeforeTarget;
private final JavaContext mContext;
/** Whether we've found one of the commit/cancel methods */
private boolean mFound;
/** Whether we've seen the target edit node yet */
private boolean mSeenTarget;
private CommitFinder(JavaContext context, MethodInvocation target,
boolean allowCommitBeforeTarget) {
mContext = context;
mTarget = target;
mAllowCommitBeforeTarget = allowCommitBeforeTarget;
}
@Override
public boolean visitMethodInvocation(MethodInvocation node) {
if (node == mTarget) {
mSeenTarget = true;
} else if (mAllowCommitBeforeTarget || mSeenTarget || node.astOperand() == mTarget) {
String name = node.astName().astValue();
boolean isCommit = "commit".equals(name);
if (isCommit || "apply".equals(name)) { //$NON-NLS-1$ //$NON-NLS-2$
// TODO: Do more flow analysis to see whether we're really calling commit/apply
// on the right type of object?
mFound = true;
ResolvedNode resolved = mContext.resolve(node);
if (resolved instanceof JavaParser.ResolvedMethod) {
ResolvedMethod method = (ResolvedMethod) resolved;
JavaParser.ResolvedClass clz = method.getContainingClass();
if (clz.isSubclassOf("android.content.SharedPreferences.Editor", false)
&& mContext.getProject().getMinSdkVersion().getApiLevel() >= 9) {
// See if the return value is read: can only replace commit with
// apply if the return value is not considered
Node parent = node.getParent();
boolean returnValueIgnored = false;
if (parent instanceof MethodDeclaration ||
parent instanceof ConstructorDeclaration ||
parent instanceof ClassDeclaration ||
parent instanceof Block ||
parent instanceof ExpressionStatement) {
returnValueIgnored = true;
} else if (parent instanceof Statement) {
if (parent instanceof If) {
returnValueIgnored = ((If) parent).astCondition() != node;
} else if (parent instanceof Return) {
returnValueIgnored = false;
} else if (parent instanceof VariableDeclaration) {
returnValueIgnored = false;
} else if (parent instanceof For) {
returnValueIgnored = ((For) parent).astCondition() != node;
} else if (parent instanceof While) {
returnValueIgnored = ((While) parent).astCondition() != node;
} else if (parent instanceof DoWhile) {
returnValueIgnored = ((DoWhile) parent).astCondition() != node;
} else if (parent instanceof Case) {
returnValueIgnored = ((Case) parent).astCondition() != node;
} else if (parent instanceof Assert) {
returnValueIgnored = ((Assert) parent).astAssertion() != node;
} else {
returnValueIgnored = true;
}
}
if (returnValueIgnored && isCommit) {
String message = "Consider using `apply()` instead; `commit` writes "
+ "its data to persistent storage immediately, whereas "
+ "`apply` will handle it in the background";
mContext.report(ISSUE, node, mContext.getLocation(node), message);
}
}
}
}
}
return super.visitMethodInvocation(node);
}
@Override
public boolean visitReturn(Return node) {
if (node.astValue() == mTarget) {
// If you just do "return editor.commit() don't warn
mFound = true;
}
return super.visitReturn(node);
}
boolean isCommitCalled() {
return mFound;
}
}
}