blob: 646c78e605fd3ba958403b0918490df2484c7bf3 [file] [log] [blame]
package org.robolectric.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
import static com.google.errorprone.matchers.Matchers.hasAnnotation;
import com.google.errorprone.BugPattern;
import com.google.errorprone.BugPattern.StandardTags;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.fixes.SuggestedFixes;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.doctree.DocCommentTree;
import com.sun.source.doctree.EndElementTree;
import com.sun.source.doctree.ReferenceTree;
import com.sun.source.doctree.StartElementTree;
import com.sun.source.doctree.TextTree;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.ModifiersTree;
import com.sun.source.util.DocSourcePositions;
import com.sun.source.util.DocTreePath;
import com.sun.source.util.DocTreePathScanner;
import com.sun.source.util.TreePathScanner;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.tree.DCTree.DCDocComment;
import com.sun.tools.javac.tree.DCTree.DCReference;
import com.sun.tools.javac.tree.JCTree.JCAssign;
import com.sun.tools.javac.tree.JCTree.JCIdent;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import javax.lang.model.element.Modifier;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
/**
* Ensure Robolectric shadow's method marked with {@code @Implementation} is protected
*
* @author christianw@google.com (Christian Williams)
*/
@BugPattern(
name = "RobolectricShadow",
summary = "Robolectric @Implementation methods should be protected.",
severity = SUGGESTION,
documentSuppression = false,
tags = StandardTags.REFACTORING)
public final class RobolectricShadow extends BugChecker implements ClassTreeMatcher {
private static final Matcher<ClassTree> implementsClassMatcher = hasAnnotation(Implements.class);
private static final Matcher<MethodTree> implementationMethodMatcher =
hasAnnotation(Implementation.class);
private boolean doScanJavadoc = false;
@Override
public Description matchClass(ClassTree classTree, VisitorState state) {
List<Optional<SuggestedFix>> fixes = new ArrayList<>();
if (implementsClassMatcher.matches(classTree, state)) {
boolean inSdk = true;
JavacTrees trees = JavacTrees.instance(state.context);
for (AnnotationTree annotationTree : classTree.getModifiers().getAnnotations()) {
JCIdent ident = (JCIdent) annotationTree.getAnnotationType();
String annotationClassName = ident.sym.getQualifiedName().toString();
if ("org.robolectric.annotation.Implements".equals(annotationClassName)) {
for (ExpressionTree expressionTree : annotationTree.getArguments()) {
JCAssign jcAnnotation = (JCAssign) expressionTree;
if ("isInAndroidSdk".equals(state.getSourceForNode(jcAnnotation.lhs))
&& "false".equals(state.getSourceForNode(jcAnnotation.rhs))) {
// shadows of classes not in the public Android SDK can keep their public methods.
inSdk = false;
}
}
}
}
if (inSdk) {
new ImplementationMethodScanner(state, fixes, trees).scan(state.getPath(), null);
}
}
SuggestedFix.Builder builder = SuggestedFix.builder();
for (Optional<SuggestedFix> fix : fixes) {
fix.ifPresent(builder::merge);
}
if (builder.isEmpty()) {
return Description.NO_MATCH;
} else {
return describeMatch(classTree, builder.build());
}
}
static final class DocTreeSymbolScanner extends DocTreePathScanner<Void, Void> {
private final JavacTrees trees;
private final List<Optional<SuggestedFix>> fixes;
DocTreeSymbolScanner(JavacTrees trees, List<Optional<SuggestedFix>> fixes) {
this.trees = trees;
this.fixes = fixes;
}
@Override
public Void visitStartElement(StartElementTree startElementTree, Void aVoid) {
if (startElementTree.getName().toString().equalsIgnoreCase("p")) {
DocTreePath path = getCurrentPath();
DCDocComment doc = (DCDocComment) path.getDocComment();
DocSourcePositions positions = trees.getSourcePositions();
CompilationUnitTree compilationUnitTree = path.getTreePath().getCompilationUnit();
int start = (int) positions.getStartPosition(compilationUnitTree, doc, startElementTree);
int end = (int) positions.getEndPosition(compilationUnitTree, doc, startElementTree);
fixes.add(Optional.of(SuggestedFix.replace(start, end, "")));
}
return super.visitStartElement(startElementTree, aVoid);
}
@Override
public Void visitEndElement(EndElementTree endElementTree, Void aVoid) {
return super.visitEndElement(endElementTree, aVoid);
}
@Override
public Void visitText(TextTree textTree, Void aVoid) {
System.out.println("textTree = " + textTree);
return super.visitText(textTree, aVoid);
}
@Override
public Void visitReference(ReferenceTree referenceTree, Void sink) {
// do this first, it attributes the referenceTree as a side-effect
trees.getElement(getCurrentPath());
com.sun.source.util.TreeScanner<Void, Void> nonRecursiveScanner =
new com.sun.source.util.TreeScanner<Void, Void>() {
@Override
public Void visitIdentifier(IdentifierTree tree, Void sink) {
Symbol sym = ASTHelpers.getSymbol(tree);
if (sym != null) {
System.out.println("sym = " + sym);
}
return null;
}
};
DCReference reference = (DCReference) referenceTree;
nonRecursiveScanner.scan(reference.qualifierExpression, sink);
nonRecursiveScanner.scan(reference.paramTypes, sink);
return null;
}
}
private class ImplementationMethodScanner extends TreePathScanner<Void, Void> {
private final com.google.errorprone.VisitorState state;
private final List<Optional<SuggestedFix>> fixes;
private final JavacTrees trees;
ImplementationMethodScanner(
com.google.errorprone.VisitorState state,
List<Optional<SuggestedFix>> fixes,
JavacTrees trees) {
this.state = state;
this.fixes = fixes;
this.trees = trees;
}
@Override
public Void visitMethod(MethodTree methodTree, Void aVoid) {
if (implementationMethodMatcher.matches(methodTree, state)) {
processImplementationMethod(methodTree);
}
return super.visitMethod(methodTree, aVoid);
}
private void processImplementationMethod(MethodTree methodTree) {
String methodName = methodTree.getName().toString();
if ("toString".equals(methodName)
|| "equals".equals(methodName)
|| "hashCode".equals(methodName)) {
return; // they need to remain public
}
ModifiersTree modifiersTree = methodTree.getModifiers();
for (AnnotationTree annotationTree : modifiersTree.getAnnotations()) {
JCIdent ident = (JCIdent) annotationTree.getAnnotationType();
String annotationClassName = ident.sym.getQualifiedName().toString();
if ("java.lang.Override".equals(annotationClassName)) {
// can't have more restrictive permissions than the overridden method.
return;
}
if ("org.robolectric.annotation.HiddenApi".equals(annotationClassName)) {
// @HiddenApi implementation methods can stay public for the convenience of tests.
return;
}
}
Set<Modifier> modifiers = modifiersTree.getFlags();
if (!modifiers.contains(Modifier.PROTECTED)) {
fixes.add(
SuggestedFixes.removeModifiers(methodTree, state, Modifier.PUBLIC, Modifier.PRIVATE));
fixes.add(SuggestedFixes.addModifiers(methodTree, state, Modifier.PROTECTED));
}
if (doScanJavadoc) {
scanJavadoc();
}
}
private void scanJavadoc() {
DocCommentTree commentTree = trees.getDocCommentTree(getCurrentPath());
if (commentTree != null) {
DocTreePath docTrees = new DocTreePath(getCurrentPath(), commentTree);
new DocTreeSymbolScanner(trees, fixes).scan(docTrees, null);
}
}
}
}