Skip to content

Commit

Permalink
Introduce support for detecting conflicts when renaming methods
Browse files Browse the repository at this point in the history
by moving reusable conflict detection code from `JUnitMethodDeclaration` to a
new `ConflictDetection` class.
  • Loading branch information
eric-picnic authored and Stephan202 committed Feb 11, 2023
1 parent 14b5fa1 commit 1146e2a
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
import static com.google.errorprone.matchers.Matchers.hasModifier;
import static com.google.errorprone.matchers.Matchers.not;
import static java.util.function.Predicate.not;
import static tech.picnic.errorprone.bugpatterns.util.ConflictDetection.findMethodRenameBlocker;
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import static tech.picnic.errorprone.bugpatterns.util.JavaKeywords.isValidIdentifier;
import static tech.picnic.errorprone.bugpatterns.util.MoreJUnitMatchers.SETUP_OR_TEARDOWN_METHOD;
import static tech.picnic.errorprone.bugpatterns.util.MoreJUnitMatchers.TEST_METHOD;

Expand All @@ -25,15 +25,10 @@
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ImportTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import com.sun.tools.javac.code.Type;
import java.util.Optional;
import javax.lang.model.element.Modifier;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;

/** A {@link BugChecker} that flags non-canonical JUnit method declarations. */
// XXX: Consider introducing a class-level check that enforces that test classes:
Expand Down Expand Up @@ -106,61 +101,7 @@ private void reportMethodRenameBlocker(MethodTree tree, String reason, VisitorSt
.build());
}

/**
* If applicable, returns a human-readable argument against assigning the given name to an
* existing method.
*
* <p>This method implements imperfect heuristics. Things it currently does not consider include
* the following:
*
* <ul>
* <li>Whether the rename would merely introduce a method overload, rather than clashing with an
* existing method declaration.
* <li>Whether the rename would cause a method in a superclass to be overridden.
* <li>Whether the rename would in fact clash with a static import. (It could be that a static
* import of the same name is only referenced from lexical scopes in which the method under
* consideration cannot be referenced directly.)
* </ul>
*/
private static Optional<String> findMethodRenameBlocker(
MethodSymbol method, String newName, VisitorState state) {
if (isExistingMethodName(method.owner.type, newName, state)) {
return Optional.of(
String.format(
"a method named `%s` is already defined in this class or a supertype", newName));
}

if (isSimpleNameStaticallyImported(newName, state)) {
return Optional.of(String.format("`%s` is already statically imported", newName));
}

if (!isValidIdentifier(newName)) {
return Optional.of(String.format("`%s` is not a valid identifier", newName));
}

return Optional.empty();
}

private static boolean isExistingMethodName(Type clazz, String name, VisitorState state) {
return ASTHelpers.matchingMethods(state.getName(name), method -> true, clazz, state.getTypes())
.findAny()
.isPresent();
}

private static boolean isSimpleNameStaticallyImported(String simpleName, VisitorState state) {
return state.getPath().getCompilationUnit().getImports().stream()
.filter(ImportTree::isStatic)
.map(ImportTree::getQualifiedIdentifier)
.map(tree -> getStaticImportSimpleName(tree, state))
.anyMatch(simpleName::contentEquals);
}

private static CharSequence getStaticImportSimpleName(Tree tree, VisitorState state) {
String source = SourceCode.treeToString(tree, state);
return source.subSequence(source.lastIndexOf('.') + 1, source.length());
}

