Compare commits

...

7 Commits

Author SHA1 Message Date
Gijs de Jong
3132810c47 Add support for migrating groups attribute 2022-10-26 11:28:24 +02:00
Gijs de Jong
5de2496035 Add jdk.compiler/parser module to compilation 2022-10-25 10:30:21 +02:00
Gijs de Jong
b8ab20b754 Remove redundant naming 2022-10-25 10:30:21 +02:00
Gijs de Jong
46aab0f5ba Improve AnnotationAttributeReplacement 2022-10-25 10:30:21 +02:00
Gijs de Jong
84086d9fee Handle test setup and teardown migration 2022-10-25 10:30:20 +02:00
Gijs de Jong
4451319fb8 Ignore extends clause in TestNGClassLevelTestAnnotation 2022-10-25 10:30:20 +02:00
Gijs de Jong
af46c602e7 Introduce BugCheckers TestNG -> JUnit migration
Fix `TestNGDataProviderCheckTest`

Suggestions

Suggested changes

Introduce `BugChecker`s TestNG -> JUnit migration

Fix `TestNGDataProviderCheckTest`

Suggestions

Suggested changes

Apply fixes for new bugchecks

Rename checks for `BugPatternNaming`

Only match migratable tests

Update javadoc with *legal* characters

Remove static method in inner class

Requested changes

Introduce support for 1d array dataprovider

Retain comments in data provider return tree

Self-apply EP checks

Swapped around a few methods and fix mutable issue

Suggested changes

Suggestions

Suggestions

Suggestions 2

Prefer `orElseThrow()` over `.get()`
2022-10-25 10:30:20 +02:00
14 changed files with 1726 additions and 2 deletions

View File

@@ -141,12 +141,12 @@
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>

View File

@@ -0,0 +1,213 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.errorprone.BugPattern.LinkType.NONE;
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import static com.google.errorprone.BugPattern.StandardTags.REFACTORING;
import com.google.auto.service.AutoService;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
import com.google.errorprone.fixes.Replacement;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.AssignmentTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
import com.sun.tools.javac.tree.JCTree;
import java.util.Optional;
import java.util.Set;
import tech.picnic.errorprone.bugpatterns.util.AnnotationAttributeMatcher;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/**
* A {@link BugChecker} that replaces a predefined list of annotation attributes with its own
* separate annotation.
*/
@AutoService(BugChecker.class)
@BugPattern(
summary = "Replace annotation attributes with an annotation",
linkType = NONE,
tags = REFACTORING,
severity = ERROR)
public final class AnnotationAttributeReplacement extends BugChecker implements MethodTreeMatcher {
private static final long serialVersionUID = 1L;
private static final ImmutableMap<AnnotationAttributeMatcher, AnnotationAttributeReplacer>
ANNOTATION_ATTRIBUTE_REPLACEMENT =
ImmutableMap.<AnnotationAttributeMatcher, AnnotationAttributeReplacer>builder()
.put(
singleArgumentMatcher("org.testng.annotations.Test#singleThreaded"),
(annotation, argument, state) ->
Optional.of(
SuggestedFix.builder()
.merge(removeAnnotationArgument(annotation, argument, state))
.merge(
SuggestedFix.prefixWith(
annotation,
"// XXX: Removed argument `singleThreaded = true`, as this cannot be migrated to JUnit!\n"))
.build()))
.put(
singleArgumentMatcher("org.testng.annotations.Test#priority"),
(annotation, argument, state) -> {
ClassTree classTree = state.findEnclosing(ClassTree.class);
if (classTree == null) {
return Optional.empty();
}
if (argument.getKind() != Tree.Kind.ASSIGNMENT) {
return Optional.empty();
}
AssignmentTree assignmentTree = (AssignmentTree) argument;
return Optional.of(
SuggestedFix.builder()
.merge(removeAnnotationArgument(annotation, argument, state))
.merge(
SuggestedFix.postfixWith(
annotation,
String.format(
"\n@org.junit.jupiter.api.Order(%s)",
SourceCode.treeToString(
assignmentTree.getExpression(), state))))
.merge(
SuggestedFix.prefixWith(
classTree,
"@TestMethodOrder(MethodOrderer.OrderAnnotation.class)\n"))
.addImport("org.junit.jupiter.api.TestMethodOrder")
.addImport("org.junit.jupiter.api.MethodOrderer")
.build());
})
.put(
singleArgumentMatcher("org.testng.annotations.Test#description"),
(annotation, argument, state) ->
Optional.of(argument)
.filter(AssignmentTree.class::isInstance)
.map(AssignmentTree.class::cast)
.map(
assignmentTree ->
SuggestedFix.builder()
.merge(removeAnnotationArgument(annotation, argument, state))
.merge(
SuggestedFix.postfixWith(
annotation,
String.format(
"\n@org.junit.jupiter.api.DisplayName(%s)",
SourceCode.treeToString(
assignmentTree.getExpression(), state))))
.build()))
.put(
singleArgumentMatcher("org.testng.annotations.Test#groups"),
(annotation, argument, state) ->
Optional.of(argument)
.filter(AssignmentTree.class::isInstance)
.map(AssignmentTree.class::cast)
.map(
assignmentTree ->
SuggestedFix.builder()
.merge(removeAnnotationArgument(annotation, argument, state))
.merge(
SuggestedFix.postfixWith(
annotation,
String.format(
"\n@org.junit.jupiter.api.Tag(%s)",
SourceCode.treeToString(
assignmentTree.getExpression(), state))))
.build()))
.build();
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
ImmutableList<AnnotationTree> annotations =
ASTHelpers.getAnnotations(tree).stream().collect(toImmutableList());
SuggestedFix.Builder builder = SuggestedFix.builder();
annotations.forEach(
annotation -> {
ANNOTATION_ATTRIBUTE_REPLACEMENT.forEach(
(matcher, fixBuilder) ->
matcher
.extractMatchingArguments(annotation)
.forEach(
argument ->
fixBuilder
.buildFix(annotation, argument, state)
.ifPresent(builder::merge)));
tryRemoveTrailingParenthesis(annotation, builder.build(), state)
.ifPresent(builder::merge);
});
return builder.isEmpty() ? Description.NO_MATCH : describeMatch(tree, builder.build());
}
private static Optional<SuggestedFix> tryRemoveTrailingParenthesis(
AnnotationTree annotation, SuggestedFix fix, VisitorState state) {
JCTree.JCCompilationUnit compileUnit =
((JCTree.JCCompilationUnit) state.findEnclosing(CompilationUnitTree.class));
if (compileUnit == null) {
return Optional.empty();
}
Set<Replacement> replacements = fix.getReplacements(compileUnit.endPositions);
String annotationSource = SourceCode.treeToString(annotation, state).replace(", ", ",");
String annotationArguments =
annotationSource.substring(
annotationSource.indexOf("(") + 1, annotationSource.length() - 1);
int argumentReplacementLength = replacements.stream().mapToInt(Replacement::length).sum();
if (argumentReplacementLength != annotationArguments.length()) {
return Optional.empty();
}
return replacements.stream()
.filter(replacement -> replacement.length() != 0)
.map(Replacement::startPosition)
.reduce(Integer::min)
.flatMap(
min ->
replacements.stream()
.filter(replacement -> replacement.length() != 0)
.map(Replacement::endPosition)
.reduce(Integer::max)
.map(
max ->
SuggestedFix.builder()
.merge(SuggestedFix.replace(min - 1, min, ""))
.merge(SuggestedFix.replace(max, max + 1, ""))
.build()));
}
private static SuggestedFix removeAnnotationArgument(
AnnotationTree annotation, ExpressionTree argument, VisitorState state) {
String annotationSource = SourceCode.treeToString(annotation, state);
String argumentSource = SourceCode.treeToString(argument, state);
int argumentSourceIndex = annotationSource.indexOf(argumentSource);
boolean endsWithComma =
annotationSource
.substring(
argumentSourceIndex + argumentSource.length(),
argumentSourceIndex + argumentSource.length() + 1)
.equals(",");
return SuggestedFix.builder().replace(argument, "", 0, endsWithComma ? 1 : 0).build();
}
private static AnnotationAttributeMatcher singleArgumentMatcher(String fullyQualifiedArgument) {
return AnnotationAttributeMatcher.create(
Optional.of(ImmutableList.of(fullyQualifiedArgument)), ImmutableList.of());
}
@FunctionalInterface
interface AnnotationAttributeReplacer {
Optional<SuggestedFix> buildFix(
AnnotationTree annotation, ExpressionTree argument, VisitorState state);
}
}

