Update all tests to use text blocks

All `CompilationTestHelper` and `BugCheckerRefactoringTestHelper` test
code is now specified using a single text block. These changes were
automated thanks to the new text block support added to the
`ErrorProneTestHelperSourceFormat` check.
This commit is contained in:
Stephan Schroevers
2022-08-14 14:36:15 +02:00
parent 24636372c8
commit 13a1cd2f4b
7 changed files with 452 additions and 54 deletions

View File

@@ -233,6 +233,12 @@ Other highly relevant commands:
against _all_ code in the current working directory. For more information
check the [PIT Maven plugin][pitest-maven].
The `BugChecker` implementations provided by this project are tested using
Error Prone's `CompilationTestHelper` and `BugCheckerRefactoringTestHelper`
classes. These utilities accept text blocks containing inline Java source code.
To ease modification of this inline source code, consider using IntelliJ IDEA's
[language injection][idea-language-injection] feature.
## 💡 How it works
This project provides additional [`BugChecker`][error-prone-bugchecker]
@@ -268,6 +274,7 @@ channel; please see our [security policy][security] for details.
[github-actions-build-badge]: https://github.com/PicnicSupermarket/error-prone-support/actions/workflows/build.yml/badge.svg
[github-actions-build-master]: https://github.com/PicnicSupermarket/error-prone-support/actions/workflows/build.yml?query=branch:master&event=push
[google-java-format]: https://github.com/google/google-java-format
[idea-language-injection]: https://www.jetbrains.com/help/idea/using-language-injections.html
[license-badge]: https://img.shields.io/github/license/PicnicSupermarket/error-prone-support
[license]: https://github.com/PicnicSupermarket/error-prone-support/blob/master/LICENSE.md
[maven-central-badge]: https://img.shields.io/maven-central/v/tech.picnic.error-prone-support/error-prone-support?color=blue

View File

