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.ASTHelpersBackports.isStatic; 26 import static com.uber.nullaway.ErrorMessage.MessageTypes.FIELD_NO_INIT; 27 import static com.uber.nullaway.ErrorMessage.MessageTypes.GET_ON_EMPTY_OPTIONAL; 28 import static com.uber.nullaway.ErrorMessage.MessageTypes.METHOD_NO_INIT; 29 import static com.uber.nullaway.ErrorMessage.MessageTypes.NONNULL_FIELD_READ_BEFORE_INIT; 30 import static com.uber.nullaway.NullAway.CORE_CHECK_NAME; 31 import static com.uber.nullaway.NullAway.INITIALIZATION_CHECK_NAME; 32 import static com.uber.nullaway.NullAway.OPTIONAL_CHECK_NAME; 33 import static com.uber.nullaway.NullAway.getTreesInstance; 34 import static com.uber.nullaway.Nullness.hasNullableAnnotation; 35 36 import com.google.common.base.Joiner; 37 import com.google.common.base.Preconditions; 38 import com.google.common.collect.ImmutableList; 39 import com.google.common.collect.ImmutableSet; 40 import com.google.common.collect.Iterables; 41 import com.google.common.collect.Lists; 42 import com.google.errorprone.VisitorState; 43 import com.google.errorprone.fixes.SuggestedFix; 44 import com.google.errorprone.matchers.Description; 45 import com.google.errorprone.util.ASTHelpers; 46 import com.sun.source.tree.AnnotationTree; 47 import com.sun.source.tree.ClassTree; 48 import com.sun.source.tree.MethodInvocationTree; 49 import com.sun.source.tree.MethodTree; 50 import com.sun.source.tree.ModifiersTree; 51 import com.sun.source.tree.Tree; 52 import com.sun.source.tree.VariableTree; 53 import com.sun.source.util.TreePath; 54 import com.sun.tools.javac.code.Symbol; 55 import com.sun.tools.javac.tree.JCTree.JCCompilationUnit; 56 import com.sun.tools.javac.util.DiagnosticSource; 57 import com.sun.tools.javac.util.JCDiagnostic.DiagnosticPosition; 58 import com.uber.nullaway.fixserialization.SerializationService; 59 import java.util.Iterator; 60 import java.util.List; 61 import java.util.Set; 62 import java.util.stream.StreamSupport; 63 import javax.annotation.Nullable; 64 import javax.lang.model.element.Element; 65 import javax.lang.model.element.ElementKind; 66 import javax.tools.JavaFileObject; 67 68 /** A class to construct error message to be displayed after the analysis finds error. */ 69 public class ErrorBuilder { 70 71 private final Config config; 72 73 /** Checker name that can be used to suppress the warnings. */ 74 private final String suppressionName; 75 76 /** Additional identifiers for this check, to be checked for in @SuppressWarnings annotations. */ 77 private final Set<String> allNames; 78 ErrorBuilder(Config config, String suppressionName, Set<String> allNames)79 ErrorBuilder(Config config, String suppressionName, Set<String> allNames) { 80 this.config = config; 81 this.suppressionName = suppressionName; 82 this.allNames = allNames; 83 } 84 85 /** 86 * create an error description for a nullability warning 87 * 88 * @param errorMessage the error message object. 89 * @param descriptionBuilder the description builder for the error. 90 * @param state the visitor state (used for e.g. suppression finding). 91 * @param nonNullTarget if non-null, this error involved a pseudo-assignment of a @Nullable 92 * expression into a @NonNull target, and this parameter is the Symbol for that target. 93 * @return the error description 94 */ createErrorDescription( ErrorMessage errorMessage, Description.Builder descriptionBuilder, VisitorState state, @Nullable Symbol nonNullTarget)95 public Description createErrorDescription( 96 ErrorMessage errorMessage, 97 Description.Builder descriptionBuilder, 98 VisitorState state, 99 @Nullable Symbol nonNullTarget) { 100 Tree enclosingSuppressTree = suppressibleNode(state.getPath()); 101 return createErrorDescription( 102 errorMessage, enclosingSuppressTree, descriptionBuilder, state, nonNullTarget); 103 } 104 105 /** 106 * create an error description for a nullability warning 107 * 108 * @param errorMessage the error message object. 109 * @param suggestTree the location at which a fix suggestion should be made 110 * @param descriptionBuilder the description builder for the error. 111 * @param state the visitor state (used for e.g. suppression finding). 112 * @param nonNullTarget if non-null, this error involved a pseudo-assignment of a @Nullable 113 * expression into a @NonNull target, and this parameter is the Symbol for that target. 114 * @return the error description 115 */ createErrorDescription( ErrorMessage errorMessage, @Nullable Tree suggestTree, Description.Builder descriptionBuilder, VisitorState state, @Nullable Symbol nonNullTarget)116 public Description createErrorDescription( 117 ErrorMessage errorMessage, 118 @Nullable Tree suggestTree, 119 Description.Builder descriptionBuilder, 120 VisitorState state, 121 @Nullable Symbol nonNullTarget) { 122 Description.Builder builder = descriptionBuilder.setMessage(errorMessage.message); 123 String checkName = CORE_CHECK_NAME; 124 if (errorMessage.messageType.equals(GET_ON_EMPTY_OPTIONAL)) { 125 checkName = OPTIONAL_CHECK_NAME; 126 } else if (errorMessage.messageType.equals(FIELD_NO_INIT) 127 || errorMessage.messageType.equals(METHOD_NO_INIT) 128 || errorMessage.messageType.equals(NONNULL_FIELD_READ_BEFORE_INIT)) { 129 checkName = INITIALIZATION_CHECK_NAME; 130 } 131 132 // Mildly expensive state.getPath() traversal, occurs only once per potentially 133 // reported error. 134 if (hasPathSuppression(state.getPath(), checkName)) { 135 return Description.NO_MATCH; 136 } 137 138 if (config.suggestSuppressions() && suggestTree != null) { 139 builder = addSuggestedSuppression(errorMessage, suggestTree, builder, state); 140 } 141 142 if (config.serializationIsActive()) { 143 if (nonNullTarget != null) { 144 SerializationService.serializeFixSuggestion(config, state, nonNullTarget, errorMessage); 145 } 146 // For the case of initializer errors, the leaf of state.getPath() may not be the field / 147 // method on which the error is being reported (since we do a class-wide analysis to find such 148 // errors). In such cases, the suggestTree is the appropriate field / method tree, so use 149 // that as the errorTree for serialization. 150 Tree errorTree = 151 (suggestTree != null 152 && (errorMessage.messageType.equals(FIELD_NO_INIT) 153 || errorMessage.messageType.equals(METHOD_NO_INIT))) 154 ? suggestTree 155 : state.getPath().getLeaf(); 156 SerializationService.serializeReportingError( 157 config, state, errorTree, nonNullTarget, errorMessage); 158 } 159 160 // #letbuildersbuild 161 return builder.build(); 162 } 163 canHaveSuppressWarningsAnnotation(Tree tree)164 private static boolean canHaveSuppressWarningsAnnotation(Tree tree) { 165 return tree instanceof MethodTree 166 || (tree instanceof ClassTree && ((ClassTree) tree).getSimpleName().length() != 0) 167 || tree instanceof VariableTree; 168 } 169 170 /** 171 * Find out if a particular subchecker (e.g. NullAway.Optional) is being suppressed in a given 172 * path. 173 * 174 * <p>This requires a tree path traversal, which is expensive, but we only do this when we would 175 * otherwise report an error, which means this won't happen for most nodes/files. 176 * 177 * @param treePath The path with the error location as the leaf. 178 * @param subcheckerName The string to check for inside @SuppressWarnings 179 * @return Whether the subchecker is being suppressed at treePath. 180 */ hasPathSuppression(TreePath treePath, String subcheckerName)181 private boolean hasPathSuppression(TreePath treePath, String subcheckerName) { 182 return StreamSupport.stream(treePath.spliterator(), false) 183 .filter(ErrorBuilder::canHaveSuppressWarningsAnnotation) 184 .map(tree -> ASTHelpers.getSymbol(tree)) 185 .filter(symbol -> symbol != null) 186 .anyMatch( 187 symbol -> 188 symbolHasSuppressWarningsAnnotation(symbol, subcheckerName) 189 || symbolIsExcludedClassSymbol(symbol)); 190 } 191 addSuggestedSuppression( ErrorMessage errorMessage, Tree suggestTree, Description.Builder builder, VisitorState state)192 private Description.Builder addSuggestedSuppression( 193 ErrorMessage errorMessage, 194 Tree suggestTree, 195 Description.Builder builder, 196 VisitorState state) { 197 switch (errorMessage.messageType) { 198 case DEREFERENCE_NULLABLE: 199 case RETURN_NULLABLE: 200 case PASS_NULLABLE: 201 case ASSIGN_FIELD_NULLABLE: 202 case SWITCH_EXPRESSION_NULLABLE: 203 if (config.getCastToNonNullMethod() != null && canBeCastToNonNull(suggestTree)) { 204 builder = addCastToNonNullFix(suggestTree, builder, state); 205 } else { 206 // When there is a castToNonNull method, suggestTree is set to the expression to be 207 // casted, which is not suppressible. For simplicity, we just always recompute the 208 // suppressible node here. 209 Tree suppressibleNode = suppressibleNode(state.getPath()); 210 if (suppressibleNode != null) { 211 builder = addSuppressWarningsFix(suppressibleNode, builder, suppressionName); 212 } 213 } 214 break; 215 case CAST_TO_NONNULL_ARG_NONNULL: 216 builder = removeCastToNonNullFix(suggestTree, builder, state); 217 break; 218 case WRONG_OVERRIDE_RETURN: 219 builder = addSuppressWarningsFix(suggestTree, builder, suppressionName); 220 break; 221 case WRONG_OVERRIDE_PARAM: 222 builder = addSuppressWarningsFix(suggestTree, builder, suppressionName); 223 break; 224 case METHOD_NO_INIT: 225 case FIELD_NO_INIT: 226 builder = addSuppressWarningsFix(suggestTree, builder, INITIALIZATION_CHECK_NAME); 227 break; 228 case ANNOTATION_VALUE_INVALID: 229 break; 230 default: 231 builder = addSuppressWarningsFix(suggestTree, builder, suppressionName); 232 } 233 return builder; 234 } 235 236 /** 237 * create an error description for a generalized @Nullable value to @NonNull location assignment. 238 * 239 * <p>This includes: field assignments, method arguments and method returns 240 * 241 * @param errorMessage the error message object. 242 * @param suggestTreeIfCastToNonNull the location at which a fix suggestion should be made if a 243 * castToNonNull method is available (usually the expression to cast) 244 * @param descriptionBuilder the description builder for the error. 245 * @param state the visitor state for the location which triggered the error (i.e. for suppression 246 * finding) 247 * @param nonNullTarget if non-null, this error involved a pseudo-assignment of a @Nullable 248 * expression into a @NonNull target, and this parameter is the Symbol for that target. 249 * @return the error description. 250 */ createErrorDescriptionForNullAssignment( ErrorMessage errorMessage, @Nullable Tree suggestTreeIfCastToNonNull, Description.Builder descriptionBuilder, VisitorState state, @Nullable Symbol nonNullTarget)251 Description createErrorDescriptionForNullAssignment( 252 ErrorMessage errorMessage, 253 @Nullable Tree suggestTreeIfCastToNonNull, 254 Description.Builder descriptionBuilder, 255 VisitorState state, 256 @Nullable Symbol nonNullTarget) { 257 if (config.getCastToNonNullMethod() != null) { 258 return createErrorDescription( 259 errorMessage, suggestTreeIfCastToNonNull, descriptionBuilder, state, nonNullTarget); 260 } else { 261 return createErrorDescription( 262 errorMessage, 263 suppressibleNode(state.getPath()), 264 descriptionBuilder, 265 state, 266 nonNullTarget); 267 } 268 } 269 addSuppressWarningsFix( Tree suggestTree, Description.Builder builder, String suppressionName)270 Description.Builder addSuppressWarningsFix( 271 Tree suggestTree, Description.Builder builder, String suppressionName) { 272 SuppressWarnings extantSuppressWarnings = null; 273 Symbol treeSymbol = ASTHelpers.getSymbol(suggestTree); 274 if (treeSymbol != null) { 275 extantSuppressWarnings = treeSymbol.getAnnotation(SuppressWarnings.class); 276 } 277 SuggestedFix fix; 278 if (extantSuppressWarnings == null) { 279 fix = 280 SuggestedFix.prefixWith( 281 suggestTree, 282 "@SuppressWarnings(\"" 283 + suppressionName 284 + "\") " 285 + config.getAutofixSuppressionComment()); 286 } else { 287 // need to update the existing list of warnings 288 final List<String> suppressions = Lists.newArrayList(extantSuppressWarnings.value()); 289 suppressions.add(suppressionName); 290 // find the existing annotation, so we can replace it 291 final ModifiersTree modifiers = 292 (suggestTree instanceof MethodTree) 293 ? ((MethodTree) suggestTree).getModifiers() 294 : ((VariableTree) suggestTree).getModifiers(); 295 final List<? extends AnnotationTree> annotations = modifiers.getAnnotations(); 296 // noinspection ConstantConditions 297 com.google.common.base.Optional<? extends AnnotationTree> suppressWarningsAnnot = 298 Iterables.tryFind( 299 annotations, 300 annot -> annot.getAnnotationType().toString().endsWith("SuppressWarnings")); 301 if (!suppressWarningsAnnot.isPresent()) { 302 throw new AssertionError("something went horribly wrong"); 303 } 304 final String replacement = 305 "@SuppressWarnings({" 306 + Joiner.on(',').join(Iterables.transform(suppressions, s -> '"' + s + '"')) 307 + "}) " 308 + config.getAutofixSuppressionComment(); 309 fix = SuggestedFix.replace(suppressWarningsAnnot.get(), replacement); 310 } 311 return builder.addFix(fix); 312 } 313 314 /** 315 * Adapted from {@link com.google.errorprone.fixes.SuggestedFixes}. 316 * 317 * <p>TODO: actually use {@link 318 * com.google.errorprone.fixes.SuggestedFixes#addSuppressWarnings(VisitorState, String)} instead 319 */ 320 @Nullable suppressibleNode(@ullable TreePath path)321 private Tree suppressibleNode(@Nullable TreePath path) { 322 if (path == null) { 323 return null; 324 } 325 return StreamSupport.stream(path.spliterator(), false) 326 .filter(ErrorBuilder::canHaveSuppressWarningsAnnotation) 327 .findFirst() 328 .orElse(null); 329 } 330 331 /** 332 * Checks if it would be appropriate to wrap {@code tree} in a {@code castToNonNull} call. There 333 * are two cases where this method returns {@code false}: 334 * 335 * <ol> 336 * <li>{@code tree} represents the {@code null} literal 337 * <li>{@code tree} represents a {@code @Nullable} formal parameter of the enclosing method 338 * </ol> 339 */ canBeCastToNonNull(Tree tree)340 private boolean canBeCastToNonNull(Tree tree) { 341 switch (tree.getKind()) { 342 case NULL_LITERAL: 343 // never do castToNonNull(null) 344 return false; 345 case IDENTIFIER: 346 // Don't wrap a @Nullable parameter in castToNonNull, as this misleads callers into thinking 347 // they can pass in null without causing an NPE. A more appropriate fix would likely be to 348 // make the parameter @NonNull and add casts at call sites, but that is beyond the scope of 349 // our suggested fixes 350 Symbol symbol = ASTHelpers.getSymbol(tree); 351 return !(symbol != null 352 && symbol.getKind().equals(ElementKind.PARAMETER) 353 && hasNullableAnnotation(symbol, config)); 354 default: 355 return true; 356 } 357 } 358 addCastToNonNullFix( Tree suggestTree, Description.Builder builder, VisitorState state)359 private Description.Builder addCastToNonNullFix( 360 Tree suggestTree, Description.Builder builder, VisitorState state) { 361 final String fullMethodName = config.getCastToNonNullMethod(); 362 if (fullMethodName == null) { 363 throw new IllegalStateException("cast-to-non-null method not set"); 364 } 365 // Add a call to castToNonNull around suggestTree: 366 final String[] parts = fullMethodName.split("\\."); 367 final String shortMethodName = parts[parts.length - 1]; 368 final String replacement = shortMethodName + "(" + state.getSourceForNode(suggestTree) + ")"; 369 final SuggestedFix fix = 370 SuggestedFix.builder() 371 .replace(suggestTree, replacement) 372 .addStaticImport(fullMethodName) // ensure castToNonNull static import 373 .build(); 374 return builder.addFix(fix); 375 } 376 removeCastToNonNullFix( Tree suggestTree, Description.Builder builder, VisitorState state)377 private Description.Builder removeCastToNonNullFix( 378 Tree suggestTree, Description.Builder builder, VisitorState state) { 379 // Note: Here suggestTree refers to the argument being cast. We need to find the 380 // castToNonNull(...) invocation to be replaced by it. Fortunately, state.getPath() 381 // should be currently pointing at said call. 382 Tree currTree = state.getPath().getLeaf(); 383 Preconditions.checkArgument( 384 currTree.getKind() == Tree.Kind.METHOD_INVOCATION, 385 String.format("Expected castToNonNull invocation expression, found:\n%s", currTree)); 386 final MethodInvocationTree invTree = (MethodInvocationTree) currTree; 387 Preconditions.checkArgument( 388 invTree.getArguments().contains(suggestTree), 389 String.format( 390 "Method invocation tree %s does not contain the expression %s as an argument being cast", 391 invTree, suggestTree)); 392 // Remove the call to castToNonNull: 393 final SuggestedFix fix = 394 SuggestedFix.builder().replace(invTree, state.getSourceForNode(suggestTree)).build(); 395 return builder.addFix(fix); 396 } 397 398 /** 399 * Reports initialization errors where a constructor fails to guarantee non-null fields are 400 * initialized along all paths at exit points. 401 * 402 * @param methodSymbol Constructor symbol. 403 * @param message Error message. 404 * @param state The VisitorState object. 405 * @param descriptionBuilder the description builder for the error. 406 * @param nonNullFields list of @Nonnull fields that are not guaranteed to be initialized along 407 * all paths at exit points of the constructor. 408 */ reportInitializerError( Symbol.MethodSymbol methodSymbol, String message, VisitorState state, Description.Builder descriptionBuilder, ImmutableList<Symbol> nonNullFields)409 void reportInitializerError( 410 Symbol.MethodSymbol methodSymbol, 411 String message, 412 VisitorState state, 413 Description.Builder descriptionBuilder, 414 ImmutableList<Symbol> nonNullFields) { 415 // Check needed here, despite check in hasPathSuppression because initialization 416 // checking happens at the class-level (meaning state.getPath() might not include the 417 // method itself). 418 if (symbolHasSuppressWarningsAnnotation(methodSymbol, INITIALIZATION_CHECK_NAME)) { 419 return; 420 } 421 Tree methodTree = getTreesInstance(state).getTree(methodSymbol); 422 ErrorMessage errorMessage = new ErrorMessage(METHOD_NO_INIT, message); 423 state.reportMatch( 424 createErrorDescription(errorMessage, methodTree, descriptionBuilder, state, null)); 425 if (config.serializationIsActive()) { 426 // For now, we serialize each fix suggestion separately and measure their effectiveness 427 // separately 428 nonNullFields.forEach( 429 symbol -> 430 SerializationService.serializeFixSuggestion(config, state, symbol, errorMessage)); 431 } 432 } 433 symbolHasSuppressWarningsAnnotation(Symbol symbol, String suppression)434 boolean symbolHasSuppressWarningsAnnotation(Symbol symbol, String suppression) { 435 SuppressWarnings annotation = symbol.getAnnotation(SuppressWarnings.class); 436 if (annotation != null) { 437 for (String s : annotation.value()) { 438 // we need to check for standard suppression here also since we may report initialization 439 // errors outside the normal ErrorProne match* methods 440 if (s.equals(suppression) || allNames.stream().anyMatch(s::equals)) { 441 return true; 442 } 443 } 444 } 445 return false; 446 } 447 symbolIsExcludedClassSymbol(Symbol symbol)448 private boolean symbolIsExcludedClassSymbol(Symbol symbol) { 449 if (symbol instanceof Symbol.ClassSymbol) { 450 ImmutableSet<String> excludedClassAnnotations = config.getExcludedClassAnnotations(); 451 return ((Symbol.ClassSymbol) symbol) 452 .getAnnotationMirrors().stream() 453 .map(anno -> anno.getAnnotationType().toString()) 454 .anyMatch(excludedClassAnnotations::contains); 455 } 456 return false; 457 } 458 getLineNumForElement(Element uninitField, VisitorState state)459 static int getLineNumForElement(Element uninitField, VisitorState state) { 460 Tree tree = getTreesInstance(state).getTree(uninitField); 461 if (tree == null) { 462 throw new RuntimeException( 463 "When getting the line number for uninitialized field, can't get the tree from the element."); 464 } 465 DiagnosticPosition position = 466 (DiagnosticPosition) tree; // Expect Tree to be JCTree and thus implement DiagnosticPosition 467 TreePath path = state.getPath(); 468 JCCompilationUnit compilation = (JCCompilationUnit) path.getCompilationUnit(); 469 JavaFileObject file = compilation.getSourceFile(); 470 DiagnosticSource source = new DiagnosticSource(file, null); 471 return source.getLineNumber(position.getStartPosition()); 472 } 473 474 /** 475 * Generate the message for uninitialized fields, including the line number for fields. 476 * 477 * @param uninitFields the set of uninitialized fields as the type of Element. 478 * @param state the VisitorState object. 479 * @return the error message for uninitialized fields with line numbers. 480 */ errMsgForInitializer(Set<Element> uninitFields, VisitorState state)481 static String errMsgForInitializer(Set<Element> uninitFields, VisitorState state) { 482 StringBuilder message = new StringBuilder("initializer method does not guarantee @NonNull "); 483 Element uninitField; 484 if (uninitFields.size() == 1) { 485 uninitField = uninitFields.iterator().next(); 486 message.append("field "); 487 message.append(uninitField.toString()); 488 message.append(" (line "); 489 message.append(getLineNumForElement(uninitField, state)); 490 message.append(") is initialized"); 491 } else { 492 message.append("fields "); 493 Iterator<Element> it = uninitFields.iterator(); 494 while (it.hasNext()) { 495 uninitField = it.next(); 496 message.append( 497 uninitField.toString() + " (line " + getLineNumForElement(uninitField, state) + ")"); 498 if (it.hasNext()) { 499 message.append(", "); 500 } else { 501 message.append(" are initialized"); 502 } 503 } 504 } 505 message.append( 506 " along all control-flow paths (remember to check for exceptions or early returns)."); 507 return message.toString(); 508 } 509 reportInitErrorOnField(Symbol symbol, VisitorState state, Description.Builder builder)510 void reportInitErrorOnField(Symbol symbol, VisitorState state, Description.Builder builder) { 511 // Check needed here, despite check in hasPathSuppression because initialization 512 // checking happens at the class-level (meaning state.getPath() might not include the 513 // field itself). 514 if (symbolHasSuppressWarningsAnnotation(symbol, INITIALIZATION_CHECK_NAME)) { 515 return; 516 } 517 Tree tree = getTreesInstance(state).getTree(symbol); 518 519 String fieldName = symbol.toString(); 520 521 if (symbol.enclClass().getNestingKind().isNested()) { 522 String flatName = symbol.enclClass().flatName().toString(); 523 int index = flatName.lastIndexOf(".") + 1; 524 fieldName = flatName.substring(index) + "." + fieldName; 525 } 526 527 if (isStatic(symbol)) { 528 state.reportMatch( 529 createErrorDescription( 530 new ErrorMessage( 531 FIELD_NO_INIT, "@NonNull static field " + fieldName + " not initialized"), 532 tree, 533 builder, 534 state, 535 symbol)); 536 } else { 537 state.reportMatch( 538 createErrorDescription( 539 new ErrorMessage(FIELD_NO_INIT, "@NonNull field " + fieldName + " not initialized"), 540 tree, 541 builder, 542 state, 543 symbol)); 544 } 545 } 546 } 547