1 package org.robolectric.errorprone.bugpatterns; 2 3 import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION; 4 import static com.google.errorprone.matchers.Matchers.hasAnnotation; 5 6 import com.google.errorprone.BugPattern; 7 import com.google.errorprone.BugPattern.StandardTags; 8 import com.google.errorprone.VisitorState; 9 import com.google.errorprone.bugpatterns.BugChecker; 10 import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher; 11 import com.google.errorprone.fixes.SuggestedFix; 12 import com.google.errorprone.fixes.SuggestedFixes; 13 import com.google.errorprone.matchers.Description; 14 import com.google.errorprone.matchers.Matcher; 15 import com.google.errorprone.util.ASTHelpers; 16 import com.sun.source.doctree.DocCommentTree; 17 import com.sun.source.doctree.EndElementTree; 18 import com.sun.source.doctree.ReferenceTree; 19 import com.sun.source.doctree.StartElementTree; 20 import com.sun.source.doctree.TextTree; 21 import com.sun.source.tree.AnnotationTree; 22 import com.sun.source.tree.ClassTree; 23 import com.sun.source.tree.CompilationUnitTree; 24 import com.sun.source.tree.ExpressionTree; 25 import com.sun.source.tree.IdentifierTree; 26 import com.sun.source.tree.MethodTree; 27 import com.sun.source.tree.ModifiersTree; 28 import com.sun.source.util.DocSourcePositions; 29 import com.sun.source.util.DocTreePath; 30 import com.sun.source.util.DocTreePathScanner; 31 import com.sun.source.util.TreePathScanner; 32 import com.sun.tools.javac.api.JavacTrees; 33 import com.sun.tools.javac.code.Symbol; 34 import com.sun.tools.javac.tree.DCTree.DCDocComment; 35 import com.sun.tools.javac.tree.DCTree.DCReference; 36 import com.sun.tools.javac.tree.JCTree.JCAssign; 37 import com.sun.tools.javac.tree.JCTree.JCIdent; 38 import java.util.ArrayList; 39 import java.util.List; 40 import java.util.Optional; 41 import java.util.Set; 42 import javax.lang.model.element.Modifier; 43 import org.robolectric.annotation.Implementation; 44 import org.robolectric.annotation.Implements; 45 46 /** 47 * Ensure Robolectric shadow's method marked with {@code @Implementation} is protected 48 * 49 * @author christianw@google.com (Christian Williams) 50 */ 51 @BugPattern( 52 summary = "Robolectric @Implementation methods should be protected.", 53 severity = SUGGESTION, 54 documentSuppression = false, 55 tags = StandardTags.REFACTORING) 56 public final class RobolectricShadow extends BugChecker implements ClassTreeMatcher { 57 private static final Matcher<ClassTree> implementsClassMatcher = hasAnnotation(Implements.class); 58 59 private static final Matcher<MethodTree> implementationMethodMatcher = 60 hasAnnotation(Implementation.class); 61 62 private final boolean doScanJavadoc = false; 63 64 @Override matchClass(ClassTree classTree, VisitorState state)65 public Description matchClass(ClassTree classTree, VisitorState state) { 66 List<Optional<SuggestedFix>> fixes = new ArrayList<>(); 67 68 if (implementsClassMatcher.matches(classTree, state)) { 69 boolean inSdk = true; 70 71 JavacTrees trees = JavacTrees.instance(state.context); 72 for (AnnotationTree annotationTree : classTree.getModifiers().getAnnotations()) { 73 JCIdent ident = (JCIdent) annotationTree.getAnnotationType(); 74 String annotationClassName = ident.sym.getQualifiedName().toString(); 75 if ("org.robolectric.annotation.Implements".equals(annotationClassName)) { 76 for (ExpressionTree expressionTree : annotationTree.getArguments()) { 77 JCAssign jcAnnotation = (JCAssign) expressionTree; 78 if ("isInAndroidSdk".equals(state.getSourceForNode(jcAnnotation.lhs)) 79 && "false".equals(state.getSourceForNode(jcAnnotation.rhs))) { 80 // shadows of classes not in the public Android SDK can keep their public methods. 81 inSdk = false; 82 } 83 } 84 } 85 } 86 87 if (inSdk) { 88 new ImplementationMethodScanner(state, fixes, trees).scan(state.getPath(), null); 89 } 90 } 91 92 SuggestedFix.Builder builder = SuggestedFix.builder(); 93 for (Optional<SuggestedFix> fix : fixes) { 94 fix.ifPresent(builder::merge); 95 } 96 97 if (builder.isEmpty()) { 98 return Description.NO_MATCH; 99 } else { 100 return describeMatch(classTree, builder.build()); 101 } 102 } 103 104 static final class DocTreeSymbolScanner extends DocTreePathScanner<Void, Void> { 105 private final JavacTrees trees; 106 private final List<Optional<SuggestedFix>> fixes; 107 DocTreeSymbolScanner(JavacTrees trees, List<Optional<SuggestedFix>> fixes)108 DocTreeSymbolScanner(JavacTrees trees, List<Optional<SuggestedFix>> fixes) { 109 this.trees = trees; 110 this.fixes = fixes; 111 } 112 113 @Override visitStartElement(StartElementTree startElementTree, Void aVoid)114 public Void visitStartElement(StartElementTree startElementTree, Void aVoid) { 115 if (startElementTree.getName().toString().equalsIgnoreCase("p")) { 116 DocTreePath path = getCurrentPath(); 117 DCDocComment doc = (DCDocComment) path.getDocComment(); 118 DocSourcePositions positions = trees.getSourcePositions(); 119 CompilationUnitTree compilationUnitTree = path.getTreePath().getCompilationUnit(); 120 int start = (int) positions.getStartPosition(compilationUnitTree, doc, startElementTree); 121 int end = (int) positions.getEndPosition(compilationUnitTree, doc, startElementTree); 122 123 fixes.add(Optional.of(SuggestedFix.replace(start, end, ""))); 124 } 125 return super.visitStartElement(startElementTree, aVoid); 126 } 127 128 @Override visitEndElement(EndElementTree endElementTree, Void aVoid)129 public Void visitEndElement(EndElementTree endElementTree, Void aVoid) { 130 return super.visitEndElement(endElementTree, aVoid); 131 } 132 133 @Override visitText(TextTree textTree, Void aVoid)134 public Void visitText(TextTree textTree, Void aVoid) { 135 System.out.println("textTree = " + textTree); 136 return super.visitText(textTree, aVoid); 137 } 138 139 @Override visitReference(ReferenceTree referenceTree, Void sink)140 public Void visitReference(ReferenceTree referenceTree, Void sink) { 141 // do this first, it attributes the referenceTree as a side-effect 142 trees.getElement(getCurrentPath()); 143 com.sun.source.util.TreeScanner<Void, Void> nonRecursiveScanner = 144 new com.sun.source.util.TreeScanner<Void, Void>() { 145 @Override 146 public Void visitIdentifier(IdentifierTree tree, Void sink) { 147 Symbol sym = ASTHelpers.getSymbol(tree); 148 if (sym != null) { 149 System.out.println("sym = " + sym); 150 } 151 return null; 152 } 153 }; 154 DCReference reference = (DCReference) referenceTree; 155 nonRecursiveScanner.scan(reference.qualifierExpression, sink); 156 nonRecursiveScanner.scan(reference.paramTypes, sink); 157 return null; 158 } 159 } 160 161 private class ImplementationMethodScanner extends TreePathScanner<Void, Void> { 162 163 private final com.google.errorprone.VisitorState state; 164 private final List<Optional<SuggestedFix>> fixes; 165 private final JavacTrees trees; 166 ImplementationMethodScanner( com.google.errorprone.VisitorState state, List<Optional<SuggestedFix>> fixes, JavacTrees trees)167 ImplementationMethodScanner( 168 com.google.errorprone.VisitorState state, 169 List<Optional<SuggestedFix>> fixes, 170 JavacTrees trees) { 171 this.state = state; 172 this.fixes = fixes; 173 this.trees = trees; 174 } 175 176 @Override visitMethod(MethodTree methodTree, Void aVoid)177 public Void visitMethod(MethodTree methodTree, Void aVoid) { 178 if (implementationMethodMatcher.matches(methodTree, state)) { 179 processImplementationMethod(methodTree); 180 } 181 return super.visitMethod(methodTree, aVoid); 182 } 183 processImplementationMethod(MethodTree methodTree)184 private void processImplementationMethod(MethodTree methodTree) { 185 String methodName = methodTree.getName().toString(); 186 if ("toString".equals(methodName) 187 || "equals".equals(methodName) 188 || "hashCode".equals(methodName)) { 189 return; // they need to remain public 190 } 191 ModifiersTree modifiersTree = methodTree.getModifiers(); 192 for (AnnotationTree annotationTree : modifiersTree.getAnnotations()) { 193 JCIdent ident = (JCIdent) annotationTree.getAnnotationType(); 194 String annotationClassName = ident.sym.getQualifiedName().toString(); 195 if ("java.lang.Override".equals(annotationClassName)) { 196 // can't have more restrictive permissions than the overridden method. 197 return; 198 } 199 if ("org.robolectric.annotation.HiddenApi".equals(annotationClassName)) { 200 // @HiddenApi implementation methods can stay public for the convenience of tests. 201 return; 202 } 203 } 204 205 Set<Modifier> modifiers = modifiersTree.getFlags(); 206 if (!modifiers.contains(Modifier.PROTECTED)) { 207 fixes.add( 208 SuggestedFixes.removeModifiers(methodTree, state, Modifier.PUBLIC, Modifier.PRIVATE)); 209 fixes.add(SuggestedFixes.addModifiers(methodTree, state, Modifier.PROTECTED)); 210 } 211 212 if (doScanJavadoc) { 213 scanJavadoc(); 214 } 215 } 216 scanJavadoc()217 private void scanJavadoc() { 218 DocCommentTree commentTree = trees.getDocCommentTree(getCurrentPath()); 219 if (commentTree != null) { 220 DocTreePath docTrees = new DocTreePath(getCurrentPath(), commentTree); 221 new DocTreeSymbolScanner(trees, fixes).scan(docTrees, null); 222 } 223 } 224 } 225 } 226