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 * - 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 { /** * 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 projectUsesTargetType = new HashMap<>(); // You can store any data structure you need // Map> projectClasses = new HashMap<>(); // Map> 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 getScanner(Accumulator acc) { return new JavaIsoVisitor() { @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 generate(Accumulator acc, ExecutionContext ctx) { List newFiles = new ArrayList<>(); // Example: Generate a file for each project that uses the target type for (Map.Entry 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 getVisitor(Accumulator acc) { return new JavaIsoVisitor() { @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 { public static class Accumulator { Map 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 getScanner(Accumulator acc) { return new JavaIsoVisitor() { @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 getVisitor(Accumulator acc) { // Print the results return new JavaIsoVisitor() { 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 - 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 */