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