Introduce a vistior into Recipe that marks AST nodes that Recipe will be modifying
The idea is coming from the integration of rewrite recipes into IDE features such as problem marking, quick fixes and quick assists.
The new "marker" visitor would visit the source and mark AST nodes (with special marker) that a recipe would modify with the main visitor. Once all recipes with such visitor visited the CompilationUnit, Java reconciler can find the markers and create IDE markers such as problems, warnings, hints etc. or. Code Action providers could create quick assists for the markers on AST nodes found at specific location in the source. The fix for a problem or assist would run a recipe (or its main visitor) on a project (list of sources) to fix all such issues in a project, on the Compilation Unit to fix all such issues in a given file or on a sub-branch of Compilation Units's AST to fix only the place identified by the marker. Running a recipe on the sub-branch is optional as it may not be supported... i get away with calling RecipeIntrospectionUtils.recipeVisitor(recipe).visit(astNode) the trick if figure out which ast node is ok to visit to fix the marked ast node. The ast node to run the visitor on is kept on the marker in a form of ast node's UUID.
Therefore, the proposal is:
interface IssueMarkerRecipe {
JavaIsoVisitor<ExecutionContext> getMarkerVisitor();
}
The visitor is expected to create FixAssistMarkers markers :
public class FixAssistMarker implements Marker {
private UUID id; // marker's id
private UUID scope; // optional id of an ast node on which recipe's visitor can be executed on to fix this only ast node with an issue
private String recipeId; // name of the recipe that made the marker
private Map<String, Object> parameters = Collections.emptyMap(); // optional parameters to set on the recipe to fix the ast node with the mareker
}
Example of replacing @RequestMapping with GetMapping etc.
public JavaVisitor<ExecutionContext> getMarkerVisitor() {
return new JavaIsoVisitor<ExecutionContext>() {
@Override
public J.Annotation visitAnnotation(J.Annotation annotation, ExecutionContext ctx) {
J.Annotation a = super.visitAnnotation(annotation, ctx);
if (REQUEST_MAPPING_ANNOTATION_MATCHER.matches(a) && getCursor().getParentOrThrow().getValue() instanceof J.MethodDeclaration) {
FixAssistMarker fixAssistMarker = new FixAssistMarker(Tree.randomId())
.withRecipeId(getRecipeId())
.withScope(a.getId());
a = a.withMarkers(a.getMarkers().add(fixAssistMarker));
}
return a;
}
};
}
Note that withScope(a.getId()) means that running recipe's visitor on a single annotation ast node would replace this specific annotation with @GetMapping etc.
Another example is converting @Autowired field into constructor argument. This would be an example of setting parameters map on the marker.
public JavaVisitor<ExecutionContext> getMarkerVisitor() {
return new JavaIsoVisitor<>() {
@Override
public VariableDeclarations visitVariableDeclarations(VariableDeclarations multiVariable,
ExecutionContext p) {
VariableDeclarations m = super.visitVariableDeclarations(multiVariable, p);
Cursor blockCursor = getCursor().dropParentUntil(Block.class::isInstance);
if (multiVariable.getVariables().size() == 1
&& multiVariable.getLeadingAnnotations().stream().anyMatch(a -> TypeUtils.isOfClassType(a.getType(), AUTOWIRED))
&& blockCursor.getParent().getValue() instanceof ClassDeclaration) {
ClassDeclaration classDeclaration = (ClassDeclaration) blockCursor.getParent().getValue();
FullyQualified fqType = TypeUtils.asFullyQualified(classDeclaration.getType());
if (fqType != null && isApplicableType(fqType)) {
m = m.withMarkers(m.getMarkers().add(new FixAssistMarker(Tree.randomId())
.withRecipeId(getRecipeId())
.withScope(classDeclaration.getId())
.withParameters(Map.of("classFqName", fqType.getFullyQualifiedName(), "fieldName", multiVariable.getVariables().get(0).getSimpleName()))));
}
}
return m;
}
private boolean isApplicableType(FullyQualified type) {
return !AnnotationHierarchies
.getTransitiveSuperAnnotations(type, fq -> fq.getFullyQualifiedName().startsWith("java."))
.contains("org.springframework.boot.test.context.SpringBootTest");
}
};
}
The marker creation code has withParameters(Map.of("classFqName", fqType.getFullyQualifiedName(), "fieldName", multiVariable.getVariables().get(0).getSimpleName())). Recipe has optional parameters classFqName and fieldName. Setting these parameters would convert only a specific field from a specific class into constructor parameter.