Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 61 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,46 @@
- Central DTO: `CodebaseGraphDTO` (JGraphT graphs + disharmony lists + metrics)
- CLI entry point: `org.hjug.refactorfirst.Main` → `ReportCommand`
- Fat jar location: `cli/target/refactor-first-cli-*.jar`
- **Anonymous/synthetic classes are first-class graph members.** Java `Outer$N`/`Outer$` (anonymous/synthetic inner classes) and the Kotlin literal `"<anonymous>"` FQN are **not** sieved out by `GraphDependencyCollector`; they genuinely participate in cycles and can harbour antipatterns, so they are vertices in the class graph and rendered with `$` as the enclosing-class separator. `GraphDependencyCollector` keeps only the `from == to` self-edge guard, plus a degenerate-package guard so a packageless `"<anonymous>"` source never creates an `""` package-graph vertex. **Sink-only** anonymous/synthetic vertices (those with no outgoing edges) are suppressed only at render time in `HtmlReport.isSinkAnonymousOrSyntheticVertex` to keep the Class/Cycle Map DOT graph readable; active ones still render.
- **Anonymous DOT node ids are source-file derived.** OpenRewrite attributes a Kotlin anonymous object / function-literal type with {@code "<anonymous>"} as the trailing simple-name segment of its FQN: standalone ({@code "<anonymous>"}) or, in real graphs (e.g. FXGL), prefixed by the enclosing class/package ({@code "dev.DeveloperWASDControl.<anonymous>"}). {@code HtmlReport.isAnonymousFqn(vertex)} detects a vertex when its trailing segment starts with {@code <}. {@code HtmlReport.renderSafeNodeId(vertex, codebaseGraphDTO)} then derives the enclosing owner from the vertex's mapped source-file path in {@code CodebaseGraphDTO.classToSourceFilePathMapping} (file base name without extension, e.g. {@code DeveloperWASDControl.kt} -> {@code DeveloperWASDControl}). The DOT node id renders as {@code DeveloperWASDControl_anonymous} and the human-readable label as {@code DeveloperWASDControl\$anonymous} ({@code $} escaped as {@code \$} for DOT). When no source path is mapped (or DTO is null) it degrades to the reversible {@code lt_}/{@code _gt} {@code <}/{@code >} encoding. The renderer is responsible for DOT/HTML-safe encoding of the literal {@code "<anonymous>"} FQN ({@code <}/{@code >} are illegal in Graphviz node ids; {@code <}/{@code >} escaping in HTML table labels).

## Kotlin analysis (hard dependency)

`rewrite-kotlin` (`org.openrewrite:rewrite-kotlin`) is a **non-optional
compile dependency** of the `codebase-graph-builder` module, pulled in via that
module's `rewrite-recipe-bom` import (`rewrite-recipe-bom:3.36.0`). The Kotlin
parser is therefore always on the classpath of any consumer of
`codebase-graph-builder`; there is no opt-in and no reflective "is Kotlin
present?" guard. (An earlier, never-merged iteration made it `<optional>` with a
`CompositeGraphBuilder.isKotlinAvailable()` reflection guard, but the Kotlin
builder and visitors import `org.openrewrite.kotlin.*` directly and are
constructed via `new`, so the guard was dead code — it would have thrown
`NoClassDefFoundError` at `new KotlinSourceFileGraphBuilder()` before the guard
could ever run. The guard has been removed and the optionality dropped.)

**Distribution impact:** because the dependency is mandatory, the Maven plugin
and the CLI fat-jar bundle the Kotlin compiler —
`kotlin-compiler-embeddable:2.x` (verified at `2.3.20` in this build) and its
`kotlin-script-runtime` / `kotlin-daemon-embeddable` /
`kotlinx-coroutines-core-jvm` transitives — into **every** consumer's runtime,
including pure-Java projects that never contain a `.kt` file. As of this branch
the CLI fat-jar is `cli/target/cli-<version>.jar` and measures **~144 MB**
(verified via `du -sh cli/target/cli-0.10.0-SNAPSHOT.jar` after
`mvn clean install -DskipTests`); the Kotlin compiler and its transitives are a
material fraction of that. A pure-Java consumer therefore pays this size/cost
(the dependency is always on the classpath regardless).

**No opt-out:** Kotlin analysis runs unconditionally — there is no
`analyzeKotlin` switch on `GraphBuilderConfig`. The Kotlin parser is always
exercised. The config field `kotlinLanguageLevel` is kept as a `String` to
avoid importing `rewrite-kotlin`'s enum into the config DTO.