View File

@@ -0,0 +1,57 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.LinkType.NONE;
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.google.errorprone.BugPattern.StandardTags.REFACTORING;
import static com.google.errorprone.matchers.Matchers.isType;
import com.google.auto.service.AutoService;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.MethodTree;
/**
* A {@link BugChecker} that replaces TestNG annotations with their JUnit counterpart, if one exists
*/
@AutoService(BugChecker.class)
@BugPattern(
summary = "Migrate TestNG test annotations to JUnit",
linkType = NONE,
severity = WARNING,
tags = REFACTORING)
public final class TestNGAnnotation extends BugChecker implements MethodTreeMatcher {
private static final long serialVersionUID = 1L;
private static final ImmutableMap<Matcher<AnnotationTree>, String>
TESTNG_ANNOTATION_REPLACEMENTS =
ImmutableMap.<Matcher<AnnotationTree>, String>builder()
.put(isType("org.testng.annotations.AfterClass"), "@org.junit.jupiter.api.AfterAll")
.put(isType("org.testng.annotations.AfterMethod"), "@org.junit.jupiter.api.AfterEach")
.put(isType("org.testng.annotations.BeforeClass"), "@org.junit.jupiter.api.BeforeAll")
.put(
isType("org.testng.annotations.BeforeMethod"),
"@org.junit.jupiter.api.BeforeEach")
.put(isType("org.testng.annotations.Test"), "@org.junit.jupiter.api.Test")
.build();
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
SuggestedFix.Builder fix = SuggestedFix.builder();
ASTHelpers.getAnnotations(tree).stream()
.filter(annotation -> annotation.getArguments().isEmpty())
.forEach(
annotation ->
TESTNG_ANNOTATION_REPLACEMENTS.entrySet().stream()
.filter(entry -> entry.getKey().matches(annotation, state))
.forEach(entry -> fix.replace(annotation, entry.getValue())));
return fix.isEmpty() ? Description.NO_MATCH : describeMatch(tree, fix.build());
}
}

View File

@@ -0,0 +1,85 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.LinkType.NONE;
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import static com.google.errorprone.BugPattern.StandardTags.REFACTORING;
import static com.google.errorprone.matchers.Matchers.allOf;
import static com.google.errorprone.matchers.Matchers.anyOf;
import static com.google.errorprone.matchers.Matchers.hasAnnotation;
import static com.google.errorprone.matchers.Matchers.isType;
import static com.google.errorprone.matchers.Matchers.methodHasVisibility;
import static com.google.errorprone.matchers.Matchers.not;
import static com.google.errorprone.matchers.MethodVisibility.Visibility.PUBLIC;
import com.google.auto.service.AutoService;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.MethodTree;
import java.util.Optional;
import java.util.function.Predicate;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/** A {@link BugChecker} which flags class level `@Test` annotations from TestNG. */
@AutoService(BugChecker.class)
@BugPattern(
summary = "A bug pattern to migrate TestNG Test annotations to methods",
linkType = NONE,
tags = REFACTORING,
severity = ERROR)
public final class TestNGClassLevelTestAnnotation extends BugChecker implements ClassTreeMatcher {
private static final long serialVersionUID = 1L;
private static final Matcher<ClassTree> CLASS_TREE = hasAnnotation("org.testng.annotations.Test");
private static final Matcher<MethodTree> UNMIGRATED_TESTNG_TEST_METHOD =
allOf(
methodHasVisibility(PUBLIC),
not(
anyOf(
hasAnnotation("org.testng.annotations.Test"),
hasAnnotation("org.testng.annotations.AfterClass"),
hasAnnotation("org.testng.annotations.AfterMethod"),
hasAnnotation("org.testng.annotations.BeforeClass"),
hasAnnotation("org.testng.annotations.BeforeMethod"))));
private static final Matcher<AnnotationTree> TESTNG_ANNOTATION =
isType("org.testng.annotations.Test");
@Override
public Description matchClass(ClassTree tree, VisitorState state) {
if (!CLASS_TREE.matches(tree, state)) {
return Description.NO_MATCH;
}
Optional<? extends AnnotationTree> testAnnotation =
ASTHelpers.getAnnotations(tree).stream()
.filter(annotation -> TESTNG_ANNOTATION.matches(annotation, state))
.findFirst();
if (testAnnotation.isEmpty()) {
return Description.NO_MATCH;
}
SuggestedFix.Builder fix = SuggestedFix.builder();
tree.getMembers().stream()
.filter(MethodTree.class::isInstance)
.map(MethodTree.class::cast)
.filter(method -> UNMIGRATED_TESTNG_TEST_METHOD.matches(method, state))
.filter(Predicate.not(ASTHelpers::isGeneratedConstructor))
.forEach(
methodTree ->
fix.merge(
SuggestedFix.prefixWith(
methodTree,
String.format(
"%s\n", SourceCode.treeToString(testAnnotation.get(), state)))));
fix.delete(testAnnotation.get());
return describeMatch(testAnnotation.get(), fix.build());
}
}

View File