@@ -6,19 +6,24 @@ import static com.google.errorprone.BugPattern.StandardTags.STYLE;
import static com.google.errorprone.matchers.Matchers.anyOf;
import static com.google.errorprone.matchers.Matchers.instanceMethod;
import static com.google.errorprone.matchers.Matchers.staticMethod;
import static com.sun.tools.javac.util.Position.NOPOS;
import static java.util.stream.Collectors.joining;
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.base.CharMatcher;
import com.google.common.base.Splitter;
import com.google.errorprone.BugPattern;
import com.google.errorprone.ErrorProneFlags;
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.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.suppliers.Supplier;
import com.google.errorprone.util.ASTHelpers;
import com.google.errorprone.util.SourceVersion;
import com.google.googlejavaformat.java.Formatter;
import com.google.googlejavaformat.java.FormatterException;
import com.google.googlejavaformat.java.ImportOrderer;
@@ -27,9 +32,12 @@ import com.google.googlejavaformat.java.RemoveUnusedImports;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.Tree;
import com.sun.tools.javac.util.Position;
import com.sun.tools.javac.api.MultiTaskListener;
import com.sun.tools.javac.util.Context;
import java.util.List;
import java.util.Optional;
import javax.inject.Inject;
import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/**
* A {@link BugChecker} that flags improperly formatted Error Prone test code.
@@ -45,20 +53,33 @@ import java.util.Optional;
* are not able to) remove imports that become obsolete as a result of applying their suggested
* fix(es).
*/
// XXX: Once we target JDK 17 (optionally?) suggest text block fixes.
// XXX: The check does not flag well-formatted text blocks with insufficient indentation. Cover this
// using an generic check or wait for Google Java Format support (see
// https://github.com/google/google-java-format/issues/883#issuecomment-1404336418).
// XXX: The check does not flag well-formatted text blocks with excess indentation.
// XXX: ^ Validate this claim.
// XXX: GJF guesses the line separator to be used by inspecting the source. When using text blocks
// this may cause the current unconditional use of `\n` not to be sufficient when building on
// Windows; TBD.
// XXX: Forward compatibility: ignore "malformed" code in tests that, based on an
// `@DisabledForJreRange` or `@EnableForJreRange` annotation, target a Java runtime greater than the
// current runtime.
@AutoService(BugChecker.class)
@BugPattern(
summary = "Test code should follow the Google Java style",
summary =
"Test code should follow the Google Java style (and when targeting JDK 15+ be "
+ "specified using a single text block)",
link = BUG_PATTERNS_BASE_URL + "ErrorProneTestHelperSourceFormat",
linkType = CUSTOM,
severity = SUGGESTION,
tags = STYLE)
// XXX: Drop suppression if/when the `avoidTextBlocks` field is dropped.
@SuppressWarnings("java:S2160" /* Super class equality definition suffices. */)
public final class ErrorProneTestHelperSourceFormat extends BugChecker
implements MethodInvocationTreeMatcher {
private static final long serialVersionUID = 1L;
private static final String FLAG_AVOID_TEXT_BLOCKS =
"ErrorProneTestHelperSourceFormat:AvoidTextBlocks";
private static final Formatter FORMATTER = new Formatter();
private static final Matcher<ExpressionTree> INPUT_SOURCE_ACCEPTING_METHOD =
anyOf(
@@ -77,9 +98,31 @@ public final class ErrorProneTestHelperSourceFormat extends BugChecker
instanceMethod()
.onDescendantOf("com.google.errorprone.BugCheckerRefactoringTestHelper.ExpectOutput")
.named("addOutputLines");
private static final Supplier<Boolean> IS_JABEL_ENABLED =
VisitorState.memoize(ErrorProneTestHelperSourceFormat::isJabelEnabled);
// XXX: Proper name for this?
// XXX: Something about tabs.
private static final String TEXT_BLOCK_MARKER = "\"\"\"";
private static final String TEXT_BLOCK_LINE_SEPARATOR = "\n";
private static final String DEFAULT_TEXT_BLOCK_INDENTATION = " ".repeat(12);
private static final String METHOD_SELECT_ARGUMENT_RELATIVE_INDENTATION = " ".repeat(8);
/** Instantiates a new {@link ErrorProneTestHelperSourceFormat} instance. */
public ErrorProneTestHelperSourceFormat() {}
private final boolean avoidTextBlocks;
/** Instantiates a default {@link ErrorProneTestHelperSourceFormat} instance. */
public ErrorProneTestHelperSourceFormat() {
this(ErrorProneFlags.empty());
}
/**
* Instantiates a customized {@link ErrorProneTestHelperSourceFormat}.
*
* @param flags Any provided command line flags.
*/
@Inject
ErrorProneTestHelperSourceFormat(ErrorProneFlags flags) {
avoidTextBlocks = flags.getBoolean(FLAG_AVOID_TEXT_BLOCKS).orElse(Boolean.FALSE);
}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
@@ -95,22 +138,23 @@ public final class ErrorProneTestHelperSourceFormat extends BugChecker
return buildDescription(tree).setMessage("No source code provided").build();
}
int startPos = ASTHelpers.getStartPosition(sourceLines.get(0));
int endPos = state.getEndPosition(sourceLines.get(sourceLines.size() - 1));
/* Attempt to format the source code only if it fully consists of constant expressions. */
return getConstantSourceCode(sourceLines)
.map(source -> flagFormattingIssues(startPos, endPos, source, isOutputSource, state))
.map(source -> flagFormattingIssues(sourceLines, source, isOutputSource, state))
.orElse(Description.NO_MATCH);
}
private Description flagFormattingIssues(
int startPos, int endPos, String source, boolean retainUnusedImports, VisitorState state) {
Tree methodInvocation = state.getPath().getLeaf();
List<? extends ExpressionTree> sourceLines,
String source,
boolean retainUnusedImports,
VisitorState state) {
MethodInvocationTree methodInvocation = (MethodInvocationTree) state.getPath().getLeaf();
String formatted;
try {
formatted = formatSourceCode(source, retainUnusedImports).trim();
String gjfResult = formatSourceCode(source, retainUnusedImports);
formatted = canUseTextBlocks(sourceLines, state) ? gjfResult : gjfResult.stripTrailing();
} catch (
@SuppressWarnings("java:S1166" /* Stack trace not relevant. */)
FormatterException e) {
@@ -119,31 +163,113 @@ public final class ErrorProneTestHelperSourceFormat extends BugChecker
.build();
}
if (source.trim().equals(formatted)) {
boolean isFormatted = source.equals(formatted);
boolean hasStringLiteralMismatch = shouldUpdateStringLiteralFormat(sourceLines, state);
if (isFormatted && !hasStringLiteralMismatch) {
return Description.NO_MATCH;
}
if (startPos == Position.NOPOS || endPos == Position.NOPOS) {
/*
* We have insufficient source information to emit a fix, so we only flag the fact that the
* code isn't properly formatted.
*/
return describeMatch(methodInvocation);
}
int startPos = ASTHelpers.getStartPosition(sourceLines.get(0));
int endPos = state.getEndPosition(sourceLines.get(sourceLines.size() - 1));
boolean hasNewlineMismatch =
!isFormatted && source.stripTrailing().equals(formatted.stripTrailing());
/*
* The code isn't properly formatted; replace all lines with the properly formatted
* alternatives.
* The source code is not properly formatted and/or not specified using a single text block.
* Report the more salient of the violations, and suggest a fix if sufficient source information
* is available.
*/
return describeMatch(
methodInvocation,
SuggestedFix.replace(
startPos,
endPos,
Splitter.on(System.lineSeparator())
.splitToStream(formatted)
.map(state::getConstantExpression)
.collect(joining(", "))));
boolean isTextBlockUsageIssue = isFormatted || (hasNewlineMismatch && hasStringLiteralMismatch);
boolean canSuggestFix = startPos != NOPOS && endPos != NOPOS;
return buildDescription(methodInvocation)
.setMessage(
isTextBlockUsageIssue
? String.format(
"Test code should %sbe specified using a single text block",
avoidTextBlocks ? "not " : "")
: String.format(
"Test code should follow the Google Java style%s",
hasNewlineMismatch ? " (pay attention to trailing newlines)" : ""))
.addFix(
canSuggestFix
? SuggestedFix.replace(
startPos,
endPos,
canUseTextBlocks(sourceLines, state)
? toTextBlockExpression(methodInvocation, formatted, state)
: toLineEnumeration(formatted, state))
: SuggestedFix.emptyFix())
.build();
}
private boolean shouldUpdateStringLiteralFormat(
List<? extends ExpressionTree> sourceLines, VisitorState state) {
return canUseTextBlocks(sourceLines, state)
? (sourceLines.size() > 1 || !SourceCode.isTextBlock(sourceLines.get(0), state))
: sourceLines.stream().anyMatch(tree -> SourceCode.isTextBlock(tree, state));
}
private boolean canUseTextBlocks(List<? extends ExpressionTree> sourceLines, VisitorState state) {
return !avoidTextBlocks
&& (SourceVersion.supportsTextBlocks(state.context)
|| IS_JABEL_ENABLED.get(state)
|| sourceLines.stream().anyMatch(line -> SourceCode.isTextBlock(line, state)));
}
private static String toTextBlockExpression(
MethodInvocationTree tree, String source, VisitorState state) {
String indentation = suggestTextBlockIndentation(tree, state);
// XXX: Verify trailing """ on new line.
return TEXT_BLOCK_MARKER
+ System.lineSeparator()
+ indentation
+ source
.replace(TEXT_BLOCK_LINE_SEPARATOR, System.lineSeparator() + indentation)
.replace("\\", "\\\\")
.replace(TEXT_BLOCK_MARKER, "\"\"\\\"")
+ TEXT_BLOCK_MARKER;
}
private static String toLineEnumeration(String source, VisitorState state) {
return Splitter.on(TEXT_BLOCK_LINE_SEPARATOR)
.splitToStream(source)
.map(state::getConstantExpression)
.collect(joining(", "));
}
// XXX: This makes certain assumptions; document these.
private static String suggestTextBlockIndentation(
MethodInvocationTree target, VisitorState state) {
CharSequence sourceCode = state.getSourceCode();
if (sourceCode == null) {
return DEFAULT_TEXT_BLOCK_INDENTATION;
}
String source = sourceCode.toString();
return getIndentation(target.getArguments().get(1), source)
.or(() -> getIndentation(target.getArguments().get(0), source))
.or(
() ->
getIndentation(target.getMethodSelect(), source)
.map(METHOD_SELECT_ARGUMENT_RELATIVE_INDENTATION::concat))
.orElse(DEFAULT_TEXT_BLOCK_INDENTATION);
}
private static Optional<String> getIndentation(Tree tree, String source) {
int startPos = ASTHelpers.getStartPosition(tree);
if (startPos == NOPOS) {
return Optional.empty();
}
int finalNewLine = source.lastIndexOf(System.lineSeparator(), startPos);
if (finalNewLine < 0) {
return Optional.empty();
}
return Optional.of(source.substring(finalNewLine + 1, startPos))
.filter(CharMatcher.whitespace()::matchesAllOf);
}
private static String formatSourceCode(String source, boolean retainUnusedImports)
@@ -162,14 +288,29 @@ public final class ErrorProneTestHelperSourceFormat extends BugChecker
StringBuilder source = new StringBuilder();
for (ExpressionTree sourceLine : sourceLines) {
if (source.length() > 0) {
source.append(TEXT_BLOCK_LINE_SEPARATOR);
}
String value = ASTHelpers.constValue(sourceLine, String.class);
if (value == null) {
return Optional.empty();
}
source.append(value).append(System.lineSeparator());
source.append(value);
}
return Optional.of(source.toString());
}
/**
* Tells whether Jabel appears to be enabled, indicating that text blocks are supported, even if
* may <em>appear</em> that they {@link SourceVersion#supportsTextBlocks(Context) aren't}.
*
* @see <a href="https://github.com/bsideup/jabel">Jabel</a>
*/
private static boolean isJabelEnabled(VisitorState state) {
return MultiTaskListener.instance(state.context).getTaskListeners().stream()
.anyMatch(listener -> listener.toString().contains("com.github.bsideup.jabel"));
}
}

