-
Notifications
You must be signed in to change notification settings - Fork 39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce RefasterRuleTestExtractor
for documentation generation
#1317
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
package tech.picnic.errorprone.documentation; | ||
|
||
import static com.google.common.collect.ImmutableList.toImmutableList; | ||
import static com.google.errorprone.matchers.Matchers.isSubtypeOf; | ||
import static java.util.stream.Collectors.joining; | ||
|
||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize; | ||
import com.google.auto.service.AutoService; | ||
import com.google.auto.value.AutoValue; | ||
import com.google.common.base.Splitter; | ||
import com.google.common.base.Supplier; | ||
import com.google.common.base.VerifyException; | ||
import com.google.common.collect.ImmutableList; | ||
import com.google.errorprone.VisitorState; | ||
import com.google.errorprone.annotations.FormatMethod; | ||
import com.google.errorprone.annotations.Immutable; | ||
import com.google.errorprone.matchers.Matcher; | ||
import com.sun.source.tree.ClassTree; | ||
import com.sun.source.tree.MethodTree; | ||
import java.net.URI; | ||
import java.util.Optional; | ||
import java.util.regex.Pattern; | ||
import tech.picnic.errorprone.documentation.RefasterRuleCollectionTestExtractor.RefasterTestCases; | ||
import tech.picnic.errorprone.utils.SourceCode; | ||
|
||
/** | ||
* An {@link Extractor} that describes how to extract data from Refaster rule input and output test | ||
* classes. | ||
*/ | ||
// XXX: Drop this extractor if/when the Refaster test framework is reimplemented such that tests can | ||
// be located alongside rules, rather than in two additional resource files as currently required by | ||
// `RefasterRuleCollection`. | ||
@Immutable | ||
@AutoService(Extractor.class) | ||
@SuppressWarnings("rawtypes" /* See https://github.com/google/auto/issues/870. */) | ||
public final class RefasterRuleCollectionTestExtractor implements Extractor<RefasterTestCases> { | ||
private static final Matcher<ClassTree> IS_REFASTER_RULE_COLLECTION_TEST_CASE = | ||
isSubtypeOf("tech.picnic.errorprone.refaster.test.RefasterRuleCollectionTestCase"); | ||
private static final Pattern TEST_CLASS_NAME_PATTERN = Pattern.compile("(.*)Test"); | ||
private static final Pattern TEST_CLASS_FILE_NAME_PATTERN = | ||
Pattern.compile(".*(Input|Output)\\.java"); | ||
private static final Pattern TEST_METHOD_NAME_PATTERN = Pattern.compile("test(.*)"); | ||
private static final String LINE_SEPARATOR = "\n"; | ||
private static final Splitter LINE_SPLITTER = Splitter.on(LINE_SEPARATOR); | ||
|
||
/** Instantiates a new {@link RefasterRuleCollectionTestExtractor} instance. */ | ||
public RefasterRuleCollectionTestExtractor() {} | ||
|
||
@Override | ||
public String identifier() { | ||
return "refaster-rule-collection-test"; | ||
} | ||
|
||
@Override | ||
public Optional<RefasterTestCases> tryExtract(ClassTree tree, VisitorState state) { | ||
if (!IS_REFASTER_RULE_COLLECTION_TEST_CASE.matches(tree, state)) { | ||
return Optional.empty(); | ||
} | ||
|
||
URI sourceFile = state.getPath().getCompilationUnit().getSourceFile().toUri(); | ||
return Optional.of( | ||
RefasterTestCases.create( | ||
sourceFile, | ||
getRuleCollectionName(tree), | ||
isInputFile(sourceFile), | ||
getRefasterTestCases(tree, state))); | ||
} | ||
|
||
private static String getRuleCollectionName(ClassTree tree) { | ||
String className = tree.getSimpleName().toString(); | ||
|
||
// XXX: Instead of throwing an error here, it'd be nicer to have a bug checker validate key | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unless I am missing something, you can also argue that throwing an error provides faster feedback when developing / running tests locally. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The bug checker would also run at compile time, but would provide a (much) better error message. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense 👍 |
||
// aspects of `RefasterRuleCollectionTestCase` subtypes. | ||
return tryExtractPatternGroup(className, TEST_CLASS_NAME_PATTERN) | ||
.orElseThrow( | ||
violation( | ||
"Refaster rule collection test class name '%s' does not match '%s'", | ||
className, TEST_CLASS_NAME_PATTERN)); | ||
} | ||
|
||
private static boolean isInputFile(URI sourceFile) { | ||
String path = sourceFile.getPath(); | ||
|
||
// XXX: Instead of throwing an error here, it'd be nicer to have a bug checker validate key | ||
// aspects of `RefasterRuleCollectionTestCase` subtypes. | ||
return "Input" | ||
.equals( | ||
tryExtractPatternGroup(path, TEST_CLASS_FILE_NAME_PATTERN) | ||
.orElseThrow( | ||
violation( | ||
"Refaster rule collection test file name '%s' does not match '%s'", | ||
path, TEST_CLASS_FILE_NAME_PATTERN))); | ||
} | ||
|
||
private static ImmutableList<RefasterTestCase> getRefasterTestCases( | ||
ClassTree tree, VisitorState state) { | ||
return tree.getMembers().stream() | ||
.filter(MethodTree.class::isInstance) | ||
.map(MethodTree.class::cast) | ||
.flatMap(m -> tryExtractRefasterTestCase(m, state).stream()) | ||
.collect(toImmutableList()); | ||
} | ||
|
||
private static Optional<RefasterTestCase> tryExtractRefasterTestCase( | ||
MethodTree method, VisitorState state) { | ||
return tryExtractPatternGroup(method.getName().toString(), TEST_METHOD_NAME_PATTERN) | ||
.map(name -> RefasterTestCase.create(name, getFormattedSource(method, state))); | ||
} | ||
|
||
/** | ||
* Returns the source code for the specified method. | ||
* | ||
* @implNote This operation attempts to trim leading whitespace, such that the start and end of | ||
* the method declaration are aligned. The implemented heuristic assumes that the code is | ||
* formatted using Google Java Format. | ||
*/ | ||
// XXX: Leading Javadoc and other comments are currently not extracted. Consider fixing this. | ||
private static String getFormattedSource(MethodTree method, VisitorState state) { | ||
String source = SourceCode.treeToString(method, state); | ||
int finalNewline = source.lastIndexOf(LINE_SEPARATOR); | ||
if (finalNewline < 0) { | ||
Check warning on line 121 in documentation-support/src/main/java/tech/picnic/errorprone/documentation/RefasterRuleCollectionTestExtractor.java GitHub Actions / pitest2 different changes can be made to line 121 without causing a test to fail
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pitest flags that this case isn't covered. There is actually an empty method test, but I suppose that source code does have a trailing newline (didn't check). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Usually with checks like this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, I generally prefer
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rebased and reverted this change as discussed offline. |
||
return source; | ||
} | ||
|
||
int indentation = Math.max(0, source.lastIndexOf(' ') - finalNewline); | ||
String prefixToStrip = " ".repeat(indentation); | ||
Check warning on line 126 in documentation-support/src/main/java/tech/picnic/errorprone/documentation/RefasterRuleCollectionTestExtractor.java GitHub Actions / pitestA change can be made to line 126 without causing a test to fail
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pitest flags that the |
||
|
||
return LINE_SPLITTER | ||
.splitToStream(source) | ||
.map(line -> line.startsWith(prefixToStrip) ? line.substring(indentation) : line) | ||
.collect(joining(LINE_SEPARATOR)); | ||
} | ||
|
||
private static Optional<String> tryExtractPatternGroup(String input, Pattern pattern) { | ||
java.util.regex.Matcher matcher = pattern.matcher(input); | ||
return matcher.matches() ? Optional.of(matcher.group(1)) : Optional.empty(); | ||
} | ||
|
||
@FormatMethod | ||
private static Supplier<VerifyException> violation(String format, Object... args) { | ||
return () -> new VerifyException(String.format(format, args)); | ||
} | ||
|
||
@AutoValue | ||
@JsonDeserialize(as = AutoValue_RefasterRuleCollectionTestExtractor_RefasterTestCases.class) | ||
abstract static class RefasterTestCases { | ||
static RefasterTestCases create( | ||
URI source, | ||
String ruleCollection, | ||
boolean isInput, | ||
ImmutableList<RefasterTestCase> testCases) { | ||
return new AutoValue_RefasterRuleCollectionTestExtractor_RefasterTestCases( | ||
source, ruleCollection, isInput, testCases); | ||
} | ||
|
||
abstract URI source(); | ||
|
||
abstract String ruleCollection(); | ||
|
||
abstract boolean isInput(); | ||
|
||
abstract ImmutableList<RefasterTestCase> testCases(); | ||
} | ||
|
||
@AutoValue | ||
@JsonDeserialize(as = AutoValue_RefasterRuleCollectionTestExtractor_RefasterTestCase.class) | ||
abstract static class RefasterTestCase { | ||
static RefasterTestCase create(String name, String content) { | ||
return new AutoValue_RefasterRuleCollectionTestExtractor_RefasterTestCase(name, content); | ||
Check warning on line 169 in documentation-support/src/main/java/tech/picnic/errorprone/documentation/RefasterRuleCollectionTestExtractor.java GitHub Actions / pitestA change can be made to line 169 without causing a test to fail
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pitest flags that these parameters can be swapped without failing a test. That's because we only validate that the type can be (de)serialized, without asserting the exact serialization format. That's a conscious choice. The final PR will improve coverage in this respect. |
||
} | ||
|
||
abstract String name(); | ||
|
||
abstract String content(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renaming for disambiguation.