private static Optional<String> tryCanonicalizeMethodName(Symbol symbol) {
private static Optional<String> tryCanonicalizeMethodName(MethodSymbol symbol) {
return Optional.of(symbol.getQualifiedName().toString())
.filter(name -> name.startsWith(TEST_PREFIX))
.map(name -> name.substring(TEST_PREFIX.length()))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package tech.picnic.errorprone.bugpatterns.util;

import static tech.picnic.errorprone.bugpatterns.util.JavaKeywords.isValidIdentifier;

import com.google.errorprone.VisitorState;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ImportTree;
import com.sun.source.tree.Tree;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import com.sun.tools.javac.code.Type;
import java.util.Optional;

/**
* A set of helper methods for finding conflicts which would be caused by applying certain fixes.
*/
public final class ConflictDetection {
private ConflictDetection() {}

/**
* If applicable, returns a human-readable argument against assigning the given name to an
* existing method.
*
* <p>This method implements imperfect heuristics. Things it currently does not consider include
* the following:
*
* <ul>
* <li>Whether the rename would merely introduce a method overload, rather than clashing with an
* existing method declaration.
* <li>Whether the rename would cause a method in a superclass to be overridden.
* <li>Whether the rename would in fact clash with a static import. (It could be that a static
* import of the same name is only referenced from lexical scopes in which the method under
* consideration cannot be referenced directly.)
* </ul>
*
* @param method The method considered for being renamed.
* @param newName The proposed name to assign.
* @param state The {@link VisitorState} to use for searching for blockers.
* @return A human-readable argument against assigning the proposed name to an existing method, or
* {@link Optional#empty()} if no blocker was found.
*/
public static Optional<String> findMethodRenameBlocker(
MethodSymbol method, String newName, VisitorState state) {
if (isExistingMethodName(method.owner.type, newName, state)) {
return Optional.of(
String.format(
"a method named `%s` is already defined in this class or a supertype", newName));
}

if (isSimpleNameStaticallyImported(newName, state)) {
return Optional.of(String.format("`%s` is already statically imported", newName));
}

if (!isValidIdentifier(newName)) {
return Optional.of(String.format("`%s` is not a valid identifier", newName));
}

return Optional.empty();
}

private static boolean isExistingMethodName(Type clazz, String name, VisitorState state) {
return ASTHelpers.matchingMethods(state.getName(name), method -> true, clazz, state.getTypes())
.findAny()
.isPresent();
}

private static boolean isSimpleNameStaticallyImported(String simpleName, VisitorState state) {
return state.getPath().getCompilationUnit().getImports().stream()
.filter(ImportTree::isStatic)
.map(ImportTree::getQualifiedIdentifier)
.map(tree -> getStaticImportSimpleName(tree, state))
.anyMatch(simpleName::contentEquals);
}

private static CharSequence getStaticImportSimpleName(Tree tree, VisitorState state) {
String source = SourceCode.treeToString(tree, state);
return source.subSequence(source.lastIndexOf('.') + 1, source.length());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package tech.picnic.errorprone.bugpatterns.util;

import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;

import com.google.errorprone.BugPattern;
import com.google.errorprone.CompilationTestHelper;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.MethodTree;
import com.sun.tools.javac.code.Symbol;
import org.junit.jupiter.api.Test;

final class ConflictDetectionTest {
@Test
void matcher() {
CompilationTestHelper.newInstance(RenameBlockerFlagger.class, getClass())
.addSourceLines(
"/A.java",
"import static pkg.B.foo3t;",
"",
"class A {",
" private void foo1() {",
" foo3t();",
" }",
"",
" // BUG: Diagnostic contains: a method named `foo2t` is already defined in this class or a",
" // supertype",
" private void foo2() {}",
"",
" private void foo2t() {}",
"",
" // BUG: Diagnostic contains: `foo3t` is already statically imported",
" private void foo3() {}",
"",
" // BUG: Diagnostic contains: `int` is not a valid identifier",
" private void in() {}",
"}")
.addSourceLines(
"/pkg/B.java",
"package pkg;",
"",
"public class B {",
" public static void foo3t() {}",
"}")
.doTest();
}

/**
* A {@link BugChecker} that flags method rename blockers found by {@link
* ConflictDetection#findMethodRenameBlocker(Symbol.MethodSymbol, String, VisitorState)}.
*/
@BugPattern(summary = "Flags blockers for renaming methods", severity = ERROR)
public static final class RenameBlockerFlagger extends BugChecker implements MethodTreeMatcher {
private static final long serialVersionUID = 1L;

@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
return ConflictDetection.findMethodRenameBlocker(
ASTHelpers.getSymbol(tree), tree.getName() + "t", state)
.map(blocker -> buildDescription(tree).setMessage(blocker).build())
.orElse(Description.NO_MATCH);
}
}
}

0 comments on commit 1146e2a

Please sign in to comment.