From 51677e61699263c4c189250c6789f8acfae6c95a Mon Sep 17 00:00:00 2001 From: Jim Bethancourt Date: Sun, 20 Sep 2026 15:14:22 -0500 Subject: [PATCH 1/3] #157 WIP Implement Gradle Plugin Support Gradle plugin now working, but publishes reports to the wrong location(s) --- README.md | 42 +- plans/finish-pr-157.md | 1134 +++++++++++++++++ plans/moustache-implementation-plan.md | 1092 ++++++++++++++++ pom.xml | 8 +- refactor-first-gradle-plugin/build.gradle | 70 +- refactor-first-gradle-plugin/pom.xml | 16 +- .../org/hjug/gradlereport/CsvReportTask.java | 65 +- .../gradlereport/GradleProjectAdapter.java | 74 ++ .../org/hjug/gradlereport/HtmlReportTask.java | 97 +- .../org/hjug/gradlereport/JsonReportTask.java | 42 +- .../gradlereport/RefactorFirstExtension.java | 129 +- .../gradlereport/RefactorFirstPlugin.java | 87 +- .../RefactorFirstPluginException.java | 18 + .../gradlereport/SimpleHtmlReportTask.java | 95 +- .../fixtures/multi-module-java/build.gradle | 3 + .../multi-module-java/module1/build.gradle | 8 + .../main/java/com/example/Module1Class.java | 7 + .../multi-module-java/settings.gradle | 2 + .../fixtures/simple-java-project/build.gradle | 12 + .../main/java/com/example/SimpleClass.java | 7 + .../simple-kotlin-project/build.gradle.kts | 12 + .../main/kotlin/com/example/SimpleClass.kt | 5 + .../hjug/gradlereport/CsvReportTaskTest.java | 57 + .../GradleProjectAdapterTest.java | 70 + .../hjug/gradlereport/HtmlReportTaskTest.java | 82 ++ .../hjug/gradlereport/JsonReportTaskTest.java | 42 + .../RefactorFirstExtensionTest.java | 54 + .../RefactorFirstPluginExceptionTest.java | 40 + .../gradlereport/RefactorFirstPluginTest.java | 60 + .../RefactorFirstMavenJsonGeneratorTest.java | 66 + .../report/SimpleHtmlReport.java | 2 +- 31 files changed, 3336 insertions(+), 162 deletions(-) create mode 100644 plans/finish-pr-157.md create mode 100644 plans/moustache-implementation-plan.md create mode 100644 refactor-first-gradle-plugin/src/main/java/org/hjug/gradlereport/GradleProjectAdapter.java create mode 100644 refactor-first-gradle-plugin/src/main/java/org/hjug/gradlereport/RefactorFirstPluginException.java create mode 100644 refactor-first-gradle-plugin/src/test/fixtures/multi-module-java/build.gradle create mode 100644 refactor-first-gradle-plugin/src/test/fixtures/multi-module-java/module1/build.gradle create mode 100644 refactor-first-gradle-plugin/src/test/fixtures/multi-module-java/module1/src/main/java/com/example/Module1Class.java create mode 100644 refactor-first-gradle-plugin/src/test/fixtures/multi-module-java/settings.gradle create mode 100644 refactor-first-gradle-plugin/src/test/fixtures/simple-java-project/build.gradle create mode 100644 refactor-first-gradle-plugin/src/test/fixtures/simple-java-project/src/main/java/com/example/SimpleClass.java create mode 100644 refactor-first-gradle-plugin/src/test/fixtures/simple-kotlin-project/build.gradle.kts create mode 100644 refactor-first-gradle-plugin/src/test/fixtures/simple-kotlin-project/src/main/kotlin/com/example/SimpleClass.kt create mode 100644 refactor-first-gradle-plugin/src/test/java/org/hjug/gradlereport/CsvReportTaskTest.java create mode 100644 refactor-first-gradle-plugin/src/test/java/org/hjug/gradlereport/GradleProjectAdapterTest.java create mode 100644 refactor-first-gradle-plugin/src/test/java/org/hjug/gradlereport/HtmlReportTaskTest.java create mode 100644 refactor-first-gradle-plugin/src/test/java/org/hjug/gradlereport/JsonReportTaskTest.java create mode 100644 refactor-first-gradle-plugin/src/test/java/org/hjug/gradlereport/RefactorFirstExtensionTest.java create mode 100644 refactor-first-gradle-plugin/src/test/java/org/hjug/gradlereport/RefactorFirstPluginExceptionTest.java create mode 100644 refactor-first-gradle-plugin/src/test/java/org/hjug/gradlereport/RefactorFirstPluginTest.java create mode 100644 refactor-first-maven-plugin/src/test/java/org/hjug/mavenreport/RefactorFirstMavenJsonGeneratorTest.java diff --git a/README.md b/README.md index effa9c20..5840bb4f 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,18 @@ Code map viewers are powered by [3D Force Graph](https://vasturiano.github.io/3d
If there are more than 4000 classes + relationships, a simplified 3D viewer will be available to avoid page load slowdowns. Features will be toggleable in the 3D UI in a future release. ## How to Use RefactorFirst Quickly -Run the command below in your Java project's top-level directory. You'll need Git, Java 11 (or newer) and Maven 3 installed. This command will analyze Maven and non-Maven projects: +Run one of the commands below in your Java project's top-level directory. You'll need Git, Java 17 (or newer) and Maven 3 or Gradle installed. This command will analyze Maven and non-Maven projects: ```bash mvn org.hjug.refactorfirst.plugin:refactor-first-maven-plugin:0.10.0:htmlReport ``` -View the report at ```target/site/refactor-first-report.html``` in your project. +```bash +./gradlew --init-script refactorFirstPlugin.gradle refactorFirstHtmlReport +``` + +View the report at +```target/site/refactor-first-report.html``` for Maven projects or +```build/reports/refactor-first/refactor-first-report.html``` for Gradle projects. + Full instructions for various usage scenarios are below. Great effort has been taken to make both the analysis and page rendering times as fast as possible. @@ -33,7 +40,7 @@ See [DIAGRAM.md](./graph-algorithms/src/main/java/org/hjug/feedback/arc/pageRank ### How to understand the Relationship Removal Priority table -The Relationship Removal Priority tables shows the most optimal relationships to remove from your codebase to remove all cycles. +The Relationship Removal Priority tables show the most optimal relationships to remove from your codebase to remove all cycles. The table is sorted by the number of cycles that a relationship exists in and then the change proneness of the classes in the relationship. - Classes that should be broken apart / removed from the codebase have a *. - If only one class is bold, the shared functionality should be moved to the non-bold class or classes. @@ -102,6 +109,34 @@ A RefactorFirst report will show up in the site report when you run ```mvn site` ``` +## Gradle Plugin + +Add the plugin to a Gradle project and configure it in the same way as the Maven plugin: + +```kotlin +plugins { + id("java") + id("org.hjug.refactorfirst") version "0.11.0-SNAPSHOT" +} + +refactorFirst { + showDetails.set(false) + backEdgeAnalysisCount.set(50) + analyzeCycles.set(true) + excludeTests.set(true) + minifyHtml.set(false) + outputDirectory.set(file("build/reports/refactor-first")) +} +``` + +Then run: + +```bash +./gradlew refactorFirstHtmlReport +./gradlew refactorFirstCsvReport +./gradlew refactorFirstJsonReport +``` + ## Configuration Options Care has been taken to use sensible defaults, though if you wish to override these defaults you can specify the following parameters. Specify with -D if running on the command line. e.g. ```-DbackEdgeAnalysisCount=0 `DanalyzeCycles=false``` or in the configuration section (as in the above examples) if including in a Maven build. @@ -185,7 +220,6 @@ There is still much to be done. Your feedback and collaboration would be greatl If you find this plugin useful, please star this repository and share with your friends & colleagues and on social media. ## Future Plans -* Add a Gradle plugin. * Incorporate Unit Test coverage metrics to quickly identify the safety of refactoring classes. * Incorporate additional meaningful metrics. diff --git a/plans/finish-pr-157.md b/plans/finish-pr-157.md new file mode 100644 index 00000000..b5a88868 --- /dev/null +++ b/plans/finish-pr-157.md @@ -0,0 +1,1134 @@ +# Implementation Plan: Finish PR #157 - Gradle Plugin Support + +## Objective +Complete the Gradle plugin implementation for RefactorFirst to provide the same capabilities as the existing Maven plugin, with Java 21 as the current version ceiling, using Test-Driven Development (TDD) methodology. + +## Java Version Support Policy +- **Supported Versions**: Java 11, 17, 21 +- **Version Ceiling**: Java 21 (maximum supported in this iteration) +- **Future Support**: Java 25 support will be added in a future iteration +- **Error Handling**: Provide clear error messages for Java versions beyond 21 + +## Implementation Tasks + +### Phase 1: Core Plugin Implementation (TDD Approach) + +#### Task 1.1: Test and Implement RefactorFirstPluginException +**Test File**: `../refactor-first-gradle-plugin/src/test/java/org/hjug/gradlereport/RefactorFirstPluginExceptionTest.java` +**Implementation File**: `../refactor-first-gradle-plugin/src/main/java/org/hjug/gradlereport/RefactorFirstPluginException.java` + +**Step 1: Write Failing Test (Red)** +```java +package org.hjug.gradlereport; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class RefactorFirstPluginExceptionTest { + + @Test + void exceptionFormatsMessageWithPrefix() { + RefactorFirstPluginException exception = new RefactorFirstPluginException("test error"); + + assertTrue(exception.getMessage().startsWith("RefactorFirst plugin error:")); + assertTrue(exception.getMessage().contains("test error")); + } + + @Test + void exceptionIncludesHelpLink() { + RefactorFirstPluginException exception = new RefactorFirstPluginException("test error"); + + assertTrue(exception.getMessage().contains("https://github.com/refactorfirst/RefactorFirst/wiki/Troubleshooting")); + } + + @Test + void exceptionWithCausePreservesCause() { + Throwable cause = new RuntimeException("original cause"); + RefactorFirstPluginException exception = new RefactorFirstPluginException("test error", cause); + + assertEquals(cause, exception.getCause()); + } + + @Test + void exceptionIsGradleException() { + RefactorFirstPluginException exception = new RefactorFirstPluginException("test error"); + + assertTrue(exception instanceof org.gradle.api.GradleException); + } +} +``` + +**Step 2: Implement to Pass Test (Green)** +```java +package org.hjug.gradlereport; + +import org.gradle.api.GradleException; + +public class RefactorFirstPluginException extends GradleException { + public RefactorFirstPluginException(String message, Throwable cause) { + super(formatErrorMessage(message), cause); + } + + public RefactorFirstPluginException(String message) { + super(formatErrorMessage(message)); + } + + private static String formatErrorMessage(String message) { + return "RefactorFirst plugin error: " + message + "\n" + + "For help, see: https://github.com/refactorfirst/RefactorFirst/wiki/Troubleshooting"; + } +} +``` + +**Step 3: Refactor if needed** + +#### Task 1.2: Test and Implement RefactorFirstExtension +**Test File**: `../refactor-first-gradle-plugin/src/test/java/org/hjug/gradlereport/RefactorFirstExtensionTest.java` +**Implementation File**: `../refactor-first-gradle-plugin/src/main/java/org/hjug/gradlereport/RefactorFirstExtension.java` + +**Step 1: Write Failing Test (Red)** +```java +package org.hjug.gradlereport; + +import org.gradle.api.Project; +import org.gradle.testfixtures.ProjectBuilder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; +import static org.junit.jupiter.api.Assertions.*; + +class RefactorFirstExtensionTest { + private RefactorFirstExtension extension; + + @BeforeEach + void setUp() { + Project project = ProjectBuilder.builder().build(); + extension = project.getExtensions().create("refactorFirst", RefactorFirstExtension.class); + } + + @Test + void hasDefaultValues() { + assertEquals(false, extension.getShowDetails().get()); + assertEquals(50, extension.getBackEdgeAnalysisCount().get()); + assertEquals(true, extension.getAnalyzeCycles().get()); + assertEquals(true, extension.getExcludeTests().get()); + assertEquals(false, extension.getMinifyHtml().get()); + } + + @Test + void validatePassesWithValidConfiguration() { + extension.getBackEdgeAnalysisCount().set(100); + + assertDoesNotThrow(() -> extension.validate()); + } + + @Test + void validateThrowsExceptionForNegativeBackEdgeAnalysisCount() { + extension.getBackEdgeAnalysisCount().set(-1); + + RefactorFirstPluginException exception = assertThrows( + RefactorFirstPluginException.class, + () -> extension.validate() + ); + + assertTrue(exception.getMessage().contains("backEdgeAnalysisCount must be >= 0")); + } + + @Test + void validateThrowsExceptionForZeroBackEdgeAnalysisCount() { + extension.getBackEdgeAnalysisCount().set(0); + + assertDoesNotThrow(() -> extension.validate(), "Zero should be valid"); + } +} +``` + +**Step 2: Implement to Pass Test (Green)** +```java +package org.hjug.gradlereport; + +import org.gradle.api.provider.Property; + +public abstract class RefactorFirstExtension { + public abstract Property getShowDetails(); + public abstract Property getBackEdgeAnalysisCount(); + public abstract Property getAnalyzeCycles(); + public abstract Property getExcludeTests(); + public abstract Property getMinifyHtml(); + public abstract Property getTestSourceDirectory(); + public abstract Property getOutputDirectory(); + + public RefactorFirstExtension() { + getShowDetails().convention(false); + getBackEdgeAnalysisCount().convention(50); + getAnalyzeCycles().convention(true); + getExcludeTests().convention(true); + getMinifyHtml().convention(false); + } + + public void validate() { + if (getBackEdgeAnalysisCount().get() < 0) { + throw new RefactorFirstPluginException( + "backEdgeAnalysisCount must be >= 0, got: " + getBackEdgeAnalysisCount().get()); + } + } +} +``` + +**Step 3: Refactor if needed** + +#### Task 1.3: Test and Implement GradleProjectAdapter +**Test File**: `../refactor-first-gradle-plugin/src/test/java/org/hjug/gradlereport/GradleProjectAdapterTest.java` +**Implementation File**: `../refactor-first-gradle-plugin/src/main/java/org/hjug/gradlereport/GradleProjectAdapter.java` + +**Step 1: Write Failing Test (Red)** +```java +package org.hjug.gradlereport; + +import org.gradle.api.Project; +import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.testfixtures.ProjectBuilder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; +import java.io.File; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class GradleProjectAdapterTest { + private Project project; + private RefactorFirstExtension extension; + + @BeforeEach + void setUp() { + project = ProjectBuilder.builder().build(); + extension = project.getExtensions().create("refactorFirst", RefactorFirstExtension.class); + } + + @Test + void getSourceDirectoriesReturnsMainSourceSet() { + extension.getExcludeTests().set(true); + GradleProjectAdapter adapter = new GradleProjectAdapter(project, extension); + + List sources = adapter.getSourceDirectories(); + + assertFalse(sources.isEmpty(), "Should return main source directories"); + } + + @Test + void getSourceDirectoriesIncludesTestSourcesWhenNotExcluded() { + extension.getExcludeTests().set(false); + GradleProjectAdapter adapter = new GradleProjectAdapter(project, extension); + + List sources = adapter.getSourceDirectories(); + + assertTrue(sources.size() >= 2, "Should include both main and test sources"); + } + + @Test + void getProjectBaseDirReturnsProjectDirectory() { + GradleProjectAdapter adapter = new GradleProjectAdapter(project, extension); + + File baseDir = adapter.getProjectBaseDir(); + + assertEquals(project.getProjectDir(), baseDir); + } + + @Test + void getProjectNameReturnsProjectName() { + GradleProjectAdapter adapter = new GradleProjectAdapter(project, extension); + + String name = adapter.getProjectName(); + + assertEquals(project.getName(), name); + } + + @Test + void getProjectVersionReturnsProjectVersion() { + GradleProjectAdapter adapter = new GradleProjectAdapter(project, extension); + + String version = adapter.getProjectVersion(); + + assertEquals(project.getVersion().toString(), version); + } +} +``` + +**Step 2: Implement to Pass Test (Green)** +```java +package org.hjug.gradlereport; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import org.gradle.api.Project; +import org.gradle.api.tasks.SourceSetContainer; + +public class GradleProjectAdapter { + private final Project project; + private final RefactorFirstExtension extension; + + public GradleProjectAdapter(Project project, RefactorFirstExtension extension) { + this.project = project; + this.extension = extension; + } + + public List getSourceDirectories() { + List sources = new ArrayList<>(); + SourceSetContainer sourceSets = project.getExtensions() + .getByType(SourceSetContainer.class); + + // Main source sets + sources.addAll(sourceSets.getByName("main") + .getAllJava() + .getSrcDirs()); + + // Test source sets if not excluded + if (!extension.getExcludeTests().get()) { + sources.addAll(sourceSets.getByName("test") + .getAllJava() + .getSrcDirs()); + } + return sources; + } + + public File getProjectBaseDir() { + return project.getProjectDir(); + } + + public String getProjectName() { + return project.getName(); + } + + public String getProjectVersion() { + return project.getVersion().toString(); + } +} +``` + +**Step 3: Refactor if needed** + +#### Task 1.4: Test and Implement HtmlReportTask +**Test File**: `../refactor-first-gradle-plugin/src/test/java/org/hjug/gradlereport/HtmlReportTaskTest.java` +**Implementation File**: `../refactor-first-gradle-plugin/src/main/java/org/hjug/gradlereport/HtmlReportTask.java` + +**Step 1: Write Failing Test (Red)** +```java +package org.hjug.gradlereport; + +import org.gradle.api.Project; +import org.gradle.testfixtures.ProjectBuilder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; +import static org.junit.jupiter.api.Assertions.*; + +class HtmlReportTaskTest { + private Project project; + private HtmlReportTask task; + + @BeforeEach + void setUp() { + project = ProjectBuilder.builder().build(); + task = project.getTasks().register("refactorFirstHtmlReport", HtmlReportTask.class).get(); + } + + @Test + void taskIsCacheable() { + assertTrue(task.getClass().isAnnotationPresent(org.gradle.api.tasks.CacheableTask.class)); + } + + @Test + void taskHasOutputFileProperty() { + assertNotNull(task.getReportFile()); + } + + @Test + void taskHasExtensionProperty() { + assertNotNull(task.getExtension()); + } + + @Test + void taskHasProjectAdapterProperty() { + assertNotNull(task.getProjectAdapter()); + } + + @Test + void taskHasProjectNameProperty() { + assertNotNull(task.getProjectName()); + } + + @Test + void taskHasProjectVersionProperty() { + assertNotNull(task.getProjectVersion()); + } +} +``` + +**Step 2: Implement to Pass Test (Green)** +```java +package org.hjug.gradlereport; + +import org.gradle.api.DefaultTask; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.CacheableTask; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; +import org.hjug.refactorfirst.report.SimpleHtmlReport; + +@CacheableTask +public abstract class HtmlReportTask extends DefaultTask { + + public abstract Property getExtension(); + public abstract Property getProjectAdapter(); + public abstract Property getProjectName(); + public abstract Property getProjectVersion(); + + @OutputFile + public abstract RegularFileProperty getReportFile(); + + @TaskAction + public void generate() { + RefactorFirstExtension extension = getExtension().get(); + extension.validate(); + + GradleProjectAdapter adapter = getProjectAdapter().get(); + SimpleHtmlReport htmlReport = new SimpleHtmlReport(); + + htmlReport.execute( + extension.getBackEdgeAnalysisCount().get(), + extension.getAnalyzeCycles().get(), + extension.getShowDetails().get(), + extension.getMinifyHtml().get(), + extension.getExcludeTests().get(), + extension.getTestSourceDirectory().isPresent() ? + extension.getTestSourceDirectory().get().getAbsolutePath() : null, + getProjectName().get(), + getProjectVersion().get(), + adapter.getProjectBaseDir(), + extension.getOutputDirectory().get().getAbsolutePath() + ); + } +} +``` + +**Step 3: Refactor if needed** + +#### Task 1.5: Test and Implement RefactorFirstPlugin +**Test File**: `../refactor-first-gradle-plugin/src/test/java/org/hjug/gradlereport/RefactorFirstPluginTest.java` +**Implementation File**: `../refactor-first-gradle-plugin/src/main/java/org/hjug/gradlereport/RefactorFirstPlugin.java` + +**Step 1: Write Failing Test (Red)** +```java +package org.hjug.gradlereport; + +import org.gradle.api.Project; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.testfixtures.ProjectBuilder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; +import static org.junit.jupiter.api.Assertions.*; + +class RefactorFirstPluginTest { + private Project project; + + @BeforeEach + void setUp() { + project = ProjectBuilder.builder().build(); + } + + @Test + void pluginCreatesExtension() { + new RefactorFirstPlugin().apply(project); + + assertNotNull(project.getExtensions().findByName("refactorFirst")); + } + + @Test + void pluginRegistersHtmlReportTask() { + new RefactorFirstPlugin().apply(project); + + TaskProvider task = project.getTasks().named("refactorFirstHtmlReport"); + assertNotNull(task); + } + + @Test + void extensionHasDefaultValues() { + new RefactorFirstPlugin().apply(project); + + RefactorFirstExtension extension = project.getExtensions() + .getByType(RefactorFirstExtension.class); + + assertEquals(false, extension.getShowDetails().get()); + assertEquals(50, extension.getBackEdgeAnalysisCount().get()); + } +} +``` + +**Step 2: Implement to Pass Test (Green)** +```java +package org.hjug.gradlereport; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; + +public class RefactorFirstPlugin implements Plugin { + @Override + public void apply(Project project) { + // Create extension + RefactorFirstExtension extension = project.getExtensions() + .create("refactorFirst", RefactorFirstExtension.class); + + // Create project adapter + GradleProjectAdapter projectAdapter = new GradleProjectAdapter(project, extension); + + // Register HTML report task + project.getTasks().register("refactorFirstHtmlReport", HtmlReportTask.class, task -> { + task.getExtension().set(extension); + task.getProjectAdapter().set(projectAdapter); + task.getProjectName().set(project.getName()); + task.getProjectVersion().set(project.getVersion().toString()); + task.getReportFile().set( + project.getLayout().getBuildDirectory() + .file("reports/refactor-first/refactor-first-report.html") + ); + }); + + // TODO: Register CSV and JSON report tasks in subsequent implementation + } +} +``` + +**Step 3: Refactor if needed** + +### Phase 2: Build Configuration + +#### Task 2.1: Update build.gradle +**File**: `../refactor-first-gradle-plugin/build.gradle` + +Update the Gradle build configuration: + +```gradle +plugins { + id 'java-gradle-plugin' + id 'maven-publish' +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +repositories { + mavenCentral() + maven { url 'target/dependencies' } + mavenLocal() +} + +dependencies { + compileOnly gradleApi() + implementation "org.hjug.refactorfirst.graphdatagenerator:graph-data-generator:${version}" + implementation "org.hjug.refactorfirst.report:report:${version}" +} + +gradlePlugin { + plugins { + refactorFirstPlugin { + id = 'org.hjug.refactorfirst' + displayName = 'RefactorFirst' + description = 'Plugin that identifies God classes and other code disharmonies in a codebase and suggests which classes should be refactored first.' + implementationClass = 'org.hjug.gradlereport.RefactorFirstPlugin' + } + } +} + +publishing { + publications { + maven(MavenPublication) { + from components.java + + pom { + name = 'RefactorFirst Gradle Plugin' + description = 'Plugin that identifies God classes and other code disharmonies in a codebase and suggests which classes should be refactored first.' + url = 'https://github.com/refactorfirst/RefactorFirst' + + licenses { + license { + name = 'Apache License 2.0' + url = 'http://www.apache.org/licenses/' + } + } + + developers { + developer { + name = 'Jim Bethancourt' + email = 'jimbethancourt@gmail.com' + } + } + + scm { + connection = 'scm:git:https://github.com/refactorfirst/RefactorFirst' + developerConnection = 'scm:git:https://github.com/refactorfirst/RefactorFirst' + url = 'https://github.com/refactorfirst/RefactorFirst' + } + } + } + } +} +``` + +#### Task 2.2: Update parent pom.xml +**File**: `../pom.xml` + +Uncomment the Gradle plugin module in the parent POM: + +```xml + + test-resources + codebase-graph-builder + graph-algorithms + change-proneness-ranker + effort-ranker + cost-benefit-calculator + graph-data-generator + refactor-first-maven-plugin + refactor-first-gradle-plugin + coverage + report + + +``` + +#### Task 2.3: Update Gradle plugin pom.xml +**File**: `../refactor-first-gradle-plugin/pom.xml` + +Update dependencies to include both required modules: + +```xml + + + org.hjug.refactorfirst.graphdatagenerator + graph-data-generator + + + org.hjug.refactorfirst.report + report + + +``` + +### Phase 3: Additional Task Implementation (TDD Approach) + +#### Task 3.1: Test and Implement CsvReportTask +**Test File**: `../refactor-first-gradle-plugin/src/test/java/org/hjug/gradlereport/CsvReportTaskTest.java` +**Implementation File**: `../refactor-first-gradle-plugin/src/main/java/org/hjug/gradlereport/CsvReportTask.java` + +**Step 1: Write Failing Test (Red)** +```java +package org.hjug.gradlereport; + +import org.gradle.api.Project; +import org.gradle.testfixtures.ProjectBuilder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; +import static org.junit.jupiter.api.Assertions.*; + +class CsvReportTaskTest { + private Project project; + private CsvReportTask task; + + @BeforeEach + void setUp() { + project = ProjectBuilder.builder().build(); + task = project.getTasks().register("refactorFirstCsvReport", CsvReportTask.class).get(); + } + + @Test + void taskIsCacheable() { + assertTrue(task.getClass().isAnnotationPresent(org.gradle.api.tasks.CacheableTask.class)); + } + + @Test + void taskHasOutputFileProperty() { + assertNotNull(task.getReportFile()); + } + + @Test + void taskHasExtensionProperty() { + assertNotNull(task.getExtension()); + } + + @Test + void taskHasProjectAdapterProperty() { + assertNotNull(task.getProjectAdapter()); + } +} +``` + +**Step 2: Implement to Pass Test (Green)** +```java +package org.hjug.gradlereport; + +import org.gradle.api.DefaultTask; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.CacheableTask; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; +import org.hjug.refactorfirst.report.CsvReport; + +@CacheableTask +public abstract class CsvReportTask extends DefaultTask { + + public abstract Property getExtension(); + public abstract Property getProjectAdapter(); + public abstract Property getProjectName(); + public abstract Property getProjectVersion(); + + @OutputFile + public abstract RegularFileProperty getReportFile(); + + @TaskAction + public void generate() { + RefactorFirstExtension extension = getExtension().get(); + extension.validate(); + + GradleProjectAdapter adapter = getProjectAdapter().get(); + CsvReport csvReport = new CsvReport(); + + csvReport.execute( + extension.getBackEdgeAnalysisCount().get(), + extension.getAnalyzeCycles().get(), + extension.getExcludeTests().get(), + extension.getTestSourceDirectory().isPresent() ? + extension.getTestSourceDirectory().get().getAbsolutePath() : null, + getProjectName().get(), + getProjectVersion().get(), + adapter.getProjectBaseDir(), + extension.getOutputDirectory().get().getAbsolutePath() + ); + } +} +``` + +**Step 3: Refactor if needed** + +#### Task 3.2: Test and Implement JsonReportTask +**Test File**: `../refactor-first-gradle-plugin/src/test/java/org/hjug/gradlereport/JsonReportTaskTest.java` +**Implementation File**: `../refactor-first-gradle-plugin/src/main/java/org/hjug/gradlereport/JsonReportTask.java` + +**Step 1: Write Failing Test (Red)** +```java +package org.hjug.gradlereport; + +import org.gradle.api.Project; +import org.gradle.testfixtures.ProjectBuilder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; +import static org.junit.jupiter.api.Assertions.*; + +class JsonReportTaskTest { + private Project project; + private JsonReportTask task; + + @BeforeEach + void setUp() { + project = ProjectBuilder.builder().build(); + task = project.getTasks().register("refactorFirstJsonReport", JsonReportTask.class).get(); + } + + @Test + void taskIsCacheable() { + assertTrue(task.getClass().isAnnotationPresent(org.gradle.api.tasks.CacheableTask.class)); + } + + @Test + void taskHasOutputFileProperty() { + assertNotNull(task.getReportFile()); + } + + @Test + void taskHasExtensionProperty() { + assertNotNull(task.getExtension()); + } + + @Test + void taskHasProjectAdapterProperty() { + assertNotNull(task.getProjectAdapter()); + } +} +``` + +**Step 2: Implement to Pass Test (Green)** +```java +package org.hjug.gradlereport; + +import org.gradle.api.DefaultTask; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.CacheableTask; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; +import org.hjug.refactorfirst.report.JsonReport; + +@CacheableTask +public abstract class JsonReportTask extends DefaultTask { + + public abstract Property getExtension(); + public abstract Property getProjectAdapter(); + public abstract Property getProjectName(); + public abstract Property getProjectVersion(); + + @OutputFile + public abstract RegularFileProperty getReportFile(); + + @TaskAction + public void generate() { + RefactorFirstExtension extension = getExtension().get(); + extension.validate(); + + GradleProjectAdapter adapter = getProjectAdapter().get(); + JsonReport jsonReport = new JsonReport(); + + jsonReport.execute( + extension.getBackEdgeAnalysisCount().get(), + extension.getAnalyzeCycles().get(), + extension.getExcludeTests().get(), + extension.getTestSourceDirectory().isPresent() ? + extension.getTestSourceDirectory().get().getAbsolutePath() : null, + getProjectName().get(), + getProjectVersion().get(), + adapter.getProjectBaseDir(), + extension.getOutputDirectory().get().getAbsolutePath() + ); + } +} +``` + +**Step 3: Refactor if needed** + +#### Task 3.3: Test and Update RefactorFirstPlugin with Additional Tasks +**Test File**: Update `../refactor-first-gradle-plugin/src/test/java/org/hjug/gradlereport/RefactorFirstPluginTest.java` +**Implementation File**: `../refactor-first-gradle-plugin/src/main/java/org/hjug/gradlereport/RefactorFirstPlugin.java` + +**Step 1: Add Failing Test (Red)** +```java +// Add to RefactorFirstPluginTest class +@Test +void pluginRegistersCsvReportTask() { + new RefactorFirstPlugin().apply(project); + + TaskProvider task = project.getTasks().named("refactorFirstCsvReport"); + assertNotNull(task); +} + +@Test +void pluginRegistersJsonReportTask() { + new RefactorFirstPlugin().apply(project); + + TaskProvider task = project.getTasks().named("refactorFirstJsonReport"); + assertNotNull(task); +} +``` + +**Step 2: Implement to Pass Test (Green)** +```java +// Add to the apply() method after HtmlReportTask registration + +// Register CSV report task +project.getTasks().register("refactorFirstCsvReport", CsvReportTask.class, task -> { + task.getExtension().set(extension); + task.getProjectAdapter().set(projectAdapter); + task.getProjectName().set(project.getName()); + task.getProjectVersion().set(project.getVersion().toString()); + task.getReportFile().set( + project.getLayout().getBuildDirectory() + .file("reports/refactor-first/refactor-first-report.csv") + ); +}); + +// Register JSON report task +project.getTasks().register("refactorFirstJsonReport", JsonReportTask.class, task -> { + task.getExtension().set(extension); + task.getProjectAdapter().set(projectAdapter); + task.getProjectName().set(project.getName()); + task.getProjectVersion().set(project.getVersion().toString()); + task.getReportFile().set( + project.getLayout().getBuildDirectory() + .file("reports/refactor-first/refactor-first-report.json") + ); +}); +``` + +**Step 3: Refactor if needed** + +### Phase 4: Testing + +#### Task 4.1: Create Test Fixtures +**Directory**: `../refactor-first-gradle-plugin/src/test/fixtures` + +Create test fixture projects: + +``` +refactor-first-gradle-plugin/src/test/fixtures/ +├── simple-java-project/ +│ ├── build.gradle +│ └── src/main/java/com/example/SimpleClass.java +├── simple-kotlin-project/ +│ ├── build.gradle.kts +│ └── src/main/kotlin/com/example/SimpleClass.kt +└── multi-module-java/ + ├── settings.gradle + ├── build.gradle + ├── module1/build.gradle + └── module1/src/main/java/com/example/Module1Class.java +``` + +#### Task 4.2: Create Integration Tests +**File**: `refactor-first-gradle-plugin/src/test/java/org/hjug/gradlereport/RefactorFirstPluginIntegrationTest.java` + +```java +package org.hjug.gradlereport; + +import org.gradle.testkit.runner.GradleRunner; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class RefactorFirstPluginIntegrationTest { + + @Test + void generatesHtmlReportForSimpleJavaProject(@TempDir Path tempDir) { + // Copy simple-java-project fixture to tempDir + // Run Gradle task + GradleRunner runner = GradleRunner.create() + .withProjectDir(tempDir.toFile()) + .withPluginClasspath() + .withArguments("refactorFirstHtmlReport") + .build(); + + // Verify report was generated + File reportFile = tempDir.resolve("build/reports/refactor-first/refactor-first-report.html").toFile(); + assertTrue(reportFile.exists(), "HTML report should be generated"); + } + + @Test + void respectsExcludeTestsConfiguration(@TempDir Path tempDir) { + // Test with excludeTests = true + // Verify test classes are not analyzed + } + + @Test + void handlesMultiModuleProjects(@TempDir Path tempDir) { + // Test with multi-module fixture + // Verify all modules are analyzed + } +} +``` + +### Phase 5: Documentation + +#### Task 5.1: Update Main README.md with Plugin Usage +**File**: `../README.md` + +Add Gradle plugin usage section: + +```markdown +## Gradle Plugin + +### Installation + +Add the plugin to your `build.gradle.kts`: + +```kotlin +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'org.hjug.refactorfirst.plugin:refactor-first-gradle-plugin:0.11.0' + } +} + +apply plugin: 'org.hjug.refactorfirst' +``` + +### Configuration + +```kotlin +refactorFirst { + showDetails.set(false) + backEdgeAnalysisCount.set(50) + analyzeCycles.set(true) + excludeTests.set(true) + minifyHtml.set(false) + outputDirectory.set(file("build/reports/refactor-first")) +} +``` + +### Usage + +Generate reports: + +```bash +./gradlew refactorFirstHtmlReport +./gradlew refactorFirstCsvReport +./gradlew refactorFirstJsonReport +``` + +### Java Version Support + +- Supported: Java 11, 17, 21 +- Java 25 support will be added in a future iteration +``` + +#### Task 5.2: Create Plugin-Specific README +**File**: `refactor-first-gradle-plugin/README.md` + +Create comprehensive plugin documentation: + +```markdown +# RefactorFirst Gradle Plugin + +## Overview +Gradle plugin for RefactorFirst code analysis tool. + +## Quick Start + +1. Add plugin dependency to build.gradle.kts +2. Configure plugin extension +3. Run report generation tasks + +## Configuration Reference + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| showDetails | Boolean | false | Show detailed metrics | +| backEdgeAnalysisCount | Integer | 50 | Number of back edges to analyze (0 = all) | +| analyzeCycles | Boolean | true | Analyze circular dependencies | +| excludeTests | Boolean | true | Exclude test classes from analysis | +| minifyHtml | Boolean | false | Minify HTML output | +| testSourceDirectory | File | src/test/java | Custom test source directory | +| outputDirectory | File | build/reports/refactor-first | Report output directory | + +## Migration from Maven Plugin + +See main README for migration guide. + +## Hybrid Build Architecture + +This plugin uses a hybrid Maven-Gradle build system: +- Maven orchestrates the Gradle build via exec-maven-plugin +- Gradle handles plugin compilation and testing +- Dependencies are copied by Maven and consumed by Gradle + +This approach maintains consistency with the overall RefactorFirst project structure. +``` + +#### Task 5.3: Update AGENTS.md +**File**: `../AGENTS.md` + +Add Gradle plugin build commands: + +```markdown +## Gradle Plugin Build Commands + +# Build Gradle plugin via Maven +mvn clean install -pl refactor-first-gradle-plugin + +# Build Gradle plugin directly via Gradle +cd refactor-first-gradle-plugin +./gradlew clean build + +# Run Gradle plugin tests +./gradlew test +``` + +### Phase 6: CI/CD Integration + +#### Task 6.1: Update GitHub Actions +**File**: `.github/workflows/build.yml` + +Add Gradle plugin to existing CI pipeline: + +```yaml +- name: Build Gradle Plugin + run: mvn clean install -pl refactor-first-gradle-plugin + +- name: Test Gradle Plugin + run: cd refactor-first-gradle-plugin && ./gradlew test +``` + +#### Task 6.2: Configure Maven Central Publishing +**File**: `../refactor-first-gradle-plugin/build.gradle` + +Add Maven Central publishing configuration (already included in Task 2.1) + +## Implementation Order + +Execute tasks in this order following TDD red-green-refactor cycle: + +1. Task 1.1: Test and Implement RefactorFirstPluginException (Red-Green-Refactor) +2. Task 1.2: Test and Implement RefactorFirstExtension (Red-Green-Refactor) +3. Task 1.3: Test and Implement GradleProjectAdapter (Red-Green-Refactor) +4. Task 1.4: Test and Implement HtmlReportTask (Red-Green-Refactor) +5. Task 1.5: Test and Implement RefactorFirstPlugin (Red-Green-Refactor) +6. Task 2.1: Update build.gradle +7. Task 2.2: Update parent pom.xml +8. Task 2.3: Update Gradle plugin pom.xml +9. Task 3.1: Test and Implement CsvReportTask (Red-Green-Refactor) +10. Task 3.2: Test and Implement JsonReportTask (Red-Green-Refactor) +11. Task 3.3: Test and Update RefactorFirstPlugin with Additional Tasks (Red-Green-Refactor) +12. Task 4.1: Create Test Fixtures +13. Task 4.2: Create Integration Tests +14. Task 5.1: Update Main README.md with Plugin Usage +15. Task 5.2: Create Plugin-Specific README +16. Task 5.3: Update AGENTS.md +17. Task 6.1: Update GitHub Actions +18. Task 6.2: Configure Maven Central Publishing + +## TDD Process for Each Task + +For each numbered task in Phase 1 and Phase 3, follow this cycle: + +1. **Red**: Write a failing test that defines the expected behavior +2. **Green**: Write the minimum implementation code to make the test pass +3. **Refactor**: Improve the code while keeping tests green +4. **Verify**: Run all tests to ensure nothing broke + +This ensures that all production code is backed by tests and follows the TDD methodology. + +## Validation + +After implementation, verify: + +- [ ] All unit tests pass (Phase 1 and Phase 3 tasks - TDD verification) +- [ ] All integration tests pass (Phase 4 tasks) +- [ ] Plugin can be applied to a Gradle project without errors +- [ ] All three report types (HTML, CSV, JSON) can be generated +- [ ] Configuration options work as expected +- [ ] Plugin works with Java 11, 17, and 21 projects +- [ ] Plugin works with Kotlin projects +- [ ] Manual testing on real projects succeeds +- [ ] Documentation is complete and accurate +- [ ] TDD cycle was followed for all code implementation tasks (Red-Green-Refactor) +- [ ] README.md updated with Gradle plugin usage instructions + +## TDD Methodology Notes + +This implementation plan follows strict Test-Driven Development (TDD) methodology: + +- **Red Phase**: Write failing tests first to define expected behavior +- **Green Phase**: Write minimal implementation code to make tests pass +- **Refactor Phase**: Improve code while keeping tests green +- **Verification**: Run all tests after each cycle to ensure no regressions + +All production code in Phase 1 (Core Plugin Implementation) and Phase 3 (Additional Task Implementation) must be developed using this TDD cycle. Configuration tasks (Phase 2) and documentation tasks (Phase 5) do not require TDD but should be validated through the existing test suite. + +--- + +**END OF IMPLEMENTATION PLAN** diff --git a/plans/moustache-implementation-plan.md b/plans/moustache-implementation-plan.md new file mode 100644 index 00000000..c8942d2e --- /dev/null +++ b/plans/moustache-implementation-plan.md @@ -0,0 +1,1092 @@ +# RefactorFirst Mustache Template & JSON Generator Implementation Plan + +## Executive Summary + +This plan outlines the end-to-end implementation of a decoupled, data-driven reporting system for RefactorFirst. The goal is to modernize the current report generation pipeline—which currently relies on server-side monolithic string concatenation in [`HtmlReport`](file:///C:/Code/RefactorFirst/report/src/main/java/org/hjug/refactorfirst/report/HtmlReport.java) and Google Charts in [`GraphDataGenerator`](file:///C:/Code/RefactorFirst/graph-data-generator/src/main/java/org/hjug/gdg/GraphDataGenerator.java)—by introducing: + +1. **[`JsonGenerator`](file:///C:/Code/RefactorFirst/report/src/main/java/org/hjug/refactorfirst/report/JsonGenerator.java)**: A dedicated generator adjacent to [`HtmlReport`](file:///C:/Code/RefactorFirst/report/src/main/java/org/hjug/refactorfirst/report/HtmlReport.java) that exports all codebase analysis results, metrics, graph metadata, and properly escaped Graphviz DOT language into a clean JSON file named `refactor-first.json` inside a `.refactorfirst` directory (creating directory and file if missing, overwriting if existing). +2. **[`RefactorFirstMavenJsonGenerator`](file:///C:/Code/RefactorFirst/refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstMavenJsonGenerator.java)**: A Maven plugin Mojo adjacent to [`RefactorFirstHtmlReport`](file:///C:/Code/RefactorFirst/refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstHtmlReport.java) that extends [`AbstractMojo`](file:///C:/Code/RefactorFirst/refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstHtmlReport.java#L21) and invokes [`JsonGenerator`](file:///C:/Code/RefactorFirst/report/src/main/java/org/hjug/refactorfirst/report/JsonGenerator.java). +3. **Mustache Template (`refactor-first-report.mustache`)**: A Mustache template replicating the complete HTML report structure currently produced by [`HtmlReport`](file:///C:/Code/RefactorFirst/report/src/main/java/org/hjug/refactorfirst/report/HtmlReport.java), matching the sample output in [`refactor-first-report.html`](file:///C:/Code/junit4/target/site/refactor-first-report.html). +4. **Client-Side HTML Runner (`index.html`)**: An HTML viewer that loads the JSON data and Mustache template purely in the browser using client-side JavaScript modules (` + + + + + + + + + + + + +
+ + +
+

