1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.errorprone.bugpatterns.android; 18 19 import static com.google.errorprone.BugPattern.LinkType.NONE; 20 import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; 21 import static com.google.errorprone.matchers.Description.NO_MATCH; 22 import static com.google.errorprone.util.ASTHelpers.getStartPosition; 23 import static com.google.errorprone.util.ASTHelpers.getSymbol; 24 25 import com.google.auto.service.AutoService; 26 import com.google.errorprone.BugPattern; 27 import com.google.errorprone.VisitorState; 28 import com.google.errorprone.bugpatterns.BugChecker; 29 import com.google.errorprone.fixes.SuggestedFix; 30 import com.google.errorprone.matchers.Description; 31 import com.google.errorprone.util.ASTHelpers; 32 import com.google.errorprone.util.ErrorProneToken; 33 import com.google.errorprone.util.ErrorProneTokens; 34 import com.sun.source.tree.ClassTree; 35 import com.sun.source.tree.CompilationUnitTree; 36 import com.sun.source.tree.MethodTree; 37 import com.sun.source.tree.NewClassTree; 38 import com.sun.source.tree.Tree; 39 import com.sun.source.tree.VariableTree; 40 import com.sun.tools.javac.parser.Tokens; 41 42 import java.util.HashMap; 43 import java.util.Map; 44 import java.util.Optional; 45 46 import javax.lang.model.element.ElementKind; 47 48 /** 49 * Bug checker to warn about {@code @hide} directives in comments. 50 * 51 * {@code @hide} tags are only meaningful inside of Javadoc comments. Errorprone has checks for 52 * standard Javadoc tags but doesn't know anything about {@code @hide} since it's an Android 53 * specific tag. 54 */ 55 @AutoService(BugChecker.class) 56 @BugPattern( 57 name = "AndroidHideInComments", 58 summary = "Warns when there are @hide declarations in comments rather than javadoc", 59 linkType = NONE, 60 severity = WARNING) 61 public class HideInCommentsChecker extends BugChecker implements 62 BugChecker.CompilationUnitTreeMatcher { 63 64 @Override matchCompilationUnit(CompilationUnitTree tree, VisitorState state)65 public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState state) { 66 final Map<Integer, Tree> javadocableTrees = findJavadocableTrees(tree); 67 final String sourceCode = state.getSourceCode().toString(); 68 for (ErrorProneToken token : ErrorProneTokens.getTokens(sourceCode, state.context)) { 69 for (Tokens.Comment comment : token.comments()) { 70 if (!javadocableTrees.containsKey(token.pos())) { 71 continue; 72 } 73 generateFix(comment).ifPresent(fix -> { 74 final Tree javadocableTree = javadocableTrees.get(token.pos()); 75 state.reportMatch(describeMatch(javadocableTree, fix)); 76 }); 77 } 78 } 79 // We might have multiple matches, so report them via VisitorState rather than the return 80 // value from the match function. 81 return NO_MATCH; 82 } 83 generateFix(Tokens.Comment comment)84 private static Optional<SuggestedFix> generateFix(Tokens.Comment comment) { 85 final String text = comment.getText(); 86 if (text.startsWith("/**")) { 87 return Optional.empty(); 88 } 89 90 if (!text.contains("@hide")) { 91 return Optional.empty(); 92 } 93 94 if (text.startsWith("/*")) { 95 final int pos = comment.getSourcePos(1); 96 return Optional.of(SuggestedFix.replace(pos, pos, "*")); 97 } else if (text.startsWith("//")) { 98 final int endPos = comment.getSourcePos(text.length() - 1); 99 final char endChar = text.charAt(text.length() - 1); 100 String javadocClose = " */"; 101 if (endChar != ' ') { 102 javadocClose = endChar + javadocClose; 103 } 104 final SuggestedFix fix = SuggestedFix.builder() 105 .replace(comment.getSourcePos(1), comment.getSourcePos(2), "**") 106 .replace(endPos, endPos + 1, javadocClose) 107 .build(); 108 return Optional.of(fix); 109 } 110 111 return Optional.empty(); 112 } 113 114 findJavadocableTrees(CompilationUnitTree tree)115 private Map<Integer, Tree> findJavadocableTrees(CompilationUnitTree tree) { 116 Map<Integer, Tree> javadoccableTrees = new HashMap<>(); 117 new SuppressibleTreePathScanner<Void, Void>() { 118 @Override 119 public Void visitClass(ClassTree classTree, Void unused) { 120 javadoccableTrees.put(getStartPosition(classTree), classTree); 121 return super.visitClass(classTree, null); 122 } 123 124 @Override 125 public Void visitMethod(MethodTree methodTree, Void unused) { 126 // Generated constructors never have comments 127 if (!ASTHelpers.isGeneratedConstructor(methodTree)) { 128 javadoccableTrees.put(getStartPosition(methodTree), methodTree); 129 } 130 return super.visitMethod(methodTree, null); 131 } 132 133 @Override 134 public Void visitVariable(VariableTree variableTree, Void unused) { 135 ElementKind kind = getSymbol(variableTree).getKind(); 136 if (kind == ElementKind.FIELD) { 137 javadoccableTrees.put(getStartPosition(variableTree), variableTree); 138 } 139 if (kind == ElementKind.ENUM_CONSTANT) { 140 javadoccableTrees.put(getStartPosition(variableTree), variableTree); 141 if (variableTree.getInitializer() instanceof NewClassTree) { 142 // Skip the generated class definition 143 ClassTree classBody = 144 ((NewClassTree) variableTree.getInitializer()).getClassBody(); 145 if (classBody != null) { 146 scan(classBody.getMembers(), null); 147 } 148 return null; 149 } 150 } 151 return super.visitVariable(variableTree, null); 152 } 153 154 }.scan(tree, null); 155 return javadoccableTrees; 156 } 157 158 } 159