@@ -0,0 +1,210 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.LinkType.NONE;
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import static com.google.errorprone.BugPattern.StandardTags.REFACTORING;
import static com.google.errorprone.matchers.Matchers.isType;
import static com.sun.source.tree.Tree.Kind.NEW_ARRAY;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toMap;
import com.google.auto.service.AutoService;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.util.ASTHelpers;
import com.google.errorprone.util.ErrorProneToken;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.NewArrayTree;
import com.sun.source.tree.ReturnTree;
import com.sun.tools.javac.code.Symbol.ClassSymbol;
import com.sun.tools.javac.parser.Tokens.Comment;
import com.sun.tools.javac.util.Name;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/**
* A {@link BugChecker} which flags TestNG {@link org.testng.annotations.DataProvider} methods and
* provides an equivalent JUnit {@link org.junit.jupiter.params.ParameterizedTest} replacement.
*/
@AutoService(BugChecker.class)
@BugPattern(
summary = "Migrate TestNG DataProvider to JUnit argument streams",
linkType = NONE,
tags = REFACTORING,
severity = ERROR)
public final class TestNGDataProvider extends BugChecker implements MethodTreeMatcher {
private static final long serialVersionUID = 1L;
private static final Matcher<AnnotationTree> TESTNG_DATAPROVIDER_ANNOTATION =
isType("org.testng.annotations.DataProvider");
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
Optional<? extends AnnotationTree> dataProviderAnnotation =
ASTHelpers.getAnnotations(tree).stream()
.filter(annotation -> TESTNG_DATAPROVIDER_ANNOTATION.matches(annotation, state))
.findFirst();
if (dataProviderAnnotation.isEmpty()) {
return Description.NO_MATCH;
}
String methodName = tree.getName().toString();
Name migratedName = state.getName(methodName + "Junit");
ClassTree classTree = state.findEnclosing(ClassTree.class);
if (classTree == null
|| isMethodAlreadyMigratedInEnclosingClass(ASTHelpers.getSymbol(classTree), migratedName)) {
return Description.NO_MATCH;
}
ReturnTree returnTree = getReturnTree(tree);
Optional<NewArrayTree> returnArrayTree = getDataProviderReturnTree(returnTree);
if (returnArrayTree.isEmpty()) {
return Description.NO_MATCH;
}
return describeMatch(
dataProviderAnnotation.get(),
SuggestedFix.builder()
.addStaticImport("org.junit.jupiter.params.provider.Arguments.arguments")
.addImport("java.util.stream.Stream")
.addImport("org.junit.jupiter.params.provider.Arguments")
.merge(
SuggestedFix.postfixWith(
tree,
buildMethodSource(
classTree.getSimpleName().toString(),
migratedName.toString(),
tree,
returnTree,
returnArrayTree.orElseThrow(),
state)))
.build());
}
private static boolean isMethodAlreadyMigratedInEnclosingClass(
ClassSymbol enclosingClassSymbol, Name methodName) {
return enclosingClassSymbol.members().getSymbolsByName(methodName).iterator().hasNext();
}
private static ReturnTree getReturnTree(MethodTree methodTree) {
return methodTree.getBody().getStatements().stream()
.filter(ReturnTree.class::isInstance)
.findFirst()
.map(ReturnTree.class::cast)
.orElseThrow();
}
private static Optional<NewArrayTree> getDataProviderReturnTree(ReturnTree returnTree) {
if (returnTree.getExpression().getKind() != NEW_ARRAY
|| ((NewArrayTree) returnTree.getExpression()).getInitializers().isEmpty()) {
return Optional.empty();
}
return Optional.of((NewArrayTree) returnTree.getExpression());
}
private static String buildMethodSource(
String className,
String name,
MethodTree methodTree,
ReturnTree returnTree,
NewArrayTree newArrayTree,
VisitorState state) {
StringBuilder sourceBuilder =
new StringBuilder(
"@SuppressWarnings(\"UnusedMethod\" /* This is an intermediate state for the JUnit migration. */)\n")
.append(" private static Stream<Arguments> ")
.append(name)
.append(" () ");
if (!methodTree.getThrows().isEmpty()) {
sourceBuilder
.append(" throws ")
.append(
methodTree.getThrows().stream()
.filter(IdentifierTree.class::isInstance)
.map(IdentifierTree.class::cast)
.map(identifierTree -> identifierTree.getName().toString())
.collect(joining(", ")));
}
return sourceBuilder
.append(" {\n")
.append(extractMethodBodyWithoutReturnStatement(methodTree, returnTree, state))
.append(" return ")
.append(buildArgumentStream(className, newArrayTree, state))
.append(";\n}")
.toString();
}
private static String extractMethodBodyWithoutReturnStatement(
MethodTree methodTree, ReturnTree returnTree, VisitorState state) {
String body = SourceCode.treeToString(methodTree.getBody(), state);
return body.substring(2, body.indexOf(SourceCode.treeToString(returnTree, state)) - 1);
}
private static String buildArgumentStream(
String className, NewArrayTree newArrayTree, VisitorState state) {
StringBuilder argumentsBuilder = new StringBuilder();
int startPos = ASTHelpers.getStartPosition(newArrayTree);
int endPos = state.getEndPosition(newArrayTree);
Map<Integer, List<Comment>> comments =
state.getOffsetTokens(startPos, endPos).stream()
.collect(
toMap(ErrorProneToken::pos, ErrorProneToken::comments, (a, b) -> b, HashMap::new));
argumentsBuilder.append(
newArrayTree.getInitializers().stream()
.map(
expression ->
buildArguments(
expression,
comments.getOrDefault(
ASTHelpers.getStartPosition(expression), ImmutableList.of()),
state))
.collect(joining(",\n")));
// This regex expression replaces all instances of "this.getClass()" or "getClass()"
// with the fully qualified class name to retain functionality in static context.
return String.format("Stream.of(\n%s\n )", argumentsBuilder)
.replaceAll("((?<!\\b\\.)|(\\bthis\\.))(getClass\\(\\))", className + ".class");
}
private static String buildArguments(
ExpressionTree expressionTree, List<Comment> comments, VisitorState state) {
if (expressionTree.getKind() == NEW_ARRAY) {
return buildArgumentsFromArray(((NewArrayTree) expressionTree), comments, state);
} else {
return buildArgumentsFromExpression(expressionTree, comments, state);
}
}
private static String buildArgumentsFromExpression(
ExpressionTree expressionTree, List<Comment> comments, VisitorState state) {
return String.format(
"\t\t%s\n\t\targuments(%s)",
comments.stream().map(Comment::getText).collect(joining("\n")),
SourceCode.treeToString(expressionTree, state));
}
private static String buildArgumentsFromArray(
NewArrayTree argumentArray, List<Comment> comments, VisitorState state) {
String argSource = SourceCode.treeToString(argumentArray, state);
return String.format(
"\t\t%s\n\t\targuments(%s)",
comments.stream().map(Comment::getText).collect(joining("\n")),
argSource.substring(1, argSource.length() - 1));
}
}

View File

