-
Notifications
You must be signed in to change notification settings - Fork 39
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce
RefasterRuleTestExtractor
for documentation generation (#…
…1317) This new `Extractor` implementation collects Refaster example input and output code from rule collection tests. This change also introduces explicit compilation steps for the test code. As a side-effect this produces faster feedback in case of invalid input or output code.
- Loading branch information
1 parent
9f222e9
commit b3ca01a
Showing
11 changed files
with
455 additions
and
46 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
176 changes: 176 additions & 0 deletions
176
...c/main/java/tech/picnic/errorprone/documentation/RefasterRuleCollectionTestExtractor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
// 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) { | ||
return source; | ||
} | ||
|
||
int indentation = Math.max(0, source.lastIndexOf(' ') - finalNewline); | ||
String prefixToStrip = " ".repeat(indentation); | ||
|
||
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); | ||
} | ||
|
||
abstract String name(); | ||
|
||
abstract String content(); | ||
} | ||
} |
Oops, something went wrong.