+ RefactorFirst Report for + {{project.name}} {{project.version}} +

+ + +
+

Show RefactorFirst some ❤️

+ Star + Fork + Watch + Issue + Sponsor +
+ + +
+ + +
+

Class Map

+ + + + + +
+ Red lines represent relationships to remove.
+ Red nodes represent classes to remove.
+ Zoom in / out with your mouse wheel and click/move to drag the image.
+ Number of classes: {{classMap.classCount}} Number of relationships: {{classMap.relationshipCount}}
+
+ + {{#classMap.dotThresholdExceeded}} +
SVG is too big to render quickly
+ {{/classMap.dotThresholdExceeded}} + {{^classMap.dotThresholdExceeded}} +
+ {{/classMap.dotThresholdExceeded}} +
+ + + {{#classRelationshipsToRemove.relationships.length}} +
+ +

Refactor Starting with Priority 1

+
+ Current Class Cycle Count: {{classRelationshipsToRemove.cycleCount}}
+ Number of Class Relationships to Remove: {{classRelationshipsToRemove.relationshipsToRemoveCount}}
+ Classes with * should be broken apart
+ Removing class relationships below will eliminate class cycles +
+
+ + + + + + + + + + + + + {{#classRelationshipsToRemove.relationships}} + + + + + + + + + {{/classRelationshipsToRemove.relationships}} + +
Class RelationshipPriorityIn Class
Cycles
Relationship
Strength
Also Removes Pkg
Cycle Relationship
In Package
Cycles
{{{renderedLabel}}}{{priority}}{{cycleCount}}{{effortRank}}{{#alsoRemovesPackageRelationship}}true{{/alsoRemovesPackageRelationship}}{{^alsoRemovesPackageRelationship}}false{{/alsoRemovesPackageRelationship}}{{packageCycleCount}}
+
+
+ {{/classRelationshipsToRemove.relationships.length}} + + + {{#disharmonies}} +
+ +
+ + + +
Problem:{{problem}}
Solution:{{{solution}}}
+
+ + +
+ +
+ +

{{title}} Chart Legend:

+ + + + + + + +
X-Axis: Effort to refactor
Y-Axis: Relative churn
Color: Priority of what to fix first
Circle size: Priority (Visual) of what to fix first
+
+ +

{{title}} by the numbers: (Refactor Starting with Priority 1)

+
+ + + + {{#table.headers}} + + {{/table.headers}} + + + + {{#table.rows}} + + {{#cells}} + + {{/cells}} + + {{/table.rows}} + +
{{.}}
{{{content}}}
+
+
+
+ {{/disharmonies}} + + + {{#classCycles.hasCycles}} +
+ +

Class Cycles by the numbers:

+
+ + + + + + + + {{#classCycles.summary}} + + + + + + + {{/classCycles.summary}} + +
Cycle NamePriorityClass CountRelationship Count
{{cycleName}}{{priority}}{{classCount}}{{relationshipCount}}
+
+ + {{#classCycles.largestCycle.hasCycleMap}} +

Largest Class Cycle : {{classCycles.largestCycle.cycleName}}

+

Limiting number of cycles displayed to 1 to keep page load time fast

+ + + + + + +
+ + +
+ + + + + + {{#classCycles.largestCycle.breakdown}} + + + + + {{/classCycles.largestCycle.breakdown}} + +
ClassesRelationships
{{{className}}}{{{edgesHtml}}}
+
+ {{/classCycles.largestCycle.hasCycleMap}} +
+ {{/classCycles.hasCycles}} + +
+

+ Last Published: {{project.scanTimestamp}} +
+ + +``` + +--- + +## Client-Side HTML Viewer (`index.html`) + +This file is completely self-contained and operates entirely in the browser using client-side JavaScript. **No Node.js, Bun, NPM, or server-side tools are permitted or required**. + +### Browser CORS & Loading Strategy +When opened directly in a browser via the `file:///` protocol, some browsers restrict `fetch('./refactor-first.json')`. To ensure frictionless operation under all conditions, `index.html` provides: +1. **Automated Fetch**: Tries to `fetch('./refactor-first.json')` and `fetch('./refactor-first-report.mustache')` when run on a local HTTP server or permissive browser. +2. **File Picker / Drag-and-Drop Fallback**: If `fetch` is rejected by local CORS, a clean UI drag-and-drop zone appears allowing the user to select their generated `refactor-first.json` file. +3. **Template Inlining**: The Mustache template can be loaded from file or embedded as a `