@@ -0,0 +1,164 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.auto.common.MoreStreams.toImmutableList;
import static com.google.errorprone.BugPattern.LinkType.NONE;
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import static com.google.errorprone.BugPattern.StandardTags.REFACTORING;
import static com.google.errorprone.matchers.Matchers.isType;
import static com.sun.source.tree.Tree.Kind.MEMBER_SELECT;
import static com.sun.source.tree.Tree.Kind.NEW_ARRAY;
import static java.util.stream.Collectors.joining;
import com.google.auto.service.AutoService;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.AssignmentTree;
import com.sun.source.tree.BlockTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.NewArrayTree;
import java.util.Optional;
import org.junit.jupiter.api.function.Executable;
import org.testng.annotations.Test;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/**
* A {@link BugChecker} which flags {@link Test#expectedExceptions()} and suggests a JUnit
* equivalent replacement.
*
* <p>The method body is wrapped in a {@link org.junit.jupiter.api.Assertions#assertThrows(Class,
* Executable)} statement.
*
* <p>This {@link BugChecker} does not support migrating more than one exception and will therefore
* omit extra {@code expectedExceptions}. As this is not behavior preserving, a note with
* explanation is added in a comment.
*/
@AutoService(BugChecker.class)
@BugPattern(
summary = "Migrate TestNG expected exceptions to JUnit",
linkType = NONE,
tags = REFACTORING,
severity = ERROR)
public final class TestNGExpectedExceptions extends BugChecker implements MethodTreeMatcher {
private static final long serialVersionUID = 1L;
private static final Matcher<AnnotationTree> TESTNG_ANNOTATION =
isType("org.testng.annotations.Test");
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
Optional<? extends AnnotationTree> testAnnotation =
ASTHelpers.getAnnotations(tree).stream()
.filter(annotation -> TESTNG_ANNOTATION.matches(annotation, state))
.findFirst();
if (testAnnotation.isEmpty()) {
return Description.NO_MATCH;
}
Optional<AssignmentTree> assignmentTree =
testAnnotation.get().getArguments().stream()
.filter(AssignmentTree.class::isInstance)
.map(AssignmentTree.class::cast)
.filter(
assignment ->
SourceCode.treeToString(assignment.getVariable(), state)
.equals("expectedExceptions"))
.findFirst();
if (assignmentTree.isEmpty()) {
return Description.NO_MATCH;
}
ExpressionTree argumentExpression = assignmentTree.orElseThrow().getExpression();
if (argumentExpression == null) {
return Description.NO_MATCH;
}
Optional<String> expectedException = getExpectedException(argumentExpression, state);
if (expectedException.isEmpty()) {
return Description.NO_MATCH;
}
SuggestedFix.Builder fix =
SuggestedFix.builder()
.replace(
tree.getBody(),
buildWrappedBody(tree.getBody(), expectedException.orElseThrow(), state))
.replace(
testAnnotation.get(),
buildAnnotationReplacementSource(
testAnnotation.get(), assignmentTree.orElseThrow(), state));
ImmutableList<String> removedExceptions = getRemovedExceptions(argumentExpression, state);
if (!removedExceptions.isEmpty()) {
fix.prefixWith(
testAnnotation.get(),
String.format(
"// XXX: Removed handling of `%s` because this migration doesn't support it.\n",
String.join(", ", removedExceptions)));
}
return describeMatch(testAnnotation.get(), fix.build());
}
private static String buildAnnotationReplacementSource(
AnnotationTree annotationTree, AssignmentTree argumentToRemove, VisitorState state) {
StringBuilder replacement = new StringBuilder();
replacement.append(
String.format("@%s", SourceCode.treeToString(annotationTree.getAnnotationType(), state)));
String arguments =
annotationTree.getArguments().stream()
.filter(argument -> !argument.equals(argumentToRemove))
.map(argument -> SourceCode.treeToString(argument, state))
.collect(joining(", "));
if (!arguments.isEmpty()) {
replacement.append(String.format("(%s)", arguments));
}
return replacement.toString();
}
private static Optional<String> getExpectedException(
ExpressionTree expectedExceptions, VisitorState state) {
if (expectedExceptions.getKind() == NEW_ARRAY) {
NewArrayTree arrayTree = (NewArrayTree) expectedExceptions;
if (arrayTree.getInitializers().isEmpty()) {
return Optional.empty();
}
return Optional.of(SourceCode.treeToString(arrayTree.getInitializers().get(0), state));
} else if (expectedExceptions.getKind() == MEMBER_SELECT) {
return Optional.of(SourceCode.treeToString(expectedExceptions, state));
}
return Optional.empty();
}
private static ImmutableList<String> getRemovedExceptions(
ExpressionTree expectedExceptions, VisitorState state) {
if (expectedExceptions.getKind() != NEW_ARRAY) {
return ImmutableList.of();
}
NewArrayTree arrayTree = (NewArrayTree) expectedExceptions;
if (arrayTree.getInitializers().size() <= 1) {
return ImmutableList.of();
}
return arrayTree.getInitializers().subList(1, arrayTree.getInitializers().size()).stream()
.map(initializer -> SourceCode.treeToString(initializer, state))
.collect(toImmutableList());
}
private static String buildWrappedBody(BlockTree tree, String exception, VisitorState state) {
return String.format(
"{\norg.junit.jupiter.api.Assertions.assertThrows(%s, () -> %s);\n}",
exception, SourceCode.treeToString(tree, state));
}
}

View File

