16
16
17
17
package com .google .errorprone ;
18
18
19
+ import static com .google .errorprone .BugPattern .SeverityLevel .WARNING ;
20
+
19
21
import com .google .common .collect .ImmutableSet ;
20
22
import com .google .errorprone .annotations .CheckReturnValue ;
21
23
import com .google .errorprone .annotations .Immutable ;
24
+ import com .google .errorprone .bugpatterns .BugChecker ;
25
+ import com .google .errorprone .matchers .Description ;
22
26
import com .google .errorprone .matchers .Suppressible ;
23
27
import com .google .errorprone .suppliers .Supplier ;
24
28
import com .google .errorprone .util .ASTHelpers ;
25
29
import com .sun .source .tree .ClassTree ;
26
30
import com .sun .source .tree .CompilationUnitTree ;
31
+ import com .sun .source .tree .Tree ;
27
32
import com .sun .source .util .SimpleTreeVisitor ;
33
+ import com .sun .source .util .Trees ;
28
34
import com .sun .tools .javac .code .Attribute ;
29
35
import com .sun .tools .javac .code .Symbol ;
30
36
import com .sun .tools .javac .code .Symbol .ClassSymbol ;
31
37
import com .sun .tools .javac .code .Symbol .MethodSymbol ;
38
+ import com .sun .tools .javac .processing .JavacProcessingEnvironment ;
32
39
import com .sun .tools .javac .util .Name ;
33
40
import com .sun .tools .javac .util .Pair ;
34
41
import java .util .Collections ;
35
42
import java .util .HashSet ;
36
43
import java .util .Set ;
37
44
import java .util .concurrent .atomic .AtomicBoolean ;
45
+ import org .jspecify .annotations .Nullable ;
38
46
39
47
/**
40
48
* Immutable container of "suppression signals" - annotations or other information gathered from
@@ -61,11 +69,85 @@ public class SuppressionInfo {
61
69
62
70
private final boolean inGeneratedCode ;
63
71
72
+ @ BugPattern (summary = "Warns when a @SuppressWarnings is not needed" , severity = WARNING )
73
+ private static final class UnusedSuppressionChecker extends BugChecker {}
74
+
75
+ private static final UnusedSuppressionChecker UNUSED_SUPPRESSION_CHECKER =
76
+ new UnusedSuppressionChecker ();
77
+
78
+ // for tracking unneeded suppressions
79
+ static class SuppressionInfoForSymbol {
80
+ private final @ Nullable Symbol symbol ;
81
+ private final ImmutableSet <String > suppressWarningStringsForSymbol ;
82
+ private final Set <String > usedSuppressWarningStrings = new HashSet <>();
83
+ private final ImmutableSet <Name > customSuppressionsForSymbol ;
84
+ private final @ Nullable SuppressionInfoForSymbol parent ;
85
+
86
+ public SuppressionInfoForSymbol (
87
+ @ Nullable Symbol symbol ,
88
+ ImmutableSet <String > suppressWarningStringsForSymbol ,
89
+ ImmutableSet <Name > customSuppressionsForSymbol ,
90
+ @ Nullable SuppressionInfoForSymbol parent ) {
91
+ this .symbol = symbol ;
92
+ this .suppressWarningStringsForSymbol = suppressWarningStringsForSymbol ;
93
+ this .customSuppressionsForSymbol = customSuppressionsForSymbol ;
94
+ this .parent = parent ;
95
+ }
96
+
97
+ public void markSuppressionStringAsUsed (String suppressionName ) {
98
+ if (suppressWarningStringsForSymbol .contains (suppressionName )) {
99
+ usedSuppressWarningStrings .add (suppressionName );
100
+ } else if (parent != null ) {
101
+ parent .markSuppressionStringAsUsed (suppressionName );
102
+ } else {
103
+ throw new IllegalArgumentException ("Suppression string not found: " + suppressionName );
104
+ }
105
+ }
106
+ }
107
+
108
+ private final @ Nullable SuppressionInfoForSymbol infoForClosestSymbol ;
109
+
110
+ public void updatedUsedSuppressions (Suppressed suppressed ) {
111
+ String suppressionName = suppressed .getSuppressionName ();
112
+ if (suppressionName != null && suppressed .isUsed ()) {
113
+ infoForClosestSymbol .markSuppressionStringAsUsed (suppressionName );
114
+ }
115
+ }
116
+
117
+ public void warnOnUnusedSuppressions (VisitorState state ) {
118
+ if (infoForClosestSymbol == null ) {
119
+ return ;
120
+ }
121
+ for (String warn : infoForClosestSymbol .suppressWarningStringsForSymbol ) {
122
+ if (!infoForClosestSymbol .usedSuppressWarningStrings .contains (warn )) {
123
+ Tree tree =
124
+ Trees .instance (JavacProcessingEnvironment .instance (state .context ))
125
+ .getTree (infoForClosestSymbol .symbol );
126
+ Description description =
127
+ UNUSED_SUPPRESSION_CHECKER
128
+ .buildDescription (tree )
129
+ .setMessage ("Unnecessary @SuppressWarnings(\" " + warn + "\" )" )
130
+ .overrideSeverity (WARNING )
131
+ .build ();
132
+ state .reportMatch (description );
133
+ }
134
+ }
135
+ }
136
+
64
137
private SuppressionInfo (
65
138
Set <String > suppressWarningsStrings , Set <Name > customSuppressions , boolean inGeneratedCode ) {
139
+ this (suppressWarningsStrings , customSuppressions , inGeneratedCode , null );
140
+ }
141
+
142
+ private SuppressionInfo (
143
+ Set <String > suppressWarningsStrings ,
144
+ Set <Name > customSuppressions ,
145
+ boolean inGeneratedCode ,
146
+ @ Nullable SuppressionInfoForSymbol infoForClosestSymbol ) {
66
147
this .suppressWarningsStrings = ImmutableSet .copyOf (suppressWarningsStrings );
67
148
this .customSuppressions = ImmutableSet .copyOf (customSuppressions );
68
149
this .inGeneratedCode = inGeneratedCode ;
150
+ this .infoForClosestSymbol = infoForClosestSymbol ;
69
151
}
70
152
71
153
private static boolean isGenerated (Symbol sym , VisitorState state ) {
@@ -82,18 +164,29 @@ private static boolean isGenerated(Symbol sym, VisitorState state) {
82
164
public SuppressedState suppressedState (
83
165
Suppressible suppressible , boolean suppressedInGeneratedCode , VisitorState state ) {
84
166
if (inGeneratedCode && suppressedInGeneratedCode ) {
85
- return SuppressedState . SUPPRESSED ;
167
+ return new Suppressed ( null ) ;
86
168
}
87
169
if (suppressible .supportsSuppressWarnings ()
88
170
&& (suppressWarningsStrings .contains ("all" )
89
171
|| !Collections .disjoint (suppressible .allNames (), suppressWarningsStrings ))) {
90
- return SuppressedState .SUPPRESSED ;
172
+ String name ;
173
+ if (suppressWarningsStrings .contains ("all" )) {
174
+ name = "all" ;
175
+ } else {
176
+ // find the first name that suppresses this check
177
+ name =
178
+ suppressible .allNames ().stream ()
179
+ .filter (suppressWarningsStrings ::contains )
180
+ .findAny ()
181
+ .get ();
182
+ }
183
+ return new Suppressed (name );
91
184
}
92
185
if (suppressible .suppressedByAnyOf (customSuppressions , state )) {
93
- return SuppressedState . SUPPRESSED ;
186
+ return new Suppressed ( null ) ;
94
187
}
95
188
96
- return SuppressedState .UNSUPPRESSED ;
189
+ return Unsuppressed .UNSUPPRESSED ;
97
190
}
98
191
99
192
/**
@@ -128,14 +221,20 @@ public Void visitClass(ClassTree node, Void unused) {
128
221
* @param sym The {@code Symbol} for the AST node currently being scanned
129
222
* @param state VisitorState for checking the current tree, as well as for getting the {@code
130
223
* SuppressWarnings symbol type}.
224
+ * @param warnOnUnneededSuppressWarningsStrings
131
225
*/
132
226
public SuppressionInfo withExtendedSuppressions (
133
- Symbol sym , VisitorState state , Set <? extends Name > customSuppressionAnnosToLookFor ) {
227
+ Symbol sym ,
228
+ VisitorState state ,
229
+ Set <? extends Name > customSuppressionAnnosToLookFor ,
230
+ Set <String > warnOnUnneededSuppressWarningsStrings ) {
134
231
boolean newInGeneratedCode = inGeneratedCode || isGenerated (sym , state );
135
232
boolean anyModification = newInGeneratedCode != inGeneratedCode ;
136
233
137
234
/* Handle custom suppression annotations. */
138
235
Set <Name > lookingFor = new HashSet <>(customSuppressionAnnosToLookFor );
236
+ // TODO what if we have nested suppressions with the same customSuppression annotation?
237
+ // Is the inner one unused? Let's just say no for now. We'll report on the outermost one.
139
238
lookingFor .removeAll (customSuppressions );
140
239
Set <Name > newlyPresent = ASTHelpers .annotationsAmong (sym , lookingFor , state );
141
240
Set <Name > newCustomSuppressions ;
@@ -151,6 +250,7 @@ public SuppressionInfo withExtendedSuppressions(
151
250
Name suppressLint = ANDROID_SUPPRESS_LINT .get (state );
152
251
Name valueName = VALUE .get (state );
153
252
Set <String > newSuppressions = null ;
253
+ Set <String > newWarnOnUnneededSuppressions = new HashSet <>();
154
254
// Iterate over annotations on this symbol, looking for SuppressWarnings
155
255
for (Attribute .Compound attr : sym .getAnnotationMirrors ()) {
156
256
if ((attr .type .tsym == state .getSymtab ().suppressWarningsType .tsym )
@@ -167,6 +267,9 @@ public SuppressionInfo withExtendedSuppressions(
167
267
newSuppressions = new HashSet <>(suppressWarningsStrings );
168
268
}
169
269
newSuppressions .add (suppressedWarning );
270
+ if (warnOnUnneededSuppressWarningsStrings .contains (suppressedWarning )) {
271
+ newWarnOnUnneededSuppressions .add (suppressedWarning );
272
+ }
170
273
}
171
274
}
172
275
} else {
@@ -187,11 +290,56 @@ public SuppressionInfo withExtendedSuppressions(
187
290
if (newSuppressions == null ) {
188
291
newSuppressions = suppressWarningsStrings ;
189
292
}
190
- return new SuppressionInfo (newSuppressions , newCustomSuppressions , newInGeneratedCode );
293
+
294
+ SuppressionInfoForSymbol newInfoForClosestSymbol =
295
+ new SuppressionInfoForSymbol (
296
+ sym ,
297
+ ImmutableSet .copyOf (newWarnOnUnneededSuppressions ),
298
+ ImmutableSet .copyOf (newlyPresent ),
299
+ infoForClosestSymbol );
300
+ return new SuppressionInfo (
301
+ newSuppressions , newCustomSuppressions , newInGeneratedCode , newInfoForClosestSymbol );
191
302
}
192
303
193
- public enum SuppressedState {
194
- UNSUPPRESSED ,
195
- SUPPRESSED
304
+ public sealed interface SuppressedState permits Suppressed , Unsuppressed {
305
+ boolean isSuppressed ();
306
+ }
307
+
308
+ public static final class Unsuppressed implements SuppressedState {
309
+ public static final Unsuppressed UNSUPPRESSED = new Unsuppressed ();
310
+
311
+ private Unsuppressed () {}
312
+
313
+ @ Override
314
+ public boolean isSuppressed () {
315
+ return false ;
316
+ }
317
+ }
318
+
319
+ public static final class Suppressed implements SuppressedState {
320
+ private final @ Nullable String suppressionName ;
321
+
322
+ private boolean used = false ;
323
+
324
+ public Suppressed (@ Nullable String suppressionName ) {
325
+ this .suppressionName = suppressionName ;
326
+ }
327
+
328
+ public @ Nullable String getSuppressionName () {
329
+ return suppressionName ;
330
+ }
331
+
332
+ public boolean isUsed () {
333
+ return used ;
334
+ }
335
+
336
+ public void setAsUsed () {
337
+ this .used = true ;
338
+ }
339
+
340
+ @ Override
341
+ public boolean isSuppressed () {
342
+ return true ;
343
+ }
196
344
}
197
345
}
0 commit comments