View File

@@ -12,6 +12,7 @@ import com.google.errorprone.VisitorState;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.util.ErrorProneToken;
import com.google.errorprone.util.ErrorProneTokens;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.Tree;
import com.sun.tools.javac.util.JCDiagnostic.DiagnosticPosition;
@@ -26,8 +27,30 @@ public final class SourceCode {
/** The complement of {@link CharMatcher#whitespace()}. */
private static final CharMatcher NON_WHITESPACE_MATCHER = CharMatcher.whitespace().negate();
// XXX: Proper name for this?
private static final String TEXT_BLOCK_MARKER = "\"\"\"";
private SourceCode() {}
/**
* Tells whether the given expression is a text block.
*
* @param tree The AST node of interest.
* @param state A {@link VisitorState} describing the context in which the given {@link
* ExpressionTree} is found.
* @return {@code true} iff the given expression is a text block.
*/
// XXX: Add tests!
public static boolean isTextBlock(ExpressionTree tree, VisitorState state) {
if (tree.getKind() != Tree.Kind.STRING_LITERAL) {
return false;
}
/* If the source code is unavailable then we assume that this literal is _not_ a text block. */
String src = state.getSourceForNode(tree);
return src != null && src.startsWith(TEXT_BLOCK_MARKER);
}
/**
* Returns a string representation of the given {@link Tree}, preferring the original source code
* (if available) over its prettified representation.

View File

@@ -75,8 +75,12 @@ public enum ThirdPartyLibrary {
*
* <p>The {@link VisitorState}'s symbol table is consulted first. If the type has not yet been
* loaded, then an attempt is made to do so.
*
* @param className The type of interest.
* @param state The context under consideration.
* @return {@code true} iff the indicated type is on the classpath.
*/
private static boolean isKnownClass(String className, VisitorState state) {
public static boolean isKnownClass(String className, VisitorState state) {
return state.getTypeFromString(className) != null || canLoadClass(className, state);
}

View File

@@ -4,8 +4,15 @@ import com.google.errorprone.BugCheckerRefactoringTestHelper;
import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;
import com.google.errorprone.CompilationTestHelper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledForJreRange;
import org.junit.jupiter.api.condition.JRE;
final class ErrorProneTestHelperSourceFormatTest {
// XXX: Add tests cases for `ErrorProneTestHelperSourceFormat:IgnoreMalformedCode`.
// XXX: Consider reducing the `@DisabledForJreRange(max = JRE.JAVA_14)` test scope by moving the
// text blocks to smaller test methods.
@DisabledForJreRange(max = JRE.JAVA_14)
@Test
void identification() {
CompilationTestHelper.newInstance(ErrorProneTestHelperSourceFormat.class, getClass())
@@ -17,13 +24,70 @@ final class ErrorProneTestHelperSourceFormatTest {
"import tech.picnic.errorprone.bugpatterns.EmptyMethod;",
"",
"class A {",
" private final CompilationTestHelper compilationTestHelper =",
" CompilationTestHelper.newInstance(EmptyMethod.class, getClass());",
" private final BugCheckerRefactoringTestHelper refactoringTestHelper =",
" BugCheckerRefactoringTestHelper.newInstance(EmptyMethod.class, getClass());",
"",
" void m() {",
" compilationTestHelper",
" CompilationTestHelper.newInstance(EmptyMethod.class, getClass())",
" // BUG: Diagnostic contains: No source code provided",
" .addSourceLines(\"A.java\")",
" // BUG: Diagnostic contains: Source code is malformed:",
" .addSourceLines(\"B.java\", \"class B {\")",
" // BUG: Diagnostic contains: Test code should be specified using a single text block",
" .addSourceLines(\"C.java\", \"class C {}\")",
" // Malformed code, but not compile-time constant, so not flagged.",
" .addSourceLines(\"D.java\", \"class D {\" + getClass())",
" // BUG: Diagnostic contains: Test code should follow the Google Java style",
" .addSourceLines(\"E.java\", \"class E { }\")",
" // Well-formed code, so not flagged.",
" .addSourceLines(\"F.java\", \"\"\"",
" class F {}",
" \"\"\")",
" // BUG: Diagnostic contains: Test code should follow the Google Java style (pay attention to",
" // trailing newlines)",
" .addSourceLines(\"G.java\", \"\"\"",
" class G {}\"\"\")",
" .doTest();",
"",
" BugCheckerRefactoringTestHelper.newInstance(EmptyMethod.class, getClass())",
" // BUG: Diagnostic contains: Test code should follow the Google Java style",
" .addInputLines(\"in/A.java\", \"class A { }\")",
" // BUG: Diagnostic contains: Test code should follow the Google Java style",
" .addOutputLines(\"out/A.java\", \"class A { }\")",
" // BUG: Diagnostic contains: Test code should follow the Google Java style",
" .addInputLines(",
" \"in/B.java\",",
" \"\"\"",
" import java.util.Map;",
"",
" class B {}",
" \"\"\")",
" // Unused import, but in an output file, so not flagged.",
" .addOutputLines(",
" \"out/B.java\",",
" \"\"\"",
" import java.util.Map;",
"",
" class B {}",
" \"\"\")",
" .doTest(TestMode.TEXT_MATCH);",
" }",
"}")
.doTest();
}
@DisabledForJreRange(max = JRE.JAVA_14)
@Test
void identificationAvoidTextBlocks() {
CompilationTestHelper.newInstance(ErrorProneTestHelperSourceFormat.class, getClass())
.setArgs("-XepOpt:ErrorProneTestHelperSourceFormat:AvoidTextBlocks=true")
.addSourceLines(
"A.java",
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
"import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;",
"import com.google.errorprone.CompilationTestHelper;",
"import tech.picnic.errorprone.bugpatterns.EmptyMethod;",
"",
"class A {",
" void m() {",
" CompilationTestHelper.newInstance(EmptyMethod.class, getClass())",
" // BUG: Diagnostic contains: No source code provided",
" .addSourceLines(\"A.java\")",
" // BUG: Diagnostic contains: Source code is malformed:",
@@ -34,9 +98,16 @@ final class ErrorProneTestHelperSourceFormatTest {
" .addSourceLines(\"D.java\", \"class D {\" + getClass())",
" // BUG: Diagnostic contains: Test code should follow the Google Java style",
" .addSourceLines(\"E.java\", \"class E { }\")",
" // BUG: Diagnostic contains: Test code should not be specified using a single text block",
" .addSourceLines(\"F.java\", \"\"\"",
" class F {}",
" \"\"\")",
" // BUG: Diagnostic contains: Test code should follow the Google Java style (pay attention to",
" // trailing newlines)",
" .addSourceLines(\"G.java\", \"class G {}\", \"\")",
" .doTest();",
"",
" refactoringTestHelper",
" BugCheckerRefactoringTestHelper.newInstance(EmptyMethod.class, getClass())",
" // BUG: Diagnostic contains: Test code should follow the Google Java style",
" .addInputLines(\"A.java\", \"class A { }\")",
" // BUG: Diagnostic contains: Test code should follow the Google Java style",
@@ -51,11 +122,14 @@ final class ErrorProneTestHelperSourceFormatTest {
.doTest();
}
// XXX: Add `replacement` test.
@DisabledForJreRange(max = JRE.JAVA_14)
@Test
void replacement() {
/*
* Verifies that import sorting and code formatting is performed unconditionally, while unused
* imports are removed unless part of a `BugCheckerRefactoringTestHelper` expected output file.
* Also verifies that text blocks are properly indented.
*/
BugCheckerRefactoringTestHelper.newInstance(ErrorProneTestHelperSourceFormat.class, getClass())
.addInputLines(
@@ -66,13 +140,104 @@ final class ErrorProneTestHelperSourceFormatTest {
"import tech.picnic.errorprone.bugpatterns.EmptyMethod;",
"",
"class A {",
" private final CompilationTestHelper compilationTestHelper =",
" CompilationTestHelper.newInstance(EmptyMethod.class, getClass());",
" private final BugCheckerRefactoringTestHelper refactoringTestHelper =",
" BugCheckerRefactoringTestHelper.newInstance(EmptyMethod.class, getClass());",
"",
" void m() {",
" compilationTestHelper",
" CompilationTestHelper.newInstance(EmptyMethod.class, getClass())",
" .addSourceLines(",
" \"A.java\",",
" \"\"\"",
" import java.util.Map;",
" import java.util.Collection;",
" import java.util.List;",
"",
" interface A extends List<A>, Map<A,A> { }\"\"\")",
" .addSourceLines(\"B.java\", \"class B {}\")",
" .doTest();",
"",
" BugCheckerRefactoringTestHelper.newInstance(EmptyMethod.class, getClass())",
" .addInputLines(",
" \"in/A.java\",",
" \"\"\"",
" import java.util.Map;",
" import java.util.Collection;",
" import java.util.List;",
"",
" interface A extends List<A>, Map<A,A> { }\"\"\")",
" .addOutputLines(",
" \"out/A.java\",",
" \"\"\"",
" import java.util.Map;",
" import java.util.Collection;",
" import java.util.List;",
"",
" interface A extends List<A>, Map<A,A> { }\"\"\")",
" .doTest(TestMode.TEXT_MATCH);",
" }",
"}")
.addOutputLines(
"out/A.java",
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
"import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;",
"import com.google.errorprone.CompilationTestHelper;",
"import tech.picnic.errorprone.bugpatterns.EmptyMethod;",
"",
"class A {",
" void m() {",
" CompilationTestHelper.newInstance(EmptyMethod.class, getClass())",
" .addSourceLines(",
" \"A.java\",",
" \"\"\"",
" import java.util.List;",
" import java.util.Map;",
"",
" interface A extends List<A>, Map<A, A> {}",
" \"\"\")",
" .addSourceLines(\"B.java\", \"\"\"",
" class B {}",
" \"\"\")",
" .doTest();",
"",
" BugCheckerRefactoringTestHelper.newInstance(EmptyMethod.class, getClass())",
" .addInputLines(",
" \"in/A.java\",",
" \"\"\"",
" import java.util.List;",
" import java.util.Map;",
"",
" interface A extends List<A>, Map<A, A> {}",
" \"\"\")",
" .addOutputLines(",
" \"out/A.java\",",
" \"\"\"",
" import java.util.Collection;",
" import java.util.List;",
" import java.util.Map;",
"",
" interface A extends List<A>, Map<A, A> {}",
" \"\"\")",
" .doTest(TestMode.TEXT_MATCH);",
" }",
"}")
.doTest(TestMode.TEXT_MATCH);
}
@Test
void replacementAvoidTextBlocks() {
/*
* Verifies that import sorting and code formatting is performed unconditionally, while unused
* imports are removed unless part of a `BugCheckerRefactoringTestHelper` expected output file.
*/
BugCheckerRefactoringTestHelper.newInstance(ErrorProneTestHelperSourceFormat.class, getClass())
.setArgs("-XepOpt:ErrorProneTestHelperSourceFormat:AvoidTextBlocks=true")
.addInputLines(
"in/A.java",
"import com.google.errorprone.BugCheckerRefactoringTestHelper;",
"import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;",
"import com.google.errorprone.CompilationTestHelper;",
"import tech.picnic.errorprone.bugpatterns.EmptyMethod;",
"",
"class A {",
" void m() {",
" CompilationTestHelper.newInstance(EmptyMethod.class, getClass())",
" .addSourceLines(",
" \"A.java\",",
" \"import java.util.Map;\",",
@@ -82,7 +247,7 @@ final class ErrorProneTestHelperSourceFormatTest {
" \"interface A extends List<A>, Map<A,A> { }\")",
" .doTest();",
"",
" refactoringTestHelper",
" BugCheckerRefactoringTestHelper.newInstance(EmptyMethod.class, getClass())",
" .addInputLines(",
" \"A.java\",",
" \"import java.util.Map;\",",
@@ -108,13 +273,8 @@ final class ErrorProneTestHelperSourceFormatTest {
"import tech.picnic.errorprone.bugpatterns.EmptyMethod;",
"",
"class A {",
" private final CompilationTestHelper compilationTestHelper =",
" CompilationTestHelper.newInstance(EmptyMethod.class, getClass());",
" private final BugCheckerRefactoringTestHelper refactoringTestHelper =",
" BugCheckerRefactoringTestHelper.newInstance(EmptyMethod.class, getClass());",
"",
" void m() {",
" compilationTestHelper",
" CompilationTestHelper.newInstance(EmptyMethod.class, getClass())",
" .addSourceLines(",
" \"A.java\",",
" \"import java.util.List;\",",
@@ -123,7 +283,7 @@ final class ErrorProneTestHelperSourceFormatTest {
" \"interface A extends List<A>, Map<A, A> {}\")",
" .doTest();",
"",
" refactoringTestHelper",
" BugCheckerRefactoringTestHelper.newInstance(EmptyMethod.class, getClass())",
" .addInputLines(",
" \"A.java\",",
" \"import java.util.List;\",",

View File

@@ -5,21 +5,52 @@ import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import com.google.errorprone.BugCheckerRefactoringTestHelper;
import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;
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.AnnotationTreeMatcher;
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
import com.google.errorprone.bugpatterns.BugChecker.ReturnTreeMatcher;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.ReturnTree;
import com.sun.source.tree.Tree;
import javax.lang.model.element.Name;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledForJreRange;
import org.junit.jupiter.api.condition.JRE;
final class SourceCodeTest {
@DisabledForJreRange(max = JRE.JAVA_14)
@Test
void isTextBlock() {
CompilationTestHelper.newInstance(TextBlockFlagger.class, getClass())
.addSourceLines(
"A.java",
"class A {",
" String negative1() {",
" return toString();",
" }",
"",
" String negative2() {",
" return \"foo\";",
" }",
"",
" String positive1() {",
" // BUG: Diagnostic contains:",
" return \"\"\"",
" foo",
" \"\"\";",
" }",
"}")
.doTest();
}
@Test
void deleteWithTrailingWhitespaceAnnotations() {
BugCheckerRefactoringTestHelper.newInstance(
@@ -228,6 +259,22 @@ final class SourceCodeTest {
.doTest(TestMode.TEXT_MATCH);
}
/**
* A {@link BugChecker} that delegates to {@link SourceCode#isTextBlock(ExpressionTree,
* VisitorState)}.
*/
@BugPattern(summary = "Interacts with `SourceCode` for testing purposes", severity = ERROR)
public static final class TextBlockFlagger extends BugChecker implements ReturnTreeMatcher {
private static final long serialVersionUID = 1L;
@Override
public Description matchReturn(ReturnTree tree, VisitorState state) {
return SourceCode.isTextBlock(tree.getExpression(), state)
? describeMatch(tree)
: Description.NO_MATCH;
}
}
/**
* A {@link BugChecker} that uses {@link SourceCode#deleteWithTrailingWhitespace(Tree,
* VisitorState)} to suggest the deletion of annotations and methods with a name containing

16
to-11.sh Executable file
View File

@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -e -u -o pipefail
# XXX: Clean this up.
# XXX: Drop this class.
mvn clean test-compile fmt:format \
-s "$(dirname "${0}")/settings.xml" \
-T 1.0C \
-Perror-prone \
-Perror-prone-fork \
-Ppatch \
-Pself-check \
-Derror-prone.patch-checks=ErrorProneTestHelperSourceFormat \
-Derror-prone.self-check-args='-XepOpt:ErrorProneTestHelperSourceFormat:AvoidTextBlocks=true -XepDisableAllChecks' \
-Dverification.skip