@@ -0,0 +1,110 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.LinkType.NONE;
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import static com.google.errorprone.BugPattern.StandardTags.REFACTORING;
import static com.google.errorprone.matchers.Matchers.allOf;
import static com.google.errorprone.matchers.Matchers.hasArgumentWithValue;
import static com.google.errorprone.matchers.Matchers.isType;
import static com.google.errorprone.matchers.Matchers.stringLiteral;
import static com.sun.source.tree.Tree.Kind.STRING_LITERAL;
import com.google.auto.service.AutoService;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.fixes.SuggestedFixes;
import com.google.errorprone.matchers.AnnotationMatcherUtils;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.LiteralTree;
import com.sun.source.tree.MethodTree;
import java.util.Optional;
/**
* A {@link BugChecker} that will flag TestNG {@link org.testng.annotations.Test} annotations that
* can be migrated to a JUnit {@link org.junit.jupiter.params.ParameterizedTest}. These methods will
* only be flagged if a migrated version of the data provider is available, these are migrated using
* {@link TestNGDataProvider}.
*/
@AutoService(BugChecker.class)
@BugPattern(
summary = "Migrate TestNG parameterized tests to JUnit",
linkType = NONE,
tags = REFACTORING,
severity = ERROR)
public final class TestNGParameterized extends BugChecker implements MethodTreeMatcher {
private static final long serialVersionUID = 1L;
private static final Matcher<AnnotationTree> SUPPRESS_WARNINGS_ANNOTATION =
allOf(
isType("java.lang.SuppressWarnings"),
hasArgumentWithValue("value", stringLiteral("UnusedMethod")));
private static final Matcher<AnnotationTree> TESTNG_ANNOTATION =
isType("org.testng.annotations.Test");
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
Optional<? extends AnnotationTree> testAnnotation =
ASTHelpers.getAnnotations(tree).stream()
.filter(annotation -> TESTNG_ANNOTATION.matches(annotation, state))
.findFirst();
if (testAnnotation.isEmpty() || testAnnotation.get().getArguments().size() != 1) {
return Description.NO_MATCH;
}
ExpressionTree argumentExpression =
AnnotationMatcherUtils.getArgument(testAnnotation.get(), "dataProvider");
if (argumentExpression == null || argumentExpression.getKind() != STRING_LITERAL) {
return Description.NO_MATCH;
}
ClassTree classTree = state.findEnclosing(ClassTree.class);
if (classTree == null) {
return Description.NO_MATCH;
}
String providerName = ((LiteralTree) argumentExpression).getValue().toString();
Optional<MethodTree> providerMethod = findMethodInClassWithName(classTree, providerName);
Optional<MethodTree> migratedMethod =
findMethodInClassWithName(classTree, providerName + "Junit");
if (migratedMethod.isEmpty() || providerMethod.isEmpty()) {
return Description.NO_MATCH;
}
Optional<? extends AnnotationTree> suppressWarningsAnnotation =
ASTHelpers.getAnnotations(migratedMethod.orElseThrow()).stream()
.filter(annotation -> SUPPRESS_WARNINGS_ANNOTATION.matches(annotation, state))
.findFirst();
if (suppressWarningsAnnotation.isEmpty()) {
return Description.NO_MATCH;
}
return describeMatch(
testAnnotation.get(),
SuggestedFix.builder()
.addImport("org.junit.jupiter.params.ParameterizedTest")
.addImport("org.junit.jupiter.params.provider.MethodSource")
.merge(SuggestedFixes.renameMethod(migratedMethod.orElseThrow(), providerName, state))
.delete(providerMethod.orElseThrow())
.delete(suppressWarningsAnnotation.get())
.replace(
testAnnotation.get(), "@ParameterizedTest\n@MethodSource(\"" + providerName + "\")")
.build());
}
private static Optional<MethodTree> findMethodInClassWithName(ClassTree classTree, String name) {
return classTree.getMembers().stream()
.filter(MethodTree.class::isInstance)
.map(MethodTree.class::cast)
.filter(method -> method.getName().contentEquals(name))
.findFirst();
}
}

View File

@@ -0,0 +1,113 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode.TEXT_MATCH;
import com.google.errorprone.BugCheckerRefactoringTestHelper;
import com.google.errorprone.CompilationTestHelper;
import org.junit.jupiter.api.Test;
final class AnnotationAttributeReplacementTest {
private final CompilationTestHelper compilationTestHelper =
CompilationTestHelper.newInstance(AnnotationAttributeReplacement.class, getClass());
private final BugCheckerRefactoringTestHelper refactoringTestHelper =
BugCheckerRefactoringTestHelper.newInstance(AnnotationAttributeReplacement.class, getClass());
@Test
void identification() {
compilationTestHelper
.addSourceLines(
"A.java",
"import org.testng.annotations.Test;",
"",
"class A {",
" @Test(priority = 10)",
" // BUG: Diagnostic contains:",
" public void foo() {}",
"",
" @Test(description = \"unit\")",
" // BUG: Diagnostic contains:",
" public void bar() {}",
"",
" @Test(dataProvider = \"unit\")",
" public void baz() {}",
"}")
.doTest();
}
@Test
void replacement() {
refactoringTestHelper
.addInputLines(
"A.java",
"import org.testng.annotations.Test;",
"",
"class A {",
" @Test(priority = 1, groups = \"unit\", description = \"test\")",
" public void foo() {}",
"}")
.addOutputLines(
"A.java",
"import org.junit.jupiter.api.MethodOrderer;",
"import org.junit.jupiter.api.TestMethodOrder;",
"import org.testng.annotations.Test;",
"",
"@TestMethodOrder(MethodOrderer.OrderAnnotation.class)",
"class A {",
" @Test",
" @org.junit.jupiter.api.Order(1)",
" @org.junit.jupiter.api.DisplayName(\"test\")",
" @org.junit.jupiter.api.Tag(\"unit\")",
" public void foo() {}",
"}")
.doTest(TEXT_MATCH);
}
@Test
void replacementUpdateArgumentListAfterSingleArgument() {
refactoringTestHelper
.addInputLines(
"A.java",
"import org.testng.annotations.Test;",
"",
"class A {",
" @Test(priority = 2, invocationTimeOut = 10L)",
" public void foo() {}",
"}")
.addOutputLines(
"A.java",
"import org.junit.jupiter.api.MethodOrderer;",
"import org.junit.jupiter.api.TestMethodOrder;",
"import org.testng.annotations.Test;",
"",
"@TestMethodOrder(MethodOrderer.OrderAnnotation.class)",
"class A {",
" @Test(invocationTimeOut = 10L)",
" @org.junit.jupiter.api.Order(2)",
" public void foo() {}",
"}")
.doTest(TEXT_MATCH);
}
@Test
void replacementSingleThreaded() {
refactoringTestHelper
.addInputLines(
"A.java",
"import org.testng.annotations.Test;",
"",
"class A {",
" @Test(singleThreaded = true)",
" public void foo() {}",
"}")
.addOutputLines(
"A.java",
"import org.testng.annotations.Test;",
"",
"class A {",
" // XXX: Removed argument `singleThreaded = true`, as this cannot be migrated to JUnit!",
" @Test",
" public void foo() {}",
"}")
.doTest(TEXT_MATCH);
}
}

View File

@@ -0,0 +1,71 @@
package tech.picnic.errorprone.bugpatterns;
import com.google.errorprone.BugCheckerRefactoringTestHelper;
import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;
import com.google.errorprone.CompilationTestHelper;
import org.junit.jupiter.api.Test;
final class TestNGAnnotationTest {
private final CompilationTestHelper compilationTestHelper =
CompilationTestHelper.newInstance(TestNGAnnotation.class, getClass());
private final BugCheckerRefactoringTestHelper refactoringTestHelper =
BugCheckerRefactoringTestHelper.newInstance(TestNGAnnotation.class, getClass());
@Test
void identification() {
compilationTestHelper
.addSourceLines(
"A.java",
"import org.testng.annotations.BeforeMethod;",
"import org.testng.annotations.Test;",
"",
"@Test",
"class A {",
" @BeforeMethod",
" // BUG: Diagnostic contains:",
" public void init() {}",
"",
" @Test",
" // BUG: Diagnostic contains:",
" public void foo() {",
" int number = 10;",
" }",
"",
" @Test(description = \"unit\")",
" public void bar() {",
" int number = 10;",
" }",
"}")
.doTest();
}
@Test
void replacement() {
refactoringTestHelper
.addInputLines(
"A.java",
"import org.testng.annotations.BeforeMethod;",
"import org.testng.annotations.Test;",
"",
"class A {",
" @BeforeMethod",
" public void init() {}",
"",
" @Test",
" public void foo() {}",
"}")
.addOutputLines(
"A.java",
"import org.testng.annotations.BeforeMethod;",
"import org.testng.annotations.Test;",
"",
"class A {",
" @org.junit.jupiter.api.BeforeEach",
" public void init() {}",
"",
" @org.junit.jupiter.api.Test",
" public void foo() {}",
"}")
.doTest(TestMode.TEXT_MATCH);
}
}

