diff --git a/.gitignore b/.gitignore index 887c9e4..30dfd44 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ benchmarking/datasets/ benchmarking/outputs/ benchmarking/sources/ benchmarking/compiled_classes/ +searchResults/ # Ignore IDE specific files .vscode diff --git a/build.gradle b/build.gradle index fcc9159..5b75f16 100644 --- a/build.gradle +++ b/build.gradle @@ -55,6 +55,9 @@ dependencies { runtimeOnly 'org.apache.logging.log4j:log4j-core' // Log4j JSON-encoding support runtimeOnly 'org.apache.logging.log4j:log4j-layout-template-json' + + // Argument parsing library + implementation 'info.picocli:picocli:4.7.7' } checkerFramework { diff --git a/src/main/java/RefactoringEngine.java b/src/main/java/RefactoringEngine.java index ca68934..aa1394f 100644 --- a/src/main/java/RefactoringEngine.java +++ b/src/main/java/RefactoringEngine.java @@ -1,5 +1,3 @@ -import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import org.apache.logging.log4j.Logger; @@ -19,32 +17,18 @@ */ public class RefactoringEngine { private static final Logger LOGGER = LogManager.getLogger(); + private static final Logger SEARCH_LOGGER = LogManager.getLogger("SearchLogger"); /** * List of refactorings to apply */ private final List refactorings; - public RefactoringEngine(List refactoringNames) { - refactorings = new ArrayList<>(); - - for (String name : refactoringNames) { - switch (name) { - case AddNullCheckBeforeDereferenceRefactoring.NAME -> - refactorings.add(new AddNullCheckBeforeDereferenceRefactoring()); - case BooleanFlagRefactoring.NAME -> refactorings.add(new BooleanFlagRefactoring()); - case SentinelRefactoring.NAME -> refactorings.add(new SentinelRefactoring()); - case NestedNullRefactoring.NAME -> refactorings.add(new NestedNullRefactoring()); - case "All" -> refactorings.addAll(Arrays.asList(new AddNullCheckBeforeDereferenceRefactoring(), - new BooleanFlagRefactoring(), new SentinelRefactoring(), new NestedNullRefactoring())); - default -> System.err.println("Unknown refactoring: " + name); - } - - if (refactorings.isEmpty()) { - System.err.println("No valid refactorings specified. Exiting."); - System.exit(1); - } + public RefactoringEngine(List refactorings) { + if (refactorings.isEmpty()) { + throw new IllegalArgumentException("No valid refactorings specified"); } + this.refactorings = refactorings; } /** @@ -85,4 +69,43 @@ public void preVisit(ASTNode node) { return document.get(); } + /** + * Searches a given source file for applicable refactorings in + * {@value refactorings} Search results are logged to a CSV file via a dedicated + * logger ("SearchLogger") + * + * @param cu + * The compilation unit to use + * @param sourceCode + * A string containing the source code of the file to search through + * @param filePath + * A string containing the patth of the file to search through + * @param fileName + * A string containing the name of the file to search through + */ + public void searchRefactorings(CompilationUnit cu, String sourceCode, String filePath, String fileName) { + Document document = new Document(sourceCode); + + for (Refactoring refactoring : refactorings) { + cu.accept(new ASTVisitor() { + @Override + public void preVisit(ASTNode node) { + LOGGER.debug("Visiting AST Node {}", node); + + if (!refactoring.isApplicable(node)) { + return; + } + + String refactoringModule = refactoring.getClass().getSimpleName(); + int lineNumber = cu.getLineNumber(node.getStartPosition()); + String nodeClass = node.getClass().getSimpleName(); + // Escape all new lines and quote the code snippet to preserve csv formatting + String codeSnippet = "\"" + node.toString().replace("\"", "\"\"") + "\""; + + SEARCH_LOGGER.info("{},{},{},{},{},{}", refactoringModule, filePath, fileName, lineNumber, + nodeClass, codeSnippet); + } + }); + } + } } diff --git a/src/main/java/VGRTool.java b/src/main/java/VGRTool.java index 85cb167..fe58d5b 100644 --- a/src/main/java/VGRTool.java +++ b/src/main/java/VGRTool.java @@ -3,7 +3,7 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; -import java.util.Collections; +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -20,45 +20,65 @@ import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.NonNull; +import picocli.CommandLine; +import picocli.CommandLine.*; + /** * Program entrypoint class; Runs the refactoring engine on given source code */ -public class VGRTool { +@Command(name = "VGRTool", mixinStandardHelpOptions = true, version = "0.1", description = "Program entrypoint class; Runs the refactoring engine on given source code") +public class VGRTool implements Runnable { private static final Logger LOGGER = LogManager.getLogger(); + // List of valid refactoring module names for Picocli + static class ValidRefactoringModules extends ArrayList { + ValidRefactoringModules() { + super(List.of(AddNullCheckBeforeDereferenceRefactoring.NAME, BooleanFlagRefactoring.NAME, + SentinelRefactoring.NAME, NestedNullRefactoring.NAME, "All")); + } + } + + // Picocli automatically assigns values to arguments during runtime, + // guaranteeing initialization + @SuppressWarnings("initialization.field.uninitialized") + // First position argument represents target directory + @Parameters(index = "0", description = "Path of directory to execute program on") + private String targetDir; + + // Picocli automatically assigns values to arguments during runtime, + // guaranteeing initialization + @SuppressWarnings("initialization.field.uninitialized") + // All remaining positional arguments are parsed as module names. + @Parameters(index = "1", arity = "1..*", description = "Refactoring module(s) to use. Valid values: ${COMPLETION-CANDIDATES}", completionCandidates = ValidRefactoringModules.class) + private List refactoringModules; + + @Option(names = "--search", description = "Only search for valid refactorings without actually refactoring") + private static boolean searchOnly = false; + + // Parses command-line arguments and executes run() + public static void main(String[] args) { + int exitCode = new CommandLine(new VGRTool()).execute(args); + System.exit(exitCode); + } + /** * Main method for the program; Runs refactorings to all Java files in a given * directory - * - * @param args - * Path of directory to execute program on (args[0]) and refactoring - * modules to use (args[1...]) */ - public static void main(String[] args) { - if (args.length < 2) { - - LOGGER.info("Usage: java VGRTool "); - LOGGER.info("Available Modules:"); - LOGGER.info(" - " + AddNullCheckBeforeDereferenceRefactoring.NAME); - LOGGER.info(" - " + NestedNullRefactoring.NAME); - System.exit(1); - } - - String targetDir = args[0]; - String refactoringModule = args[1]; - + public void run() { LOGGER.debug("Processing directory: {}", targetDir); - LOGGER.debug("Selected Refactoring Module: {}", refactoringModule); + LOGGER.debug("Selected Refactoring Module(s): {}", refactoringModules); try { // Step 1: Collect all Java files in the target directory List javaFiles = getJavaFiles(targetDir); - // Step 2: Process each Java file using the selected refactoring module + // Step 2: Process each Java file using the selected refactoring module(s) for (File file : javaFiles) { LOGGER.debug("Processing file: {}", file.getPath()); - processFile(file, refactoringModule); + List selectedRefactorings = processRefactorings(refactoringModules); + processFile(file, selectedRefactorings); } LOGGER.info("Refactoring completed successfully!"); @@ -67,6 +87,35 @@ public static void main(String[] args) { } } + /** + * Parses and converts {@value refactoringModuleNames} into a list of + * refactorings without duplicates. + * + * @param refactoringModuleNames + * A List of refactoringModule names to parse + **/ + private static List processRefactorings(List refactoringModuleNames) { + List refactorings = new ArrayList<>(); + for (String name : refactoringModuleNames) { + switch (name) { + case AddNullCheckBeforeDereferenceRefactoring.NAME -> + refactorings.add(new AddNullCheckBeforeDereferenceRefactoring()); + case BooleanFlagRefactoring.NAME -> refactorings.add(new BooleanFlagRefactoring()); + case SentinelRefactoring.NAME -> refactorings.add(new SentinelRefactoring()); + case NestedNullRefactoring.NAME -> refactorings.add(new NestedNullRefactoring()); + case "All" -> refactorings.addAll(Arrays.asList(new AddNullCheckBeforeDereferenceRefactoring(), + new BooleanFlagRefactoring(), new SentinelRefactoring(), new NestedNullRefactoring())); + // Should already be caught by Picocli + default -> throw new IllegalArgumentException("Unknown refactoring module: " + name); + } + } + + if (refactorings.isEmpty()) { + throw new IllegalArgumentException("No valid refactorings specified"); + } + return refactorings; + } + /** * Returns a list of all java files in the given directory path * @@ -86,10 +135,10 @@ public static void main(String[] args) { * * @param file * The file to refactor - * @param refactoringModule + * @param refactoringModules * the refactoring to apply to the file */ - private static void processFile(File file, String refactoringModule) { + private static void processFile(File file, List refactoringModules) { try { // Step 3: Read the file content String content = Files.readString(file.toPath()); @@ -114,18 +163,22 @@ private static void processFile(File file, String refactoringModule) { CompilationUnit cu = (CompilationUnit) parser.createAST(null); - // Step 5: Initialize RefactoringEngine with the selected module - List selectedModules = Collections.singletonList(refactoringModule); - RefactoringEngine refactoringEngine = new RefactoringEngine(selectedModules); + // Step 5: Initialize RefactoringEngine with the selected modules + RefactoringEngine refactoringEngine = new RefactoringEngine(refactoringModules); - // Step 6: Apply refactorings using RefactoringEngine + // Step 6: Search or apply refactorings using RefactoringEngine + if (searchOnly) { + String filePath = file.getPath(); + String fileName = file.getName(); + refactoringEngine.searchRefactorings(cu, content, filePath, fileName); + return; + } String refactoredSourceCode = refactoringEngine.applyRefactorings(cu, content); // Step 7: Write the refactored code back to the file Files.writeString(file.toPath(), refactoredSourceCode); LOGGER.info("Refactored file saved: {}", file.getPath()); - } catch (IOException e) { LOGGER.error("Error processing file: {}", file.getPath(), e); } diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml index be144b8..1c4508e 100644 --- a/src/main/resources/log4j2.xml +++ b/src/main/resources/log4j2.xml @@ -4,10 +4,38 @@ + + + + + + + + + +
Refactoring Module,File Path,File Name,Line Number,Node Type,Code Snippet%n
+
+ + + + + +
+ + + + + + + diff --git a/src/test/java/BooleanFlagTesting.java b/src/test/java/BooleanFlagTesting.java index 1682cf8..642e1a3 100644 --- a/src/test/java/BooleanFlagTesting.java +++ b/src/test/java/BooleanFlagTesting.java @@ -8,7 +8,7 @@ public class BooleanFlagTesting { public void test(String input, String expectedOutput) { - TestingEngine.testSingleRefactoring(input, expectedOutput, "BooleanFlagRefactoring"); + TestingEngine.testSingleRefactoring(input, expectedOutput, new BooleanFlagRefactoring()); } @Test diff --git a/src/test/java/DereferenceTesting.java b/src/test/java/DereferenceTesting.java index 1757a0d..d3bb5ad 100644 --- a/src/test/java/DereferenceTesting.java +++ b/src/test/java/DereferenceTesting.java @@ -8,7 +8,7 @@ public class DereferenceTesting { public void test(String input, String expectedOutput) { - TestingEngine.testSingleRefactoring(input, expectedOutput, AddNullCheckBeforeDereferenceRefactoring.NAME); + TestingEngine.testSingleRefactoring(input, expectedOutput, new AddNullCheckBeforeDereferenceRefactoring()); } @Test diff --git a/src/test/java/NestedNullTesting.java b/src/test/java/NestedNullTesting.java index d28422c..aa8d422 100644 --- a/src/test/java/NestedNullTesting.java +++ b/src/test/java/NestedNullTesting.java @@ -9,7 +9,7 @@ public class NestedNullTesting { public void test(String input, String expectedOutput) { - TestingEngine.testSingleRefactoring(input, expectedOutput, "NestedNullRefactoring"); + TestingEngine.testSingleRefactoring(input, expectedOutput, new NestedNullRefactoring()); } @Test diff --git a/src/test/java/SentinelTesting.java b/src/test/java/SentinelTesting.java index b644444..e05beb6 100644 --- a/src/test/java/SentinelTesting.java +++ b/src/test/java/SentinelTesting.java @@ -8,7 +8,7 @@ public class SentinelTesting { public void test(String input, String expectedOutput) { - TestingEngine.testSingleRefactoring(input, expectedOutput, "SentinelRefactoring"); + TestingEngine.testSingleRefactoring(input, expectedOutput, new SentinelRefactoring()); } @Test diff --git a/src/test/java/TestingEngine.java b/src/test/java/TestingEngine.java index 6036649..df71fab 100644 --- a/src/test/java/TestingEngine.java +++ b/src/test/java/TestingEngine.java @@ -14,8 +14,8 @@ public class TestingEngine { * RefactoringEngine to use to run tests */ private static RefactoringEngine fullEngine = new RefactoringEngine( - Lists.newArrayList(AddNullCheckBeforeDereferenceRefactoring.NAME, BooleanFlagRefactoring.NAME, - NestedNullRefactoring.NAME, SentinelRefactoring.NAME)); + Lists.newArrayList(new AddNullCheckBeforeDereferenceRefactoring(), new BooleanFlagRefactoring(), + new NestedNullRefactoring(), new SentinelRefactoring())); // TODO: WRITE VARIANTS FOR SUPPORTED JAVA VERSIONS private static ASTParser parser = ASTParser.newParser(AST.getJLSLatest()); // Use appropriate @@ -31,7 +31,7 @@ public static void testAllRefactorings(String input, String expectedOutput) { runTest(input, expectedOutput, fullEngine); } - public static void testSingleRefactoring(String input, String expectedOutput, String refactoring) { + public static void testSingleRefactoring(String input, String expectedOutput, Refactoring refactoring) { runTest(input, expectedOutput, new RefactoringEngine(Collections.singletonList(refactoring))); }