**Orchestration & fallback:** `CompositeGraphBuilder.getCodebaseGraphDTO(path,
config)` is the single orchestrator — it builds the Java graph, then the Kotlin
graph and merges them. A Kotlin build *failure* (parse error, IO, etc.) falls
back to returning the Java-only DTO with a `log.warn`
(`"Kotlin analysis failed; falling back to Java-only graph"`). This fallback
is for build failures, not for "Kotlin is absent".

## Testing Notes
- JUnit 5 with parameterized tests
Expand All @@ -36,4 +76,24 @@ Configuration options (most important):
- `backEdgeAnalysisCount`: 0 = analyze all back edges (default: 50)
- `analyzeCycles`: Whether to analyze cycles (default: true)
- `excludeTests`: Exclude test classes (default: true)
- `minifyHtml`: Minify HTML report (default: false)
- `minifyHtml`: Minify HTML report (default: false)

## CVE Pinning
Transitive dependencies surfaced by an OWASP dependency-check are pinned centrally in the
parent `pom.xml` `<dependencyManagement>` so child modules reference them by bare
GAV (no `<version>`). If a new transitive surfaces, add its fixed-version pin to the parent's
`<dependencyManagement>` block labelled "Centralized CVE mitigations",
recording the CVE ID, the NVD-quoted CVSS, and the affected range in the
comment, and drop the corresponding `<version>` from whichever child module introduced the
transitive. Currently pinned:
- `io.micrometer:micrometer-core:1.17.0` — CVE-2026-40984, CVSS 7.5, affected 1.9.0–1.9.17 / 1.13.0–1.13.18 / 1.14.0–1.14.15 / 1.15.0–1.15.11 / 1.16.0–1.16.5 (rewrite-core 8.86.0)
- `io.quarkus.gizmo:gizmo:1.9.0` — CVSS > 8.0 advisory in 1.0.11, no public CVE (rewrite-core)
- `org.apache.commons:commons-lang3:3.18.0` — CVE-2025-48924, CVSS 5.3, affected 3.0 before 3.18.0 (pmd-java, maven-reporting-impl)
- `org.iq80.snappy:snappy:0.5` — CVE-2024-36124, CVSS 5.3 (maven-core)
- `commons-beanutils:commons-beanutils:1.11.0` — CVE-2025-48734, CVSS 8.8 (maven-reporting-impl 4.0.0)

Note: `mvn clean install -Plocal` invokes the OWASP `dependency-check-maven`
plugin which requires NVD network access; in sandboxed / offline environments
the plugin emits HTTP 429 or `JdbcBatchUpdateException` and the build fails
on a network precondition rather than a code issue.
Re-verify any CVE ID quoted here against the NVD before bumping a pin; the citations were last verified on 2026-08-09.
2 changes: 1 addition & 1 deletion change-proneness-ranker/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,4 @@

</dependencies>