View File

@@ -0,0 +1,79 @@
package tech.picnic.errorprone.bugpatterns;
import com.google.errorprone.BugCheckerRefactoringTestHelper;
import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;
import com.google.errorprone.CompilationTestHelper;
import org.junit.jupiter.api.Test;
final class TestNGClassLevelTestAnnotationTest {
private final CompilationTestHelper compilationTestHelper =
CompilationTestHelper.newInstance(TestNGClassLevelTestAnnotation.class, getClass());
private final BugCheckerRefactoringTestHelper refactoringTestHelper =
BugCheckerRefactoringTestHelper.newInstance(TestNGClassLevelTestAnnotation.class, getClass());
@Test
void identification() {
compilationTestHelper
.addSourceLines(
"A.java",
"import org.testng.annotations.Test;",
"",
"// BUG: Diagnostic contains:",
"@Test",
"class A {",
" public void foo() {}",
"",
" @Test(description = \"unit\")",
" public void bar() {}",
"}")
.doTest();
}
@Test
void replacement() {
refactoringTestHelper
.addInputLines(
"A.java",
"import org.testng.annotations.BeforeMethod;",
"import org.testng.annotations.Test;",
"",
"@Test",
"class A {",
" @BeforeMethod",
" public void init() {}",
"",
" public void foo() {}",
"",
" @Test(priority = 12)",
" public void bar() {}",
"",
" private void baz() {}",
"",
" protected void qux() {}",
"",
" void quux() {}",
"}")
.addOutputLines(
"A.java",
"import org.testng.annotations.BeforeMethod;",
"import org.testng.annotations.Test;",
"",
"class A {",
" @BeforeMethod",
" public void init() {}",
"",
" @Test",
" public void foo() {}",
"",
" @Test(priority = 12)",
" public void bar() {}",
"",
" private void baz() {}",
"",
" protected void qux() {}",
"",
" void quux() {}",
"}")
.doTest(TestMode.TEXT_MATCH);
}
}

View File

