• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) 2019 Uber Technologies, Inc.
3  *
4  * Permission is hereby granted, free of charge, to any person obtaining a copy
5  * of this software and associated documentation files (the "Software"), to deal
6  * in the Software without restriction, including without limitation the rights
7  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8  * copies of the Software, and to permit persons to whom the Software is
9  * furnished to do so, subject to the following conditions:
10  *
11  * The above copyright notice and this permission notice shall be included in
12  * all copies or substantial portions of the Software.
13  *
14  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20  * THE SOFTWARE.
21  */
22 
23 package com.uber.nullaway;
24 
25 import static com.uber.nullaway.ErrorMessage.MessageTypes.FIELD_NO_INIT;
26 import static com.uber.nullaway.ErrorMessage.MessageTypes.GET_ON_EMPTY_OPTIONAL;
27 import static com.uber.nullaway.ErrorMessage.MessageTypes.METHOD_NO_INIT;
28 import static com.uber.nullaway.ErrorMessage.MessageTypes.NONNULL_FIELD_READ_BEFORE_INIT;
29 import static com.uber.nullaway.NullAway.CORE_CHECK_NAME;
30 import static com.uber.nullaway.NullAway.INITIALIZATION_CHECK_NAME;
31 import static com.uber.nullaway.NullAway.OPTIONAL_CHECK_NAME;
32 import static com.uber.nullaway.NullAway.getTreesInstance;
33 
34 import com.google.common.base.Joiner;
35 import com.google.common.collect.ImmutableSet;
36 import com.google.common.collect.Iterables;
37 import com.google.common.collect.Lists;
38 import com.google.errorprone.VisitorState;
39 import com.google.errorprone.fixes.SuggestedFix;
40 import com.google.errorprone.matchers.Description;
41 import com.google.errorprone.util.ASTHelpers;
42 import com.sun.source.tree.AnnotationTree;
43 import com.sun.source.tree.ClassTree;
44 import com.sun.source.tree.MethodInvocationTree;
45 import com.sun.source.tree.MethodTree;
46 import com.sun.source.tree.ModifiersTree;
47 import com.sun.source.tree.Tree;
48 import com.sun.source.tree.VariableTree;
49 import com.sun.source.util.TreePath;
50 import com.sun.tools.javac.code.Symbol;
51 import com.sun.tools.javac.tree.JCTree.JCCompilationUnit;
52 import com.sun.tools.javac.util.DiagnosticSource;
53 import com.sun.tools.javac.util.JCDiagnostic.DiagnosticPosition;
54 import java.util.Iterator;
55 import java.util.List;
56 import java.util.Set;
57 import java.util.stream.StreamSupport;
58 import javax.annotation.Nullable;
59 import javax.lang.model.element.Element;
60 import javax.tools.JavaFileObject;
61 
62 /** A class to construct error message to be displayed after the analysis finds error. */
63 public class ErrorBuilder {
64 
65   private final Config config;
66 
67   /** Checker name that can be used to suppress the warnings. */
68   private final String suppressionName;
69 
70   /** Additional identifiers for this check, to be checked for in @SuppressWarnings annotations. */
71   private final Set<String> allNames;
72 
ErrorBuilder(Config config, String suppressionName, Set<String> allNames)73   ErrorBuilder(Config config, String suppressionName, Set<String> allNames) {
74     this.config = config;
75     this.suppressionName = suppressionName;
76     this.allNames = allNames;
77   }
78 
79   /**
80    * create an error description for a nullability warning
81    *
82    * @param errorMessage the error message object.
83    * @param descriptionBuilder the description builder for the error.
84    * @param state the visitor state (used for e.g. suppression finding).
85    * @return the error description
86    */
createErrorDescription( ErrorMessage errorMessage, Description.Builder descriptionBuilder, VisitorState state)87   Description createErrorDescription(
88       ErrorMessage errorMessage, Description.Builder descriptionBuilder, VisitorState state) {
89     Tree enclosingSuppressTree = suppressibleNode(state.getPath());
90     return createErrorDescription(errorMessage, enclosingSuppressTree, descriptionBuilder, state);
91   }
92 
93   /**
94    * create an error description for a nullability warning
95    *
96    * @param errorMessage the error message object.
97    * @param suggestTree the location at which a fix suggestion should be made
98    * @param descriptionBuilder the description builder for the error.
99    * @param state the visitor state (used for e.g. suppression finding).
100    * @return the error description
101    */
createErrorDescription( ErrorMessage errorMessage, @Nullable Tree suggestTree, Description.Builder descriptionBuilder, VisitorState state)102   public Description createErrorDescription(
103       ErrorMessage errorMessage,
104       @Nullable Tree suggestTree,
105       Description.Builder descriptionBuilder,
106       VisitorState state) {
107     Description.Builder builder = descriptionBuilder.setMessage(errorMessage.message);
108     String checkName = CORE_CHECK_NAME;
109     if (errorMessage.messageType.equals(GET_ON_EMPTY_OPTIONAL)) {
110       checkName = OPTIONAL_CHECK_NAME;
111     } else if (errorMessage.messageType.equals(FIELD_NO_INIT)
112         || errorMessage.messageType.equals(METHOD_NO_INIT)
113         || errorMessage.messageType.equals(NONNULL_FIELD_READ_BEFORE_INIT)) {
114       checkName = INITIALIZATION_CHECK_NAME;
115     }
116 
117     // Mildly expensive state.getPath() traversal, occurs only once per potentially
118     // reported error.
119     if (hasPathSuppression(state.getPath(), checkName)) {
120       return Description.NO_MATCH;
121     }
122 
123     if (config.suggestSuppressions() && suggestTree != null) {
124       builder = addSuggestedSuppression(errorMessage, suggestTree, builder);
125     }
126     // #letbuildersbuild
127     return builder.build();
128   }
129 
canHaveSuppressWarningsAnnotation(Tree tree)130   private static boolean canHaveSuppressWarningsAnnotation(Tree tree) {
131     return tree instanceof MethodTree
132         || (tree instanceof ClassTree && ((ClassTree) tree).getSimpleName().length() != 0)
133         || tree instanceof VariableTree;
134   }
135 
136   /**
137    * Find out if a particular subchecker (e.g. NullAway.Optional) is being suppressed in a given
138    * path.
139    *
140    * <p>This requires a tree path traversal, which is expensive, but we only do this when we would
141    * otherwise report an error, which means this won't happen for most nodes/files.
142    *
143    * @param treePath The path with the error location as the leaf.
144    * @param subcheckerName The string to check for inside @SuppressWarnings
145    * @return Whether the subchecker is being suppressed at treePath.
146    */
hasPathSuppression(TreePath treePath, String subcheckerName)147   private boolean hasPathSuppression(TreePath treePath, String subcheckerName) {
148     return StreamSupport.stream(treePath.spliterator(), false)
149         .filter(ErrorBuilder::canHaveSuppressWarningsAnnotation)
150         .map(tree -> ASTHelpers.getSymbol(tree))
151         .filter(symbol -> symbol != null)
152         .anyMatch(
153             symbol ->
154                 symbolHasSuppressWarningsAnnotation(symbol, subcheckerName)
155                     || symbolIsExcludedClassSymbol(symbol));
156   }
157 
addSuggestedSuppression( ErrorMessage errorMessage, Tree suggestTree, Description.Builder builder)158   private Description.Builder addSuggestedSuppression(
159       ErrorMessage errorMessage, Tree suggestTree, Description.Builder builder) {
160     switch (errorMessage.messageType) {
161       case DEREFERENCE_NULLABLE:
162       case RETURN_NULLABLE:
163       case PASS_NULLABLE:
164       case ASSIGN_FIELD_NULLABLE:
165       case SWITCH_EXPRESSION_NULLABLE:
166         if (config.getCastToNonNullMethod() != null) {
167           builder = addCastToNonNullFix(suggestTree, builder);
168         } else {
169           builder = addSuppressWarningsFix(suggestTree, builder, suppressionName);
170         }
171         break;
172       case CAST_TO_NONNULL_ARG_NONNULL:
173         builder = removeCastToNonNullFix(suggestTree, builder);
174         break;
175       case WRONG_OVERRIDE_RETURN:
176         builder = addSuppressWarningsFix(suggestTree, builder, suppressionName);
177         break;
178       case WRONG_OVERRIDE_PARAM:
179         builder = addSuppressWarningsFix(suggestTree, builder, suppressionName);
180         break;
181       case METHOD_NO_INIT:
182       case FIELD_NO_INIT:
183         builder = addSuppressWarningsFix(suggestTree, builder, INITIALIZATION_CHECK_NAME);
184         break;
185       case ANNOTATION_VALUE_INVALID:
186         break;
187       default:
188         builder = addSuppressWarningsFix(suggestTree, builder, suppressionName);
189     }
190     return builder;
191   }
192 
193   /**
194    * create an error description for a generalized @Nullable value to @NonNull location assignment.
195    *
196    * <p>This includes: field assignments, method arguments and method returns
197    *
198    * @param errorMessage the error message object.
199    * @param suggestTreeIfCastToNonNull the location at which a fix suggestion should be made if a
200    *     castToNonNull method is available (usually the expression to cast)
201    * @param descriptionBuilder the description builder for the error.
202    * @param state the visitor state for the location which triggered the error (i.e. for suppression
203    *     finding)
204    * @return the error description.
205    */
createErrorDescriptionForNullAssignment( ErrorMessage errorMessage, @Nullable Tree suggestTreeIfCastToNonNull, Description.Builder descriptionBuilder, VisitorState state)206   Description createErrorDescriptionForNullAssignment(
207       ErrorMessage errorMessage,
208       @Nullable Tree suggestTreeIfCastToNonNull,
209       Description.Builder descriptionBuilder,
210       VisitorState state) {
211     if (config.getCastToNonNullMethod() != null) {
212       return createErrorDescription(
213           errorMessage, suggestTreeIfCastToNonNull, descriptionBuilder, state);
214     } else {
215       return createErrorDescription(
216           errorMessage, suppressibleNode(state.getPath()), descriptionBuilder, state);
217     }
218   }
219 
addSuppressWarningsFix( Tree suggestTree, Description.Builder builder, String suppressionName)220   Description.Builder addSuppressWarningsFix(
221       Tree suggestTree, Description.Builder builder, String suppressionName) {
222     SuppressWarnings extantSuppressWarnings = null;
223     Symbol treeSymbol = ASTHelpers.getSymbol(suggestTree);
224     if (treeSymbol != null) {
225       extantSuppressWarnings = treeSymbol.getAnnotation(SuppressWarnings.class);
226     }
227     SuggestedFix fix;
228     if (extantSuppressWarnings == null) {
229       fix =
230           SuggestedFix.prefixWith(
231               suggestTree,
232               "@SuppressWarnings(\""
233                   + suppressionName
234                   + "\") "
235                   + config.getAutofixSuppressionComment());
236     } else {
237       // need to update the existing list of warnings
238       final List<String> suppressions = Lists.newArrayList(extantSuppressWarnings.value());
239       suppressions.add(suppressionName);
240       // find the existing annotation, so we can replace it
241       final ModifiersTree modifiers =
242           (suggestTree instanceof MethodTree)
243               ? ((MethodTree) suggestTree).getModifiers()
244               : ((VariableTree) suggestTree).getModifiers();
245       final List<? extends AnnotationTree> annotations = modifiers.getAnnotations();
246       // noinspection ConstantConditions
247       com.google.common.base.Optional<? extends AnnotationTree> suppressWarningsAnnot =
248           Iterables.tryFind(
249               annotations,
250               annot -> annot.getAnnotationType().toString().endsWith("SuppressWarnings"));
251       if (!suppressWarningsAnnot.isPresent()) {
252         throw new AssertionError("something went horribly wrong");
253       }
254       final String replacement =
255           "@SuppressWarnings({"
256               + Joiner.on(',').join(Iterables.transform(suppressions, s -> '"' + s + '"'))
257               + "}) "
258               + config.getAutofixSuppressionComment();
259       fix = SuggestedFix.replace(suppressWarningsAnnot.get(), replacement);
260     }
261     return builder.addFix(fix);
262   }
263 
264   /**
265    * Adapted from {@link com.google.errorprone.fixes.SuggestedFixes}.
266    *
267    * <p>TODO: actually use {@link
268    * com.google.errorprone.fixes.SuggestedFixes#addSuppressWarnings(VisitorState, String)} instead
269    */
270   @Nullable
suppressibleNode(@ullable TreePath path)271   private Tree suppressibleNode(@Nullable TreePath path) {
272     if (path == null) {
273       return null;
274     }
275     return StreamSupport.stream(path.spliterator(), false)
276         .filter(ErrorBuilder::canHaveSuppressWarningsAnnotation)
277         .findFirst()
278         .orElse(null);
279   }
280 
addCastToNonNullFix(Tree suggestTree, Description.Builder builder)281   private Description.Builder addCastToNonNullFix(Tree suggestTree, Description.Builder builder) {
282     final String fullMethodName = config.getCastToNonNullMethod();
283     assert fullMethodName != null;
284     // Add a call to castToNonNull around suggestTree:
285     final String[] parts = fullMethodName.split("\\.");
286     final String shortMethodName = parts[parts.length - 1];
287     final String replacement = shortMethodName + "(" + suggestTree.toString() + ")";
288     final SuggestedFix fix =
289         SuggestedFix.builder()
290             .replace(suggestTree, replacement)
291             .addStaticImport(fullMethodName) // ensure castToNonNull static import
292             .build();
293     return builder.addFix(fix);
294   }
295 
removeCastToNonNullFix( Tree suggestTree, Description.Builder builder)296   private Description.Builder removeCastToNonNullFix(
297       Tree suggestTree, Description.Builder builder) {
298     assert suggestTree.getKind() == Tree.Kind.METHOD_INVOCATION;
299     final MethodInvocationTree invTree = (MethodInvocationTree) suggestTree;
300     final Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(invTree);
301     final String qualifiedName =
302         ASTHelpers.enclosingClass(methodSymbol) + "." + methodSymbol.getSimpleName().toString();
303     if (!qualifiedName.equals(config.getCastToNonNullMethod())) {
304       throw new RuntimeException("suggestTree should point to the castToNonNull invocation.");
305     }
306     // Remove the call to castToNonNull:
307     final SuggestedFix fix =
308         SuggestedFix.builder()
309             .replace(suggestTree, invTree.getArguments().get(0).toString())
310             .build();
311     return builder.addFix(fix);
312   }
313 
reportInitializerError( Symbol.MethodSymbol methodSymbol, String message, VisitorState state, Description.Builder descriptionBuilder)314   void reportInitializerError(
315       Symbol.MethodSymbol methodSymbol,
316       String message,
317       VisitorState state,
318       Description.Builder descriptionBuilder) {
319     // Check needed here, despite check in hasPathSuppression because initialization
320     // checking happens at the class-level (meaning state.getPath() might not include the
321     // method itself).
322     if (symbolHasSuppressWarningsAnnotation(methodSymbol, INITIALIZATION_CHECK_NAME)) {
323       return;
324     }
325     Tree methodTree = getTreesInstance(state).getTree(methodSymbol);
326     state.reportMatch(
327         createErrorDescription(
328             new ErrorMessage(METHOD_NO_INIT, message), methodTree, descriptionBuilder, state));
329   }
330 
symbolHasSuppressWarningsAnnotation(Symbol symbol, String suppression)331   boolean symbolHasSuppressWarningsAnnotation(Symbol symbol, String suppression) {
332     SuppressWarnings annotation = symbol.getAnnotation(SuppressWarnings.class);
333     if (annotation != null) {
334       for (String s : annotation.value()) {
335         // we need to check for standard suppression here also since we may report initialization
336         // errors outside the normal ErrorProne match* methods
337         if (s.equals(suppression) || allNames.stream().anyMatch(s::equals)) {
338           return true;
339         }
340       }
341     }
342     return false;
343   }
344 
symbolIsExcludedClassSymbol(Symbol symbol)345   private boolean symbolIsExcludedClassSymbol(Symbol symbol) {
346     if (symbol instanceof Symbol.ClassSymbol) {
347       ImmutableSet<String> excludedClassAnnotations = config.getExcludedClassAnnotations();
348       return ((Symbol.ClassSymbol) symbol)
349           .getAnnotationMirrors()
350           .stream()
351           .map(anno -> anno.getAnnotationType().toString())
352           .anyMatch(excludedClassAnnotations::contains);
353     }
354     return false;
355   }
356 
getLineNumForElement(Element uninitField, VisitorState state)357   static int getLineNumForElement(Element uninitField, VisitorState state) {
358     Tree tree = getTreesInstance(state).getTree(uninitField);
359     if (tree == null) {
360       throw new RuntimeException(
361           "When getting the line number for uninitialized field, can't get the tree from the element.");
362     }
363     DiagnosticPosition position =
364         (DiagnosticPosition) tree; // Expect Tree to be JCTree and thus implement DiagnosticPosition
365     TreePath path = state.getPath();
366     JCCompilationUnit compilation = (JCCompilationUnit) path.getCompilationUnit();
367     JavaFileObject file = compilation.getSourceFile();
368     DiagnosticSource source = new DiagnosticSource(file, null);
369     return source.getLineNumber(position.getStartPosition());
370   }
371 
372   /**
373    * Generate the message for uninitialized fields, including the line number for fields.
374    *
375    * @param uninitFields the set of uninitialized fields as the type of Element.
376    * @param state the VisitorState object.
377    * @return the error message for uninitialized fields with line numbers.
378    */
errMsgForInitializer(Set<Element> uninitFields, VisitorState state)379   static String errMsgForInitializer(Set<Element> uninitFields, VisitorState state) {
380     StringBuilder message = new StringBuilder("initializer method does not guarantee @NonNull ");
381     Element uninitField;
382     if (uninitFields.size() == 1) {
383       uninitField = uninitFields.iterator().next();
384       message.append("field ");
385       message.append(uninitField.toString());
386       message.append(" (line ");
387       message.append(getLineNumForElement(uninitField, state));
388       message.append(") is initialized");
389     } else {
390       message.append("fields ");
391       Iterator<Element> it = uninitFields.iterator();
392       while (it.hasNext()) {
393         uninitField = it.next();
394         message.append(
395             uninitField.toString() + " (line " + getLineNumForElement(uninitField, state) + ")");
396         if (it.hasNext()) {
397           message.append(", ");
398         } else {
399           message.append(" are initialized");
400         }
401       }
402     }
403     message.append(
404         " along all control-flow paths (remember to check for exceptions or early returns).");
405     return message.toString();
406   }
407 
reportInitErrorOnField(Symbol symbol, VisitorState state, Description.Builder builder)408   void reportInitErrorOnField(Symbol symbol, VisitorState state, Description.Builder builder) {
409     // Check needed here, despite check in hasPathSuppression because initialization
410     // checking happens at the class-level (meaning state.getPath() might not include the
411     // field itself).
412     if (symbolHasSuppressWarningsAnnotation(symbol, INITIALIZATION_CHECK_NAME)) {
413       return;
414     }
415     Tree tree = getTreesInstance(state).getTree(symbol);
416 
417     String fieldName = symbol.toString();
418 
419     if (symbol.enclClass().getNestingKind().isNested()) {
420       String flatName = symbol.enclClass().flatName().toString();
421       int index = flatName.lastIndexOf(".") + 1;
422       fieldName = flatName.substring(index) + "." + fieldName;
423     }
424 
425     if (symbol.isStatic()) {
426       state.reportMatch(
427           createErrorDescription(
428               new ErrorMessage(
429                   FIELD_NO_INIT, "@NonNull static field " + fieldName + " not initialized"),
430               tree,
431               builder,
432               state));
433     } else {
434       state.reportMatch(
435           createErrorDescription(
436               new ErrorMessage(FIELD_NO_INIT, "@NonNull field " + fieldName + " not initialized"),
437               tree,
438               builder,
439               state));
440     }
441   }
442 }
443