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