@@ -0,0 +1,391 @@
package tech.picnic.errorprone.bugpatterns;
import com.google.errorprone.BugCheckerRefactoringTestHelper;
import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;
import com.google.errorprone.CompilationTestHelper;
import org.junit.jupiter.api.Test;
final class TestNGDataProviderTest {
private final CompilationTestHelper compilationTestHelper =
CompilationTestHelper.newInstance(TestNGDataProvider.class, getClass());
private final BugCheckerRefactoringTestHelper refactoringTestHelper =
BugCheckerRefactoringTestHelper.newInstance(TestNGDataProvider.class, getClass());
@Test
void identification() {
compilationTestHelper
.addSourceLines(
"A.java",
"import org.testng.annotations.DataProvider;",
"",
"class A {",
" // BUG: Diagnostic contains:",
" @DataProvider",
" private Object[][] fooNumbers() {",
" return new Object[][] {",
" {\"1\", 1},",
" {\"2\", 2}",
" };",
" }",
"",
" // BUG: Diagnostic contains:",
" @DataProvider",
" private Object[] barNumbers() {",
" return new Object[][] {",
" {\"1\", 1},",
" {\"2\", 2}",
" };",
" }",
"",
" // BUG: Diagnostic contains:",
" @DataProvider",
" private Object[] bazNumbers() {",
" return new Object[] {1, 2};",
" }",
"}")
.doTest();
}
@Test
void alreadyMigratedIdentification() {
compilationTestHelper
.addSourceLines(
"A.java",
"import static org.junit.jupiter.params.provider.Arguments.arguments;",
"",
"import java.util.stream.Stream;",
"import org.junit.jupiter.params.provider.Arguments;",
"import org.testng.annotations.DataProvider;",
"",
"class A {",
" @DataProvider",
" private Object[][] quxNumbers() {",
" return new Object[][] {",
" {\"1\", 1},",
" {\"2\", 2}",
" };",
" }",
"",
" @SuppressWarnings(\"UnusedMethod\" /* This is an intermediate state for the JUnit migration. */)",
" private static final Stream<Arguments> quxNumbersJunit() {",
" return Stream.of(arguments(\"1\", 1), arguments(\"2\", 2));",
" }",
"}")
.doTest();
}
@Test
void replacement1DArray() {
refactoringTestHelper
.addInputLines(
"A.java",
"import org.testng.annotations.DataProvider;",
"",
"class A {",
" @DataProvider",
" private Object[] numbers() {",
" int[] values = new int[] {1, 2};",
" return new Object[] {",
" // first",
" values[0],",
" // second",
" values[1]",
" };",
" }",
"}")
.addOutputLines(
"A.java",
"import static org.junit.jupiter.params.provider.Arguments.arguments;",
"",
"import java.util.stream.Stream;",
"import org.junit.jupiter.params.provider.Arguments;",
"import org.testng.annotations.DataProvider;",
"",
"class A {",
" @DataProvider",
" private Object[] numbers() {",
" int[] values = new int[] {1, 2};",
" return new Object[] {",
" // first",
" values[0],",
" // second",
" values[1]",
" };",
" }",
"",
" @SuppressWarnings(\"UnusedMethod\" /* This is an intermediate state for the JUnit migration. */)",
" private static Stream<Arguments> numbersJunit() {",
" int[] values = new int[] {1, 2};",
" return Stream.of(",
" // first",
" arguments(values[0]),",
" // second",
" arguments(values[1]));",
" }",
"}")
.doTest(TestMode.TEXT_MATCH);
}
@Test
void replacement1DArray2DReturnType() {
refactoringTestHelper
.addInputLines(
"A.java",
"import org.testng.annotations.DataProvider;",
"",
"class A {",
" @DataProvider",
" private Object[] numbers() {",
" int[] values = new int[] {1, 2};",
" return new Object[][] {",
" {String.valueOf(values[0]), values[0]},",
" {String.valueOf(values[1]), values[1]}",
" };",
" }",
"}")
.addOutputLines(
"A.java",
"import static org.junit.jupiter.params.provider.Arguments.arguments;",
"",
"import java.util.stream.Stream;",
"import org.junit.jupiter.params.provider.Arguments;",
"import org.testng.annotations.DataProvider;",
"",
"class A {",
" @DataProvider",
" private Object[] numbers() {",
" int[] values = new int[] {1, 2};",
" return new Object[][] {",
" {String.valueOf(values[0]), values[0]},",
" {String.valueOf(values[1]), values[1]}",
" };",
" }",
"",
" @SuppressWarnings(\"UnusedMethod\" /* This is an intermediate state for the JUnit migration. */)",
" private static Stream<Arguments> numbersJunit() {",
" int[] values = new int[] {1, 2};",
" return Stream.of(",
" arguments(String.valueOf(values[0]), values[0]),",
" arguments(String.valueOf(values[1]), values[1]));",
" }",
"}")
.doTest(TestMode.TEXT_MATCH);
}
@Test
void replacement2DArray() {
refactoringTestHelper
.addInputLines(
"A.java",
"import org.testng.annotations.DataProvider;",
"",
"class A {",
" @DataProvider",
" private Object[][] numbers() {",
" int[] values = new int[] {1, 2};",
" return new Object[][] {",
" {\"1\", /* comment */ values[0]},",
" {\"2\", values[1]}",
" };",
" }",
"}")
.addOutputLines(
"A.java",
"import static org.junit.jupiter.params.provider.Arguments.arguments;",
"",
"import java.util.stream.Stream;",
"import org.junit.jupiter.params.provider.Arguments;",
"import org.testng.annotations.DataProvider;",
"",
"class A {",
" @DataProvider",
" private Object[][] numbers() {",
" int[] values = new int[] {1, 2};",
" return new Object[][] {",
" {\"1\", /* comment */ values[0]},",
" {\"2\", values[1]}",
" };",
" }",
"",
" @SuppressWarnings(\"UnusedMethod\" /* This is an intermediate state for the JUnit migration. */)",
" private static Stream<Arguments> numbersJunit() {",
" int[] values = new int[] {1, 2};",
" return Stream.of(arguments(\"1\", /* comment */ values[0]), arguments(\"2\", values[1]));",
" }",
"}")
.doTest(TestMode.TEXT_MATCH);
}
@Test
void replacementBody() {
refactoringTestHelper
.addInputLines(
"A.java",
"import org.testng.annotations.DataProvider;",
"",
"class A {",
" @DataProvider",
" private Object[][] numbers() {",
" // create value array",
" int[] values = new int[2];",
" values[0] = 1;",
"",
" // floating comment",
"",
" /* multi line comment */",
" values[1] = 2;",
" return new Object[][] {",
" // first",
" {\"1\", /* second */ values[0]},",
" // third",
" {",
" /* fourth */",
" \"2\", values[1]",
" }",
" };",
" }",
"}")
.addOutputLines(
"A.java",
"import static org.junit.jupiter.params.provider.Arguments.arguments;",
"",
"import java.util.stream.Stream;",
"import org.junit.jupiter.params.provider.Arguments;",
"import org.testng.annotations.DataProvider;",
"",
"class A {",
" @DataProvider",
" private Object[][] numbers() {",
" // create value array",
" int[] values = new int[2];",
" values[0] = 1;",
"",
" // floating comment",
"",
" /* multi line comment */",
" values[1] = 2;",
" return new Object[][] {",
" // first",
" {\"1\", /* second */ values[0]},",
" // third",
" {",
" /* fourth */",
" \"2\", values[1]",
" }",
" };",
" }",
"",
" @SuppressWarnings(\"UnusedMethod\" /* This is an intermediate state for the JUnit migration. */)",
" private static Stream<Arguments> numbersJunit() {",
" // create value array",
" int[] values = new int[2];",
" values[0] = 1;",
"",
" // floating comment",
"",
" /* multi line comment */",
" values[1] = 2;",
" return Stream.of(",
" // first",
" arguments(\"1\", /* second */ values[0]),",
" // third",
" arguments(",
" /* fourth */",
" \"2\", values[1]));",
" }",
"}")
.doTest(TestMode.TEXT_MATCH);
}
@Test
void replacementThrows() {
refactoringTestHelper
.addInputLines(
"A.java",
"import java.io.IOException;",
"import org.testng.annotations.DataProvider;",
"",
"class A {",
" @DataProvider",
" private Object[][] numbers() throws InterruptedException, IOException {",
" // create value array",
" int[] values = new int[] {1, 2};",
" return new Object[][] {",
" {\"1\", values[0]},",
" {\"2\", values[1]}",
" };",
" }",
"}")
.addOutputLines(
"A.java",
"import static org.junit.jupiter.params.provider.Arguments.arguments;",
"",
"import java.io.IOException;",
"import java.util.stream.Stream;",
"import org.junit.jupiter.params.provider.Arguments;",
"import org.testng.annotations.DataProvider;",
"",
"class A {",
" @DataProvider",
" private Object[][] numbers() throws InterruptedException, IOException {",
" // create value array",
" int[] values = new int[] {1, 2};",
" return new Object[][] {",
" {\"1\", values[0]},",
" {\"2\", values[1]}",
" };",
" }",
"",
" @SuppressWarnings(\"UnusedMethod\" /* This is an intermediate state for the JUnit migration. */)",
" private static Stream<Arguments> numbersJunit() throws InterruptedException, IOException {",
" // create value array",
" int[] values = new int[] {1, 2};",
" return Stream.of(arguments(\"1\", values[0]), arguments(\"2\", values[1]));",
" }",
"}")
.doTest(TestMode.TEXT_MATCH);
}
@Test
void replacementGetClass() {
refactoringTestHelper
.addInputLines(
"A.java",
"import org.testng.annotations.DataProvider;",
"",
"class A {",
"",
" @DataProvider",
" private Object[][] numbers() {",
" return new Object[][] {",
" {getClass().getSimpleName(), 1},",
" {this.getClass().getSimpleName(), 2}",
" };",
" }",
"}")
.addOutputLines(
"A.java",
"import static org.junit.jupiter.params.provider.Arguments.arguments;",
"",
"import java.util.stream.Stream;",
"import org.junit.jupiter.params.provider.Arguments;",
"import org.testng.annotations.DataProvider;",
"",
"class A {",
"",
" @DataProvider",
" private Object[][] numbers() {",
" return new Object[][] {",
" {getClass().getSimpleName(), 1},",
" {this.getClass().getSimpleName(), 2}",
" };",
" }",
"",
" @SuppressWarnings(\"UnusedMethod\" /* This is an intermediate state for the JUnit migration. */)",
" private static Stream<Arguments> numbersJunit() {",
" return Stream.of(arguments(A.class.getSimpleName(), 1), arguments(A.class.getSimpleName(), 2));",
" }",
"}")
.doTest(TestMode.TEXT_MATCH);
}
}

View File

