mirror of
https://github.com/jlengrand/error-prone-support.git
synced 2026-03-10 15:49:33 +00:00
Compare commits
73 Commits
maxsumrall
...
pdsoels/we
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6417a5e9bf | ||
|
|
21effe0aa2 | ||
|
|
290ad2c8ee | ||
|
|
3b2e42cfbb | ||
|
|
43b8b32d90 | ||
|
|
2c914752cd | ||
|
|
8d70f1db64 | ||
|
|
809b4c6f90 | ||
|
|
0ea11dd8ef | ||
|
|
f5f814f49f | ||
|
|
a8187dbd29 | ||
|
|
a211eaaac4 | ||
|
|
1fd6e35e00 | ||
|
|
2cc8c42155 | ||
|
|
e9829d93bf | ||
|
|
b273502e88 | ||
|
|
8c6bd1b6e7 | ||
|
|
0c1817c589 | ||
|
|
73cf28e7ff | ||
|
|
8a0abf5957 | ||
|
|
5fb4aed3ad | ||
|
|
aef9c5da7a | ||
|
|
7069e7a6d8 | ||
|
|
334c374ca1 | ||
|
|
57cd084f82 | ||
|
|
0b3be1b75b | ||
|
|
902538fd4a | ||
|
|
50f6b770e4 | ||
|
|
47e0a779bd | ||
|
|
973d3c3cd9 | ||
|
|
edb7290e2e | ||
|
|
d5c45e003f | ||
|
|
f784c64150 | ||
|
|
978c90db9d | ||
|
|
ae89a37934 | ||
|
|
8f1d1df747 | ||
|
|
04368e9243 | ||
|
|
156df71616 | ||
|
|
64b1c7eea4 | ||
|
|
80d0d85826 | ||
|
|
d30c99a28f | ||
|
|
29c23542da | ||
|
|
62c1c277ae | ||
|
|
8580e89008 | ||
|
|
06c8b164e9 | ||
|
|
fd9d3157bc | ||
|
|
a623f73c1c | ||
|
|
f9d0cd99d6 | ||
|
|
9bec3de372 | ||
|
|
4164514c5b | ||
|
|
c3cd535b16 | ||
|
|
64195279cc | ||
|
|
61c9f67f66 | ||
|
|
4bb14b01ec | ||
|
|
b267b4dba8 | ||
|
|
03f0e0493b | ||
|
|
2111c81784 | ||
|
|
43d50f2ef9 | ||
|
|
2d972fd975 | ||
|
|
ee265a87ae | ||
|
|
6b4fba62da | ||
|
|
e883e28e34 | ||
|
|
d84de6efba | ||
|
|
4dca61a144 | ||
|
|
dc9597a603 | ||
|
|
ec9853ac88 | ||
|
|
5bb1dd1a10 | ||
|
|
fd6a45ebd8 | ||
|
|
82d4677509 | ||
|
|
1fdf1016b7 | ||
|
|
80e537fce2 | ||
|
|
d85897ea62 | ||
|
|
c5bde3999d |
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -42,9 +42,9 @@ Please replace this sentence with log output, if applicable.
|
||||
<!-- Please complete the following information: -->
|
||||
|
||||
- Operating system (e.g. MacOS Monterey).
|
||||
- Java version (i.e. `java --version`, e.g. `17.0.3`).
|
||||
- Error Prone version (e.g. `2.15.0`).
|
||||
- Error Prone Support version (e.g. `0.3.0`).
|
||||
- Java version (i.e. `java --version`, e.g. `17.0.6`).
|
||||
- Error Prone version (e.g. `2.18.0`).
|
||||
- Error Prone Support version (e.g. `0.8.0`).
|
||||
|
||||
### Additional context
|
||||
|
||||
|
||||
6
.github/workflows/build.yaml
vendored
6
.github/workflows/build.yaml
vendored
@@ -10,16 +10,16 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-22.04 ]
|
||||
jdk: [ 11.0.16, 17.0.4, 19 ]
|
||||
jdk: [ 11.0.18, 17.0.6, 19.0.2 ]
|
||||
distribution: [ temurin ]
|
||||
experimental: [ false ]
|
||||
include:
|
||||
- os: macos-12
|
||||
jdk: 17.0.4
|
||||
jdk: 17.0.6
|
||||
distribution: temurin
|
||||
experimental: false
|
||||
- os: windows-2022
|
||||
jdk: 17.0.4
|
||||
jdk: 17.0.6
|
||||
distribution: temurin
|
||||
experimental: false
|
||||
- os: ubuntu-22.04
|
||||
|
||||
21
.github/workflows/deploy-website.yaml
vendored
21
.github/workflows/deploy-website.yaml
vendored
@@ -19,16 +19,21 @@ jobs:
|
||||
bundler-cache: true
|
||||
- name: Configure Github Pages
|
||||
uses: actions/configure-pages@v2.1.3
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v3.5.1
|
||||
with:
|
||||
java-version: 17.0.4
|
||||
distribution: temurin
|
||||
cache: maven
|
||||
- name: Compile project and extract data
|
||||
# XXX: Remove `-Dverification.{skip|warn}` and update `website/README.md`
|
||||
# once module `docgen` has no errors.
|
||||
run: |
|
||||
mvn -T1C clean install -DskipTests -Dverification.warn
|
||||
mvn -T1C clean install -DskipTests -Dverification.skip -Pdocgen
|
||||
- name: Generate documentation
|
||||
run: ./generate-docs.sh
|
||||
- name: Build website with Jekyll
|
||||
working-directory: ./website
|
||||
run: bundle exec jekyll build
|
||||
- name: Validate HTML output
|
||||
working-directory: ./website
|
||||
# XXX: Drop `--disable_external true` once we fully adopted the
|
||||
# "Refaster rules" terminology on our website and in the code.
|
||||
run: bundle exec htmlproofer --disable_external true --check-external-hash false ./_site
|
||||
run: bundle exec ruby generate-docs.rb
|
||||
- name: Upload website as artifact
|
||||
uses: actions/upload-pages-artifact@v1.0.5
|
||||
with:
|
||||
|
||||
2
.github/workflows/pitest-analyze-pr.yml
vendored
2
.github/workflows/pitest-analyze-pr.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v3.8.0
|
||||
with:
|
||||
java-version: 17.0.4
|
||||
java-version: 17.0.6
|
||||
distribution: temurin
|
||||
cache: maven
|
||||
- name: Run Pitest
|
||||
|
||||
2
.github/workflows/pitest-update-pr.yml
vendored
2
.github/workflows/pitest-update-pr.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v3.8.0
|
||||
with:
|
||||
java-version: 17.0.4
|
||||
java-version: 17.0.6
|
||||
distribution: temurin
|
||||
cache: maven
|
||||
- name: Download Pitest analysis artifact
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<groupId>tech.picnic.error-prone-support</groupId>
|
||||
<artifactId>error-prone-support</artifactId>
|
||||
<version>0.8.1-SNAPSHOT</version>
|
||||
<version>0.9.1-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>documentation-support</artifactId>
|
||||
@@ -27,6 +27,10 @@
|
||||
<groupId>${groupId.error-prone}</groupId>
|
||||
<artifactId>error_prone_check_api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${groupId.error-prone}</groupId>
|
||||
<artifactId>error_prone_core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${groupId.error-prone}</groupId>
|
||||
<artifactId>error_prone_test_helpers</artifactId>
|
||||
|
||||
@@ -5,17 +5,19 @@ import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import com.google.auto.common.AnnotationMirrors;
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.annotations.Immutable;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.AnnotationTree;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import com.sun.tools.javac.code.Attribute;
|
||||
import com.sun.tools.javac.code.Symbol.ClassSymbol;
|
||||
import com.sun.tools.javac.util.Context;
|
||||
import java.util.Optional;
|
||||
import javax.lang.model.element.AnnotationValue;
|
||||
import tech.picnic.errorprone.documentation.BugPatternExtractor.BugPatternDocumentation;
|
||||
|
||||
@@ -23,29 +25,38 @@ import tech.picnic.errorprone.documentation.BugPatternExtractor.BugPatternDocume
|
||||
* An {@link Extractor} that describes how to extract data from a {@code @BugPattern} annotation.
|
||||
*/
|
||||
@Immutable
|
||||
final class BugPatternExtractor implements Extractor<BugPatternDocumentation> {
|
||||
@Override
|
||||
public BugPatternDocumentation extract(ClassTree tree, Context context) {
|
||||
ClassSymbol symbol = ASTHelpers.getSymbol(tree);
|
||||
BugPattern annotation = symbol.getAnnotation(BugPattern.class);
|
||||
requireNonNull(annotation, "BugPattern annotation must be present");
|
||||
@AutoService(Extractor.class)
|
||||
public final class BugPatternExtractor implements Extractor<BugPatternDocumentation> {
|
||||
/** Instantiates a new {@link BugPatternExtractor} instance. */
|
||||
public BugPatternExtractor() {}
|
||||
|
||||
return new AutoValue_BugPatternExtractor_BugPatternDocumentation(
|
||||
symbol.getQualifiedName().toString(),
|
||||
annotation.name().isEmpty() ? tree.getSimpleName().toString() : annotation.name(),
|
||||
ImmutableList.copyOf(annotation.altNames()),
|
||||
annotation.link(),
|
||||
ImmutableList.copyOf(annotation.tags()),
|
||||
annotation.summary(),
|
||||
annotation.explanation(),
|
||||
annotation.severity(),
|
||||
annotation.disableable(),
|
||||
annotation.documentSuppression() ? getSuppressionAnnotations(tree) : ImmutableList.of());
|
||||
@Override
|
||||
public String identifier() {
|
||||
return "bugpattern";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canExtract(ClassTree tree) {
|
||||
return ASTHelpers.hasDirectAnnotationWithSimpleName(tree, BugPattern.class.getSimpleName());
|
||||
public Optional<BugPatternDocumentation> tryExtract(ClassTree tree, VisitorState state) {
|
||||
ClassSymbol symbol = ASTHelpers.getSymbol(tree);
|
||||
BugPattern annotation = symbol.getAnnotation(BugPattern.class);
|
||||
if (annotation == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(
|
||||
new AutoValue_BugPatternExtractor_BugPatternDocumentation(
|
||||
symbol.getQualifiedName().toString(),
|
||||
annotation.name().isEmpty() ? tree.getSimpleName().toString() : annotation.name(),
|
||||
ImmutableList.copyOf(annotation.altNames()),
|
||||
annotation.link(),
|
||||
ImmutableList.copyOf(annotation.tags()),
|
||||
annotation.summary(),
|
||||
annotation.explanation(),
|
||||
annotation.severity(),
|
||||
annotation.disableable(),
|
||||
annotation.documentSuppression()
|
||||
? getSuppressionAnnotations(tree)
|
||||
: ImmutableList.of()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import static com.google.errorprone.matchers.Matchers.allOf;
|
||||
import static com.google.errorprone.matchers.Matchers.anything;
|
||||
import static com.google.errorprone.matchers.Matchers.argument;
|
||||
import static com.google.errorprone.matchers.Matchers.classLiteral;
|
||||
import static com.google.errorprone.matchers.Matchers.hasAnnotation;
|
||||
import static com.google.errorprone.matchers.Matchers.instanceMethod;
|
||||
import static com.google.errorprone.matchers.Matchers.toType;
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.annotations.Immutable;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.MemberSelectTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import com.sun.source.util.TreeScanner;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.regex.Pattern;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import tech.picnic.errorprone.documentation.BugPatternTestExtractor.BugPatternTestDocumentation;
|
||||
|
||||
/**
|
||||
* An {@link Extractor} that describes how to extract data from classes that test a {@code
|
||||
* BugChecker}.
|
||||
*/
|
||||
@Immutable
|
||||
@AutoService(Extractor.class)
|
||||
public final class BugPatternTestExtractor implements Extractor<BugPatternTestDocumentation> {
|
||||
private static final Pattern TEST_CLASS_NAME_PATTERN = Pattern.compile("(.*)Test");
|
||||
private static final Matcher<Tree> JUNIT_TEST_METHOD =
|
||||
toType(MethodTree.class, hasAnnotation("org.junit.jupiter.api.Test"));
|
||||
private static final Matcher<MethodInvocationTree> BUG_PATTERN_TEST_METHOD =
|
||||
allOf(
|
||||
staticMethod()
|
||||
.onDescendantOfAny(
|
||||
"com.google.errorprone.CompilationTestHelper",
|
||||
"com.google.errorprone.BugCheckerRefactoringTestHelper")
|
||||
.named("newInstance"),
|
||||
argument(0, classLiteral(anything())));
|
||||
|
||||
/** Instantiates a new {@link BugPatternTestExtractor} instance. */
|
||||
public BugPatternTestExtractor() {}
|
||||
|
||||
@Override
|
||||
public String identifier() {
|
||||
return "bugpattern-test";
|
||||
}
|
||||
|
||||
// XXX: Improve support for correctly extracting multiple sources from a single
|
||||
// `{BugCheckerRefactoring,Compilation}TestHelper` test.
|
||||
@Override
|
||||
public Optional<BugPatternTestDocumentation> tryExtract(ClassTree tree, VisitorState state) {
|
||||
return getClassUnderTest(tree)
|
||||
.filter(bugPatternName -> testsBugPattern(bugPatternName, tree, state))
|
||||
.map(
|
||||
bugPatternName -> {
|
||||
CollectBugPatternTests scanner = new CollectBugPatternTests();
|
||||
|
||||
for (Tree m : tree.getMembers()) {
|
||||
if (JUNIT_TEST_METHOD.matches(m, state)) {
|
||||
scanner.scan(m, state);
|
||||
}
|
||||
}
|
||||
|
||||
return new AutoValue_BugPatternTestExtractor_BugPatternTestDocumentation(
|
||||
bugPatternName, scanner.getIdentificationTests(), scanner.getReplacementTests());
|
||||
});
|
||||
}
|
||||
|
||||
private static boolean testsBugPattern(
|
||||
String bugPatternName, ClassTree tree, VisitorState state) {
|
||||
AtomicBoolean result = new AtomicBoolean(false);
|
||||
|
||||
new TreeScanner<@Nullable Void, @Nullable Void>() {
|
||||
@Override
|
||||
public @Nullable Void visitMethodInvocation(MethodInvocationTree node, @Nullable Void v) {
|
||||
if (BUG_PATTERN_TEST_METHOD.matches(node, state)) {
|
||||
MemberSelectTree firstArgumentTree = (MemberSelectTree) node.getArguments().get(0);
|
||||
result.compareAndSet(
|
||||
/* expectedValue= */ false,
|
||||
bugPatternName.equals(firstArgumentTree.getExpression().toString()));
|
||||
}
|
||||
|
||||
return super.visitMethodInvocation(node, v);
|
||||
}
|
||||
}.scan(tree, null);
|
||||
|
||||
return result.get();
|
||||
}
|
||||
|
||||
private static Optional<String> getClassUnderTest(ClassTree tree) {
|
||||
return Optional.of(TEST_CLASS_NAME_PATTERN.matcher(tree.getSimpleName().toString()))
|
||||
.filter(java.util.regex.Matcher::matches)
|
||||
.map(m -> m.group(1));
|
||||
}
|
||||
|
||||
private static final class CollectBugPatternTests
|
||||
extends TreeScanner<@Nullable Void, VisitorState> {
|
||||
private static final Matcher<ExpressionTree> IDENTIFICATION_SOURCE_LINES =
|
||||
instanceMethod()
|
||||
.onDescendantOf("com.google.errorprone.CompilationTestHelper")
|
||||
.named("addSourceLines");
|
||||
private static final Matcher<ExpressionTree> REPLACEMENT_INPUT =
|
||||
instanceMethod()
|
||||
.onDescendantOf("com.google.errorprone.BugCheckerRefactoringTestHelper")
|
||||
.named("addInputLines");
|
||||
private static final Matcher<ExpressionTree> REPLACEMENT_OUTPUT =
|
||||
instanceMethod()
|
||||
.onDescendantOf("com.google.errorprone.BugCheckerRefactoringTestHelper.ExpectOutput")
|
||||
.named("addOutputLines");
|
||||
|
||||
private final List<String> identificationTests = new ArrayList<>();
|
||||
private final List<BugPatternReplacementTestDocumentation> replacementTests = new ArrayList<>();
|
||||
|
||||
public ImmutableList<String> getIdentificationTests() {
|
||||
return ImmutableList.copyOf(identificationTests);
|
||||
}
|
||||
|
||||
public ImmutableList<BugPatternReplacementTestDocumentation> getReplacementTests() {
|
||||
return ImmutableList.copyOf(replacementTests);
|
||||
}
|
||||
|
||||
// XXX: Consider:
|
||||
// - Whether to omit or handle differently identification tests without `// BUG: Diagnostic
|
||||
// (contains|matches)` markers.
|
||||
// - Whether to omit or handle differently replacement tests with identical input and output.
|
||||
// (Though arguably we should have a separate checker which replaces such cases with
|
||||
// `.expectUnchanged()`.)
|
||||
// - Whether to track `.expectUnchanged()` test cases.
|
||||
@Override
|
||||
public @Nullable Void visitMethodInvocation(MethodInvocationTree node, VisitorState state) {
|
||||
if (IDENTIFICATION_SOURCE_LINES.matches(node, state)) {
|
||||
getSourceCode(node).ifPresent(identificationTests::add);
|
||||
} else if (REPLACEMENT_OUTPUT.matches(node, state)) {
|
||||
ExpressionTree receiver = ASTHelpers.getReceiver(node);
|
||||
// XXX: Make this code nicer.
|
||||
if (REPLACEMENT_INPUT.matches(receiver, state)) {
|
||||
getSourceCode(node)
|
||||
.ifPresent(
|
||||
output ->
|
||||
getSourceCode((MethodInvocationTree) receiver)
|
||||
.ifPresent(
|
||||
input ->
|
||||
replacementTests.add(
|
||||
new AutoValue_BugPatternTestExtractor_BugPatternReplacementTestDocumentation(
|
||||
input, output))));
|
||||
}
|
||||
}
|
||||
|
||||
return super.visitMethodInvocation(node, state);
|
||||
}
|
||||
|
||||
// XXX: Duplicated from `ErrorProneTestSourceFormat`. Can we do better?
|
||||
private static Optional<String> getSourceCode(MethodInvocationTree tree) {
|
||||
List<? extends ExpressionTree> sourceLines =
|
||||
tree.getArguments().subList(1, tree.getArguments().size());
|
||||
StringBuilder source = new StringBuilder();
|
||||
|
||||
for (ExpressionTree sourceLine : sourceLines) {
|
||||
String value = ASTHelpers.constValue(sourceLine, String.class);
|
||||
if (value == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
source.append(value).append('\n');
|
||||
}
|
||||
|
||||
return Optional.of(source.toString());
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: Rename?
|
||||
@AutoValue
|
||||
abstract static class BugPatternTestDocumentation {
|
||||
abstract String name();
|
||||
|
||||
abstract ImmutableList<String> identificationTests();
|
||||
|
||||
abstract ImmutableList<BugPatternReplacementTestDocumentation> replacementTests();
|
||||
}
|
||||
|
||||
// XXX: Rename?
|
||||
@AutoValue
|
||||
abstract static class BugPatternReplacementTestDocumentation {
|
||||
abstract String inputLines();
|
||||
|
||||
abstract String outputLines();
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,14 @@ import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
|
||||
import com.fasterxml.jackson.annotation.PropertyAccessor;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import com.sun.source.tree.CompilationUnitTree;
|
||||
import com.sun.source.util.TaskEvent;
|
||||
import com.sun.source.util.TaskEvent.Kind;
|
||||
import com.sun.source.util.TaskListener;
|
||||
import com.sun.source.util.TreePath;
|
||||
import com.sun.tools.javac.api.JavacTrees;
|
||||
import com.sun.tools.javac.util.Context;
|
||||
import java.io.File;
|
||||
@@ -19,6 +23,7 @@ import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ServiceLoader;
|
||||
import javax.tools.JavaFileObject;
|
||||
|
||||
/**
|
||||
@@ -27,6 +32,13 @@ import javax.tools.JavaFileObject;
|
||||
*/
|
||||
// XXX: Find a better name for this class; it doesn't generate documentation per se.
|
||||
final class DocumentationGeneratorTaskListener implements TaskListener {
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
private static final ImmutableList<Extractor<?>> EXTRACTORS =
|
||||
(ImmutableList)
|
||||
ImmutableList.copyOf(
|
||||
ServiceLoader.load(
|
||||
Extractor.class, DocumentationGeneratorTaskListener.class.getClassLoader()));
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER =
|
||||
new ObjectMapper().setVisibility(PropertyAccessor.FIELD, Visibility.ANY);
|
||||
|
||||
@@ -51,19 +63,25 @@ final class DocumentationGeneratorTaskListener implements TaskListener {
|
||||
return;
|
||||
}
|
||||
|
||||
ClassTree classTree = JavacTrees.instance(context).getTree(taskEvent.getTypeElement());
|
||||
JavaFileObject sourceFile = taskEvent.getSourceFile();
|
||||
if (classTree == null || sourceFile == null) {
|
||||
CompilationUnitTree compilationUnit = taskEvent.getCompilationUnit();
|
||||
ClassTree classTree = JavacTrees.instance(context).getTree(taskEvent.getTypeElement());
|
||||
if (sourceFile == null || compilationUnit == null || classTree == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ExtractorType.findMatchingType(classTree)
|
||||
.ifPresent(
|
||||
extractorType ->
|
||||
writeToFile(
|
||||
extractorType.getIdentifier(),
|
||||
getSimpleClassName(sourceFile.toUri()),
|
||||
extractorType.getExtractor().extract(classTree, context)));
|
||||
VisitorState state =
|
||||
VisitorState.createForUtilityPurposes(context)
|
||||
.withPath(new TreePath(new TreePath(compilationUnit), classTree));
|
||||
|
||||
for (Extractor<?> extractor : EXTRACTORS) {
|
||||
extractor
|
||||
.tryExtract(classTree, state)
|
||||
.ifPresent(
|
||||
data ->
|
||||
writeToFile(
|
||||
extractor.identifier(), getSimpleClassName(sourceFile.toUri()), data));
|
||||
}
|
||||
}
|
||||
|
||||
private void createDocsDirectory() {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.annotations.Immutable;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import com.sun.tools.javac.util.Context;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Interface implemented by classes that define how to extract data of some type {@link T} from a
|
||||
@@ -13,21 +14,20 @@ import com.sun.tools.javac.util.Context;
|
||||
@Immutable
|
||||
interface Extractor<T> {
|
||||
/**
|
||||
* Extracts and returns an instance of {@link T} using the provided arguments.
|
||||
* Returns the unique identifier of this extractor.
|
||||
*
|
||||
* @param tree The {@link ClassTree} to analyze and from which to extract instances of {@link T}.
|
||||
* @param context The {@link Context} in which the current compilation takes place.
|
||||
* @return A non-null instance of {@link T}.
|
||||
* @return A non-{@code null} string.
|
||||
*/
|
||||
// XXX: Drop `Context` parameter unless used.
|
||||
T extract(ClassTree tree, Context context);
|
||||
String identifier();
|
||||
|
||||
/**
|
||||
* Tells whether this {@link Extractor} can extract documentation content from the given {@link
|
||||
* ClassTree}.
|
||||
* Attempts to extract an instance of type {@link T} using the provided arguments.
|
||||
*
|
||||
* @param tree The {@link ClassTree} of interest.
|
||||
* @return {@code true} iff data extraction is supported.
|
||||
* @param tree The {@link ClassTree} to analyze and from which to extract an instance of type
|
||||
* {@link T}.
|
||||
* @param state A {@link VisitorState} describing the context in which the given {@link ClassTree}
|
||||
* is found.
|
||||
* @return An instance of type {@link T}, if possible.
|
||||
*/
|
||||
boolean canExtract(ClassTree tree);
|
||||
Optional<T> tryExtract(ClassTree tree, VisitorState state);
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Optional;
|
||||
|
||||
/** An enumeration of {@link Extractor} types. */
|
||||
enum ExtractorType {
|
||||
BUG_PATTERN("bugpattern", new BugPatternExtractor());
|
||||
|
||||
private static final ImmutableSet<ExtractorType> TYPES =
|
||||
Sets.immutableEnumSet(EnumSet.allOf(ExtractorType.class));
|
||||
|
||||
private final String identifier;
|
||||
private final Extractor<?> extractor;
|
||||
|
||||
ExtractorType(String identifier, Extractor<?> extractor) {
|
||||
this.identifier = identifier;
|
||||
this.extractor = extractor;
|
||||
}
|
||||
|
||||
String getIdentifier() {
|
||||
return identifier;
|
||||
}
|
||||
|
||||
Extractor<?> getExtractor() {
|
||||
return extractor;
|
||||
}
|
||||
|
||||
static Optional<ExtractorType> findMatchingType(ClassTree tree) {
|
||||
return TYPES.stream().filter(type -> type.getExtractor().canExtract(tree)).findFirst();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.annotations.Immutable;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Pattern;
|
||||
import tech.picnic.errorprone.documentation.models.RefasterTemplateCollectionTestData;
|
||||
import tech.picnic.errorprone.documentation.models.RefasterTemplateTestData;
|
||||
|
||||
@Immutable
|
||||
@AutoService(Extractor.class)
|
||||
public final class RefasterTestInputExtractor
|
||||
implements Extractor<RefasterTemplateCollectionTestData> {
|
||||
private static final Pattern TEST_INPUT_CLASS_NAME_PATTERN = Pattern.compile("(.*)TestInput");
|
||||
|
||||
@Override
|
||||
public String identifier() {
|
||||
return "refaster-test-input";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<RefasterTemplateCollectionTestData> tryExtract(
|
||||
ClassTree tree, VisitorState state) {
|
||||
Optional<String> className = getClassUnderTest(tree);
|
||||
if (className.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
ImmutableList<RefasterTemplateTestData> templateTests =
|
||||
tree.getMembers().stream()
|
||||
.filter(m -> m instanceof MethodTree)
|
||||
.map(MethodTree.class::cast)
|
||||
.filter(m -> m.getName().toString().startsWith("test"))
|
||||
.map(
|
||||
m ->
|
||||
RefasterTemplateTestData.create(
|
||||
m.getName().toString().replace("test", ""), getSourceCode(m, state)))
|
||||
.collect(toImmutableList());
|
||||
|
||||
return Optional.of(
|
||||
RefasterTemplateCollectionTestData.create(className.orElseThrow(), true, templateTests));
|
||||
}
|
||||
|
||||
private static Optional<String> getClassUnderTest(ClassTree tree) {
|
||||
return Optional.of(TEST_INPUT_CLASS_NAME_PATTERN.matcher(tree.getSimpleName().toString()))
|
||||
.filter(java.util.regex.Matcher::matches)
|
||||
.map(m -> m.group(1));
|
||||
}
|
||||
|
||||
// XXX: Duplicated from `SourceCode`. Can we do better?
|
||||
private String getSourceCode(MethodTree tree, VisitorState state) {
|
||||
String src = state.getSourceForNode(tree);
|
||||
return src != null ? src : tree.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.annotations.Immutable;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Pattern;
|
||||
import tech.picnic.errorprone.documentation.models.RefasterTemplateCollectionTestData;
|
||||
import tech.picnic.errorprone.documentation.models.RefasterTemplateTestData;
|
||||
|
||||
@Immutable
|
||||
@AutoService(Extractor.class)
|
||||
public final class RefasterTestOutputExtractor
|
||||
implements Extractor<RefasterTemplateCollectionTestData> {
|
||||
private static final Pattern TEST_INPUT_CLASS_NAME_PATTERN = Pattern.compile("(.*)TestOutput");
|
||||
|
||||
@Override
|
||||
public String identifier() {
|
||||
return "refaster-test-output";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<RefasterTemplateCollectionTestData> tryExtract(
|
||||
ClassTree tree, VisitorState state) {
|
||||
Optional<String> className = getClassUnderTest(tree);
|
||||
if (className.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
ImmutableList<RefasterTemplateTestData> templateTests =
|
||||
tree.getMembers().stream()
|
||||
.filter(m -> m instanceof MethodTree)
|
||||
.map(MethodTree.class::cast)
|
||||
.filter(m -> m.getName().toString().startsWith("test"))
|
||||
.map(
|
||||
m ->
|
||||
RefasterTemplateTestData.create(
|
||||
m.getName().toString().replace("test", ""), getSourceCode(m, state)))
|
||||
.collect(toImmutableList());
|
||||
|
||||
return Optional.of(
|
||||
RefasterTemplateCollectionTestData.create(className.orElseThrow(), true, templateTests));
|
||||
}
|
||||
|
||||
private static Optional<String> getClassUnderTest(ClassTree tree) {
|
||||
return Optional.of(TEST_INPUT_CLASS_NAME_PATTERN.matcher(tree.getSimpleName().toString()))
|
||||
.filter(java.util.regex.Matcher::matches)
|
||||
.map(m -> m.group(1));
|
||||
}
|
||||
|
||||
// XXX: Duplicated from `SourceCode`. Can we do better?
|
||||
private String getSourceCode(MethodTree tree, VisitorState state) {
|
||||
String src = state.getSourceForNode(tree);
|
||||
return src != null ? src : tree.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package tech.picnic.errorprone.documentation.models;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import java.util.List;
|
||||
import tech.picnic.errorprone.documentation.models.AutoValue_RefasterTemplateCollectionData;
|
||||
|
||||
/**
|
||||
* Object containing all data related to a Refaster template collection. This is solely used for
|
||||
* serialization.
|
||||
*/
|
||||
@AutoValue
|
||||
public abstract class RefasterTemplateCollectionData {
|
||||
public static RefasterTemplateCollectionData create(
|
||||
String name, String description, String link, List<RefasterTemplateData> templates) {
|
||||
return new AutoValue_RefasterTemplateCollectionData(name, description, link, templates);
|
||||
}
|
||||
|
||||
abstract String name();
|
||||
|
||||
abstract String description();
|
||||
|
||||
abstract String link();
|
||||
|
||||
abstract List<RefasterTemplateData> templates();
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package tech.picnic.errorprone.documentation.models;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import java.util.List;
|
||||
import tech.picnic.errorprone.documentation.models.AutoValue_RefasterTemplateCollectionTestData;
|
||||
|
||||
@AutoValue
|
||||
public abstract class RefasterTemplateCollectionTestData {
|
||||
public static RefasterTemplateCollectionTestData create(
|
||||
String templateCollection, boolean isInput, List<RefasterTemplateTestData> templatesTests) {
|
||||
return new AutoValue_RefasterTemplateCollectionTestData(
|
||||
templateCollection, isInput, templatesTests);
|
||||
}
|
||||
|
||||
abstract String templateCollection();
|
||||
|
||||
abstract boolean isInput();
|
||||
|
||||
abstract List<RefasterTemplateTestData> templateTests();
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package tech.picnic.errorprone.documentation.models;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import tech.picnic.errorprone.documentation.models.AutoValue_RefasterTemplateData;
|
||||
|
||||
@AutoValue
|
||||
public abstract class RefasterTemplateData {
|
||||
public static RefasterTemplateData create(
|
||||
String name, String description, String link, SeverityLevel severityLevel) {
|
||||
return new AutoValue_RefasterTemplateData(name, description, link, severityLevel);
|
||||
}
|
||||
|
||||
abstract String name();
|
||||
|
||||
abstract String description();
|
||||
|
||||
abstract String link();
|
||||
|
||||
abstract SeverityLevel severityLevel();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package tech.picnic.errorprone.documentation.models;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import tech.picnic.errorprone.documentation.models.AutoValue_RefasterTemplateTestData;
|
||||
|
||||
@AutoValue
|
||||
public abstract class RefasterTemplateTestData {
|
||||
public static RefasterTemplateTestData create(String templateName, String templateTestContent) {
|
||||
return new AutoValue_RefasterTemplateTestData(templateName, templateTestContent);
|
||||
}
|
||||
|
||||
abstract String templateName();
|
||||
|
||||
abstract String templateTestContent();
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package tech.picnic.errorprone.documentation;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import com.google.common.io.Resources;
|
||||
import com.google.errorprone.BugPattern;
|
||||
@@ -14,7 +13,6 @@ import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
@@ -120,7 +118,8 @@ final class BugPatternExtractorTest {
|
||||
|
||||
private static void verifyFileMatchesResource(
|
||||
Path outputDirectory, String fileName, String resourceName) throws IOException {
|
||||
assertThat(Files.readString(outputDirectory.resolve(fileName)))
|
||||
assertThat(outputDirectory.resolve(fileName))
|
||||
.content(UTF_8)
|
||||
.isEqualToIgnoringWhitespace(getResource(resourceName));
|
||||
}
|
||||
|
||||
@@ -139,14 +138,10 @@ final class BugPatternExtractorTest {
|
||||
|
||||
@Override
|
||||
public Description matchClass(ClassTree tree, VisitorState state) {
|
||||
BugPatternExtractor extractor = new BugPatternExtractor();
|
||||
|
||||
assertThatThrownBy(() -> extractor.extract(tree, state.context))
|
||||
.isInstanceOf(NullPointerException.class)
|
||||
.hasMessage("BugPattern annotation must be present");
|
||||
|
||||
return buildDescription(tree)
|
||||
.setMessage(String.format("Can extract: %s", extractor.canExtract(tree)))
|
||||
.setMessage(
|
||||
String.format(
|
||||
"Can extract: %s", new BugPatternExtractor().tryExtract(tree, state).isPresent()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.google.common.io.Resources;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
final class BugPatternTestExtractorTest {
|
||||
@Test
|
||||
void noBugPatternTest(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"TestCheckerWithoutAnnotation.java",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"public final class TestCheckerWithoutAnnotation extends BugChecker {}");
|
||||
|
||||
assertThat(outputDirectory.toAbsolutePath()).isEmptyDirectory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void bugPatternTestFileWithoutTestSuffix(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"TestCheckerWithWrongSuffix.java",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"import com.google.errorprone.CompilationTestHelper;",
|
||||
"",
|
||||
"final class TestCheckerWithoutTestSuffix {",
|
||||
" private static class TestCheckerWithout extends BugChecker {}",
|
||||
"",
|
||||
" CompilationTestHelper compilationTestHelper = CompilationTestHelper.newInstance(TestCheckerWithout.class, getClass());",
|
||||
"}");
|
||||
|
||||
assertThat(outputDirectory.toAbsolutePath()).isEmptyDirectory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void minimalBugPatternTest(@TempDir Path outputDirectory) throws IOException {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"TestCheckerTest.java",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"import com.google.errorprone.CompilationTestHelper;",
|
||||
"",
|
||||
"final class TestCheckerTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" CompilationTestHelper compilationTestHelper = CompilationTestHelper.newInstance(TestChecker.class, getClass());",
|
||||
"}");
|
||||
|
||||
verifyFileMatchesResource(
|
||||
outputDirectory,
|
||||
"bugpattern-test-TestCheckerTest.json",
|
||||
"bugpattern-test-documentation-minimal.json");
|
||||
}
|
||||
|
||||
@Test
|
||||
void differentBugPatternAsClassVariableTest(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"TestCheckerTest.java",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"import com.google.errorprone.CompilationTestHelper;",
|
||||
"",
|
||||
"final class TestCheckerTest {",
|
||||
" private static class DifferentChecker extends BugChecker {}",
|
||||
"",
|
||||
" CompilationTestHelper compilationTestHelper = CompilationTestHelper.newInstance(DifferentChecker.class, getClass());",
|
||||
"}");
|
||||
|
||||
assertThat(outputDirectory.toAbsolutePath()).isEmptyDirectory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void differentBugPatternAsLocalVariable(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"TestCheckerTest.java",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"import com.google.errorprone.CompilationTestHelper;",
|
||||
"import org.junit.jupiter.api.Test;",
|
||||
"",
|
||||
"final class TestCheckerTest {",
|
||||
" private static class DifferentChecker extends BugChecker {}",
|
||||
"",
|
||||
" @Test",
|
||||
" void identification() {",
|
||||
" CompilationTestHelper.newInstance(DifferentChecker.class, getClass())",
|
||||
" .addSourceLines(\"A.java\", \"class A {}\")",
|
||||
" .doTest();",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
assertThat(outputDirectory.toAbsolutePath()).isEmptyDirectory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void bugPatternTestSingleIdentification(@TempDir Path outputDirectory) throws IOException {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"TestCheckerTest.java",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"import com.google.errorprone.CompilationTestHelper;",
|
||||
"import org.junit.jupiter.api.Test;",
|
||||
"",
|
||||
"final class TestCheckerTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" @Test",
|
||||
" void identification() {",
|
||||
" CompilationTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addSourceLines(\"A.java\", \"class A {}\")",
|
||||
" .doTest();",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
verifyFileMatchesResource(
|
||||
outputDirectory,
|
||||
"bugpattern-test-TestCheckerTest.json",
|
||||
"bugpattern-test-documentation-identification.json");
|
||||
}
|
||||
|
||||
@Test
|
||||
void bugPatternTestIdentificationMultipleSourceLines(@TempDir Path outputDirectory)
|
||||
throws IOException {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"TestCheckerTest.java",
|
||||
"package pkg;",
|
||||
"",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"import com.google.errorprone.CompilationTestHelper;",
|
||||
"import org.junit.jupiter.api.Test;",
|
||||
"",
|
||||
"final class TestCheckerTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" @Test",
|
||||
" void identification() {",
|
||||
" CompilationTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addSourceLines(\"A.java\", \"class A {}\")",
|
||||
" .addSourceLines(\"B.java\", \"class B {}\")",
|
||||
" .doTest();",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
verifyFileMatchesResource(
|
||||
outputDirectory,
|
||||
"bugpattern-test-TestCheckerTest.json",
|
||||
"bugpattern-test-documentation-identification-two-sources.json");
|
||||
}
|
||||
|
||||
@Test
|
||||
void bugPatternTestSingleReplacement(@TempDir Path outputDirectory) throws IOException {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"TestCheckerTest.java",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"import org.junit.jupiter.api.Test;",
|
||||
"",
|
||||
"final class TestCheckerTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" @Test",
|
||||
" void replacement() {",
|
||||
" BugCheckerRefactoringTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addInputLines(\"A.java\", \"class A {}\")",
|
||||
" .addOutputLines(\"A.java\", \"class A {}\")",
|
||||
" .doTest(TestMode.TEXT_MATCH);",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
verifyFileMatchesResource(
|
||||
outputDirectory,
|
||||
"bugpattern-test-TestCheckerTest.json",
|
||||
"bugpattern-test-documentation-replacement.json");
|
||||
}
|
||||
|
||||
@Test
|
||||
void bugPatternTestMultipleReplacementSources(@TempDir Path outputDirectory) throws IOException {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"TestCheckerTest.java",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"import org.junit.jupiter.api.Test;",
|
||||
"",
|
||||
"final class TestCheckerTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" @Test",
|
||||
" void replacement() {",
|
||||
" BugCheckerRefactoringTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addInputLines(\"A.java\", \"class A {}\")",
|
||||
" .addOutputLines(\"A.java\", \"class A {}\")",
|
||||
" .addInputLines(\"B.java\", \"class B {}\")",
|
||||
" .addOutputLines(\"B.java\", \"class B {}\")",
|
||||
" .doTest(TestMode.TEXT_MATCH);",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
verifyFileMatchesResource(
|
||||
outputDirectory,
|
||||
"bugpattern-test-TestCheckerTest.json",
|
||||
"bugpattern-test-documentation-replacement-two-sources.json");
|
||||
}
|
||||
|
||||
@Test
|
||||
void bugPatternReplacementExpectUnchanged(@TempDir Path outputDirectory) throws IOException {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"TestCheckerTest.java",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"import org.junit.jupiter.api.Test;",
|
||||
"",
|
||||
"final class TestCheckerTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" @Test",
|
||||
" void replacement() {",
|
||||
" BugCheckerRefactoringTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addInputLines(\"A.java\", \"class A {}\")",
|
||||
" .expectUnchanged()",
|
||||
" .doTest(TestMode.TEXT_MATCH);",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
verifyFileMatchesResource(
|
||||
outputDirectory,
|
||||
"bugpattern-test-TestCheckerTest.json",
|
||||
"bugpattern-test-documentation-replacement-expect-unchanged.json");
|
||||
}
|
||||
|
||||
@Test
|
||||
void bugPatternTestIdentificationAndReplacement(@TempDir Path outputDirectory)
|
||||
throws IOException {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"TestCheckerTest.java",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"import com.google.errorprone.CompilationTestHelper;",
|
||||
"import org.junit.jupiter.api.Test;",
|
||||
"",
|
||||
"final class TestCheckerTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" @Test",
|
||||
" void identification() {",
|
||||
" CompilationTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addSourceLines(\"A.java\", \"class A {}\")",
|
||||
" .doTest();",
|
||||
" }",
|
||||
"",
|
||||
" @Test",
|
||||
" void replacement() {",
|
||||
" BugCheckerRefactoringTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addInputLines(\"A.java\", \"class A {}\")",
|
||||
" .addOutputLines(\"A.java\", \"class A {}\")",
|
||||
" .doTest(TestMode.TEXT_MATCH);",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
verifyFileMatchesResource(
|
||||
outputDirectory,
|
||||
"bugpattern-test-TestCheckerTest.json",
|
||||
"bugpattern-test-documentation-identification-and-replacement.json");
|
||||
}
|
||||
|
||||
@Test
|
||||
void bugPatternTestMultipleIdentificationAndReplacement(@TempDir Path outputDirectory)
|
||||
throws IOException {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"TestCheckerTest.java",
|
||||
"package pkg;",
|
||||
"",
|
||||
"import static com.google.errorprone.BugCheckerRefactoringTestHelper.FixChoosers.SECOND;",
|
||||
"",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
|
||||
"import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"import com.google.errorprone.CompilationTestHelper;",
|
||||
"import org.junit.jupiter.api.Test;",
|
||||
"",
|
||||
"final class TestCheckerTest {",
|
||||
" private static class TestChecker extends BugChecker {}",
|
||||
"",
|
||||
" @Test",
|
||||
" void identification() {",
|
||||
" CompilationTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addSourceLines(\"A.java\", \"class A {}\")",
|
||||
" .doTest();",
|
||||
" }",
|
||||
"",
|
||||
" @Test",
|
||||
" void identification2() {",
|
||||
" CompilationTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addSourceLines(\"B.java\", \"class B {}\")",
|
||||
" .doTest();",
|
||||
" }",
|
||||
"",
|
||||
" @Test",
|
||||
" void replacementFirstSuggestedFix() {",
|
||||
" BugCheckerRefactoringTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .addInputLines(\"A.java\", \"class A {}\")",
|
||||
" .addOutputLines(\"A.java\", \"class A {}\")",
|
||||
" .doTest(TestMode.TEXT_MATCH);",
|
||||
" }",
|
||||
"",
|
||||
" @Test",
|
||||
" void replacementSecondSuggestedFix() {",
|
||||
" BugCheckerRefactoringTestHelper.newInstance(TestChecker.class, getClass())",
|
||||
" .setFixChooser(SECOND)",
|
||||
" .addInputLines(\"B.java\", \"class B {}\")",
|
||||
" .addOutputLines(\"B.java\", \"class B {}\")",
|
||||
" .doTest(TestMode.TEXT_MATCH);",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
verifyFileMatchesResource(
|
||||
outputDirectory,
|
||||
"bugpattern-test-TestCheckerTest.json",
|
||||
"bugpattern-test-documentation-multiple-identification-and-replacement.json");
|
||||
}
|
||||
|
||||
private static void verifyFileMatchesResource(
|
||||
Path outputDirectory, String fileName, String resourceName) throws IOException {
|
||||
assertThat(outputDirectory.resolve(fileName))
|
||||
.content(UTF_8)
|
||||
.isEqualToIgnoringWhitespace(getResource(resourceName));
|
||||
}
|
||||
|
||||
// XXX: Once we support only JDK 15+, drop this method in favour of including the resources as
|
||||
// text blocks in this class. (This also requires renaming the `verifyFileMatchesResource`
|
||||
// method.)
|
||||
private static String getResource(String resourceName) throws IOException {
|
||||
return Resources.toString(
|
||||
Resources.getResource(BugPatternTestExtractorTest.class, resourceName), UTF_8);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,29 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.nio.file.attribute.AclEntryPermission.ADD_SUBDIRECTORY;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.junit.jupiter.api.condition.OS.WINDOWS;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.collect.Streams;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.annotations.Immutable;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileSystemException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.AclEntry;
|
||||
import java.nio.file.attribute.AclFileAttributeView;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledOnOs;
|
||||
import org.junit.jupiter.api.condition.EnabledOnOs;
|
||||
@@ -74,4 +84,55 @@ final class DocumentationGeneratorTaskListenerTest {
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Precisely one path must be provided");
|
||||
}
|
||||
|
||||
@Test
|
||||
void extraction(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"DocumentationGeneratorTaskListenerTestClass.java",
|
||||
"class DocumentationGeneratorTaskListenerTestClass {}");
|
||||
|
||||
// XXX: Once we support only JDK 15+, use a text block for the `expected` string.
|
||||
assertThat(
|
||||
outputDirectory.resolve(
|
||||
"documentation-generator-task-listener-test-DocumentationGeneratorTaskListenerTestClass.json"))
|
||||
.content(UTF_8)
|
||||
.isEqualToIgnoringWhitespace(
|
||||
"{\"className\":\"DocumentationGeneratorTaskListenerTestClass\",\"path\":[\"CLASS: DocumentationGeneratorTaskListenerTestClass\",\"COMPILATION_UNIT\"]}");
|
||||
}
|
||||
|
||||
@Immutable
|
||||
@AutoService(Extractor.class)
|
||||
public static final class TestExtractor implements Extractor<ExtractionParameters> {
|
||||
@Override
|
||||
public String identifier() {
|
||||
return "documentation-generator-task-listener-test";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ExtractionParameters> tryExtract(ClassTree tree, VisitorState state) {
|
||||
return Optional.of(tree.getSimpleName().toString())
|
||||
.filter(n -> n.contains(DocumentationGeneratorTaskListenerTest.class.getSimpleName()))
|
||||
.map(
|
||||
className ->
|
||||
new AutoValue_DocumentationGeneratorTaskListenerTest_ExtractionParameters(
|
||||
className,
|
||||
Streams.stream(state.getPath())
|
||||
.map(TestExtractor::describeTree)
|
||||
.collect(toImmutableList())));
|
||||
}
|
||||
|
||||
private static String describeTree(Tree tree) {
|
||||
return (tree instanceof ClassTree)
|
||||
? String.join(": ", String.valueOf(tree.getKind()), ((ClassTree) tree).getSimpleName())
|
||||
: tree.getKind().toString();
|
||||
}
|
||||
}
|
||||
|
||||
@AutoValue
|
||||
abstract static class ExtractionParameters {
|
||||
abstract String className();
|
||||
|
||||
abstract ImmutableList<String> path();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "TestChecker",
|
||||
"identificationTests": [
|
||||
"class A {}\n"
|
||||
],
|
||||
"replacementTests": [
|
||||
{
|
||||
"inputLines": "class A {}\n",
|
||||
"outputLines": "class A {}\n"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "TestChecker",
|
||||
"identificationTests": [
|
||||
"class B {}\n",
|
||||
"class A {}\n"
|
||||
],
|
||||
"replacementTests": []
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "TestChecker",
|
||||
"identificationTests": [
|
||||
"class A {}\n"
|
||||
],
|
||||
"replacementTests": []
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "TestChecker",
|
||||
"identificationTests": [],
|
||||
"replacementTests": []
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "TestChecker",
|
||||
"identificationTests": [
|
||||
"class A {}\n",
|
||||
"class B {}\n"
|
||||
],
|
||||
"replacementTests": [
|
||||
{
|
||||
"inputLines": "class A {}\n",
|
||||
"outputLines": "class A {}\n"
|
||||
},
|
||||
{
|
||||
"inputLines": "class B {}\n",
|
||||
"outputLines": "class B {}\n"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "TestChecker",
|
||||
"identificationTests": [],
|
||||
"replacementTests": []
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "TestChecker",
|
||||
"identificationTests": [],
|
||||
"replacementTests": [
|
||||
{
|
||||
"inputLines": "class B {}\n",
|
||||
"outputLines": "class B {}\n"
|
||||
},
|
||||
{
|
||||
"inputLines": "class A {}\n",
|
||||
"outputLines": "class A {}\n"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "TestChecker",
|
||||
"identificationTests": [],
|
||||
"replacementTests": [
|
||||
{
|
||||
"inputLines": "class A {}\n",
|
||||
"outputLines": "class A {}\n"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<groupId>tech.picnic.error-prone-support</groupId>
|
||||
<artifactId>error-prone-support</artifactId>
|
||||
<version>0.8.1-SNAPSHOT</version>
|
||||
<version>0.9.1-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>error-prone-contrib</artifactId>
|
||||
@@ -37,7 +37,7 @@
|
||||
<dependency>
|
||||
<groupId>${groupId.error-prone}</groupId>
|
||||
<artifactId>error_prone_test_helpers</artifactId>
|
||||
<scope>test</scope>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
@@ -146,10 +146,6 @@
|
||||
<artifactId>value-annotations</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jooq</groupId>
|
||||
<artifactId>jooq</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jspecify</groupId>
|
||||
<artifactId>jspecify</artifactId>
|
||||
@@ -261,4 +257,33 @@
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>docgen</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>build-helper-maven-plugin</artifactId>
|
||||
<version>3.3.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>add-test-source</id>
|
||||
<goals>
|
||||
<goal>add-test-source</goal>
|
||||
</goals>
|
||||
<phase>generate-test-sources</phase>
|
||||
<configuration>
|
||||
<sources>
|
||||
<source>src/test/resources</source>
|
||||
</sources>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
|
||||
import static com.google.errorprone.matchers.Matchers.allOf;
|
||||
import static com.google.errorprone.matchers.Matchers.argument;
|
||||
import static com.google.errorprone.matchers.Matchers.isSameType;
|
||||
import static com.google.errorprone.matchers.Matchers.isVariable;
|
||||
import static com.google.errorprone.matchers.Matchers.not;
|
||||
import static com.google.errorprone.matchers.Matchers.returnStatement;
|
||||
import static com.google.errorprone.matchers.Matchers.staticMethod;
|
||||
import static com.google.errorprone.matchers.Matchers.toType;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
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.BlockTreeMatcher;
|
||||
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.AssignmentTree;
|
||||
import com.sun.source.tree.BlockTree;
|
||||
import com.sun.source.tree.ExpressionStatementTree;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.source.tree.ReturnTree;
|
||||
import com.sun.source.tree.StatementTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import com.sun.source.tree.VariableTree;
|
||||
import com.sun.tools.javac.code.Symbol;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import tech.picnic.errorprone.bugpatterns.util.MoreASTHelpers;
|
||||
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags unnecessary local variable assignments preceding a return
|
||||
* statement.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Variable assignment is redundant; value can be returned directly",
|
||||
link = BUG_PATTERNS_BASE_URL + "DirectReturn",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class DirectReturn extends BugChecker implements BlockTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<StatementTree> VARIABLE_RETURN = returnStatement(isVariable());
|
||||
private static final Matcher<ExpressionTree> MOCKITO_MOCK_OR_SPY_WITH_IMPLICIT_TYPE =
|
||||
allOf(
|
||||
not(toType(MethodInvocationTree.class, argument(0, isSameType(Class.class.getName())))),
|
||||
staticMethod().onClass("org.mockito.Mockito").namedAnyOf("mock", "spy"));
|
||||
|
||||
/** Instantiates a new {@link DirectReturn} instance. */
|
||||
public DirectReturn() {}
|
||||
|
||||
@Override
|
||||
public Description matchBlock(BlockTree tree, VisitorState state) {
|
||||
List<? extends StatementTree> statements = tree.getStatements();
|
||||
if (statements.size() < 2) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
StatementTree finalStatement = statements.get(statements.size() - 1);
|
||||
if (!VARIABLE_RETURN.matches(finalStatement, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
Symbol variableSymbol = ASTHelpers.getSymbol(((ReturnTree) finalStatement).getExpression());
|
||||
StatementTree precedingStatement = statements.get(statements.size() - 2);
|
||||
|
||||
return tryMatchAssignment(variableSymbol, precedingStatement)
|
||||
.filter(resultExpr -> canInlineToReturnStatement(resultExpr, state))
|
||||
.map(
|
||||
resultExpr ->
|
||||
describeMatch(
|
||||
precedingStatement,
|
||||
SuggestedFix.builder()
|
||||
.replace(
|
||||
precedingStatement,
|
||||
String.format("return %s;", SourceCode.treeToString(resultExpr, state)))
|
||||
.delete(finalStatement)
|
||||
.build()))
|
||||
.orElse(Description.NO_MATCH);
|
||||
}
|
||||
|
||||
private static Optional<ExpressionTree> tryMatchAssignment(Symbol targetSymbol, Tree tree) {
|
||||
if (tree instanceof ExpressionStatementTree) {
|
||||
return tryMatchAssignment(targetSymbol, ((ExpressionStatementTree) tree).getExpression());
|
||||
}
|
||||
|
||||
if (tree instanceof AssignmentTree) {
|
||||
AssignmentTree assignment = (AssignmentTree) tree;
|
||||
return targetSymbol.equals(ASTHelpers.getSymbol(assignment.getVariable()))
|
||||
? Optional.of(assignment.getExpression())
|
||||
: Optional.empty();
|
||||
}
|
||||
|
||||
if (tree instanceof VariableTree) {
|
||||
VariableTree declaration = (VariableTree) tree;
|
||||
return declaration.getModifiers().getAnnotations().isEmpty()
|
||||
&& targetSymbol.equals(ASTHelpers.getSymbol(declaration))
|
||||
? Optional.ofNullable(declaration.getInitializer())
|
||||
: Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether inlining the given expression to the associated return statement can be done
|
||||
* safely.
|
||||
*
|
||||
* <p>Inlining is generally safe, but in rare cases the operation may have a functional impact.
|
||||
* The sole case considered here is the inlining of a Mockito mock or spy construction without an
|
||||
* explicit type. In such a case the type created depends on context, such as the method's return
|
||||
* type.
|
||||
*/
|
||||
private static boolean canInlineToReturnStatement(
|
||||
ExpressionTree expressionTree, VisitorState state) {
|
||||
return !MOCKITO_MOCK_OR_SPY_WITH_IMPLICIT_TYPE.matches(expressionTree, state)
|
||||
|| MoreASTHelpers.findMethodExitedOnReturn(state)
|
||||
.filter(m -> MoreASTHelpers.areSameType(expressionTree, m.getReturnType(), state))
|
||||
.isPresent();
|
||||
}
|
||||
}
|
||||
@@ -152,7 +152,7 @@ public final class ErrorProneTestHelperSourceFormat extends BugChecker
|
||||
StringBuilder source = new StringBuilder();
|
||||
|
||||
for (ExpressionTree sourceLine : sourceLines) {
|
||||
Object value = ASTHelpers.constValue(sourceLine);
|
||||
String value = ASTHelpers.constValue(sourceLine, String.class);
|
||||
if (value == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.CONCURRENCY;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.PERFORMANCE;
|
||||
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
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.MethodInvocationTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFix;
|
||||
import com.google.errorprone.fixes.SuggestedFixes;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.suppliers.Supplier;
|
||||
import com.google.errorprone.suppliers.Suppliers;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.tools.javac.code.Type;
|
||||
import com.sun.tools.javac.util.Position;
|
||||
import java.util.stream.Stream;
|
||||
import tech.picnic.errorprone.bugpatterns.util.ThirdPartyLibrary;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags {@link reactor.core.publisher.Flux} operator usages that may
|
||||
* implicitly cause the calling thread to be blocked.
|
||||
*
|
||||
* <p>Note that the methods flagged here are not themselves blocking, but iterating over the
|
||||
* resulting {@link Iterable} or {@link Stream} may be.
|
||||
*/
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Avoid iterating over `Flux`es in an implicitly blocking manner",
|
||||
link = BUG_PATTERNS_BASE_URL + "FluxImplicitBlock",
|
||||
linkType = CUSTOM,
|
||||
severity = WARNING,
|
||||
tags = {CONCURRENCY, PERFORMANCE})
|
||||
public final class FluxImplicitBlock extends BugChecker implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<ExpressionTree> FLUX_WITH_IMPLICIT_BLOCK =
|
||||
instanceMethod()
|
||||
.onDescendantOf("reactor.core.publisher.Flux")
|
||||
.namedAnyOf("toIterable", "toStream")
|
||||
.withNoParameters();
|
||||
private static final Supplier<Type> STREAM = Suppliers.typeFromString(Stream.class.getName());
|
||||
|
||||
/** Instantiates a new {@link FluxImplicitBlock} instance. */
|
||||
public FluxImplicitBlock() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
if (!FLUX_WITH_IMPLICIT_BLOCK.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
Description.Builder description =
|
||||
buildDescription(tree).addFix(SuggestedFixes.addSuppressWarnings(state, canonicalName()));
|
||||
if (ThirdPartyLibrary.GUAVA.isIntroductionAllowed(state)) {
|
||||
description.addFix(
|
||||
suggestBlockingElementCollection(
|
||||
tree, "com.google.common.collect.ImmutableList.toImmutableList", state));
|
||||
}
|
||||
description.addFix(
|
||||
suggestBlockingElementCollection(tree, "java.util.stream.Collectors.toList", state));
|
||||
|
||||
return description.build();
|
||||
}
|
||||
|
||||
private static SuggestedFix suggestBlockingElementCollection(
|
||||
MethodInvocationTree tree, String fullyQualifiedCollectorMethod, VisitorState state) {
|
||||
SuggestedFix.Builder importSuggestion = SuggestedFix.builder();
|
||||
String replacementMethodInvocation =
|
||||
SuggestedFixes.qualifyStaticImport(fullyQualifiedCollectorMethod, importSuggestion, state);
|
||||
|
||||
boolean isStream =
|
||||
ASTHelpers.isSubtype(ASTHelpers.getResultType(tree), STREAM.get(state), state);
|
||||
String replacement =
|
||||
String.format(
|
||||
".collect(%s()).block()%s", replacementMethodInvocation, isStream ? ".stream()" : "");
|
||||
return importSuggestion.merge(replaceMethodInvocation(tree, replacement, state)).build();
|
||||
}
|
||||
|
||||
private static SuggestedFix.Builder replaceMethodInvocation(
|
||||
MethodInvocationTree tree, String replacement, VisitorState state) {
|
||||
int startPosition = state.getEndPosition(ASTHelpers.getReceiver(tree));
|
||||
int endPosition = state.getEndPosition(tree);
|
||||
|
||||
checkState(
|
||||
startPosition != Position.NOPOS && endPosition != Position.NOPOS,
|
||||
"Cannot locate method to be replaced in source code");
|
||||
|
||||
return SuggestedFix.builder().replace(startPosition, endPosition, replacement);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
|
||||
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.ALL;
|
||||
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
|
||||
import static com.google.errorprone.matchers.Matchers.allOf;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.Matchers.anything;
|
||||
import static com.google.errorprone.matchers.Matchers.argument;
|
||||
import static com.google.errorprone.matchers.Matchers.argumentCount;
|
||||
import static com.google.errorprone.matchers.Matchers.classLiteral;
|
||||
import static com.google.errorprone.matchers.Matchers.hasArguments;
|
||||
import static com.google.errorprone.matchers.Matchers.isPrimitiveOrBoxedPrimitiveType;
|
||||
import static com.google.errorprone.matchers.Matchers.isSameType;
|
||||
import static com.google.errorprone.matchers.Matchers.methodHasParameters;
|
||||
import static com.google.errorprone.matchers.Matchers.staticMethod;
|
||||
import static com.google.errorprone.matchers.Matchers.toType;
|
||||
import static java.util.function.Predicate.not;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.MoreJUnitMatchers.HAS_METHOD_SOURCE;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.MoreJUnitMatchers.getMethodSourceFactoryNames;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.collect.Iterables;
|
||||
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.ClassTree;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.LambdaExpressionTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import com.sun.source.tree.NewArrayTree;
|
||||
import com.sun.source.tree.ReturnTree;
|
||||
import com.sun.source.util.TreeScanner;
|
||||
import com.sun.tools.javac.code.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.DoubleStream;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.LongStream;
|
||||
import java.util.stream.Stream;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags JUnit tests with a {@link
|
||||
* org.junit.jupiter.params.provider.MethodSource} annotation that can be replaced with an
|
||||
* equivalent {@link org.junit.jupiter.params.provider.ValueSource} annotation.
|
||||
*/
|
||||
// XXX: Where applicable, also flag `@MethodSource` annotations that reference multiple value
|
||||
// factory methods (or that repeat the same value factory method multiple times).
|
||||
// XXX: Support inlining of overloaded value factory methods.
|
||||
// XXX: Support inlining of value factory methods referenced by multiple `@MethodSource`
|
||||
// annotations.
|
||||
// XXX: Support value factory return expressions of the form `Stream.of(a, b,
|
||||
// c).map(Arguments::argument)`.
|
||||
// XXX: Support simplification of test methods that accept additional injected parameters such as
|
||||
// `TestInfo`; such parameters should be ignored for the purpose of this check.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Prefer `@ValueSource` over a `@MethodSource` where possible and reasonable",
|
||||
linkType = CUSTOM,
|
||||
link = BUG_PATTERNS_BASE_URL + "JUnitValueSource",
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class JUnitValueSource extends BugChecker implements MethodTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<ExpressionTree> SUPPORTED_VALUE_FACTORY_VALUES =
|
||||
anyOf(
|
||||
isArrayArgumentValueCandidate(),
|
||||
toType(
|
||||
MethodInvocationTree.class,
|
||||
allOf(
|
||||
staticMethod()
|
||||
.onClass("org.junit.jupiter.params.provider.Arguments")
|
||||
.namedAnyOf("arguments", "of"),
|
||||
argumentCount(1),
|
||||
argument(0, isArrayArgumentValueCandidate()))));
|
||||
private static final Matcher<ExpressionTree> ARRAY_OF_SUPPORTED_SINGLE_VALUE_ARGUMENTS =
|
||||
isSingleDimensionArrayCreationWithAllElementsMatching(SUPPORTED_VALUE_FACTORY_VALUES);
|
||||
private static final Matcher<ExpressionTree> ENUMERATION_OF_SUPPORTED_SINGLE_VALUE_ARGUMENTS =
|
||||
toType(
|
||||
MethodInvocationTree.class,
|
||||
allOf(
|
||||
staticMethod()
|
||||
.onClassAny(
|
||||
Stream.class.getName(),
|
||||
IntStream.class.getName(),
|
||||
LongStream.class.getName(),
|
||||
DoubleStream.class.getName(),
|
||||
List.class.getName(),
|
||||
Set.class.getName(),
|
||||
"com.google.common.collect.ImmutableList",
|
||||
"com.google.common.collect.ImmutableSet")
|
||||
.named("of"),
|
||||
hasArguments(AT_LEAST_ONE, anything()),
|
||||
hasArguments(ALL, SUPPORTED_VALUE_FACTORY_VALUES)));
|
||||
private static final Matcher<MethodTree> IS_UNARY_METHOD_WITH_SUPPORTED_PARAMETER =
|
||||
methodHasParameters(
|
||||
anyOf(
|
||||
isPrimitiveOrBoxedPrimitiveType(),
|
||||
isSameType(String.class),
|
||||
isSameType(state -> state.getSymtab().classType)));
|
||||
|
||||
/** Instantiates a new {@link JUnitValueSource} instance. */
|
||||
public JUnitValueSource() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethod(MethodTree tree, VisitorState state) {
|
||||
if (!IS_UNARY_METHOD_WITH_SUPPORTED_PARAMETER.matches(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
Type parameterType = ASTHelpers.getType(Iterables.getOnlyElement(tree.getParameters()));
|
||||
|
||||
return findMethodSourceAnnotation(tree, state)
|
||||
.flatMap(
|
||||
methodSourceAnnotation ->
|
||||
getSoleLocalFactoryName(methodSourceAnnotation, tree)
|
||||
.filter(factory -> !hasSiblingReferencingValueFactory(tree, factory, state))
|
||||
.flatMap(factory -> findSiblingWithName(tree, factory, state))
|
||||
.flatMap(
|
||||
factoryMethod ->
|
||||
tryConstructValueSourceFix(
|
||||
parameterType, methodSourceAnnotation, factoryMethod, state))
|
||||
.map(fix -> describeMatch(methodSourceAnnotation, fix)))
|
||||
.orElse(Description.NO_MATCH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the value factory method pointed to by the given {@code @MethodSource}
|
||||
* annotation, if it (a) is the only one and (b) is a method in the same class as the annotated
|
||||
* method.
|
||||
*/
|
||||
private static Optional<String> getSoleLocalFactoryName(
|
||||
AnnotationTree methodSourceAnnotation, MethodTree method) {
|
||||
return getElementIfSingleton(getMethodSourceFactoryNames(methodSourceAnnotation, method))
|
||||
.filter(name -> name.indexOf('#') < 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether the given method has a sibling method in the same class that depends on the
|
||||
* specified value factory method.
|
||||
*/
|
||||
private static boolean hasSiblingReferencingValueFactory(
|
||||
MethodTree tree, String valueFactory, VisitorState state) {
|
||||
return findMatchingSibling(tree, m -> hasValueFactory(m, valueFactory, state), state)
|
||||
.isPresent();
|
||||
}
|
||||
|
||||
private static Optional<MethodTree> findSiblingWithName(
|
||||
MethodTree tree, String methodName, VisitorState state) {
|
||||
return findMatchingSibling(tree, m -> m.getName().contentEquals(methodName), state);
|
||||
}
|
||||
|
||||
private static Optional<MethodTree> findMatchingSibling(
|
||||
MethodTree tree, Predicate<? super MethodTree> predicate, VisitorState state) {
|
||||
return state.findEnclosing(ClassTree.class).getMembers().stream()
|
||||
.filter(MethodTree.class::isInstance)
|
||||
.map(MethodTree.class::cast)
|
||||
.filter(not(tree::equals))
|
||||
.filter(predicate)
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
private static boolean hasValueFactory(
|
||||
MethodTree tree, String valueFactoryMethodName, VisitorState state) {
|
||||
return findMethodSourceAnnotation(tree, state).stream()
|
||||
.anyMatch(
|
||||
annotation ->
|
||||
getMethodSourceFactoryNames(annotation, tree).contains(valueFactoryMethodName));
|
||||
}
|
||||
|
||||
private static Optional<AnnotationTree> findMethodSourceAnnotation(
|
||||
MethodTree tree, VisitorState state) {
|
||||
return HAS_METHOD_SOURCE.multiMatchResult(tree, state).matchingNodes().stream().findFirst();
|
||||
}
|
||||
|
||||
private static Optional<SuggestedFix> tryConstructValueSourceFix(
|
||||
Type parameterType,
|
||||
AnnotationTree methodSourceAnnotation,
|
||||
MethodTree valueFactoryMethod,
|
||||
VisitorState state) {
|
||||
return getSingleReturnExpression(valueFactoryMethod)
|
||||
.flatMap(expression -> tryExtractValueSourceAttributeValue(expression, state))
|
||||
.map(
|
||||
valueSourceAttributeValue ->
|
||||
SuggestedFix.builder()
|
||||
.addImport("org.junit.jupiter.params.provider.ValueSource")
|
||||
.replace(
|
||||
methodSourceAnnotation,
|
||||
String.format(
|
||||
"@ValueSource(%s = %s)",
|
||||
toValueSourceAttributeName(parameterType), valueSourceAttributeValue))
|
||||
.delete(valueFactoryMethod)
|
||||
.build());
|
||||
}
|
||||
|
||||
// XXX: This pattern also occurs a few times inside Error Prone; contribute upstream.
|
||||
private static Optional<ExpressionTree> getSingleReturnExpression(MethodTree methodTree) {
|
||||
List<ExpressionTree> returnExpressions = new ArrayList<>();
|
||||
new TreeScanner<@Nullable Void, @Nullable Void>() {
|
||||
@Override
|
||||
public @Nullable Void visitClass(ClassTree node, @Nullable Void unused) {
|
||||
/* Ignore `return` statements inside anonymous/local classes. */
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Void visitReturn(ReturnTree node, @Nullable Void unused) {
|
||||
returnExpressions.add(node.getExpression());
|
||||
return super.visitReturn(node, unused);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Void visitLambdaExpression(
|
||||
LambdaExpressionTree node, @Nullable Void unused) {
|
||||
/* Ignore `return` statements inside lambda expressions. */
|
||||
return null;
|
||||
}
|
||||
}.scan(methodTree, null);
|
||||
|
||||
return getElementIfSingleton(returnExpressions);
|
||||
}
|
||||
|
||||
private static Optional<String> tryExtractValueSourceAttributeValue(
|
||||
ExpressionTree tree, VisitorState state) {
|
||||
List<? extends ExpressionTree> arguments;
|
||||
if (ENUMERATION_OF_SUPPORTED_SINGLE_VALUE_ARGUMENTS.matches(tree, state)) {
|
||||
arguments = ((MethodInvocationTree) tree).getArguments();
|
||||
} else if (ARRAY_OF_SUPPORTED_SINGLE_VALUE_ARGUMENTS.matches(tree, state)) {
|
||||
arguments = ((NewArrayTree) tree).getInitializers();
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/*
|
||||
* Join the values into a comma-separated string, unwrapping `Arguments` factory method
|
||||
* invocations if applicable.
|
||||
*/
|
||||
return Optional.of(
|
||||
arguments.stream()
|
||||
.map(
|
||||
arg ->
|
||||
arg instanceof MethodInvocationTree
|
||||
? Iterables.getOnlyElement(((MethodInvocationTree) arg).getArguments())
|
||||
: arg)
|
||||
.map(argument -> SourceCode.treeToString(argument, state))
|
||||
.collect(joining(", ")))
|
||||
.map(value -> arguments.size() > 1 ? String.format("{%s}", value) : value);
|
||||
}
|
||||
|
||||
private static String toValueSourceAttributeName(Type type) {
|
||||
String typeString = type.tsym.name.toString();
|
||||
|
||||
switch (typeString) {
|
||||
case "Class":
|
||||
return "classes";
|
||||
case "Character":
|
||||
return "chars";
|
||||
case "Integer":
|
||||
return "ints";
|
||||
default:
|
||||
return typeString.toLowerCase(Locale.ROOT) + 's';
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> Optional<T> getElementIfSingleton(Collection<T> collection) {
|
||||
return Optional.of(collection)
|
||||
.filter(elements -> elements.size() == 1)
|
||||
.map(Iterables::getOnlyElement);
|
||||
}
|
||||
|
||||
private static Matcher<ExpressionTree> isSingleDimensionArrayCreationWithAllElementsMatching(
|
||||
Matcher<? super ExpressionTree> elementMatcher) {
|
||||
return (tree, state) -> {
|
||||
if (!(tree instanceof NewArrayTree)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
NewArrayTree newArray = (NewArrayTree) tree;
|
||||
return newArray.getDimensions().isEmpty()
|
||||
&& !newArray.getInitializers().isEmpty()
|
||||
&& newArray.getInitializers().stream()
|
||||
.allMatch(element -> elementMatcher.matches(element, state));
|
||||
};
|
||||
}
|
||||
|
||||
private static Matcher<ExpressionTree> isArrayArgumentValueCandidate() {
|
||||
return anyOf(classLiteral(anything()), (tree, state) -> ASTHelpers.constValue(tree) != null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
|
||||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
|
||||
import static com.google.errorprone.matchers.Matchers.allOf;
|
||||
import static com.google.errorprone.matchers.Matchers.argument;
|
||||
import static com.google.errorprone.matchers.Matchers.isSameType;
|
||||
import static com.google.errorprone.matchers.Matchers.isVariable;
|
||||
import static com.google.errorprone.matchers.Matchers.not;
|
||||
import static com.google.errorprone.matchers.Matchers.staticMethod;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
|
||||
|
||||
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.MethodInvocationTreeMatcher;
|
||||
import com.google.errorprone.fixes.SuggestedFixes;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import com.sun.source.tree.VariableTree;
|
||||
import java.util.List;
|
||||
import tech.picnic.errorprone.bugpatterns.util.MoreASTHelpers;
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags the use of {@link org.mockito.Mockito#mock(Class)} and {@link
|
||||
* org.mockito.Mockito#spy(Class)} where instead the type to be mocked or spied can be derived from
|
||||
* context.
|
||||
*/
|
||||
// XXX: This check currently does not flag method invocation arguments. When adding support for
|
||||
// this, consider that in some cases the type to be mocked or spied must be specified explicitly so
|
||||
// as to disambiguate between method overloads.
|
||||
// XXX: This check currently does not flag (implicit or explicit) lambda return expressions.
|
||||
// XXX: This check currently does not drop suppressions that become obsolete after the
|
||||
// suggested fix is applied; consider adding support for this.
|
||||
@AutoService(BugChecker.class)
|
||||
@BugPattern(
|
||||
summary = "Don't unnecessarily pass a type to Mockito's `mock(Class)` and `spy(Class)` methods",
|
||||
link = BUG_PATTERNS_BASE_URL + "MockitoMockClassReference",
|
||||
linkType = CUSTOM,
|
||||
severity = SUGGESTION,
|
||||
tags = SIMPLIFICATION)
|
||||
public final class MockitoMockClassReference extends BugChecker
|
||||
implements MethodInvocationTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Matcher<MethodInvocationTree> MOCKITO_MOCK_OR_SPY_WITH_HARDCODED_TYPE =
|
||||
allOf(
|
||||
argument(0, allOf(isSameType(Class.class.getName()), not(isVariable()))),
|
||||
staticMethod().onClass("org.mockito.Mockito").namedAnyOf("mock", "spy"));
|
||||
|
||||
/** Instantiates a new {@link MockitoMockClassReference} instance. */
|
||||
public MockitoMockClassReference() {}
|
||||
|
||||
@Override
|
||||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
|
||||
if (!MOCKITO_MOCK_OR_SPY_WITH_HARDCODED_TYPE.matches(tree, state)
|
||||
|| !isTypeDerivableFromContext(tree, state)) {
|
||||
return Description.NO_MATCH;
|
||||
}
|
||||
|
||||
List<? extends ExpressionTree> arguments = tree.getArguments();
|
||||
return describeMatch(tree, SuggestedFixes.removeElement(arguments.get(0), arguments, state));
|
||||
}
|
||||
|
||||
private static boolean isTypeDerivableFromContext(MethodInvocationTree tree, VisitorState state) {
|
||||
Tree parent = state.getPath().getParentPath().getLeaf();
|
||||
switch (parent.getKind()) {
|
||||
case VARIABLE:
|
||||
return !ASTHelpers.hasNoExplicitType((VariableTree) parent, state)
|
||||
&& MoreASTHelpers.areSameType(tree, parent, state);
|
||||
case ASSIGNMENT:
|
||||
return MoreASTHelpers.areSameType(tree, parent, state);
|
||||
case RETURN:
|
||||
return MoreASTHelpers.findMethodExitedOnReturn(state)
|
||||
.filter(m -> MoreASTHelpers.areSameType(tree, m.getReturnType(), state))
|
||||
.isPresent();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
|
||||
import static com.google.errorprone.matchers.Matchers.allOf;
|
||||
import static com.google.errorprone.matchers.Matchers.anyMethod;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
import static com.google.errorprone.matchers.Matchers.anything;
|
||||
import static com.google.errorprone.matchers.Matchers.argumentCount;
|
||||
import static com.google.errorprone.matchers.Matchers.isNonNullUsingDataflow;
|
||||
import static com.google.errorprone.matchers.Matchers.isSameType;
|
||||
@@ -67,9 +68,7 @@ public final class RedundantStringConversion extends BugChecker
|
||||
private static final String EXTRA_STRING_CONVERSION_METHODS_FLAG =
|
||||
"RedundantStringConversion:ExtraConversionMethods";
|
||||
|
||||
@SuppressWarnings("UnnecessaryLambda")
|
||||
private static final Matcher<ExpressionTree> ANY_EXPR = (t, s) -> true;
|
||||
|
||||
private static final Matcher<ExpressionTree> ANY_EXPR = anything();
|
||||
private static final Matcher<ExpressionTree> LOCALE = isSameType(Locale.class);
|
||||
private static final Matcher<ExpressionTree> MARKER = isSubtypeOf("org.slf4j.Marker");
|
||||
private static final Matcher<ExpressionTree> STRING = isSameType(String.class);
|
||||
|
||||
@@ -74,9 +74,13 @@ public final class RequestMappingAnnotation extends BugChecker implements Method
|
||||
isSameType("java.time.ZoneId"),
|
||||
isSameType("java.util.Locale"),
|
||||
isSameType("java.util.TimeZone"),
|
||||
isSameType("jakarta.servlet.http.HttpServletRequest"),
|
||||
isSameType("jakarta.servlet.http.HttpServletResponse"),
|
||||
isSameType("javax.servlet.http.HttpServletRequest"),
|
||||
isSameType("javax.servlet.http.HttpServletResponse"),
|
||||
isSameType("org.springframework.http.HttpMethod"),
|
||||
isSameType("org.springframework.ui.Model"),
|
||||
isSameType("org.springframework.validation.BindingResult"),
|
||||
isSameType("org.springframework.web.context.request.NativeWebRequest"),
|
||||
isSameType("org.springframework.web.context.request.WebRequest"),
|
||||
isSameType("org.springframework.web.server.ServerWebExchange"),
|
||||
|
||||
@@ -5,8 +5,12 @@ import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import com.sun.source.tree.LambdaExpressionTree;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* A collection of helper methods for working with the AST.
|
||||
@@ -46,4 +50,33 @@ public final class MoreASTHelpers {
|
||||
public static boolean methodExistsInEnclosingClass(CharSequence methodName, VisitorState state) {
|
||||
return !findMethods(methodName, state).isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link MethodTree} from which control flow would exit if there would be a {@code
|
||||
* return} statement at the given {@link VisitorState}'s current {@link VisitorState#getPath()
|
||||
* path}.
|
||||
*
|
||||
* @param state The {@link VisitorState} from which to derive the AST location of interest.
|
||||
* @return A {@link MethodTree}, unless the {@link VisitorState}'s path does not point to an AST
|
||||
* node located inside a method, or if the (hypothetical) {@code return} statement would exit
|
||||
* a lambda expression instead.
|
||||
*/
|
||||
public static Optional<MethodTree> findMethodExitedOnReturn(VisitorState state) {
|
||||
return Optional.ofNullable(state.findEnclosing(LambdaExpressionTree.class, MethodTree.class))
|
||||
.filter(MethodTree.class::isInstance)
|
||||
.map(MethodTree.class::cast);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether the given trees are of the same type, after type erasure.
|
||||
*
|
||||
* @param treeA The first tree of interest.
|
||||
* @param treeB The second tree of interest.
|
||||
* @param state The {@link VisitorState} describing the context in which the given trees were
|
||||
* found.
|
||||
* @return Whether the specified trees have the same erased types.
|
||||
*/
|
||||
public static boolean areSameType(Tree treeA, Tree treeB, VisitorState state) {
|
||||
return ASTHelpers.isSameType(ASTHelpers.getType(treeA), ASTHelpers.getType(treeB), state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package tech.picnic.errorprone.bugpatterns.util;
|
||||
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
|
||||
import static com.google.errorprone.matchers.Matchers.annotations;
|
||||
import static com.google.errorprone.matchers.Matchers.anyOf;
|
||||
@@ -9,7 +9,7 @@ import static java.util.Objects.requireNonNullElse;
|
||||
import static tech.picnic.errorprone.bugpatterns.util.MoreMatchers.hasMetaAnnotation;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.errorprone.matchers.AnnotationMatcherUtils;
|
||||
import com.google.errorprone.matchers.Matcher;
|
||||
import com.google.errorprone.matchers.MultiMatcher;
|
||||
@@ -55,25 +55,59 @@ public final class MoreJUnitMatchers {
|
||||
* Returns the names of the JUnit value factory methods specified by the given {@link
|
||||
* org.junit.jupiter.params.provider.MethodSource} annotation.
|
||||
*
|
||||
* <p>This method differs from {@link #getMethodSourceFactoryDescriptors(AnnotationTree,
|
||||
* MethodTree)} in that it drops any parenthesized method parameter type enumerations. That is,
|
||||
* method descriptors such as {@code factoryMethod()} and {@code factoryMethod(java.lang.String)}
|
||||
* are both simplified to just {@code factoryMethod}. This also means that the returned method
|
||||
* names may not unambiguously reference a single value factory method; in such a case JUnit will
|
||||
* throw an error at runtime.
|
||||
*
|
||||
* @param methodSourceAnnotation The annotation from which to extract value factory method names.
|
||||
* @return One or more value factory names.
|
||||
* @param method The method on which the annotation is located.
|
||||
* @return One or more value factory descriptors, in the order defined.
|
||||
* @see #getMethodSourceFactoryDescriptors(AnnotationTree, MethodTree)
|
||||
*/
|
||||
static ImmutableSet<String> getMethodSourceFactoryNames(
|
||||
// XXX: Drop this method in favour of `#getMethodSourceFactoryDescriptors`. That will require
|
||||
// callers to either explicitly drop information, or perform a more advanced analysis.
|
||||
public static ImmutableList<String> getMethodSourceFactoryNames(
|
||||
AnnotationTree methodSourceAnnotation, MethodTree method) {
|
||||
return getMethodSourceFactoryDescriptors(methodSourceAnnotation, method).stream()
|
||||
.map(
|
||||
descriptor -> {
|
||||
int index = descriptor.indexOf('(');
|
||||
return index < 0 ? descriptor : descriptor.substring(0, index);
|
||||
})
|
||||
.collect(toImmutableList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the descriptors of the JUnit value factory methods specified by the given {@link
|
||||
* org.junit.jupiter.params.provider.MethodSource} annotation.
|
||||
*
|
||||
* @param methodSourceAnnotation The annotation from which to extract value factory method
|
||||
* descriptors.
|
||||
* @param method The method on which the annotation is located.
|
||||
* @return One or more value factory descriptors, in the order defined.
|
||||
* @see #getMethodSourceFactoryNames(AnnotationTree, MethodTree)
|
||||
*/
|
||||
// XXX: Rather than strings, have this method return instances of a value type capable of
|
||||
// resolving the value factory method pointed to.
|
||||
public static ImmutableList<String> getMethodSourceFactoryDescriptors(
|
||||
AnnotationTree methodSourceAnnotation, MethodTree method) {
|
||||
String methodName = method.getName().toString();
|
||||
ExpressionTree value = AnnotationMatcherUtils.getArgument(methodSourceAnnotation, "value");
|
||||
|
||||
if (!(value instanceof NewArrayTree)) {
|
||||
return ImmutableSet.of(toMethodSourceFactoryName(value, methodName));
|
||||
return ImmutableList.of(toMethodSourceFactoryDescriptor(value, methodName));
|
||||
}
|
||||
|
||||
return ((NewArrayTree) value)
|
||||
.getInitializers().stream()
|
||||
.map(name -> toMethodSourceFactoryName(name, methodName))
|
||||
.collect(toImmutableSet());
|
||||
.map(name -> toMethodSourceFactoryDescriptor(name, methodName))
|
||||
.collect(toImmutableList());
|
||||
}
|
||||
|
||||
private static String toMethodSourceFactoryName(
|
||||
private static String toMethodSourceFactoryDescriptor(
|
||||
@Nullable ExpressionTree tree, String annotatedMethodName) {
|
||||
return requireNonNullElse(
|
||||
Strings.emptyToNull(ASTHelpers.constValue(tree, String.class)), annotatedMethodName);
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import static com.google.errorprone.refaster.ImportPolicy.STATIC_IMPORT_ALWAYS;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
import com.google.errorprone.refaster.annotation.UseImportPolicy;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.assertj.core.api.AbstractAssert;
|
||||
import org.assertj.core.api.AbstractStringAssert;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
@@ -89,4 +94,30 @@ final class AssertJStringRules {
|
||||
return assertThat(string).doesNotMatch(regex);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatPathContent {
|
||||
@BeforeTemplate
|
||||
AbstractStringAssert<?> before(Path path, Charset charset) throws IOException {
|
||||
return assertThat(Files.readString(path, charset));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractStringAssert<?> after(Path path, Charset charset) {
|
||||
return assertThat(path).content(charset);
|
||||
}
|
||||
}
|
||||
|
||||
static final class AssertThatPathContentUtf8 {
|
||||
@BeforeTemplate
|
||||
AbstractStringAssert<?> before(Path path) throws IOException {
|
||||
return assertThat(Files.readString(path));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
AbstractStringAssert<?> after(Path path) {
|
||||
return assertThat(path).content(UTF_8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import com.google.errorprone.BugCheckerRefactoringTestHelper;
|
||||
import com.google.errorprone.BugCheckerRefactoringTestHelper.FixChooser;
|
||||
import com.google.errorprone.BugCheckerRefactoringTestHelper.FixChoosers;
|
||||
import com.google.errorprone.refaster.Refaster;
|
||||
import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
/** Refaster rules related to {@link com.google.errorprone.bugpatterns.BugChecker} classes. */
|
||||
@OnlineDocumentation
|
||||
final class BugCheckerRules {
|
||||
private BugCheckerRules() {}
|
||||
|
||||
/**
|
||||
* Avoid calling {@link BugCheckerRefactoringTestHelper#setFixChooser(FixChooser)} or {@link
|
||||
* BugCheckerRefactoringTestHelper#setImportOrder(String)} with their respective default values.
|
||||
*/
|
||||
static final class BugCheckerRefactoringTestHelperIdentity {
|
||||
@BeforeTemplate
|
||||
BugCheckerRefactoringTestHelper before(BugCheckerRefactoringTestHelper helper) {
|
||||
return Refaster.anyOf(
|
||||
helper.setFixChooser(FixChoosers.FIRST), helper.setImportOrder("static-first"));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
BugCheckerRefactoringTestHelper after(BugCheckerRefactoringTestHelper helper) {
|
||||
return helper;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer {@link BugCheckerRefactoringTestHelper.ExpectOutput#expectUnchanged()} over repeating
|
||||
* the input.
|
||||
*/
|
||||
// XXX: This rule assumes that the full source code is specified as a single string, e.g. using a
|
||||
// text block. Support for multi-line source code input would require a `BugChecker`
|
||||
// implementation instead.
|
||||
static final class BugCheckerRefactoringTestHelperAddInputLinesExpectUnchanged {
|
||||
@BeforeTemplate
|
||||
BugCheckerRefactoringTestHelper before(
|
||||
BugCheckerRefactoringTestHelper helper, String path, String source) {
|
||||
return helper.addInputLines(path, source).addOutputLines(path, source);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
BugCheckerRefactoringTestHelper after(
|
||||
BugCheckerRefactoringTestHelper helper, String path, String source) {
|
||||
return helper.addInputLines(path, source).expectUnchanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import static java.util.Collections.emptyMap;
|
||||
import static java.util.Collections.singletonMap;
|
||||
import static java.util.function.Function.identity;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Streams;
|
||||
@@ -126,8 +127,8 @@ final class ImmutableMapRules {
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't map a a stream's elements to map entries, only to subsequently collect them into an
|
||||
* {@link ImmutableMap}. The collection can be performed directly.
|
||||
* Don't map a stream's elements to map entries, only to subsequently collect them into an {@link
|
||||
* ImmutableMap}. The collection can be performed directly.
|
||||
*/
|
||||
abstract static class StreamOfMapEntriesToImmutableMap<E, K, V> {
|
||||
@Placeholder(allowsIdentity = true)
|
||||
@@ -315,6 +316,48 @@ final class ImmutableMapRules {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer creation of an immutable submap using {@link Maps#filterKeys(Map, Predicate)} over more
|
||||
* contrived alternatives.
|
||||
*/
|
||||
abstract static class ImmutableMapCopyOfMapsFilterKeys<K, V> {
|
||||
@Placeholder(allowsIdentity = true)
|
||||
abstract boolean keyFilter(@MayOptionallyUse K key);
|
||||
|
||||
@BeforeTemplate
|
||||
ImmutableMap<K, V> before(ImmutableMap<K, V> map) {
|
||||
return map.entrySet().stream()
|
||||
.filter(e -> keyFilter(e.getKey()))
|
||||
.collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
ImmutableMap<K, V> after(ImmutableMap<K, V> map) {
|
||||
return ImmutableMap.copyOf(Maps.filterKeys(map, k -> keyFilter(k)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer creation of an immutable submap using {@link Maps#filterValues(Map, Predicate)} over
|
||||
* more contrived alternatives.
|
||||
*/
|
||||
abstract static class ImmutableMapCopyOfMapsFilterValues<K, V> {
|
||||
@Placeholder(allowsIdentity = true)
|
||||
abstract boolean valueFilter(@MayOptionallyUse V value);
|
||||
|
||||
@BeforeTemplate
|
||||
ImmutableMap<K, V> before(ImmutableMap<K, V> map) {
|
||||
return map.entrySet().stream()
|
||||
.filter(e -> valueFilter(e.getValue()))
|
||||
.collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
ImmutableMap<K, V> after(ImmutableMap<K, V> map) {
|
||||
return ImmutableMap.copyOf(Maps.filterValues(map, v -> valueFilter(v)));
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: Add a rule for this:
|
||||
// Maps.transformValues(streamOfEntries.collect(groupBy(fun)), ImmutableMap::copyOf)
|
||||
// ->
|
||||
@@ -323,9 +366,4 @@ final class ImmutableMapRules {
|
||||
// map.entrySet().stream().filter(keyPred).forEach(mapBuilder::put)
|
||||
// ->
|
||||
// mapBuilder.putAll(Maps.filterKeys(map, pred))
|
||||
//
|
||||
// map.entrySet().stream().filter(entry ->
|
||||
// pred(entry.getKey())).collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue))
|
||||
// ->
|
||||
// ImmutableMap.copyOf(Maps.filterKeys(map, pred))
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
import java.util.stream.Stream;
|
||||
import org.jooq.Record;
|
||||
import org.jooq.ResultQuery;
|
||||
import tech.picnic.errorprone.refaster.annotation.Description;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
@OnlineDocumentation
|
||||
final class JooqRules {
|
||||
private JooqRules() {}
|
||||
|
||||
/** Prefer eagerly fetching data over (lazy) streaming to avoid potentially leaking resources. */
|
||||
@Description(
|
||||
"Prefer eagerly fetching data over (lazy) streaming to avoid potentially leaking resources.")
|
||||
static final class ResultQueryFetchStream {
|
||||
@BeforeTemplate
|
||||
Stream<?> before(ResultQuery<? extends Record> resultQuery) {
|
||||
return resultQuery.stream();
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Stream<?> after(ResultQuery<? extends Record> resultQuery) {
|
||||
return resultQuery.fetch().stream();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,11 +21,14 @@ import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
final class NullRules {
|
||||
private NullRules() {}
|
||||
|
||||
/** Prefer the {@code ==} operator over {@link Objects#isNull(Object)}. */
|
||||
/**
|
||||
* Prefer the {@code ==} operator (with {@code null} as the second operand) over {@link
|
||||
* Objects#isNull(Object)}.
|
||||
*/
|
||||
static final class IsNull {
|
||||
@BeforeTemplate
|
||||
boolean before(@Nullable Object object) {
|
||||
return Objects.isNull(object);
|
||||
return Refaster.anyOf(null == object, Objects.isNull(object));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@@ -34,11 +37,14 @@ final class NullRules {
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer the {@code !=} operator over {@link Objects#nonNull(Object)}. */
|
||||
/**
|
||||
* Prefer the {@code !=} operator (with {@code null} as the second operand) over {@link
|
||||
* Objects#nonNull(Object)}.
|
||||
*/
|
||||
static final class IsNotNull {
|
||||
@BeforeTemplate
|
||||
boolean before(@Nullable Object object) {
|
||||
return Objects.nonNull(object);
|
||||
return Refaster.anyOf(null != object, Objects.nonNull(object));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
|
||||
@@ -6,11 +6,13 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Preconditions.checkPositionIndex;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static com.google.errorprone.refaster.ImportPolicy.STATIC_IMPORT_ALWAYS;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
import com.google.errorprone.refaster.annotation.UseImportPolicy;
|
||||
import java.util.Objects;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
|
||||
/** Refaster templates related to statements dealing with {@link Preconditions}. */
|
||||
@@ -72,8 +74,22 @@ final class PreconditionsRules {
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer {@link Preconditions#checkNotNull(Object)} over more verbose alternatives. */
|
||||
static final class CheckNotNull<T> {
|
||||
/** Prefer {@link Objects#requireNonNull(Object)} over non-JDK alternatives. */
|
||||
static final class RequireNonNull<T> {
|
||||
@BeforeTemplate
|
||||
T before(T object) {
|
||||
return checkNotNull(object);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
T after(T object) {
|
||||
return requireNonNull(object);
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer {@link Objects#requireNonNull(Object)} over more verbose alternatives. */
|
||||
static final class RequireNonNullStatement<T> {
|
||||
@BeforeTemplate
|
||||
void before(T object) {
|
||||
if (object == null) {
|
||||
@@ -84,12 +100,26 @@ final class PreconditionsRules {
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
void after(T object) {
|
||||
checkNotNull(object);
|
||||
requireNonNull(object);
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer {@link Preconditions#checkNotNull(Object, Object)} over more verbose alternatives. */
|
||||
static final class CheckNotNullWithMessage<T> {
|
||||
/** Prefer {@link Objects#requireNonNull(Object, String)} over non-JDK alternatives. */
|
||||
static final class RequireNonNullWithMessage<T> {
|
||||
@BeforeTemplate
|
||||
T before(T object, String message) {
|
||||
return checkNotNull(object, message);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
T after(T object, String message) {
|
||||
return requireNonNull(object, message);
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer {@link Objects#requireNonNull(Object, String)} over more verbose alternatives. */
|
||||
static final class RequireNonNullWithMessageStatement<T> {
|
||||
@BeforeTemplate
|
||||
void before(T object, String message) {
|
||||
if (object == null) {
|
||||
@@ -100,7 +130,7 @@ final class PreconditionsRules {
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
void after(T object, String message) {
|
||||
checkNotNull(object, message);
|
||||
requireNonNull(object, message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.common.collect.MoreCollectors.toOptional;
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
|
||||
import static com.google.errorprone.refaster.ImportPolicy.STATIC_IMPORT_ALWAYS;
|
||||
import static java.util.function.Function.identity;
|
||||
import static java.util.stream.Collectors.toCollection;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static reactor.function.TupleUtils.function;
|
||||
|
||||
import com.google.common.collect.ImmutableCollection;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.MoreCollectors;
|
||||
import com.google.errorprone.refaster.Refaster;
|
||||
import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
@@ -17,8 +20,11 @@ import com.google.errorprone.refaster.annotation.NotMatches;
|
||||
import com.google.errorprone.refaster.annotation.Placeholder;
|
||||
import com.google.errorprone.refaster.annotation.UseImportPolicy;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.function.BiConsumer;
|
||||
@@ -699,21 +705,21 @@ final class ReactorRules {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer a collection using {@link MoreCollectors#toOptional()} over more contrived alternatives.
|
||||
*/
|
||||
/** Prefer {@link Mono#singleOptional()} over more contrived alternatives. */
|
||||
// XXX: Consider creating a plugin that flags/discourages `Mono<Optional<T>>` method return
|
||||
// types, just as we discourage nullable `Boolean`s and `Optional`s.
|
||||
static final class MonoCollectToOptional<T> {
|
||||
static final class MonoSingleOptional<T> {
|
||||
@BeforeTemplate
|
||||
Mono<Optional<T>> before(Mono<T> mono) {
|
||||
return mono.map(Optional::of).defaultIfEmpty(Optional.empty());
|
||||
return Refaster.anyOf(
|
||||
mono.flux().collect(toOptional()),
|
||||
mono.map(Optional::of).defaultIfEmpty(Optional.empty()));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
@UseImportPolicy(STATIC_IMPORT_ALWAYS)
|
||||
Mono<Optional<T>> after(Mono<T> mono) {
|
||||
return mono.flux().collect(toOptional());
|
||||
return mono.singleOptional();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -806,6 +812,31 @@ final class ReactorRules {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer {@link Flux#count()} followed by a conversion from {@code long} to {@code int} over
|
||||
* collecting into a list and counting its elements.
|
||||
*/
|
||||
static final class FluxCountMapMathToIntExact<T> {
|
||||
@BeforeTemplate
|
||||
Mono<Integer> before(Flux<T> flux) {
|
||||
return Refaster.anyOf(
|
||||
flux.collect(toImmutableList())
|
||||
.map(
|
||||
Refaster.anyOf(
|
||||
Collection::size,
|
||||
List::size,
|
||||
ImmutableCollection::size,
|
||||
ImmutableList::size)),
|
||||
flux.collect(toCollection(ArrayList::new))
|
||||
.map(Refaster.anyOf(Collection::size, List::size)));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
Mono<Integer> after(Flux<T> flux) {
|
||||
return flux.count().map(Math::toIntExact);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer {@link Mono#doOnError(Class, Consumer)} over {@link Mono#doOnError(Predicate, Consumer)}
|
||||
* where possible.
|
||||
@@ -1173,12 +1204,14 @@ final class ReactorRules {
|
||||
}
|
||||
}
|
||||
|
||||
/** Don't unnecessarily call {@link StepVerifier.Step#expectNext(Object[])}. */
|
||||
static final class StepVerifierStepExpectNextEmpty<T> {
|
||||
/** Don't unnecessarily have {@link StepVerifier.Step} expect no elements. */
|
||||
// XXX: Given an `IsEmpty` matcher that identifies a wide range of guaranteed-empty `Iterable`
|
||||
// expressions, consider also simplifying `step.expectNextSequence(someEmptyIterable)`.
|
||||
static final class StepVerifierStepIdentity<T> {
|
||||
@BeforeTemplate
|
||||
@SuppressWarnings("unchecked")
|
||||
StepVerifier.Step<T> before(StepVerifier.Step<T> step) {
|
||||
return step.expectNext();
|
||||
return Refaster.anyOf(step.expectNext(), step.expectNextCount(0));
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.google.common.collect.Streams;
|
||||
import com.google.errorprone.refaster.Refaster;
|
||||
import com.google.errorprone.refaster.annotation.AfterTemplate;
|
||||
import com.google.errorprone.refaster.annotation.BeforeTemplate;
|
||||
import com.google.errorprone.refaster.annotation.Matches;
|
||||
import com.google.errorprone.refaster.annotation.MayOptionallyUse;
|
||||
import com.google.errorprone.refaster.annotation.Placeholder;
|
||||
import com.google.errorprone.refaster.annotation.UseImportPolicy;
|
||||
@@ -19,10 +20,14 @@ import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.ToDoubleFunction;
|
||||
import java.util.function.ToIntFunction;
|
||||
import java.util.function.ToLongFunction;
|
||||
import java.util.stream.Collector;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
|
||||
import tech.picnic.errorprone.refaster.matchers.IsLambdaExpressionOrMethodReference;
|
||||
|
||||
/** Refaster rules related to expressions dealing with {@link Stream}s. */
|
||||
@OnlineDocumentation
|
||||
@@ -379,4 +384,46 @@ final class StreamRules {
|
||||
return stream.allMatch(e -> test(e));
|
||||
}
|
||||
}
|
||||
|
||||
static final class StreamMapToIntSum<T> {
|
||||
@BeforeTemplate
|
||||
int before(
|
||||
Stream<T> stream,
|
||||
@Matches(IsLambdaExpressionOrMethodReference.class) Function<? super T, Integer> mapper) {
|
||||
return stream.map(mapper).reduce(0, Integer::sum);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
int after(Stream<T> stream, ToIntFunction<T> mapper) {
|
||||
return stream.mapToInt(mapper).sum();
|
||||
}
|
||||
}
|
||||
|
||||
static final class StreamMapToDoubleSum<T> {
|
||||
@BeforeTemplate
|
||||
double before(
|
||||
Stream<T> stream,
|
||||
@Matches(IsLambdaExpressionOrMethodReference.class) Function<? super T, Double> mapper) {
|
||||
return stream.map(mapper).reduce(0.0, Double::sum);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
double after(Stream<T> stream, ToDoubleFunction<T> mapper) {
|
||||
return stream.mapToDouble(mapper).sum();
|
||||
}
|
||||
}
|
||||
|
||||
static final class StreamMapToLongSum<T> {
|
||||
@BeforeTemplate
|
||||
long before(
|
||||
Stream<T> stream,
|
||||
@Matches(IsLambdaExpressionOrMethodReference.class) Function<? super T, Long> mapper) {
|
||||
return stream.map(mapper).reduce(0L, Long::sum);
|
||||
}
|
||||
|
||||
@AfterTemplate
|
||||
long after(Stream<T> stream, ToLongFunction<T> mapper) {
|
||||
return stream.mapToLong(mapper).sum();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
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 DirectReturnTest {
|
||||
@Test
|
||||
void identification() {
|
||||
CompilationTestHelper.newInstance(DirectReturn.class, getClass())
|
||||
.addSourceLines(
|
||||
"A.java",
|
||||
"import static org.mockito.Mockito.mock;",
|
||||
"import static org.mockito.Mockito.spy;",
|
||||
"",
|
||||
"import java.util.function.Supplier;",
|
||||
"",
|
||||
"class A {",
|
||||
" private String field;",
|
||||
"",
|
||||
" void emptyMethod() {}",
|
||||
"",
|
||||
" void voidMethod() {",
|
||||
" toString();",
|
||||
" return;",
|
||||
" }",
|
||||
"",
|
||||
" String directReturnOfParam(String param) {",
|
||||
" return param;",
|
||||
" }",
|
||||
"",
|
||||
" String assignmentToField() {",
|
||||
" field = toString();",
|
||||
" return field;",
|
||||
" }",
|
||||
"",
|
||||
" Object redundantAssignmentToParam(String param) {",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" param = toString();",
|
||||
" return param;",
|
||||
" }",
|
||||
"",
|
||||
" String redundantMockAssignmentToParam(String param) {",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" param = mock();",
|
||||
" return param;",
|
||||
" }",
|
||||
"",
|
||||
" Object redundantMockWithExplicitTypeAssignmentToParam(String param) {",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" param = mock(String.class);",
|
||||
" return param;",
|
||||
" }",
|
||||
"",
|
||||
" Object salientMockAssignmentToParam(String param) {",
|
||||
" param = mock();",
|
||||
" return param;",
|
||||
" }",
|
||||
"",
|
||||
" String redundantAssignmentToLocalVariable() {",
|
||||
" String variable = null;",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" variable = toString();",
|
||||
" return variable;",
|
||||
" }",
|
||||
"",
|
||||
" String unusedAssignmentToLocalVariable(String param) {",
|
||||
" String variable = null;",
|
||||
" variable = toString();",
|
||||
" return param;",
|
||||
" }",
|
||||
"",
|
||||
" String redundantVariableDeclaration() {",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" String variable = toString();",
|
||||
" return variable;",
|
||||
" }",
|
||||
"",
|
||||
" String redundantSpyVariableDeclaration() {",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" String variable = spy();",
|
||||
" return variable;",
|
||||
" }",
|
||||
"",
|
||||
" Object redundantSpyWithExplicitTypeVariableDeclaration() {",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" String variable = spy(String.class);",
|
||||
" return variable;",
|
||||
" }",
|
||||
"",
|
||||
" Object salientSpyTypeVariableDeclaration() {",
|
||||
" String variable = spy(\"name\");",
|
||||
" return variable;",
|
||||
" }",
|
||||
"",
|
||||
" String unusedVariableDeclaration(String param) {",
|
||||
" String variable = toString();",
|
||||
" return param;",
|
||||
" }",
|
||||
"",
|
||||
" String assignmentToAnnotatedVariable() {",
|
||||
" @SuppressWarnings(\"HereBeDragons\")",
|
||||
" String variable = toString();",
|
||||
" return variable;",
|
||||
" }",
|
||||
"",
|
||||
" String complexReturnStatement() {",
|
||||
" String variable = toString();",
|
||||
" return variable + toString();",
|
||||
" }",
|
||||
"",
|
||||
" String assignmentInsideIfClause() {",
|
||||
" String variable = null;",
|
||||
" if (true) {",
|
||||
" variable = toString();",
|
||||
" }",
|
||||
" return variable;",
|
||||
" }",
|
||||
"",
|
||||
" String redundantAssignmentInsideElseClause() {",
|
||||
" String variable = toString();",
|
||||
" if (true) {",
|
||||
" return variable;",
|
||||
" } else {",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" variable = \"foo\";",
|
||||
" return variable;",
|
||||
" }",
|
||||
" }",
|
||||
"",
|
||||
" Supplier<String> redundantAssignmentInsideLambda() {",
|
||||
" return () -> {",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" String variable = toString();",
|
||||
" return variable;",
|
||||
" };",
|
||||
" }",
|
||||
"}")
|
||||
.doTest();
|
||||
}
|
||||
|
||||
@Test
|
||||
void replacement() {
|
||||
BugCheckerRefactoringTestHelper.newInstance(DirectReturn.class, getClass())
|
||||
.addInputLines(
|
||||
"A.java",
|
||||
"class A {",
|
||||
" String m1() {",
|
||||
" String variable = null;",
|
||||
" variable = toString();",
|
||||
" return variable;",
|
||||
" }",
|
||||
"",
|
||||
" String m2() {",
|
||||
" String variable = toString();",
|
||||
" return variable;",
|
||||
" }",
|
||||
"}")
|
||||
.addOutputLines(
|
||||
"A.java",
|
||||
"class A {",
|
||||
" String m1() {",
|
||||
" String variable = null;",
|
||||
" return toString();",
|
||||
" }",
|
||||
"",
|
||||
" String m2() {",
|
||||
" return toString();",
|
||||
" }",
|
||||
"}")
|
||||
.doTest(TestMode.TEXT_MATCH);
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,6 @@ final class FluxFlatMapUsageTest {
|
||||
@Test
|
||||
void replacementFirstSuggestedFix() {
|
||||
BugCheckerRefactoringTestHelper.newInstance(FluxFlatMapUsage.class, getClass())
|
||||
.setFixChooser(FixChoosers.FIRST)
|
||||
.addInputLines(
|
||||
"A.java",
|
||||
"import reactor.core.publisher.Flux;",
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
package tech.picnic.errorprone.bugpatterns;
|
||||
|
||||
import static com.google.common.base.Predicates.and;
|
||||
import static com.google.common.base.Predicates.containsPattern;
|
||||
import static com.google.common.base.Predicates.not;
|
||||
import static com.google.errorprone.BugCheckerRefactoringTestHelper.FixChoosers.SECOND;
|
||||
import static com.google.errorprone.BugCheckerRefactoringTestHelper.FixChoosers.THIRD;
|
||||
|
||||
import com.google.errorprone.BugCheckerRefactoringTestHelper;
|
||||
import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;
|
||||
import com.google.errorprone.CompilationTestHelper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.CorePublisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
final class FluxImplicitBlockTest {
|
||||
@Test
|
||||
void identification() {
|
||||
CompilationTestHelper.newInstance(FluxImplicitBlock.class, getClass())
|
||||
.expectErrorMessage(
|
||||
"X",
|
||||
and(
|
||||
containsPattern("SuppressWarnings"),
|
||||
containsPattern("toImmutableList"),
|
||||
containsPattern("toList")))
|
||||
.addSourceLines(
|
||||
"A.java",
|
||||
"import com.google.common.collect.ImmutableList;",
|
||||
"import java.util.stream.Stream;",
|
||||
"import reactor.core.publisher.Flux;",
|
||||
"",
|
||||
"class A {",
|
||||
" void m() {",
|
||||
" // BUG: Diagnostic matches: X",
|
||||
" Flux.just(1).toIterable();",
|
||||
" // BUG: Diagnostic matches: X",
|
||||
" Flux.just(2).toStream();",
|
||||
" // BUG: Diagnostic matches: X",
|
||||
" long count = Flux.just(3).toStream().count();",
|
||||
"",
|
||||
" Flux.just(4).toIterable(1);",
|
||||
" Flux.just(5).toIterable(2, null);",
|
||||
" Flux.just(6).toStream(3);",
|
||||
" new Foo().toIterable();",
|
||||
" new Foo().toStream();",
|
||||
" }",
|
||||
"",
|
||||
" class Foo<T> {",
|
||||
" Iterable<T> toIterable() {",
|
||||
" return ImmutableList.of();",
|
||||
" }",
|
||||
"",
|
||||
" Stream<T> toStream() {",
|
||||
" return Stream.empty();",
|
||||
" }",
|
||||
" }",
|
||||
"}")
|
||||
.doTest();
|
||||
}
|
||||
|
||||
@Test
|
||||
void identificationWithoutGuavaOnClasspath() {
|
||||
CompilationTestHelper.newInstance(FluxImplicitBlock.class, getClass())
|
||||
.withClasspath(CorePublisher.class, Flux.class, Publisher.class)
|
||||
.expectErrorMessage("X", not(containsPattern("toImmutableList")))
|
||||
.addSourceLines(
|
||||
"A.java",
|
||||
"import reactor.core.publisher.Flux;",
|
||||
"",
|
||||
"class A {",
|
||||
" void m() {",
|
||||
" // BUG: Diagnostic matches: X",
|
||||
" Flux.just(1).toIterable();",
|
||||
" // BUG: Diagnostic matches: X",
|
||||
" Flux.just(2).toStream();",
|
||||
" }",
|
||||
"}")
|
||||
.doTest();
|
||||
}
|
||||
|
||||
@Test
|
||||
void replacementFirstSuggestedFix() {
|
||||
BugCheckerRefactoringTestHelper.newInstance(FluxImplicitBlock.class, getClass())
|
||||
.addInputLines(
|
||||
"A.java",
|
||||
"import reactor.core.publisher.Flux;",
|
||||
"",
|
||||
"class A {",
|
||||
" void m() {",
|
||||
" Flux.just(1).toIterable();",
|
||||
" Flux.just(2).toStream();",
|
||||
" }",
|
||||
"}")
|
||||
.addOutputLines(
|
||||
"A.java",
|
||||
"import reactor.core.publisher.Flux;",
|
||||
"",
|
||||
"class A {",
|
||||
" @SuppressWarnings(\"FluxImplicitBlock\")",
|
||||
" void m() {",
|
||||
" Flux.just(1).toIterable();",
|
||||
" Flux.just(2).toStream();",
|
||||
" }",
|
||||
"}")
|
||||
.doTest(TestMode.TEXT_MATCH);
|
||||
}
|
||||
|
||||
@Test
|
||||
void replacementSecondSuggestedFix() {
|
||||
BugCheckerRefactoringTestHelper.newInstance(FluxImplicitBlock.class, getClass())
|
||||
.setFixChooser(SECOND)
|
||||
.addInputLines(
|
||||
"A.java",
|
||||
"import reactor.core.publisher.Flux;",
|
||||
"",
|
||||
"class A {",
|
||||
" void m() {",
|
||||
" Flux.just(1).toIterable();",
|
||||
" Flux.just(2).toStream();",
|
||||
" Flux.just(3).toIterable().iterator();",
|
||||
" Flux.just(4).toStream().count();",
|
||||
" Flux.just(5) /* a */./* b */ toIterable /* c */(/* d */ ) /* e */;",
|
||||
" Flux.just(6) /* a */./* b */ toStream /* c */(/* d */ ) /* e */;",
|
||||
" }",
|
||||
"}")
|
||||
.addOutputLines(
|
||||
"A.java",
|
||||
"import static com.google.common.collect.ImmutableList.toImmutableList;",
|
||||
"",
|
||||
"import reactor.core.publisher.Flux;",
|
||||
"",
|
||||
"class A {",
|
||||
" void m() {",
|
||||
" Flux.just(1).collect(toImmutableList()).block();",
|
||||
" Flux.just(2).collect(toImmutableList()).block().stream();",
|
||||
" Flux.just(3).collect(toImmutableList()).block().iterator();",
|
||||
" Flux.just(4).collect(toImmutableList()).block().stream().count();",
|
||||
" Flux.just(5).collect(toImmutableList()).block() /* e */;",
|
||||
" Flux.just(6).collect(toImmutableList()).block().stream() /* e */;",
|
||||
" }",
|
||||
"}")
|
||||
.doTest(TestMode.TEXT_MATCH);
|
||||
}
|
||||
|
||||
@Test
|
||||
void replacementThirdSuggestedFix() {
|
||||
BugCheckerRefactoringTestHelper.newInstance(FluxImplicitBlock.class, getClass())
|
||||
.setFixChooser(THIRD)
|
||||
.addInputLines(
|
||||
"A.java",
|
||||
"import reactor.core.publisher.Flux;",
|
||||
"",
|
||||
"class A {",
|
||||
" void m() {",
|
||||
" Flux.just(1).toIterable();",
|
||||
" Flux.just(2).toStream();",
|
||||
" Flux.just(3).toIterable().iterator();",
|
||||
" Flux.just(4).toStream().count();",
|
||||
" Flux.just(5) /* a */./* b */ toIterable /* c */(/* d */ ) /* e */;",
|
||||
" Flux.just(6) /* a */./* b */ toStream /* c */(/* d */ ) /* e */;",
|
||||
" }",
|
||||
"}")
|
||||
.addOutputLines(
|
||||
"A.java",
|
||||
"import static java.util.stream.Collectors.toList;",
|
||||
"",
|
||||
"import reactor.core.publisher.Flux;",
|
||||
"",
|
||||
"class A {",
|
||||
" void m() {",
|
||||
" Flux.just(1).collect(toList()).block();",
|
||||
" Flux.just(2).collect(toList()).block().stream();",
|
||||
" Flux.just(3).collect(toList()).block().iterator();",
|
||||
" Flux.just(4).collect(toList()).block().stream().count();",
|
||||
" Flux.just(5).collect(toList()).block() /* e */;",
|
||||
" Flux.just(6).collect(toList()).block().stream() /* e */;",
|
||||
" }",
|
||||
"}")
|
||||
.doTest(TestMode.TEXT_MATCH);
|
||||
}
|
||||
}
|
||||
@@ -180,7 +180,6 @@ final class IdentityConversionTest {
|
||||
@Test
|
||||
void replacementFirstSuggestedFix() {
|
||||
BugCheckerRefactoringTestHelper.newInstance(IdentityConversion.class, getClass())
|
||||
.setFixChooser(FixChoosers.FIRST)
|
||||
.addInputLines(
|
||||
"A.java",
|
||||
"import static com.google.errorprone.matchers.Matchers.staticMethod;",
|
||||
|
||||
@@ -0,0 +1,496 @@
|
||||
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 JUnitValueSourceTest {
|
||||
@Test
|
||||
void identification() {
|
||||
CompilationTestHelper.newInstance(JUnitValueSource.class, getClass())
|
||||
.addSourceLines(
|
||||
"A.java",
|
||||
"import static org.junit.jupiter.params.provider.Arguments.arguments;",
|
||||
"",
|
||||
"import java.util.Optional;",
|
||||
"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;",
|
||||
"",
|
||||
"class A {",
|
||||
" private static Stream<Arguments> identificationTestCases() {",
|
||||
" return Stream.of(arguments(1), Arguments.of(2));",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" @MethodSource(\"identificationTestCases\")",
|
||||
" void identification(int foo) {}",
|
||||
"",
|
||||
" private static int[] identificationWithParensTestCases() {",
|
||||
" return new int[] {1, 2};",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" @MethodSource(\"identificationWithParensTestCases()\")",
|
||||
" void identificationWithParens(int foo) {}",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @MethodSource(\"valueFactoryMissingTestCases\")",
|
||||
" void valueFactoryMissing(int foo) {}",
|
||||
"",
|
||||
" private static Stream<Arguments> multipleUsagesTestCases() {",
|
||||
" return Stream.of(arguments(1), Arguments.of(2));",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @MethodSource(\"multipleUsagesTestCases\")",
|
||||
" void multipleUsages1(int foo) {}",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @MethodSource(\"multipleUsagesTestCases()\")",
|
||||
" void multipleUsages2(int bar) {}",
|
||||
"",
|
||||
" private static Stream<Arguments> valueFactoryRepeatedTestCases() {",
|
||||
" return Stream.of(arguments(1), arguments(2));",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @MethodSource({\"valueFactoryRepeatedTestCases\", \"valueFactoryRepeatedTestCases\"})",
|
||||
" void valueFactoryRepeated(int foo) {}",
|
||||
"",
|
||||
" private static Stream<Arguments> multipleParametersTestCases() {",
|
||||
" return Stream.of(arguments(1, 2), arguments(3, 4));",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @MethodSource(\"multipleParametersTestCases\")",
|
||||
" void multipleParameters(int first, int second) {}",
|
||||
"",
|
||||
" private static int[] arrayWithoutInitializersTestCases() {",
|
||||
" return new int[1];",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @MethodSource(\"arrayWithoutInitializersTestCases\")",
|
||||
" void arrayWithoutInitializers(int foo) {}",
|
||||
"",
|
||||
" private static Stream<Arguments> runtimeValueTestCases() {",
|
||||
" int second = 2;",
|
||||
" return Stream.of(arguments(1), arguments(second));",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @MethodSource(\"runtimeValueTestCases\")",
|
||||
" void runtimeValue(int foo) {}",
|
||||
"",
|
||||
" private static Stream<Arguments> streamChainTestCases() {",
|
||||
" return Stream.of(1, 2).map(Arguments::arguments);",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @MethodSource(\"streamChainTestCases\")",
|
||||
" void streamChain(int number) {}",
|
||||
"",
|
||||
" private static Stream<Arguments> multipleReturnsTestCases() {",
|
||||
" if (true) {",
|
||||
" return Stream.of(arguments(1), arguments(2));",
|
||||
" } else {",
|
||||
" return Stream.of(arguments(3), arguments(4));",
|
||||
" }",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @MethodSource(\"multipleReturnsTestCases\")",
|
||||
" void multipleReturns(int number) {}",
|
||||
"",
|
||||
" private static Stream<Arguments> multipleFactoriesFooTestCases() {",
|
||||
" return Stream.of(arguments(1));",
|
||||
" }",
|
||||
"",
|
||||
" private static Stream<Arguments> multipleFactoriesBarTestCases() {",
|
||||
" return Stream.of(arguments(1));",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @MethodSource({\"multipleFactoriesFooTestCases\", \"multipleFactoriesBarTestCases\"})",
|
||||
" void multipleFactories(int i) {}",
|
||||
"",
|
||||
" private static Stream<Arguments> extraArgsTestCases() {",
|
||||
" return Stream.of(arguments(1), arguments(1, 2));",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @MethodSource(\"extraArgsTestCases\")",
|
||||
" void extraArgs(int... i) {}",
|
||||
"",
|
||||
" private static Stream<Arguments> localClassTestCases() {",
|
||||
" class Foo {",
|
||||
" Stream<Arguments> foo() {",
|
||||
" return Stream.of(arguments(1), arguments(2));",
|
||||
" }",
|
||||
" }",
|
||||
" return Stream.of(arguments(1), arguments(2));",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" @MethodSource(\"localClassTestCases\")",
|
||||
" void localClass(int i) {}",
|
||||
"",
|
||||
" private static Stream<Arguments> lambdaReturnTestCases() {",
|
||||
" int foo =",
|
||||
" Optional.of(10)",
|
||||
" .map(",
|
||||
" i -> {",
|
||||
" return i / 2;",
|
||||
" })",
|
||||
" .orElse(0);",
|
||||
" return Stream.of(arguments(1), arguments(1));",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" @MethodSource(\"lambdaReturnTestCases\")",
|
||||
" void lambdaReturn(int i) {}",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @MethodSource(\"tech.picnic.errorprone.Foo#fooTestCases\")",
|
||||
" void staticMethodReference(int foo) {}",
|
||||
"",
|
||||
" private static Stream<Arguments> valueFactoryWithArgumentTestCases(int amount) {",
|
||||
" return Stream.of(arguments(1), arguments(2));",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" @MethodSource(\"valueFactoryWithArgumentTestCases\")",
|
||||
" void valueFactoryWithArgument(int foo) {}",
|
||||
"",
|
||||
" private static Arguments[] emptyArrayValueFactoryTestCases() {",
|
||||
" return new Arguments[] {};",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @MethodSource(\"emptyArrayValueFactoryTestCases\")",
|
||||
" void emptyArrayValueFactory(int foo) {}",
|
||||
"",
|
||||
" private static Stream<Arguments> emptyStreamValueFactoryTestCases() {",
|
||||
" return Stream.of();",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @MethodSource(\"emptyStreamValueFactoryTestCases\")",
|
||||
" void emptyStreamValueFactory(int foo) {}",
|
||||
"",
|
||||
" private static Arguments[] invalidValueFactoryArgumentsTestCases() {",
|
||||
" return new Arguments[] {arguments(1), arguments(new Object() {})};",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @MethodSource(\"invalidValueFactoryArgumentsTestCases\")",
|
||||
" void invalidValueFactoryArguments(int foo) {}",
|
||||
"}")
|
||||
.doTest();
|
||||
}
|
||||
|
||||
@Test
|
||||
void replacement() {
|
||||
BugCheckerRefactoringTestHelper.newInstance(JUnitValueSource.class, getClass())
|
||||
.addInputLines(
|
||||
"A.java",
|
||||
"import static org.junit.jupiter.params.provider.Arguments.arguments;",
|
||||
"",
|
||||
"import com.google.common.collect.ImmutableList;",
|
||||
"import com.google.common.collect.ImmutableSet;",
|
||||
"import java.util.List;",
|
||||
"import java.util.Set;",
|
||||
"import java.util.stream.DoubleStream;",
|
||||
"import java.util.stream.IntStream;",
|
||||
"import java.util.stream.LongStream;",
|
||||
"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;",
|
||||
"",
|
||||
"class A {",
|
||||
" private static final boolean CONST_BOOLEAN = false;",
|
||||
" private static final byte CONST_BYTE = 42;",
|
||||
" private static final char CONST_CHARACTER = 'a';",
|
||||
" private static final short CONST_SHORT = 42;",
|
||||
" private static final int CONST_INTEGER = 42;",
|
||||
" private static final long CONST_LONG = 42;",
|
||||
" private static final float CONST_FLOAT = 42;",
|
||||
" private static final double CONST_DOUBLE = 42;",
|
||||
" private static final String CONST_STRING = \"foo\";",
|
||||
"",
|
||||
" private static Stream<Arguments> streamOfBooleanArguments() {",
|
||||
" return Stream.of(arguments(false), arguments(true), arguments(CONST_BOOLEAN));",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @MethodSource(\"streamOfBooleanArguments\")",
|
||||
" void primitiveBoolean(boolean b) {}",
|
||||
"",
|
||||
" private static Stream<Object> streamOfBooleansAndBooleanArguments() {",
|
||||
" return Stream.of(false, arguments(true), CONST_BOOLEAN);",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @MethodSource(\"streamOfBooleansAndBooleanArguments\")",
|
||||
" void boxedBoolean(Boolean b) {}",
|
||||
"",
|
||||
" private static List<Arguments> listOfByteArguments() {",
|
||||
" return List.of(arguments((byte) 0), arguments((byte) 1), arguments(CONST_BYTE));",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @MethodSource(\"listOfByteArguments\")",
|
||||
" void primitiveByte(byte b) {}",
|
||||
"",
|
||||
" private static List<Object> listOfBytesAndByteArguments() {",
|
||||
" return List.of((byte) 0, arguments((byte) 1), CONST_BYTE);",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @MethodSource(\"listOfBytesAndByteArguments\")",
|
||||
" void boxedByte(Byte b) {}",
|
||||
"",
|
||||
" private static Set<Arguments> setOfCharacterArguments() {",
|
||||
" return Set.of(arguments((char) 0), arguments((char) 1), arguments(CONST_CHARACTER));",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @MethodSource(\"setOfCharacterArguments\")",
|
||||
" void primitiveCharacter(char c) {}",
|
||||
"",
|
||||
" private static Set<Object> setOfCharactersAndCharacterArguments() {",
|
||||
" return Set.of((char) 0, arguments((char) 1), CONST_CHARACTER);",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @MethodSource(\"setOfCharactersAndCharacterArguments\")",
|
||||
" void boxedCharacter(Character c) {}",
|
||||
"",
|
||||
" private static Arguments[] arrayOfShortArguments() {",
|
||||
" return new Arguments[] {arguments((short) 0), arguments((short) 1), arguments(CONST_SHORT)};",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @MethodSource(\"arrayOfShortArguments\")",
|
||||
" void primitiveShort(short s) {}",
|
||||
"",
|
||||
" private static Object[] arrayOfShortsAndShortArguments() {",
|
||||
" return new Object[] {(short) 0, arguments((short) 1), CONST_SHORT};",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @MethodSource(\"arrayOfShortsAndShortArguments\")",
|
||||
" void boxedShort(Short s) {}",
|
||||
"",
|
||||
" private static IntStream intStream() {",
|
||||
" return IntStream.of(0, 1, CONST_INTEGER);",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @MethodSource(\"intStream\")",
|
||||
" void primitiveInteger(int i) {}",
|
||||
"",
|
||||
" private static int[] intArray() {",
|
||||
" return new int[] {0, 1, CONST_INTEGER};",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @MethodSource(\"intArray\")",
|
||||
" void boxedInteger(Integer i) {}",
|
||||
"",
|
||||
" private static LongStream longStream() {",
|
||||
" return LongStream.of(0, 1, CONST_LONG);",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @MethodSource(\"longStream\")",
|
||||
" void primitiveLong(long l) {}",
|
||||
"",
|
||||
" private static long[] longArray() {",
|
||||
" return new long[] {0, 1, CONST_LONG};",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @MethodSource(\"longArray\")",
|
||||
" void boxedLong(Long l) {}",
|
||||
"",
|
||||
" private static ImmutableList<Arguments> immutableListOfFloatArguments() {",
|
||||
" return ImmutableList.of(arguments(0.0F), arguments(1.0F), arguments(CONST_FLOAT));",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @MethodSource(\"immutableListOfFloatArguments\")",
|
||||
" void primitiveFloat(float f) {}",
|
||||
"",
|
||||
" private static Stream<Object> streamOfFloatsAndFloatArguments() {",
|
||||
" return Stream.of(0.0F, arguments(1.0F), CONST_FLOAT);",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @MethodSource(\"streamOfFloatsAndFloatArguments\")",
|
||||
" void boxedFloat(Float f) {}",
|
||||
"",
|
||||
" private static DoubleStream doubleStream() {",
|
||||
" return DoubleStream.of(0, 1, CONST_DOUBLE);",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @MethodSource(\"doubleStream\")",
|
||||
" void primitiveDouble(double d) {}",
|
||||
"",
|
||||
" private static double[] doubleArray() {",
|
||||
" return new double[] {0, 1, CONST_DOUBLE};",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @MethodSource(\"doubleArray\")",
|
||||
" void boxedDouble(Double d) {}",
|
||||
"",
|
||||
" private static ImmutableSet<Arguments> immutableSetOfStringArguments() {",
|
||||
" return ImmutableSet.of(arguments(\"foo\"), arguments(\"bar\"), arguments(CONST_STRING));",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @MethodSource(\"immutableSetOfStringArguments\")",
|
||||
" void string(String s) {}",
|
||||
"",
|
||||
" private static Stream<Class<?>> streamOfClasses() {",
|
||||
" return Stream.of(Stream.class, java.util.Map.class);",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @MethodSource(\"streamOfClasses\")",
|
||||
" void clazz(Class<?> c) {}",
|
||||
"",
|
||||
" private static Stream<Arguments> sameNameFactoryTestCases() {",
|
||||
" return Stream.of(arguments(1));",
|
||||
" }",
|
||||
"",
|
||||
" private static Stream<Arguments> sameNameFactoryTestCases(int overload) {",
|
||||
" return Stream.of(arguments(overload));",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @MethodSource(\"sameNameFactoryTestCases\")",
|
||||
" void sameNameFactory(int i) {}",
|
||||
"}")
|
||||
.addOutputLines(
|
||||
"A.java",
|
||||
"import static org.junit.jupiter.params.provider.Arguments.arguments;",
|
||||
"",
|
||||
"import com.google.common.collect.ImmutableList;",
|
||||
"import com.google.common.collect.ImmutableSet;",
|
||||
"import java.util.List;",
|
||||
"import java.util.Set;",
|
||||
"import java.util.stream.DoubleStream;",
|
||||
"import java.util.stream.IntStream;",
|
||||
"import java.util.stream.LongStream;",
|
||||
"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.junit.jupiter.params.provider.ValueSource;",
|
||||
"",
|
||||
"class A {",
|
||||
" private static final boolean CONST_BOOLEAN = false;",
|
||||
" private static final byte CONST_BYTE = 42;",
|
||||
" private static final char CONST_CHARACTER = 'a';",
|
||||
" private static final short CONST_SHORT = 42;",
|
||||
" private static final int CONST_INTEGER = 42;",
|
||||
" private static final long CONST_LONG = 42;",
|
||||
" private static final float CONST_FLOAT = 42;",
|
||||
" private static final double CONST_DOUBLE = 42;",
|
||||
" private static final String CONST_STRING = \"foo\";",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @ValueSource(booleans = {false, true, CONST_BOOLEAN})",
|
||||
" void primitiveBoolean(boolean b) {}",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @ValueSource(booleans = {false, true, CONST_BOOLEAN})",
|
||||
" void boxedBoolean(Boolean b) {}",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @ValueSource(bytes = {(byte) 0, (byte) 1, CONST_BYTE})",
|
||||
" void primitiveByte(byte b) {}",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @ValueSource(bytes = {(byte) 0, (byte) 1, CONST_BYTE})",
|
||||
" void boxedByte(Byte b) {}",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @ValueSource(chars = {(char) 0, (char) 1, CONST_CHARACTER})",
|
||||
" void primitiveCharacter(char c) {}",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @ValueSource(chars = {(char) 0, (char) 1, CONST_CHARACTER})",
|
||||
" void boxedCharacter(Character c) {}",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @ValueSource(shorts = {(short) 0, (short) 1, CONST_SHORT})",
|
||||
" void primitiveShort(short s) {}",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @ValueSource(shorts = {(short) 0, (short) 1, CONST_SHORT})",
|
||||
" void boxedShort(Short s) {}",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @ValueSource(ints = {0, 1, CONST_INTEGER})",
|
||||
" void primitiveInteger(int i) {}",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @ValueSource(ints = {0, 1, CONST_INTEGER})",
|
||||
" void boxedInteger(Integer i) {}",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @ValueSource(longs = {0, 1, CONST_LONG})",
|
||||
" void primitiveLong(long l) {}",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @ValueSource(longs = {0, 1, CONST_LONG})",
|
||||
" void boxedLong(Long l) {}",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @ValueSource(floats = {0.0F, 1.0F, CONST_FLOAT})",
|
||||
" void primitiveFloat(float f) {}",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @ValueSource(floats = {0.0F, 1.0F, CONST_FLOAT})",
|
||||
" void boxedFloat(Float f) {}",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @ValueSource(doubles = {0, 1, CONST_DOUBLE})",
|
||||
" void primitiveDouble(double d) {}",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @ValueSource(doubles = {0, 1, CONST_DOUBLE})",
|
||||
" void boxedDouble(Double d) {}",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @ValueSource(strings = {\"foo\", \"bar\", CONST_STRING})",
|
||||
" void string(String s) {}",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @ValueSource(classes = {Stream.class, java.util.Map.class})",
|
||||
" void clazz(Class<?> c) {}",
|
||||
"",
|
||||
" private static Stream<Arguments> sameNameFactoryTestCases(int overload) {",
|
||||
" return Stream.of(arguments(overload));",
|
||||
" }",
|
||||
"",
|
||||
" @ParameterizedTest",
|
||||
" @ValueSource(ints = 1)",
|
||||
" void sameNameFactory(int i) {}",
|
||||
"}")
|
||||
.doTest(TestMode.TEXT_MATCH);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
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 MockitoMockClassReferenceTest {
|
||||
@Test
|
||||
void identification() {
|
||||
CompilationTestHelper.newInstance(MockitoMockClassReference.class, getClass())
|
||||
.addSourceLines(
|
||||
"A.java",
|
||||
"import static org.mockito.Mockito.mock;",
|
||||
"import static org.mockito.Mockito.spy;",
|
||||
"import static org.mockito.Mockito.withSettings;",
|
||||
"",
|
||||
"import java.util.List;",
|
||||
"import java.util.Objects;",
|
||||
"import org.mockito.invocation.InvocationOnMock;",
|
||||
"",
|
||||
"class A {",
|
||||
" {",
|
||||
" Double d = Objects.requireNonNullElseGet(null, () -> mock(Double.class));",
|
||||
" Double d2 =",
|
||||
" Objects.requireNonNullElseGet(",
|
||||
" null,",
|
||||
" () -> {",
|
||||
" return mock(Double.class);",
|
||||
" });",
|
||||
" }",
|
||||
"",
|
||||
" void m() {",
|
||||
" Number variableMock = 42;",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" variableMock = mock(Number.class);",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" variableMock = mock(Number.class, \"name\");",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" variableMock = mock(Number.class, InvocationOnMock::callRealMethod);",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" variableMock = mock(Number.class, withSettings());",
|
||||
" variableMock = mock(Integer.class);",
|
||||
" variableMock = 42;",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" List rawMock = mock(List.class);",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" List<String> genericMock = mock(List.class);",
|
||||
" var varMock = mock(Integer.class);",
|
||||
" Class<? extends Number> numberType = Integer.class;",
|
||||
" Number variableTypeMock = mock(numberType);",
|
||||
" Object subtypeMock = mock(Integer.class);",
|
||||
"",
|
||||
" Number variableSpy = 42;",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" variableSpy = spy(Number.class);",
|
||||
" variableSpy = spy(Integer.class);",
|
||||
" variableSpy = 42;",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" List rawSpy = spy(List.class);",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" List<String> genericSpy = spy(List.class);",
|
||||
" var varSpy = spy(Integer.class);",
|
||||
" Number variableTypeSpy = spy(numberType);",
|
||||
" Object subtypeSpy = spy(Integer.class);",
|
||||
" Object objectSpy = spy(new Object());",
|
||||
"",
|
||||
" Objects.hash(mock(Integer.class));",
|
||||
" Integer i = mock(mock(Integer.class));",
|
||||
" String s = new String(mock(String.class));",
|
||||
" }",
|
||||
"",
|
||||
" Double getDoubleMock() {",
|
||||
" return Objects.requireNonNullElseGet(",
|
||||
" null,",
|
||||
" () -> {",
|
||||
" return mock(Double.class);",
|
||||
" });",
|
||||
" }",
|
||||
"",
|
||||
" Integer getIntegerMock() {",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" return mock(Integer.class);",
|
||||
" }",
|
||||
"",
|
||||
" <T> T getGenericMock(Class<T> clazz) {",
|
||||
" return mock(clazz);",
|
||||
" }",
|
||||
"",
|
||||
" Number getSubTypeMock() {",
|
||||
" return mock(Integer.class);",
|
||||
" }",
|
||||
"}")
|
||||
.doTest();
|
||||
}
|
||||
|
||||
@Test
|
||||
void replacement() {
|
||||
BugCheckerRefactoringTestHelper.newInstance(MockitoMockClassReference.class, getClass())
|
||||
.addInputLines(
|
||||
"A.java",
|
||||
"import static org.mockito.Mockito.mock;",
|
||||
"import static org.mockito.Mockito.spy;",
|
||||
"import static org.mockito.Mockito.withSettings;",
|
||||
"",
|
||||
"import org.mockito.invocation.InvocationOnMock;",
|
||||
"",
|
||||
"class A {",
|
||||
" void m() {",
|
||||
" Number simpleMock = mock(Number.class);",
|
||||
" Number namedMock = mock(Number.class, \"name\");",
|
||||
" Number customAnswerMock = mock(Number.class, InvocationOnMock::callRealMethod);",
|
||||
" Number customSettingsMock = mock(Number.class, withSettings());",
|
||||
" Number simpleSpy = spy(Number.class);",
|
||||
" }",
|
||||
"}")
|
||||
.addOutputLines(
|
||||
"A.java",
|
||||
"import static org.mockito.Mockito.mock;",
|
||||
"import static org.mockito.Mockito.spy;",
|
||||
"import static org.mockito.Mockito.withSettings;",
|
||||
"",
|
||||
"import org.mockito.invocation.InvocationOnMock;",
|
||||
"",
|
||||
"class A {",
|
||||
" void m() {",
|
||||
" Number simpleMock = mock();",
|
||||
" Number namedMock = mock(\"name\");",
|
||||
" Number customAnswerMock = mock(InvocationOnMock::callRealMethod);",
|
||||
" Number customSettingsMock = mock(withSettings());",
|
||||
" Number simpleSpy = spy();",
|
||||
" }",
|
||||
"}")
|
||||
.doTest(TestMode.TEXT_MATCH);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,8 @@ final class RequestMappingAnnotationTest {
|
||||
"import javax.servlet.http.HttpServletRequest;",
|
||||
"import javax.servlet.http.HttpServletResponse;",
|
||||
"import org.springframework.http.HttpMethod;",
|
||||
"import org.springframework.ui.Model;",
|
||||
"import org.springframework.validation.BindingResult;",
|
||||
"import org.springframework.web.bind.annotation.DeleteMapping;",
|
||||
"import org.springframework.web.bind.annotation.GetMapping;",
|
||||
"import org.springframework.web.bind.annotation.PatchMapping;",
|
||||
@@ -82,6 +84,12 @@ final class RequestMappingAnnotationTest {
|
||||
" A properHttpMethod(HttpMethod method);",
|
||||
"",
|
||||
" @RequestMapping",
|
||||
" A properModel(Model model);",
|
||||
"",
|
||||
" @RequestMapping",
|
||||
" A properBindingResult(BindingResult result);",
|
||||
"",
|
||||
" @RequestMapping",
|
||||
" A properNativeWebRequest(NativeWebRequest request);",
|
||||
"",
|
||||
" @RequestMapping",
|
||||
|
||||
@@ -48,7 +48,6 @@ final class StringCaseLocaleUsageTest {
|
||||
@Test
|
||||
void replacementFirstSuggestedFix() {
|
||||
BugCheckerRefactoringTestHelper.newInstance(StringCaseLocaleUsage.class, getClass())
|
||||
.setFixChooser(FixChoosers.FIRST)
|
||||
.addInputLines(
|
||||
"A.java",
|
||||
"class A {",
|
||||
|
||||
@@ -8,9 +8,16 @@ 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.ExpressionStatementTreeMatcher;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.ReturnTreeMatcher;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.sun.source.tree.ExpressionStatementTree;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import com.sun.source.tree.ReturnTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import com.sun.source.tree.VariableTree;
|
||||
import java.util.List;
|
||||
import java.util.function.BiFunction;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -67,7 +74,70 @@ final class MoreASTHelpersTest {
|
||||
.doTest();
|
||||
}
|
||||
|
||||
private static String createDiagnosticsMessage(
|
||||
@Test
|
||||
void findMethodExitedOnReturn() {
|
||||
CompilationTestHelper.newInstance(FindMethodReturnTestChecker.class, getClass())
|
||||
.addSourceLines(
|
||||
"A.java",
|
||||
"import java.util.stream.Stream;",
|
||||
"",
|
||||
"class A {",
|
||||
" {",
|
||||
" toString();",
|
||||
" }",
|
||||
"",
|
||||
" String topLevelMethod() {",
|
||||
" // BUG: Diagnostic contains: topLevelMethod",
|
||||
" toString();",
|
||||
" // BUG: Diagnostic contains: topLevelMethod",
|
||||
" return toString();",
|
||||
" }",
|
||||
"",
|
||||
" Stream<String> anotherMethod() {",
|
||||
" // BUG: Diagnostic contains: anotherMethod",
|
||||
" return Stream.of(1)",
|
||||
" .map(",
|
||||
" n -> {",
|
||||
" toString();",
|
||||
" return toString();",
|
||||
" });",
|
||||
" }",
|
||||
"",
|
||||
" void recursiveMethod(Runnable r) {",
|
||||
" // BUG: Diagnostic contains: recursiveMethod",
|
||||
" recursiveMethod(",
|
||||
" new Runnable() {",
|
||||
" @Override",
|
||||
" public void run() {",
|
||||
" // BUG: Diagnostic contains: run",
|
||||
" toString();",
|
||||
" }",
|
||||
" });",
|
||||
" }",
|
||||
"}")
|
||||
.doTest();
|
||||
}
|
||||
|
||||
@Test
|
||||
void areSameType() {
|
||||
CompilationTestHelper.newInstance(AreSameTypeTestChecker.class, getClass())
|
||||
.addSourceLines(
|
||||
"A.java",
|
||||
"class A {",
|
||||
" void negative1(String a, Integer b) {}",
|
||||
"",
|
||||
" void negative2(Integer a, Number b) {}",
|
||||
"",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" void positive1(String a, String b) {}",
|
||||
"",
|
||||
" // BUG: Diagnostic contains:",
|
||||
" void positive2(Iterable<String> a, Iterable<Integer> b) {}",
|
||||
"}")
|
||||
.doTest();
|
||||
}
|
||||
|
||||
private static String createMethodSearchDiagnosticsMessage(
|
||||
BiFunction<String, VisitorState, Object> valueFunction, VisitorState state) {
|
||||
return Maps.toMap(ImmutableSet.of("foo", "bar", "baz"), key -> valueFunction.apply(key, state))
|
||||
.toString();
|
||||
@@ -85,7 +155,7 @@ final class MoreASTHelpersTest {
|
||||
public Description matchMethod(MethodTree tree, VisitorState state) {
|
||||
return buildDescription(tree)
|
||||
.setMessage(
|
||||
createDiagnosticsMessage(
|
||||
createMethodSearchDiagnosticsMessage(
|
||||
(methodName, s) -> MoreASTHelpers.findMethods(methodName, s).size(), state))
|
||||
.build();
|
||||
}
|
||||
@@ -103,8 +173,55 @@ final class MoreASTHelpersTest {
|
||||
@Override
|
||||
public Description matchMethod(MethodTree tree, VisitorState state) {
|
||||
return buildDescription(tree)
|
||||
.setMessage(createDiagnosticsMessage(MoreASTHelpers::methodExistsInEnclosingClass, state))
|
||||
.setMessage(
|
||||
createMethodSearchDiagnosticsMessage(
|
||||
MoreASTHelpers::methodExistsInEnclosingClass, state))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that delegates to {@link
|
||||
* MoreASTHelpers#findMethodExitedOnReturn(VisitorState)}.
|
||||
*/
|
||||
@BugPattern(summary = "Interacts with `MoreASTHelpers` for testing purposes", severity = ERROR)
|
||||
public static final class FindMethodReturnTestChecker extends BugChecker
|
||||
implements ExpressionStatementTreeMatcher, ReturnTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Override
|
||||
public Description matchExpressionStatement(ExpressionStatementTree tree, VisitorState state) {
|
||||
return flagMethodReturnLocation(tree, state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Description matchReturn(ReturnTree tree, VisitorState state) {
|
||||
return flagMethodReturnLocation(tree, state);
|
||||
}
|
||||
|
||||
private Description flagMethodReturnLocation(Tree tree, VisitorState state) {
|
||||
return MoreASTHelpers.findMethodExitedOnReturn(state)
|
||||
.map(m -> buildDescription(tree).setMessage(m.getName().toString()).build())
|
||||
.orElse(Description.NO_MATCH);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that delegates to {@link MoreASTHelpers#areSameType(Tree, Tree,
|
||||
* VisitorState)}.
|
||||
*/
|
||||
@BugPattern(summary = "Interacts with `MoreASTHelpers` for testing purposes", severity = ERROR)
|
||||
public static final class AreSameTypeTestChecker extends BugChecker implements MethodTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Override
|
||||
public Description matchMethod(MethodTree tree, VisitorState state) {
|
||||
List<? extends VariableTree> parameters = tree.getParameters();
|
||||
return parameters.stream()
|
||||
.skip(1)
|
||||
.allMatch(p -> MoreASTHelpers.areSameType(p, parameters.get(0), state))
|
||||
? describeMatch(tree)
|
||||
: Description.NO_MATCH;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,40 @@ final class MoreJUnitMatchersTest {
|
||||
@Test
|
||||
void getMethodSourceFactoryNames() {
|
||||
CompilationTestHelper.newInstance(MethodSourceFactoryNamesTestChecker.class, getClass())
|
||||
.addSourceLines(
|
||||
"A.java",
|
||||
"import org.junit.jupiter.params.provider.MethodSource;",
|
||||
"",
|
||||
"class A {",
|
||||
" @MethodSource",
|
||||
" // BUG: Diagnostic contains: [matchingMethodSource]",
|
||||
" void matchingMethodSource(boolean b) {}",
|
||||
"",
|
||||
" @MethodSource(\"myValueFactory\")",
|
||||
" // BUG: Diagnostic contains: [myValueFactory]",
|
||||
" void singleCustomMethodSource(boolean b) {}",
|
||||
"",
|
||||
" @MethodSource({",
|
||||
" \"nullary()\",",
|
||||
" \"nullary()\",",
|
||||
" \"\",",
|
||||
" \"withStringParam(java.lang.String)\",",
|
||||
" \"paramsUnspecified\"",
|
||||
" })",
|
||||
" // BUG: Diagnostic contains: [nullary, nullary, multipleMethodSources, withStringParam,",
|
||||
" // paramsUnspecified]",
|
||||
" void multipleMethodSources(boolean b) {}",
|
||||
"",
|
||||
" @MethodSource({\"foo\", \"()\", \"bar\"})",
|
||||
" // BUG: Diagnostic contains: [foo, , bar]",
|
||||
" void methodSourceWithoutName(boolean b) {}",
|
||||
"}")
|
||||
.doTest();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getMethodSourceFactoryDescriptors() {
|
||||
CompilationTestHelper.newInstance(MethodSourceFactoryDescriptorsTestChecker.class, getClass())
|
||||
.addSourceLines(
|
||||
"A.java",
|
||||
"import org.junit.jupiter.params.provider.MethodSource;",
|
||||
@@ -119,6 +153,14 @@ final class MoreJUnitMatchersTest {
|
||||
" @MethodSource({\"myValueFactory\", \"\"})",
|
||||
" // BUG: Diagnostic contains: [myValueFactory, customAndMatchingMethodSources]",
|
||||
" void customAndMatchingMethodSources(boolean b) {}",
|
||||
"",
|
||||
" @MethodSource({\"factory\", \"\", \"factory\", \"\"})",
|
||||
" // BUG: Diagnostic contains: [factory, repeatedMethodSources, factory, repeatedMethodSources]",
|
||||
" void repeatedMethodSources(boolean b) {}",
|
||||
"",
|
||||
" @MethodSource({\"nullary()\", \"withStringParam(java.lang.String)\"})",
|
||||
" // BUG: Diagnostic contains: [nullary(), withStringParam(java.lang.String)]",
|
||||
" void methodSourcesWithParameterSpecification(boolean b) {}",
|
||||
"}")
|
||||
.doTest();
|
||||
}
|
||||
@@ -170,4 +212,25 @@ final class MoreJUnitMatchersTest {
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link BugChecker} that flags methods with a JUnit {@code @MethodSource} annotation by
|
||||
* enumerating the associated value factory method descriptors.
|
||||
*/
|
||||
@BugPattern(summary = "Interacts with `MoreJUnitMatchers` for testing purposes", severity = ERROR)
|
||||
public static final class MethodSourceFactoryDescriptorsTestChecker extends BugChecker
|
||||
implements MethodTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Override
|
||||
public Description matchMethod(MethodTree tree, VisitorState state) {
|
||||
AnnotationTree annotation =
|
||||
Iterables.getOnlyElement(HAS_METHOD_SOURCE.multiMatchResult(tree, state).matchingNodes());
|
||||
|
||||
return buildDescription(tree)
|
||||
.setMessage(
|
||||
MoreJUnitMatchers.getMethodSourceFactoryDescriptors(annotation, tree).toString())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ final class SourceCodeTest {
|
||||
.expectUnchanged()
|
||||
.addInputLines(
|
||||
"AnnotationDeletions.java",
|
||||
"",
|
||||
"interface AnnotationDeletions {",
|
||||
" class SoleAnnotation {",
|
||||
" @AnnotationToBeDeleted",
|
||||
@@ -66,7 +65,6 @@ final class SourceCodeTest {
|
||||
"}")
|
||||
.addOutputLines(
|
||||
"AnnotationDeletions.java",
|
||||
"",
|
||||
"interface AnnotationDeletions {",
|
||||
" class SoleAnnotation {",
|
||||
" void m() {}",
|
||||
@@ -101,7 +99,6 @@ final class SourceCodeTest {
|
||||
refactoringTestHelper
|
||||
.addInputLines(
|
||||
"MethodDeletions.java",
|
||||
"",
|
||||
"interface MethodDeletions {",
|
||||
" class SoleMethod {",
|
||||
" void methodToBeDeleted() {}",
|
||||
@@ -141,7 +138,6 @@ final class SourceCodeTest {
|
||||
"}")
|
||||
.addOutputLines(
|
||||
"MethodDeletions.java",
|
||||
"",
|
||||
"interface MethodDeletions {",
|
||||
" class SoleMethod {}",
|
||||
"",
|
||||
|
||||
@@ -35,6 +35,7 @@ final class RefasterRulesTest {
|
||||
AssertJThrowingCallableRules.class,
|
||||
AssortedRules.class,
|
||||
BigDecimalRules.class,
|
||||
BugCheckerRules.class,
|
||||
CollectionRules.class,
|
||||
ComparatorRules.class,
|
||||
DoubleStreamRules.class,
|
||||
@@ -49,7 +50,6 @@ final class RefasterRulesTest {
|
||||
ImmutableSortedMultisetRules.class,
|
||||
ImmutableSortedSetRules.class,
|
||||
IntStreamRules.class,
|
||||
JooqRules.class,
|
||||
JUnitRules.class,
|
||||
JUnitToAssertJRules.class,
|
||||
LongStreamRules.class,
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
import org.jooq.impl.DSL;
|
||||
import tech.picnic.errorprone.refaster.test.RefasterRuleCollectionTestCase;
|
||||
|
||||
final class JooqRulesTest implements RefasterRuleCollectionTestCase {
|
||||
Stream<?> testResultQueryFetchStream() {
|
||||
return DSL.select().stream();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
import org.jooq.impl.DSL;
|
||||
import tech.picnic.errorprone.refaster.test.RefasterRuleCollectionTestCase;
|
||||
|
||||
final class JooqRulesTest implements RefasterRuleCollectionTestCase {
|
||||
Stream<?> testResultQueryFetchStream() {
|
||||
return DSL.select().fetch().stream();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.data.Offset.offset;
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.data.Offset.offset;
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.data.Offset.offset;
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.data.Offset.offset;
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.data.Offset.offset;
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.data.Offset.offset;
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.data.Offset.offset;
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.data.Offset.offset;
|
||||
@@ -1,12 +1,22 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import org.assertj.core.api.AbstractAssert;
|
||||
import org.assertj.core.api.AbstractStringAssert;
|
||||
import tech.picnic.errorprone.refaster.test.RefasterRuleCollectionTestCase;
|
||||
|
||||
final class AssertJStringRulesTest implements RefasterRuleCollectionTestCase {
|
||||
@Override
|
||||
public ImmutableSet<?> elidedTypesAndStaticImports() {
|
||||
return ImmutableSet.of(Files.class);
|
||||
}
|
||||
|
||||
void testAbstractStringAssertStringIsEmpty() {
|
||||
assertThat("foo").isEqualTo("");
|
||||
}
|
||||
@@ -30,4 +40,12 @@ final class AssertJStringRulesTest implements RefasterRuleCollectionTestCase {
|
||||
AbstractAssert<?, ?> testAssertThatDoesNotMatch() {
|
||||
return assertThat("foo".matches(".*")).isFalse();
|
||||
}
|
||||
|
||||
AbstractStringAssert<?> testAssertThatPathContent() throws IOException {
|
||||
return assertThat(Files.readString(Paths.get(""), Charset.defaultCharset()));
|
||||
}
|
||||
|
||||
AbstractStringAssert<?> testAssertThatPathContentUtf8() throws IOException {
|
||||
return assertThat(Files.readString(Paths.get("")));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIOException;
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import java.math.BigDecimal;
|
||||
@@ -0,0 +1,29 @@
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.errorprone.BugCheckerRefactoringTestHelper;
|
||||
import com.google.errorprone.BugCheckerRefactoringTestHelper.FixChoosers;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import tech.picnic.errorprone.refaster.test.RefasterRuleCollectionTestCase;
|
||||
|
||||
final class BugCheckerRulesTest implements RefasterRuleCollectionTestCase {
|
||||
@Override
|
||||
public ImmutableSet<?> elidedTypesAndStaticImports() {
|
||||
return ImmutableSet.of(FixChoosers.class);
|
||||
}
|
||||
|
||||
ImmutableSet<BugCheckerRefactoringTestHelper> testBugCheckerRefactoringTestHelperIdentity() {
|
||||
return ImmutableSet.of(
|
||||
BugCheckerRefactoringTestHelper.newInstance(BugChecker.class, getClass())
|
||||
.setFixChooser(FixChoosers.FIRST),
|
||||
BugCheckerRefactoringTestHelper.newInstance(BugChecker.class, getClass())
|
||||
.setImportOrder("static-first"));
|
||||
}
|
||||
|
||||
BugCheckerRefactoringTestHelper
|
||||
testBugCheckerRefactoringTestHelperAddInputLinesExpectUnchanged() {
|
||||
return BugCheckerRefactoringTestHelper.newInstance(BugChecker.class, getClass())
|
||||
.addInputLines("A.java", "class A {}")
|
||||
.addOutputLines("A.java", "class A {}");
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static java.util.Comparator.naturalOrder;
|
||||
import static java.util.Comparator.reverseOrder;
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Streams;
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import com.google.common.collect.BoundType;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static com.google.common.collect.ImmutableListMultimap.flatteningToImmutableListMultimap;
|
||||
import static com.google.common.collect.ImmutableListMultimap.toImmutableListMultimap;
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static java.util.Comparator.naturalOrder;
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static com.google.common.collect.ImmutableMap.toImmutableMap;
|
||||
import static java.util.function.Function.identity;
|
||||
@@ -107,4 +107,16 @@ final class ImmutableMapRulesTest implements RefasterRuleCollectionTestCase {
|
||||
Map<String, String> testImmutableMapOf5() {
|
||||
return Map.of("k1", "v1", "k2", "v2", "k3", "v3", "k4", "v4", "k5", "v5");
|
||||
}
|
||||
|
||||
ImmutableMap<String, Integer> testImmutableMapCopyOfMapsFilterKeys() {
|
||||
return ImmutableMap.of("foo", 1).entrySet().stream()
|
||||
.filter(entry -> entry.getKey().length() > 1)
|
||||
.collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
}
|
||||
|
||||
ImmutableMap<String, Integer> testImmutableMapCopyOfMapsFilterValues() {
|
||||
return ImmutableMap.of("foo", 1).entrySet().stream()
|
||||
.filter(entry -> entry.getValue() > 0)
|
||||
.collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static com.google.common.collect.ImmutableMultiset.toImmutableMultiset;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static com.google.common.collect.ImmutableSetMultimap.flatteningToImmutableSetMultimap;
|
||||
import static com.google.common.collect.ImmutableSetMultimap.toImmutableSetMultimap;
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static com.google.common.collect.ImmutableSortedMap.toImmutableSortedMap;
|
||||
import static java.util.Comparator.naturalOrder;
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static com.google.common.collect.ImmutableSortedMultiset.toImmutableSortedMultiset;
|
||||
import static java.util.Comparator.naturalOrder;
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
|
||||
import static java.util.Comparator.naturalOrder;
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Streams;
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Streams;
|
||||
@@ -1,4 +1,4 @@
|
||||
package tech.picnic.errorprone.refasterrules;
|
||||
package tech.picnic.errorprone.refasterrules.input;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Maps;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user