</project>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public void rankChangeProneness(List<ScmLogInfo> scmLogInfos) {
for (ScmLogInfo scmLogInfo : scmLogInfos) {
if (!cachedScmLogInfos.containsKey(scmLogInfo.getPath())) {
Map.Entry<Integer, Integer> entry = suffixSums.ceilingEntry(scmLogInfo.getEarliestCommit());
int commitsInRepositorySinceCreation = (entry != null) ? entry.getValue() : 0;
int commitsInRepositorySinceCreation = entry != null ? entry.getValue() : 0;

scmLogInfo.setChangeProneness((float) scmLogInfo.getCommitCount() / commitsInRepositorySinceCreation);
cachedScmLogInfos.put(scmLogInfo.getPath(), scmLogInfo);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
public class GitLogReader implements AutoCloseable {

static final String JAVA_FILE_TYPE = ".java";
static final String KOTLIN_FILE_TYPE = ".kt";

private Repository gitRepository;

Expand Down Expand Up @@ -173,7 +174,9 @@ public TreeMap<Integer, Integer> captureChangeCountByCommitTimestamp() throws IO
int count = 0;
for (DiffEntry entry : getDiffEntries(newer, older)) {
if (entry.getNewPath().endsWith(JAVA_FILE_TYPE)
|| entry.getOldPath().endsWith(JAVA_FILE_TYPE)) {
|| entry.getOldPath().endsWith(JAVA_FILE_TYPE)
|| entry.getNewPath().endsWith(KOTLIN_FILE_TYPE)
|| entry.getOldPath().endsWith(KOTLIN_FILE_TYPE)) {
count++;
}
}
Expand Down Expand Up @@ -212,7 +215,8 @@ Map<Integer, Integer> walkFirstCommit(RevCommit firstCommit) throws IOException
if (treeWalk.isSubtree()) {
treeWalk.enterSubtree();
} else {
if (treeWalk.getPathString().endsWith(JAVA_FILE_TYPE)) {
if (treeWalk.getPathString().endsWith(JAVA_FILE_TYPE)
|| treeWalk.getPathString().endsWith(KOTLIN_FILE_TYPE)) {
firstCommitCount++;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import java.nio.file.Path;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.transport.URIish;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -40,7 +41,7 @@ void tearDown() {
void testGetRepoUrlWithGitHubSshOrigin() throws Exception {
git.remoteAdd()
.setName("origin")
.setUri(new org.eclipse.jgit.transport.URIish("git@github.com:user/repo.git"))
.setUri(new URIish("git@github.com:user/repo.git"))
.call();

try (GitLogReader gitLogReader = new GitLogReader(projectBaseDir)) {
Expand All @@ -54,7 +55,7 @@ void testGetRepoUrlWithGitHubSshOrigin() throws Exception {
void testGetRepoUrlWithGitLabSshOrigin() throws Exception {
git.remoteAdd()
.setName("origin")
.setUri(new org.eclipse.jgit.transport.URIish("git@gitlab.com:user/repo.git"))
.setUri(new URIish("git@gitlab.com:user/repo.git"))
.call();

try (GitLogReader gitLogReader = new GitLogReader(projectBaseDir)) {
Expand All @@ -68,7 +69,7 @@ void testGetRepoUrlWithGitLabSshOrigin() throws Exception {
void testGetRepoUrlWithBitBucketSshOrigin() throws Exception {
git.remoteAdd()
.setName("origin")
.setUri(new org.eclipse.jgit.transport.URIish("git@bitbucket.org:user/repo.git"))
.setUri(new URIish("git@bitbucket.org:user/repo.git"))
.call();

try (GitLogReader gitLogReader = new GitLogReader(projectBaseDir)) {
Expand All @@ -82,7 +83,7 @@ void testGetRepoUrlWithBitBucketSshOrigin() throws Exception {
void testGetRepoUrlWithHttpsOrigin() throws Exception {
git.remoteAdd()
.setName("origin")
.setUri(new org.eclipse.jgit.transport.URIish("https://github.com/user/repo.git"))
.setUri(new URIish("https://github.com/user/repo.git"))
.call();

try (GitLogReader gitLogReader = new GitLogReader(projectBaseDir)) {
Expand Down
4 changes: 1 addition & 3 deletions cli/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
<name>RefactorFirst CLI</name>

<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

Expand Down Expand Up @@ -93,4 +91,4 @@
</plugins>
</build>

</project>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ private void populateDefaultArguments() {

private void inferArgumentsFromMavenProject() {
if (baseDir.isDirectory()) {
File[] potentialPomFiles = baseDir.listFiles(f -> f.getName().equals("pom.xml"));
File[] potentialPomFiles = baseDir.listFiles(f -> "pom.xml".equals(f.getName()));
File pomFile = null;
if (potentialPomFiles != null && potentialPomFiles.length > 0) {
pomFile = potentialPomFiles[0];
Expand Down
2 changes: 1 addition & 1 deletion cli/src/main/java/org/hjug/refactorfirst/ReportType.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ public enum ReportType {
SIMPLE_HTML,
HTML,
JSON,
CSV;
CSV
}
23 changes: 17 additions & 6 deletions codebase-graph-builder/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<dependency>
<groupId>org.openrewrite.recipe</groupId>
<artifactId>rewrite-recipe-bom</artifactId>
<version>3.34.0</version>
<version>3.37.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
Expand All @@ -35,11 +35,14 @@
<artifactId>jgrapht-core</artifactId>
</dependency>

<!-- Addresses CVE-2026-40984 introduced through rewrite-core 8.86.0 -->
<!-- Parent <dependencyManagement> pins these two transitives
(gizmo, micrometer-core) per the centralized CVE-mitigation
rule so this child module imports them by bare GAV and stays
aligned with any other module that picks up the same transitive
path. -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
<version>1.10.0</version>
</dependency>
<dependency>
<groupId>org.openrewrite</groupId>
Expand All @@ -58,15 +61,23 @@
<artifactId>rewrite-java</artifactId>
</dependency>

<!--gizmo 1.0.11, used by rewrite-core has a CVSS score > 8.0 -->
<!--gizmo 1.0.11, used by rewrite-core has a CVSS score > 8.0 — pinned
in parent <dependencyManagement>. -->
<dependency>
<groupId>io.quarkus.gizmo</groupId>
<artifactId>gizmo</artifactId>
<version>1.9.0</version>
</dependency>
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-core</artifactId>
</dependency>

<!-- Kotlin language support (compile dependency: enables Kotlin
codebase analysis; see AGENTS.md "Kotlin analysis" section for
the distribution impact on the Maven plugin / CLI fat-jar). -->
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-kotlin</artifactId>
</dependency>
</dependencies>
</project>
</project>
Loading
Loading