@@ -0,0 +1,111 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode.TEXT_MATCH;
import com.google.errorprone.BugCheckerRefactoringTestHelper;
import com.google.errorprone.CompilationTestHelper;
import org.junit.jupiter.api.Test;
final class TestNGExpectedExceptionsTest {
private final CompilationTestHelper compilationTestHelper =
CompilationTestHelper.newInstance(TestNGExpectedExceptions.class, getClass());
private final BugCheckerRefactoringTestHelper refactoringTestHelper =
BugCheckerRefactoringTestHelper.newInstance(TestNGExpectedExceptions.class, getClass());
@Test
void identification() {
compilationTestHelper
.addSourceLines(
"A.java",
"import org.testng.annotations.Test;",
"",
"class A {",
" // BUG: Diagnostic contains:",
" @Test(expectedExceptions = RuntimeException.class)",
" public void foo() {",
" throw new RuntimeException(\"foo\");",
" }",
"",
" @Test(description = \"unit\")",
" public void bar() {",
" int number = 10;",
" }",
"}")
.doTest();
}
@Test
void replacement() {
refactoringTestHelper
.addInputLines(
"A.java",
"import org.testng.annotations.Test;",
"",
"class A {",
" @Test(expectedExceptions = RuntimeException.class)",
" public void foo() {",
" throw new RuntimeException(\"foo\");",
" }",
"",
" @Test(priority = 10, expectedExceptions = RuntimeException.class)",
" public void bar() {",
" throw new RuntimeException(\"bar\");",
" }",
"}")
.addOutputLines(
"A.java",
"import org.testng.annotations.Test;",
"",
"class A {",
" @Test",
" public void foo() {",
" org.junit.jupiter.api.Assertions.assertThrows(",
" RuntimeException.class,",
" () -> {",
" throw new RuntimeException(\"foo\");",
" });",
" }",
"",
" @Test(priority = 10)",
" public void bar() {",
" org.junit.jupiter.api.Assertions.assertThrows(",
" RuntimeException.class,",
" () -> {",
" throw new RuntimeException(\"bar\");",
" });",
" }",
"}")
.doTest(TEXT_MATCH);
}
@Test
void arrayReplacement() {
refactoringTestHelper
.addInputLines(
"A.java",
"import org.testng.annotations.Test;",
"",
"class A {",
" @Test(expectedExceptions = {RuntimeException.class, ArithmeticException.class})",
" public void foo() {",
" throw new RuntimeException(\"foo\");",
" }",
"}")
.addOutputLines(
"A.java",
"import org.testng.annotations.Test;",
"",
"class A {",
" // XXX: Removed handling of `ArithmeticException.class` because this migration doesn't support it.",
" @Test",
" public void foo() {",
" org.junit.jupiter.api.Assertions.assertThrows(",
" RuntimeException.class,",
" () -> {",
" throw new RuntimeException(\"foo\");",
" });",
" }",
"}")
.doTest(TEXT_MATCH);
}
}

View File

@@ -0,0 +1,118 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode.TEXT_MATCH;
import com.google.errorprone.BugCheckerRefactoringTestHelper;
import com.google.errorprone.CompilationTestHelper;
import org.junit.jupiter.api.Test;
final class TestNGParameterizedTest {
private final CompilationTestHelper compilationTestHelper =
CompilationTestHelper.newInstance(TestNGParameterized.class, getClass());
private final BugCheckerRefactoringTestHelper refactoringTestHelper =
BugCheckerRefactoringTestHelper.newInstance(TestNGParameterized.class, getClass());
@Test
void identification() {
compilationTestHelper
.addSourceLines(
"A.java",
"import static org.junit.jupiter.params.provider.Arguments.arguments;",
"",
"import java.util.stream.Stream;",
"import org.junit.jupiter.params.provider.Arguments;",
"import org.testng.annotations.DataProvider;",
"import org.testng.annotations.Test;",
"",
"class A {",
" // BUG: Diagnostic contains:",
" @Test(dataProvider = \"fooNumbers\")",
" public void foo(String first, int second) {}",
"",
" @DataProvider",
" private Object[][] fooNumbers() {",
" return new Object[][] {",
" {\"1\", 1},",
" {\"2\", 2}",
" };",
" }",
"",
" @SuppressWarnings(\"UnusedMethod\")",
" private static final Stream<Arguments> fooNumbersJunit() {",
" return Stream.of(arguments(\"1\", 1), arguments(\"2\", 2));",
" }",
"",
" @Test(dataProvider = \"barNumbers\")",
" public void bar(String first, int second) {}",
"",
" @DataProvider",
" private Object[][] barNumbers() {",
" return new Object[][] {",
" {\"1\", 1},",
" {\"2\", 2}",
" };",
" }",
"",
" @Test(dataProvider = \"fooNumbers\", description = \"foo\")",
" public void secondFoo() {}",
"}")
.doTest();
}
@Test
void replacement() {
refactoringTestHelper
.addInputLines(
"A.java",
"import static org.junit.jupiter.params.provider.Arguments.arguments;",
"",
"import java.util.stream.Stream;",
"import org.junit.jupiter.params.provider.Arguments;",
"import org.testng.annotations.DataProvider;",
"import org.testng.annotations.Test;",
"",
"class A {",
"",
" @Test(dataProvider = \"fooNumbers\")",
" public void foo(String string, int number) {}",
"",
" @DataProvider",
" private Object[][] fooNumbers() {",
" int[] values = new int[] {1, 2};",
" return new Object[][] {",
" {\"1\", values[0]},",
" {\"2\", values[1]}",
" };",
" }",
"",
" @SuppressWarnings(\"UnusedMethod\")",
" private static Stream<Arguments> fooNumbersJunit() {",
" int[] values = new int[] {1, 2};",
" return Stream.of(arguments(\"1\", values[0]), arguments(\"2\", values[1]));",
" }",
"}")
.addOutputLines(
"A.java",
"import static org.junit.jupiter.params.provider.Arguments.arguments;",
"",
"import java.util.stream.Stream;",
"import org.junit.jupiter.params.ParameterizedTest;",
"import org.junit.jupiter.params.provider.Arguments;",
"import org.junit.jupiter.params.provider.MethodSource;",
"import org.testng.annotations.DataProvider;",
"import org.testng.annotations.Test;",
"",
"class A {",
"",
" @ParameterizedTest",
" @MethodSource(\"fooNumbers\")",
" public void foo(String string, int number) {}",
"",
" private static Stream<Arguments> fooNumbers() {",
" int[] values = new int[] {1, 2};",
" return Stream.of(arguments(\"1\", values[0]), arguments(\"2\", values[1]));",
" }",
"}")
.doTest(TEXT_MATCH);
}
}

View File

@@ -878,6 +878,7 @@
<arg>--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>
<arg>--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
<arg>--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
<arg>--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg>
<arg>--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
<arg>--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
<arg>-Xmaxerrs</arg>
@@ -1023,6 +1024,7 @@
<additionalJOption>--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</additionalJOption>
<additionalJOption>--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</additionalJOption>
<additionalJOption>--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</additionalJOption>
<additionalJOption>--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</additionalJOption>
<additionalJOption>--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</additionalJOption>
<additionalJOption>--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</additionalJOption>
</additionalJOptions>