1 /* 2 * Copyright (c) 2019 Google, Inc. 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.common.truth.refactorings; 18 19 import static com.google.common.base.CaseFormat.UPPER_UNDERSCORE; 20 import static com.google.common.base.Objects.equal; 21 import static com.google.common.collect.ImmutableList.toImmutableList; 22 import static com.google.common.collect.ImmutableSet.toImmutableSet; 23 import static com.google.common.collect.ImmutableSetMultimap.toImmutableSetMultimap; 24 import static com.google.common.collect.Iterables.getOnlyElement; 25 import static com.google.common.collect.MoreCollectors.onlyElement; 26 import static com.google.common.collect.Streams.stream; 27 import static com.google.common.truth.refactorings.CorrespondenceSubclassToFactoryCall.MemberType.COMPARE_METHOD; 28 import static com.google.common.truth.refactorings.CorrespondenceSubclassToFactoryCall.MemberType.CONSTRUCTOR; 29 import static com.google.common.truth.refactorings.CorrespondenceSubclassToFactoryCall.MemberType.TO_STRING_METHOD; 30 import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION; 31 import static com.google.errorprone.fixes.SuggestedFixes.compilesWithFix; 32 import static com.google.errorprone.matchers.Description.NO_MATCH; 33 import static com.google.errorprone.util.ASTHelpers.getDeclaredSymbol; 34 import static com.google.errorprone.util.ASTHelpers.getSymbol; 35 import static com.sun.source.tree.Tree.Kind.EXPRESSION_STATEMENT; 36 import static com.sun.source.tree.Tree.Kind.IDENTIFIER; 37 import static com.sun.source.tree.Tree.Kind.MEMBER_SELECT; 38 import static com.sun.source.tree.Tree.Kind.METHOD_INVOCATION; 39 import static com.sun.source.tree.Tree.Kind.NEW_CLASS; 40 import static com.sun.source.tree.Tree.Kind.NULL_LITERAL; 41 import static com.sun.source.tree.Tree.Kind.RETURN; 42 import static java.lang.String.format; 43 import static javax.lang.model.element.ElementKind.METHOD; 44 import static javax.lang.model.element.Modifier.PRIVATE; 45 import static javax.lang.model.element.Modifier.PROTECTED; 46 import static javax.lang.model.element.Modifier.PUBLIC; 47 48 import com.google.auto.value.AutoValue; 49 import com.google.common.base.CaseFormat; 50 import com.google.common.collect.HashMultimap; 51 import com.google.common.collect.ImmutableList; 52 import com.google.common.collect.ImmutableSet; 53 import com.google.common.collect.ImmutableSetMultimap; 54 import com.google.common.collect.SetMultimap; 55 import com.google.errorprone.BugPattern; 56 import com.google.errorprone.VisitorState; 57 import com.google.errorprone.bugpatterns.BugChecker; 58 import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher; 59 import com.google.errorprone.fixes.SuggestedFix; 60 import com.google.errorprone.matchers.Description; 61 import com.google.errorprone.suppliers.Supplier; 62 import com.sun.source.tree.ClassTree; 63 import com.sun.source.tree.CompilationUnitTree; 64 import com.sun.source.tree.ExpressionStatementTree; 65 import com.sun.source.tree.ExpressionTree; 66 import com.sun.source.tree.IdentifierTree; 67 import com.sun.source.tree.MemberSelectTree; 68 import com.sun.source.tree.MethodInvocationTree; 69 import com.sun.source.tree.MethodTree; 70 import com.sun.source.tree.NewClassTree; 71 import com.sun.source.tree.ReturnTree; 72 import com.sun.source.tree.StatementTree; 73 import com.sun.source.tree.Tree; 74 import com.sun.source.util.TreeScanner; 75 import com.sun.tools.javac.code.Symbol; 76 import com.sun.tools.javac.code.Symbol.MethodSymbol; 77 import com.sun.tools.javac.code.Symbol.TypeSymbol; 78 import com.sun.tools.javac.code.Type; 79 import com.sun.tools.javac.tree.JCTree; 80 import java.util.HashSet; 81 import java.util.List; 82 import java.util.Optional; 83 import java.util.Set; 84 import javax.lang.model.element.Modifier; 85 import org.checkerframework.checker.nullness.qual.Nullable; 86 87 /** 88 * Refactors some subclasses of {@code Correspondence} to instead call {@code Correspondence.from}. 89 * The exact change generated for a given correspondence depends on the details of how it is defined 90 * and used. 91 */ 92 @BugPattern( 93 name = "CorrespondenceSubclassToFactoryCall", 94 summary = "Use the factory methods on Correspondence instead of defining a subclass.", 95 severity = SUGGESTION) 96 public final class CorrespondenceSubclassToFactoryCall extends BugChecker 97 implements ClassTreeMatcher { 98 99 private static final String CORRESPONDENCE_CLASS = "com.google.common.truth.Correspondence"; 100 101 private static final Supplier<Type> COM_GOOGLE_COMMON_TRUTH_CORRESPONDENCE = 102 VisitorState.memoize(state -> state.getTypeFromString(CORRESPONDENCE_CLASS)); 103 104 @Override matchClass(ClassTree tree, VisitorState state)105 public Description matchClass(ClassTree tree, VisitorState state) { 106 if (!isCorrespondence(tree.getExtendsClause(), state)) { 107 return NO_MATCH; 108 } 109 110 List<CorrespondenceCode> replacements = computePossibleReplacements(tree, state); 111 112 Tree parent = state.getPath().getParentPath().getLeaf(); 113 if (parent.getKind() == NEW_CLASS) { 114 // Anonymous class. Replace the whole `new Correspondence` with a Correspondence.from call. 115 for (CorrespondenceCode replacement : replacements) { 116 SuggestedFix.Builder fix = SuggestedFix.builder(); 117 118 fix.replace(parent, replacement.callSite()); 119 120 Tree methodOrField = findChildOfStrictlyEnclosing(state, ClassTree.class); 121 /* 122 * If a class declares multiple anonymous Correspondences, we might end up creating multiple 123 * compare() methods in the same scope. We might get away with it, depending on the types 124 * involved, or we might need to manually rename some. 125 */ 126 fix.postfixWith(methodOrField, replacement.supportingMethodDefinition()); 127 128 if (compilesWithFix(fix.build(), state)) { 129 return describeMatch(parent, fix.build()); 130 } 131 } 132 return NO_MATCH; 133 } 134 135 Symbol classSymbol = getDeclaredSymbol(tree); 136 /* 137 * Named class. Create a Correspondence.from call, but then decide where to put it: 138 * 139 * The "safest" thing to do is to replace the body of the class with... 140 * 141 * static final Correspondence INSTANCE = Correspondence.from(...); 142 * 143 * ...and then make all callers refer to that. (As long as the class isn't top-level, we can 144 * even delete the class entirely, putting the constant in its place.) 145 * 146 * The other option is to inline that into all use sites. That's a great option if there's a 147 * single use site in a constant or helper method, in which case we produce code like... 148 * 149 * private static Correspondence makeCorrespondence() { 150 * return Correspondence.from(...); 151 * } 152 * 153 * But the danger of inlining is twofold: 154 * 155 * 1. If there are multiple callers, we'd duplicate the definition of the Correspondence. 156 * 157 * 2. Even if there's only one caller, we might inline a large chunk of code into the middle of 158 * a comparingElementsUsing call. 159 * 160 * So we use the "safe" option unless (a) there's exactly one call and (b) it's not inside a 161 * comparingElementsUsing call. 162 */ 163 SetMultimap<ParentType, NewClassTree> calls = findCalls(classSymbol, state); 164 /* 165 * We also sometime see users use the named type for fields and return types, like... 166 * 167 * static final MyCorrespondence INSTANCE = new MyCorrespondence(); 168 * 169 * We need to replace that with the generic Correspondence type, like... 170 * 171 * static final Correspondence<String, Integer> INSTANCE = ...; 172 */ 173 Set<Tree> typeReferences = findTypeReferences(classSymbol, state); 174 175 if (calls.size() == 1 && getOnlyElement(calls.keys()) == ParentType.OTHER) { 176 // Inline it. 177 Tree call = getOnlyElement(calls.values()); 178 for (CorrespondenceCode replacement : replacements) { 179 SuggestedFix.Builder fix = SuggestedFix.builder(); 180 replaceTypeReferences(fix, typeReferences, state.getSourceForNode(tree.getExtendsClause())); 181 fix.replace(call, replacement.callSite()); 182 fix.replace(tree, replacement.supportingMethodDefinition()); 183 if (compilesWithFix(fix.build(), state)) { 184 return describeMatch(tree, fix.build()); 185 } 186 } 187 return NO_MATCH; 188 } 189 190 // Declare a constant, and make use sites refer to that. 191 192 /* 193 * If we can't find any callers, then they're probably in other files, so we're going to be 194 * stuck updating them manually. To make the manual updates as simple as possible, we'll declare 195 * a constant named INSTANCE inside the Correspondence subclass (though it won't be a 196 * Correspondence subclass after our changes, just a holder class). 197 */ 198 if (calls.isEmpty()) { 199 for (CorrespondenceCode replacement : replacements) { 200 SuggestedFix.Builder fix = SuggestedFix.builder(); 201 replaceTypeReferences(fix, typeReferences, state.getSourceForNode(tree.getExtendsClause())); 202 203 JCTree extendsClause = (JCTree) tree.getExtendsClause(); 204 // Replace everything from `extends` to the end of the class, keeping only "class Foo." 205 int startOfExtends = 206 state 207 .getSourceCode() 208 .subSequence(0, extendsClause.getStartPosition()) 209 .toString() 210 .lastIndexOf("extends"); 211 fix.replace( 212 startOfExtends, 213 state.getEndPosition(tree), 214 format( 215 "{ %s static final %s INSTANCE = %s; %s }", 216 visibilityModifierOnConstructor(tree), 217 state.getSourceForNode(extendsClause), 218 replacement.callSite(), 219 replacement.supportingMethodDefinition())); 220 221 for (Tree call : calls.values()) { 222 fix.replace(call, tree.getSimpleName() + ".INSTANCE"); 223 } 224 225 if (compilesWithFix(fix.build(), state)) { 226 return describeMatch(tree, fix.build()); 227 } 228 } 229 return NO_MATCH; 230 } 231 232 /* 233 * We found callers. Let's optimistically assume that we found them all, in which case we might 234 * as well replace the whole class with a constant. 235 */ 236 for (CorrespondenceCode replacement : replacements) { 237 SuggestedFix.Builder fix = SuggestedFix.builder(); 238 replaceTypeReferences(fix, typeReferences, state.getSourceForNode(tree.getExtendsClause())); 239 240 String name = CaseFormat.UPPER_CAMEL.to(UPPER_UNDERSCORE, tree.getSimpleName().toString()); 241 242 // TODO(cpovirk): We're unlikely to get away with declaring everything `static`. 243 fix.replace( 244 tree, 245 format( 246 "%s static final %s %s = %s; %s", 247 visibilityModifierOnConstructor(tree), 248 state.getSourceForNode(tree.getExtendsClause()), 249 name, 250 replacement.callSite(), 251 replacement.supportingMethodDefinition())); 252 253 for (Tree call : calls.values()) { 254 fix.replace(call, name); 255 } 256 257 if (compilesWithFix(fix.build(), state)) { 258 return describeMatch(tree, fix.build()); 259 } 260 } 261 return NO_MATCH; 262 } 263 replaceTypeReferences( SuggestedFix.Builder fix, Set<Tree> typeReferences, String newType)264 private static void replaceTypeReferences( 265 SuggestedFix.Builder fix, Set<Tree> typeReferences, String newType) { 266 for (Tree reference : typeReferences) { 267 fix.replace(reference, newType); 268 } 269 } 270 visibilityModifierOnConstructor(ClassTree tree)271 private String visibilityModifierOnConstructor(ClassTree tree) { 272 MethodTree constructor = 273 tree.getMembers().stream() 274 .filter(t -> t instanceof MethodTree) 275 .map(t -> (MethodTree) t) 276 .filter(t -> t.getName().contentEquals("<init>")) 277 .findAny() 278 .get(); 279 // We don't include the ModifiersTree directly in case it contains any annotations. 280 return constructor.getModifiers().getFlags().stream() 281 .filter(m -> m == PUBLIC || m == PROTECTED || m == PRIVATE) 282 .findAny() 283 .map(Modifier::toString) 284 .orElse(""); 285 } 286 287 /** Returns all calls to the constructor for the given {@code classSymbol}, organized by {@linkplain ParentType whether they happen inside a call to {@code comparingElementsUsing}. */ findCalls( Symbol classSymbol, VisitorState state)288 private static SetMultimap<ParentType, NewClassTree> findCalls( 289 Symbol classSymbol, VisitorState state) { 290 SetMultimap<ParentType, NewClassTree> calls = HashMultimap.create(); 291 new TreeScanner<Void, Void>() { 292 private ParentType parentType = ParentType.OTHER; 293 294 @Override 295 public @Nullable Void visitMethodInvocation(MethodInvocationTree node, Void unused) { 296 boolean isComparingElementsUsing = 297 Optional.of(node.getMethodSelect()) 298 .filter(t -> t.getKind() == MEMBER_SELECT) 299 .map(t -> (MemberSelectTree) t) 300 .filter(t -> t.getIdentifier().contentEquals("comparingElementsUsing")) 301 .isPresent(); 302 if (isComparingElementsUsing) { 303 ParentType oldParentType = parentType; 304 parentType = ParentType.COMPARING_ELEMENTS_USING; 305 super.visitMethodInvocation(node, unused); 306 parentType = oldParentType; 307 } else { 308 super.visitMethodInvocation(node, unused); 309 } 310 return null; 311 } 312 313 @Override 314 public @Nullable Void visitNewClass(NewClassTree node, Void unused) { 315 if (getSymbol(node.getIdentifier()).equals(classSymbol)) { 316 calls.put(parentType, node); 317 } 318 return super.visitNewClass(node, unused); 319 } 320 }.scan(state.findEnclosing(CompilationUnitTree.class), null); 321 return calls; 322 } 323 324 /** 325 * Finds all references to the name of the given type, excluding (a) when the name is defined 326 * (e.g, {@code public class MyCorrespondence}) and (b) calls to its constructor (e.g., {@code new 327 * MyCorrespondence()}). The goal is to find every reference that we aren't already modifying so 328 * that we can rewrite, e.g., fields of the given type. 329 */ findTypeReferences(Symbol classSymbol, VisitorState state)330 private static Set<Tree> findTypeReferences(Symbol classSymbol, VisitorState state) { 331 Set<Tree> references = new HashSet<>(); 332 new TreeScanner<Void, Void>() { 333 @Override 334 public @Nullable Void scan(Tree node, Void unused) { 335 if (equal(getSymbol(node), classSymbol) 336 && getDeclaredSymbol(node) == null // Don't touch the ClassTree that we're replacing. 337 ) { 338 references.add(node); 339 } 340 return super.scan(node, unused); 341 } 342 343 @Override 344 public @Nullable Void visitNewClass(NewClassTree node, Void aVoid) { 345 scan(node.getEnclosingExpression(), null); 346 // Do NOT scan node.getIdentifier(). 347 scan(node.getTypeArguments(), null); 348 scan(node.getArguments(), null); 349 scan(node.getClassBody(), null); 350 return null; 351 } 352 }.scan(state.findEnclosing(CompilationUnitTree.class), null); 353 return references; 354 } 355 356 /** 357 * Whether the instantiation of a correspondence happens directly inside a call to {@code 358 * comparingElementsUsing} or not. For example, {@code comparingElementsUsing(new 359 * MyCorrespondence())} and {@code comparingElementsUsing(new Correspondence() { ... })} are both 360 * considered to have type {@link #COMPARING_ELEMENTS_USING}, but {@code private static final 361 * MyCorrespondence CORRESPONDENCE = new MyCorrespondence()} does not. 362 */ 363 enum ParentType { 364 COMPARING_ELEMENTS_USING, 365 OTHER, 366 } 367 368 /** 369 * If the given correspondence implementation is "simple enough," returns one or more possible 370 * replacements for its definition and instantiation sites. 371 */ computePossibleReplacements( ClassTree classTree, VisitorState state)372 private static ImmutableList<CorrespondenceCode> computePossibleReplacements( 373 ClassTree classTree, VisitorState state) { 374 ImmutableSetMultimap<MemberType, Tree> members = 375 classTree.getMembers().stream() 376 .collect(toImmutableSetMultimap(m -> MemberType.from(m, state), m -> m)); 377 if (members.containsKey(MemberType.OTHER) 378 || members.get(CONSTRUCTOR).size() != 1 379 || members.get(COMPARE_METHOD).size() != 1 380 || members.get(TO_STRING_METHOD).size() != 1) { 381 return ImmutableList.of(); 382 } 383 MethodTree constructor = (MethodTree) getOnlyElement(members.get(CONSTRUCTOR)); 384 MethodTree compareMethod = (MethodTree) getOnlyElement(members.get(COMPARE_METHOD)); 385 MethodTree toStringMethod = (MethodTree) getOnlyElement(members.get(TO_STRING_METHOD)); 386 387 if (!constructorCallsOnlySuper(constructor)) { 388 return ImmutableList.of(); 389 } 390 391 ImmutableList<BinaryPredicateCode> binaryPredicates = 392 makeBinaryPredicates(classTree, compareMethod, state); 393 394 ExpressionTree toStringReturns = returnExpression(toStringMethod); 395 if (toStringReturns == null) { 396 return ImmutableList.of(); 397 } 398 /* 399 * Replace bad toString() implementations that merely `return null`, since the factories make 400 * that an immediate error. 401 */ 402 String description = 403 toStringReturns.getKind() == NULL_LITERAL 404 ? "\"corresponds to\"" 405 : state.getSourceForNode(toStringReturns); 406 return binaryPredicates.stream() 407 .map(p -> CorrespondenceCode.create(p, description)) 408 .collect(toImmutableList()); 409 } 410 411 /** Returns one or more possible replacements for the given correspondence's {@code compare} method's definition and for code to pass to {@code Correspondence.from) to construct a correspondence that uses the replacement. */ makeBinaryPredicates( ClassTree classTree, MethodTree compareMethod, VisitorState state)412 private static ImmutableList<BinaryPredicateCode> makeBinaryPredicates( 413 ClassTree classTree, MethodTree compareMethod, VisitorState state) { 414 Tree comparison = maybeMakeLambdaBody(compareMethod, state); 415 if (comparison == null) { 416 ClassTree enclosing = findStrictlyEnclosing(state, ClassTree.class); 417 CharSequence newCompareMethodOwner; 418 String newCompareMethodName; 419 if (enclosing == null) { 420 newCompareMethodName = "compare"; 421 newCompareMethodOwner = classTree.getSimpleName(); 422 } else { 423 newCompareMethodName = 424 "compare" + classTree.getSimpleName().toString().replaceFirst("Correspondence$", ""); 425 newCompareMethodOwner = enclosing.getSimpleName(); 426 } 427 // TODO(cpovirk): We're unlikely to get away with declaring everything `static`. 428 String supportingMethodDefinition = 429 format( 430 "private static boolean %s(%s, %s) %s", 431 newCompareMethodName, 432 state.getSourceForNode(compareMethod.getParameters().get(0)), 433 state.getSourceForNode(compareMethod.getParameters().get(1)), 434 state.getSourceForNode(compareMethod.getBody())); 435 return ImmutableList.of( 436 BinaryPredicateCode.create( 437 newCompareMethodOwner + "::" + newCompareMethodName, supportingMethodDefinition)); 438 } 439 // First try without types, then try with. 440 return ImmutableList.of( 441 BinaryPredicateCode.fromParamsAndExpression( 442 compareMethod.getParameters().get(0).getName(), 443 compareMethod.getParameters().get(1).getName(), 444 state.getSourceForNode(comparison)), 445 BinaryPredicateCode.fromParamsAndExpression( 446 state.getSourceForNode(compareMethod.getParameters().get(0)), 447 state.getSourceForNode(compareMethod.getParameters().get(1)), 448 state.getSourceForNode(comparison))); 449 } 450 451 /** 452 * Converts the given method into a lambda, either expression or block, if "appropriate." For 453 * details about the various cases, see implementation comments. 454 */ maybeMakeLambdaBody(MethodTree compareMethod, VisitorState state)455 private static @Nullable Tree maybeMakeLambdaBody(MethodTree compareMethod, VisitorState state) { 456 ExpressionTree comparison = returnExpression(compareMethod); 457 if (comparison != null) { 458 // compare() is defined as simply `return something;`. Create a lambda. 459 return comparison; 460 } 461 462 /* 463 * compare() has multiple statements. Let's keep it as a method (though we might change its 464 * modifiers, name, and location) and construct the Correspondence with a method reference... 465 * 466 * ...unless it relies on parameters from the enclosing method, in which case extracting a 467 * method isn't going to work because it won't be able to access those parameters. 468 */ 469 MethodTree enclosingMethod = state.findEnclosing(MethodTree.class); 470 if (enclosingMethod == null) { 471 // No enclosing method, so we're presumably not closing over anything. Safe to extract method. 472 return null; 473 } 474 475 ImmutableSet<Symbol> paramsOfEnclosingMethod = 476 enclosingMethod.getParameters().stream() 477 .map(p -> getDeclaredSymbol(p)) 478 .collect(toImmutableSet()); 479 boolean[] referenceFound = new boolean[1]; 480 new TreeScanner<Void, Void>() { 481 @Override 482 public @Nullable Void scan(Tree node, Void aVoid) { 483 if (paramsOfEnclosingMethod.contains(getSymbol(node))) { 484 referenceFound[0] = true; 485 } 486 return super.scan(node, aVoid); 487 } 488 }.scan(state.getPath().getLeaf(), null); 489 490 if (!referenceFound[0]) { 491 // No references to anything from the enclosing method. Probably safe to extract a method. 492 return null; 493 } 494 495 /* 496 * compare() both: 497 * 498 * - uses a parameter from the enclosing method and 499 * 500 * - has multiple statements 501 * 502 * So we create a block lambda. 503 */ 504 return compareMethod.getBody(); 505 } 506 507 /** Like {@link VisitorState#findEnclosing} but doesn't consider the leaf to enclose itself. */ findStrictlyEnclosing( VisitorState state, Class<T> clazz)508 private static <T extends Tree> @Nullable T findStrictlyEnclosing( 509 VisitorState state, Class<T> clazz) { 510 return stream(state.getPath().getParentPath()) 511 .filter(clazz::isInstance) 512 .map(clazz::cast) 513 .findAny() 514 .orElse(null); 515 } 516 517 /** 518 * Like {@link #findStrictlyEnclosing} but returns not the found element but its child along the 519 * path. For example, if called with {@code ClassTree}, it might return a {@code MethodTree} 520 * inside the class. 521 */ findChildOfStrictlyEnclosing( VisitorState state, Class<? extends Tree> clazz)522 private static @Nullable Tree findChildOfStrictlyEnclosing( 523 VisitorState state, Class<? extends Tree> clazz) { 524 Tree previous = state.getPath().getLeaf(); 525 for (Tree t : state.getPath().getParentPath()) { 526 if (clazz.isInstance(t)) { 527 return previous; 528 } 529 previous = t; 530 } 531 return null; 532 } 533 534 /** 535 * A {@code Correspondence.from} call to replace the instantiation site of a {@code 536 * Correspondence}. Often the call is self-contained (if it's a lambda), but sometimes it's a 537 * method reference, in which case it's accompanied by a separate method definition. 538 */ 539 @AutoValue 540 abstract static class CorrespondenceCode { create(BinaryPredicateCode binaryPredicate, String description)541 static CorrespondenceCode create(BinaryPredicateCode binaryPredicate, String description) { 542 return new AutoValue_CorrespondenceSubclassToFactoryCall_CorrespondenceCode( 543 binaryPredicate, description); 544 } 545 binaryPredicate()546 abstract BinaryPredicateCode binaryPredicate(); 547 description()548 abstract String description(); 549 callSite()550 final String callSite() { 551 return format("Correspondence.from(%s, %s)", binaryPredicate().usage(), description()); 552 } 553 supportingMethodDefinition()554 final String supportingMethodDefinition() { 555 return binaryPredicate().supportingMethodDefinition().orElse(""); 556 } 557 } 558 559 /** 560 * Code that can be inserted as the first argument of a {@code Correspondence.from} call. Often 561 * it's a lambda, but sometimes it's a method reference, in which case it's accompanied by a 562 * separate method definition. 563 */ 564 @AutoValue 565 abstract static class BinaryPredicateCode { fromParamsAndExpression( CharSequence param0, CharSequence param1, String expression)566 static BinaryPredicateCode fromParamsAndExpression( 567 CharSequence param0, CharSequence param1, String expression) { 568 return create(String.format("(%s, %s) -> %s", param0, param1, expression), null); 569 } 570 create(String usage, @Nullable String supportingMethodDefinition)571 static BinaryPredicateCode create(String usage, @Nullable String supportingMethodDefinition) { 572 return new AutoValue_CorrespondenceSubclassToFactoryCall_BinaryPredicateCode( 573 usage, Optional.ofNullable(supportingMethodDefinition)); 574 } 575 usage()576 abstract String usage(); 577 supportingMethodDefinition()578 abstract Optional<String> supportingMethodDefinition(); 579 } 580 returnExpression(MethodTree method)581 private static @Nullable ExpressionTree returnExpression(MethodTree method) { 582 List<? extends StatementTree> statements = method.getBody().getStatements(); 583 if (statements.size() != 1) { 584 return null; 585 } 586 StatementTree statement = getOnlyElement(statements); 587 if (statement.getKind() != RETURN) { 588 return null; 589 } 590 return ((ReturnTree) statement).getExpression(); 591 } 592 constructorCallsOnlySuper(MethodTree constructor)593 private static boolean constructorCallsOnlySuper(MethodTree constructor) { 594 List<? extends StatementTree> statements = constructor.getBody().getStatements(); 595 if (statements.size() != 1) { 596 return false; 597 } 598 StatementTree statement = getOnlyElement(statements); 599 if (statement.getKind() != EXPRESSION_STATEMENT) { 600 return false; 601 } 602 ExpressionTree expression = ((ExpressionStatementTree) statement).getExpression(); 603 if (expression.getKind() != METHOD_INVOCATION) { 604 return false; 605 } 606 ExpressionTree methodSelect = ((MethodInvocationTree) expression).getMethodSelect(); 607 if (methodSelect.getKind() != IDENTIFIER 608 || !((IdentifierTree) methodSelect).getName().contentEquals("super")) { 609 return false; 610 } 611 return true; 612 } 613 614 /** 615 * Whether a member is of one of the standard {@link Correspondence} methods that we know how to 616 * migrate and, if so, which one. 617 */ 618 enum MemberType { 619 CONSTRUCTOR, 620 COMPARE_METHOD, 621 TO_STRING_METHOD, 622 OTHER, 623 ; 624 from(Tree tree, VisitorState state)625 static MemberType from(Tree tree, VisitorState state) { 626 Symbol symbol = getDeclaredSymbol(tree); 627 if (!(symbol instanceof MethodSymbol)) { 628 return OTHER; 629 } 630 MethodSymbol methodSymbol = (MethodSymbol) symbol; 631 632 if (methodSymbol.getSimpleName().contentEquals("<init>")) { 633 return CONSTRUCTOR; 634 } else if (overrides(methodSymbol, CORRESPONDENCE_CLASS, "compare", state)) { 635 return COMPARE_METHOD; 636 } else if (overrides(methodSymbol, "java.lang.Object", "toString", state)) { 637 return TO_STRING_METHOD; 638 } else { 639 return OTHER; 640 } 641 } 642 } 643 overrides( MethodSymbol potentialOverrider, String clazz, String method, VisitorState state)644 private static boolean overrides( 645 MethodSymbol potentialOverrider, String clazz, String method, VisitorState state) { 646 Symbol overridable = 647 state.getTypeFromString(clazz).tsym.getEnclosedElements().stream() 648 .filter(s -> s.getKind() == METHOD) 649 .filter(m -> m.getSimpleName().contentEquals(method)) 650 .collect(onlyElement()); 651 return potentialOverrider.getSimpleName().contentEquals(method) 652 && potentialOverrider.overrides( 653 overridable, (TypeSymbol) overridable.owner, state.getTypes(), true); 654 } 655 isCorrespondence(Tree supertypeTree, VisitorState state)656 private static boolean isCorrespondence(Tree supertypeTree, VisitorState state) { 657 Type correspondenceType = COM_GOOGLE_COMMON_TRUTH_CORRESPONDENCE.get(state); 658 if (correspondenceType == null) { 659 return false; 660 } 661 return supertypeTree != null 662 && state.getTypes().isSameType(getSymbol(supertypeTree).type, correspondenceType); 663 } 664 } 665