Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:57:41 +08:00
commit c2d0b101b0
22 changed files with 6446 additions and 0 deletions

View File

@@ -0,0 +1,294 @@
package com.yourorg;
import lombok.EqualsAndHashCode;
import lombok.Value;
import org.openrewrite.*;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.tree.J;
import org.openrewrite.marker.SearchResult;
import java.util.HashMap;
import java.util.Map;
/**
* A ScanningRecipe that marks classes only if the project uses a specific type.
*
* This example demonstrates:
* - ScanningRecipe with accumulator pattern
* - Three-phase execution: scan, generate (optional), edit
* - Tracking per-project information with Map<JavaProject, T>
* - Using accumulator data to inform editing decisions
* - Proper handling of multi-module projects
*
* Use ScanningRecipe when you need to:
* - See all files before making changes
* - Generate new files based on analysis
* - Share data across multiple files
* - Make decisions based on project-wide information
*/
@Value
@EqualsAndHashCode(callSuper = false)
public class ScanningRecipeExample extends ScanningRecipe<ScanningRecipeExample.Accumulator> {
/**
* The accumulator stores data collected during the scanning phase.
* This data is then available during the editing phase.
*
* Important: For multi-module projects, track data per JavaProject.
*/
public static class Accumulator {
// Track which projects use the target type
Map<JavaProject, Boolean> projectUsesTargetType = new HashMap<>();
// You can store any data structure you need
// Map<JavaProject, Set<String>> projectClasses = new HashMap<>();
// Map<JavaProject, List<MethodInfo>> projectMethods = new HashMap<>();
}
@Override
public String getDisplayName() {
return "Mark classes in projects using target type";
}
@Override
public String getDescription() {
return "Marks classes with SearchResult only if the project uses a specific type.";
}
/**
* Initialize the accumulator before scanning begins.
*/
@Override
public Accumulator getInitialValue(ExecutionContext ctx) {
return new Accumulator();
}
/**
* Phase 1: SCANNING
*
* The scanner visits all source files and collects information
* into the accumulator. No changes are made in this phase.
*/
@Override
public TreeVisitor<?, ExecutionContext> getScanner(Accumulator acc) {
return new JavaIsoVisitor<ExecutionContext>() {
@Override
public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) {
// Get the JavaProject marker to track per-project data
JavaProject project = cu.getMarkers().findFirst(JavaProject.class).orElse(null);
if (project == null) {
return cu;
}
// Initialize project tracking if needed
acc.projectUsesTargetType.putIfAbsent(project, false);
// Scan for the target type in this file
// In this example, we're looking for usage of java.util.Optional
cu.getImports().stream()
.filter(imp -> imp.getTypeName().equals("java.util.Optional"))
.findFirst()
.ifPresent(imp -> acc.projectUsesTargetType.put(project, true));
// Continue scanning subtree
return super.visitCompilationUnit(cu, ctx);
}
@Override
public J.Import visitImport(J.Import import_, ExecutionContext ctx) {
// You can also scan at finer granularity
// Example: track specific imports, method calls, etc.
return import_;
}
};
}
/**
* Phase 2: GENERATING (optional)
*
* This phase is used to generate new source files based on the
* accumulated data. Return null if no new files are needed.
*
* Uncomment to implement:
*/
/*
@Override
public Collection<SourceFile> generate(Accumulator acc, ExecutionContext ctx) {
List<SourceFile> newFiles = new ArrayList<>();
// Example: Generate a file for each project that uses the target type
for (Map.Entry<JavaProject, Boolean> entry : acc.projectUsesTargetType.entrySet()) {
if (entry.getValue()) {
JavaProject project = entry.getKey();
// Create a new source file
// newFiles.add(createNewFile(project));
}
}
return newFiles;
}
*/
/**
* Phase 3: EDITING
*
* The editor visits all source files again and makes changes
* based on the data collected in the scanning phase.
*/
@Override
public TreeVisitor<?, ExecutionContext> getVisitor(Accumulator acc) {
return new JavaIsoVisitor<ExecutionContext>() {
@Override
public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) {
// Get the project for this file
JavaProject project = cu.getMarkers().findFirst(JavaProject.class).orElse(null);
if (project == null) {
return cu;
}
// Check the accumulator to see if this project uses the target type
Boolean usesTargetType = acc.projectUsesTargetType.get(project);
if (usesTargetType == null || !usesTargetType) {
// Don't make changes in projects that don't use the target type
return cu;
}
// This project uses the target type, continue with editing
return super.visitCompilationUnit(cu, ctx);
}
@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx);
// Get the project
JavaProject project = getCursor()
.firstEnclosing(J.CompilationUnit.class)
.getMarkers()
.findFirst(JavaProject.class)
.orElse(null);
if (project == null) {
return cd;
}
// Only mark classes in projects that use the target type
Boolean usesTargetType = acc.projectUsesTargetType.get(project);
if (usesTargetType != null && usesTargetType) {
// Mark this class with a SearchResult
return SearchResult.found(cd, "Found in project using Optional");
}
return cd;
}
};
}
}
// ============================================================
// SIMPLIFIED EXAMPLE: COUNT CLASSES
// ============================================================
/*
Here's a simpler ScanningRecipe that counts classes per project:
@Value
@EqualsAndHashCode(callSuper = false)
public class CountClasses extends ScanningRecipe<CountClasses.Accumulator> {
public static class Accumulator {
Map<JavaProject, Integer> classCounts = new HashMap<>();
}
@Override
public String getDisplayName() {
return "Count classes per project";
}
@Override
public String getDescription() {
return "Counts the number of classes in each project.";
}
@Override
public Accumulator getInitialValue(ExecutionContext ctx) {
return new Accumulator();
}
@Override
public TreeVisitor<?, ExecutionContext> getScanner(Accumulator acc) {
return new JavaIsoVisitor<ExecutionContext>() {
@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration cd, ExecutionContext ctx) {
JavaProject project = getCursor()
.firstEnclosing(J.CompilationUnit.class)
.getMarkers()
.findFirst(JavaProject.class)
.orElse(null);
if (project != null) {
acc.classCounts.merge(project, 1, Integer::sum);
}
return cd;
}
};
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor(Accumulator acc) {
// Print the results
return new JavaIsoVisitor<ExecutionContext>() {
private boolean printed = false;
@Override
public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) {
if (!printed) {
System.out.println("Class counts by project:");
acc.classCounts.forEach((project, count) ->
System.out.println(project.getProjectName() + ": " + count + " classes")
);
printed = true;
}
return cu;
}
};
}
}
*/
// ============================================================
// KEY TAKEAWAYS
// ============================================================
/*
1. ScanningRecipe has THREE phases:
- Scan: Collect data (no changes)
- Generate: Create new files (optional)
- Edit: Make changes based on collected data
2. Always track per-project data with Map<JavaProject, T>
- Don't assume single project per repository
- Get JavaProject from markers
3. Accumulator is shared across all phases
- Use it to pass data from scan to edit
- Keep it simple and focused
4. Scanner makes NO changes
- Only collect information
- Mark files or store data
5. Editor uses accumulator data
- Make informed decisions
- Can access all collected information
6. When multiple ScanningRecipes are in a recipe list:
- All scanners run first
- Then all generators run
- Then all editors run
- Scanners see state before ANY edits
- But scanners DO see generated files
*/