diff --git a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/CodebaseGraphDTO.java b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/CodebaseGraphDTO.java index c236d464..17de3da3 100644 --- a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/CodebaseGraphDTO.java +++ b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/CodebaseGraphDTO.java @@ -22,8 +22,8 @@ @ToString public class CodebaseGraphDTO { - private final Graph classReferencesGraph; - private final Graph packageReferencesGraph; + private final Graph classReferencesGraph; + private final Graph packageReferencesGraph; private final Map> classRelationshipsInPackageRelationship; // used for looking up files where classes reside private final Map classToSourceFilePathMapping; @@ -35,15 +35,17 @@ public class CodebaseGraphDTO { * Creates a new CodebaseGraphDTO. * * @param classReferencesGraph the class reference graph + * (vertices are {@link VertexInfo}) * @param packageReferencesGraph the package reference graph + * (vertices are {@link VertexInfo}) * @param classRelationshipsInPackageRelationship the class-to-package relationships * @param classToSourceFilePathMapping the class to source file path mapping * @param classDisharmonies the list of class disharmonies * @param methodDisharmonies the list of method disharmonies */ public CodebaseGraphDTO( - Graph classReferencesGraph, - Graph packageReferencesGraph, + Graph classReferencesGraph, + Graph packageReferencesGraph, Map> classRelationshipsInPackageRelationship, Map classToSourceFilePathMapping, List classDisharmonies, diff --git a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/CompositeGraphBuilder.java b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/CompositeGraphBuilder.java index eda73a53..01e7a30d 100644 --- a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/CompositeGraphBuilder.java +++ b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/CompositeGraphBuilder.java @@ -123,10 +123,20 @@ public CodebaseGraphDTO getCodebaseGraphDTO( return getCodebaseGraphDTO(repositoryPath, repositoryRoot, config); } + /** + * Combines Java and Kotlin graphs, source mappings, and disharmonies. + * + *

Shared edge weights are summed before unattributed vertices are reconciled, + * locations are hydrated, and package-to-class relationships are rebuilt. + * + * @param javaDto the Java analysis result + * @param kotlinDto the Kotlin analysis result + * @return the unified codebase graph + */ static CodebaseGraphDTO merge(CodebaseGraphDTO javaDto, CodebaseGraphDTO kotlinDto) { - Graph mergedClassGraph = + Graph mergedClassGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - Graph mergedPackageGraph = + Graph mergedPackageGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); Map> mergedClassRelationships = new HashMap<>(); Map mergedSourcePathMapping = new HashMap<>(); @@ -175,6 +185,10 @@ static CodebaseGraphDTO merge(CodebaseGraphDTO javaDto, CodebaseGraphDTO kotlinD // Reconcile fabricated cross-language vertices against real declarations reconcileUnattributedVertices(mergedClassGraph, mergedPackageGraph, mergedSourcePathMapping); + // Populate VertexInfo locations on the merged graphs now that the + // anonymous-vertex mapping and reconciliation passes are complete + VertexInfoHydrator.hydrate(mergedClassGraph, mergedPackageGraph, mergedSourcePathMapping); + // Rebuild class relationships after reconciliation since vertex contraction // may have removed/replaced edges referenced in the relationship map rebuildClassRelationshipsAfterReconciliation(mergedClassGraph, mergedPackageGraph, mergedClassRelationships); @@ -215,8 +229,8 @@ static CodebaseGraphDTO merge(CodebaseGraphDTO javaDto, CodebaseGraphDTO kotlinD * @param sourcePathMapping the merged mapping of FQN to source file path (only real declarations) */ static void reconcileUnattributedVertices( - Graph classGraph, - Graph packageGraph, + Graph classGraph, + Graph packageGraph, Map sourcePathMapping) { // Build index: simple name -> list of canonical FQNs that have a source mapping Map> bySimpleName = new HashMap<>(); @@ -226,7 +240,10 @@ static void reconcileUnattributedVertices( } // Find fabricated vertices: vertices in classGraph but NOT in sourcePathMapping - Set verticesToCheck = new HashSet<>(classGraph.vertexSet()); + Set verticesToCheck = new HashSet<>(); + for (VertexInfo vertex : classGraph.vertexSet()) { + verticesToCheck.add(vertex.getName()); + } verticesToCheck.removeAll(sourcePathMapping.keySet()); for (String fabricatedFqn : verticesToCheck) { @@ -274,23 +291,24 @@ static void reconcileUnattributedVertices( * @param fabricatedFqn the FQN of the fabricated vertex to remove */ private static void removeFabricatedExternalVertex( - Graph classGraph, - Graph packageGraph, + Graph classGraph, + Graph packageGraph, String fabricatedFqn) { + VertexInfo fabricatedVertex = VertexInfo.forName(fabricatedFqn); // Remove all incoming edges - Set incomingEdges = new HashSet<>(classGraph.incomingEdgesOf(fabricatedFqn)); + Set incomingEdges = new HashSet<>(classGraph.incomingEdgesOf(fabricatedVertex)); for (DefaultWeightedEdge edge : incomingEdges) { classGraph.removeEdge(edge); } // Remove all outgoing edges - Set outgoingEdges = new HashSet<>(classGraph.outgoingEdgesOf(fabricatedFqn)); + Set outgoingEdges = new HashSet<>(classGraph.outgoingEdgesOf(fabricatedVertex)); for (DefaultWeightedEdge edge : outgoingEdges) { classGraph.removeEdge(edge); } // Remove the vertex - classGraph.removeVertex(fabricatedFqn); + classGraph.removeVertex(fabricatedVertex); // Note: We don't modify packageGraph here since the fabricated vertex's // package matched the caller's package (which is a real codebase package). @@ -304,80 +322,89 @@ private static String simpleName(String fqn) { return lastDot >= 0 ? fqn.substring(lastDot + 1) : fqn; } + /** + * Redirects a fabricated vertex's edges to its canonical declaration, summing duplicate weights. + * Self-loops introduced by contraction are dropped, and new cross-package edges are recorded. + */ private static void contractVertex( - Graph classGraph, - Graph packageGraph, + Graph classGraph, + Graph packageGraph, String fabricatedFqn, String canonicalFqn) { + VertexInfo fabricatedVertex = VertexInfo.forName(fabricatedFqn); + VertexInfo canonicalVertex = VertexInfo.forName(canonicalFqn); // Ensure canonical vertex exists in classGraph - if (!classGraph.containsVertex(canonicalFqn)) { - classGraph.addVertex(canonicalFqn); + if (!classGraph.containsVertex(canonicalVertex)) { + classGraph.addVertex(canonicalVertex); } // Redirect incoming edges to fabricated -> canonical - Set incomingEdges = new HashSet<>(classGraph.incomingEdgesOf(fabricatedFqn)); + Set incomingEdges = new HashSet<>(classGraph.incomingEdgesOf(fabricatedVertex)); for (DefaultWeightedEdge edge : incomingEdges) { - String source = classGraph.getEdgeSource(edge); + VertexInfo source = classGraph.getEdgeSource(edge); double weight = classGraph.getEdgeWeight(edge); classGraph.removeEdge(edge); - if (source.equals(canonicalFqn)) { + if (source.equals(canonicalVertex)) { // Contraction would create a self-loop; drop the edge. continue; } - if (!classGraph.containsEdge(source, canonicalFqn)) { - DefaultWeightedEdge newEdge = classGraph.addEdge(source, canonicalFqn); + if (!classGraph.containsEdge(source, canonicalVertex)) { + DefaultWeightedEdge newEdge = classGraph.addEdge(source, canonicalVertex); classGraph.setEdgeWeight(newEdge, weight); // Add corresponding package edge if cross-package - addPackageEdgeIfCrossPackage(packageGraph, source, canonicalFqn); + addPackageEdgeIfCrossPackage(packageGraph, source, canonicalVertex); } else { - DefaultWeightedEdge existingEdge = classGraph.getEdge(source, canonicalFqn); + DefaultWeightedEdge existingEdge = classGraph.getEdge(source, canonicalVertex); classGraph.setEdgeWeight(existingEdge, classGraph.getEdgeWeight(existingEdge) + weight); } } // Redirect outgoing edges from fabricated -> canonical - Set outgoingEdges = new HashSet<>(classGraph.outgoingEdgesOf(fabricatedFqn)); + Set outgoingEdges = new HashSet<>(classGraph.outgoingEdgesOf(fabricatedVertex)); for (DefaultWeightedEdge edge : outgoingEdges) { - String target = classGraph.getEdgeTarget(edge); + VertexInfo target = classGraph.getEdgeTarget(edge); double weight = classGraph.getEdgeWeight(edge); classGraph.removeEdge(edge); - if (target.equals(canonicalFqn)) { + if (target.equals(canonicalVertex)) { // Contraction would create a self-loop; drop the edge. continue; } - if (!classGraph.containsEdge(canonicalFqn, target)) { - DefaultWeightedEdge newEdge = classGraph.addEdge(canonicalFqn, target); + if (!classGraph.containsEdge(canonicalVertex, target)) { + DefaultWeightedEdge newEdge = classGraph.addEdge(canonicalVertex, target); classGraph.setEdgeWeight(newEdge, weight); // Add corresponding package edge if cross-package - addPackageEdgeIfCrossPackage(packageGraph, canonicalFqn, target); + addPackageEdgeIfCrossPackage(packageGraph, canonicalVertex, target); } else { - DefaultWeightedEdge existingEdge = classGraph.getEdge(canonicalFqn, target); + DefaultWeightedEdge existingEdge = classGraph.getEdge(canonicalVertex, target); classGraph.setEdgeWeight(existingEdge, classGraph.getEdgeWeight(existingEdge) + weight); } } // Remove the fabricated vertex - classGraph.removeVertex(fabricatedFqn); + classGraph.removeVertex(fabricatedVertex); } + /** Adds or increments a package edge when both class endpoints have distinct, nonempty packages. */ private static void addPackageEdgeIfCrossPackage( - Graph packageGraph, String classSource, String classTarget) { - String pkgSource = packageName(classSource); - String pkgTarget = packageName(classTarget); - if (!pkgSource.equals(pkgTarget)) { - if (!packageGraph.containsVertex(pkgSource)) { - packageGraph.addVertex(pkgSource); + Graph packageGraph, VertexInfo classSource, VertexInfo classTarget) { + String pkgSource = packageName(classSource.getName()); + String pkgTarget = packageName(classTarget.getName()); + if (!pkgSource.isEmpty() && !pkgTarget.isEmpty() && !pkgSource.equals(pkgTarget)) { + VertexInfo pkgSourceVertex = VertexInfo.forName(pkgSource); + VertexInfo pkgTargetVertex = VertexInfo.forName(pkgTarget); + if (!packageGraph.containsVertex(pkgSourceVertex)) { + packageGraph.addVertex(pkgSourceVertex); } - if (!packageGraph.containsVertex(pkgTarget)) { - packageGraph.addVertex(pkgTarget); + if (!packageGraph.containsVertex(pkgTargetVertex)) { + packageGraph.addVertex(pkgTargetVertex); } - if (!packageGraph.containsEdge(pkgSource, pkgTarget)) { - DefaultWeightedEdge newEdge = packageGraph.addEdge(pkgSource, pkgTarget); + if (!packageGraph.containsEdge(pkgSourceVertex, pkgTargetVertex)) { + DefaultWeightedEdge newEdge = packageGraph.addEdge(pkgSourceVertex, pkgTargetVertex); packageGraph.setEdgeWeight(newEdge, 1); } else { - DefaultWeightedEdge existingEdge = packageGraph.getEdge(pkgSource, pkgTarget); + DefaultWeightedEdge existingEdge = packageGraph.getEdge(pkgSourceVertex, pkgTargetVertex); packageGraph.setEdgeWeight(existingEdge, packageGraph.getEdgeWeight(existingEdge) + 1); } } @@ -388,16 +415,17 @@ private static String packageName(String fqn) { return lastDot >= 0 ? fqn.substring(0, lastDot) : ""; } + /** Copies source vertices and edges into the target, adding weights for edges already present. */ private static void mergeGraph( - Graph source, Graph target) { + Graph source, Graph target) { // Add vertices - for (String vertex : source.vertexSet()) { + for (VertexInfo vertex : source.vertexSet()) { target.addVertex(vertex); } // Add edges, preserving weights for (DefaultWeightedEdge edge : source.edgeSet()) { - String sourceVertex = source.getEdgeSource(edge); - String targetVertex = source.getEdgeTarget(edge); + VertexInfo sourceVertex = source.getEdgeSource(edge); + VertexInfo targetVertex = source.getEdgeTarget(edge); double weight = source.getEdgeWeight(edge); if (!target.containsEdge(sourceVertex, targetVertex)) { @@ -410,17 +438,18 @@ private static void mergeGraph( } } + /** Maps a source DTO's package-to-class relationships onto the corresponding merged graph edges. */ private static void mergeClassRelationships( CodebaseGraphDTO dto, - Graph sourceClassGraph, - Graph mergedPackageGraph, - Graph mergedClassGraph, + Graph sourceClassGraph, + Graph mergedPackageGraph, + Graph mergedClassGraph, Map> mergedClassRelationships) { for (Map.Entry> entry : dto.getClassRelationshipsInPackageRelationship().entrySet()) { - String pkgSource = dto.getPackageReferencesGraph().getEdgeSource(entry.getKey()); - String pkgTarget = dto.getPackageReferencesGraph().getEdgeTarget(entry.getKey()); + VertexInfo pkgSource = dto.getPackageReferencesGraph().getEdgeSource(entry.getKey()); + VertexInfo pkgTarget = dto.getPackageReferencesGraph().getEdgeTarget(entry.getKey()); DefaultWeightedEdge mergedPkgEdge = mergedPackageGraph.getEdge(pkgSource, pkgTarget); if (mergedPkgEdge == null) { continue; @@ -428,8 +457,8 @@ private static void mergeClassRelationships( Set mergedClassEdges = new HashSet<>(); for (DefaultWeightedEdge sourceClassEdge : entry.getValue()) { - String classSource = sourceClassGraph.getEdgeSource(sourceClassEdge); - String classTarget = sourceClassGraph.getEdgeTarget(sourceClassEdge); + VertexInfo classSource = sourceClassGraph.getEdgeSource(sourceClassEdge); + VertexInfo classTarget = sourceClassGraph.getEdgeTarget(sourceClassEdge); DefaultWeightedEdge mergedClassEdge = mergedClassGraph.getEdge(classSource, classTarget); if (mergedClassEdge != null) { mergedClassEdges.add(mergedClassEdge); @@ -453,20 +482,23 @@ private static void mergeClassRelationships( * them by their corresponding package edge in the merged package graph. */ private static void rebuildClassRelationshipsAfterReconciliation( - Graph mergedClassGraph, - Graph mergedPackageGraph, + Graph mergedClassGraph, + Graph mergedPackageGraph, Map> mergedClassRelationships) { Map> rebuilt = new HashMap<>(); // Iterate all class edges in the final merged class graph for (DefaultWeightedEdge classEdge : mergedClassGraph.edgeSet()) { - String classSource = mergedClassGraph.getEdgeSource(classEdge); - String classTarget = mergedClassGraph.getEdgeTarget(classEdge); + VertexInfo classSource = mergedClassGraph.getEdgeSource(classEdge); + VertexInfo classTarget = mergedClassGraph.getEdgeTarget(classEdge); - String pkgSource = packageName(classSource); - String pkgTarget = packageName(classTarget); + String pkgSource = packageName(classSource.getName()); + String pkgTarget = packageName(classTarget.getName()); - DefaultWeightedEdge pkgEdge = mergedPackageGraph.getEdge(pkgSource, pkgTarget); + DefaultWeightedEdge pkgEdge = null; + if (!pkgSource.isEmpty() && !pkgTarget.isEmpty()) { + pkgEdge = mergedPackageGraph.getEdge(VertexInfo.forName(pkgSource), VertexInfo.forName(pkgTarget)); + } if (pkgEdge != null) { rebuilt.computeIfAbsent(pkgEdge, k -> new HashSet<>()).add(classEdge); } @@ -495,17 +527,18 @@ private static void rebuildClassRelationshipsAfterReconciliation( * @param sourcePathMapping the source path mapping to populate for anonymous vertices */ static void mapAnonymousVerticesToEnclosingSourcePath( - Graph classGraph, Map sourcePathMapping) { - for (String vertex : classGraph.vertexSet()) { + Graph classGraph, Map sourcePathMapping) { + for (VertexInfo vertex : classGraph.vertexSet()) { + String name = vertex.getName(); // Skip if already mapped - if (sourcePathMapping.containsKey(vertex)) { + if (sourcePathMapping.containsKey(name)) { continue; } - String enclosingFqn = getEnclosingClassFqn(vertex); + String enclosingFqn = getEnclosingClassFqn(name); if (enclosingFqn != null && sourcePathMapping.containsKey(enclosingFqn)) { // Map anonymous/synthetic vertex to enclosing class's source path - sourcePathMapping.put(vertex, sourcePathMapping.get(enclosingFqn)); + sourcePathMapping.put(name, sourcePathMapping.get(enclosingFqn)); } } } diff --git a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/GraphDependencyCollector.java b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/GraphDependencyCollector.java index 0e042709..c312d8b5 100644 --- a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/GraphDependencyCollector.java +++ b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/GraphDependencyCollector.java @@ -13,10 +13,10 @@ public class GraphDependencyCollector implements DependencyCollector { @Getter - private final Graph classReferencesGraph; + private final Graph classReferencesGraph; @Getter - private final Graph packageReferencesGraph; + private final Graph packageReferencesGraph; @Getter private final Set packagesInCodebase = new HashSet<>(); @@ -25,27 +25,55 @@ public class GraphDependencyCollector implements DependencyCollector { private final Map> classRelationshipsInPackageRelationship = new HashMap<>(); + /** + * Class locations (as reported by the visitors) captured during collection, + * keyed by class FQN. The repository-relative locations used for rendering + * are hydrated onto the {@link VertexInfo} vertices from the canonical + * class-to-source-file mapping after the walk (see {@link VertexInfoHydrator}); + * this map keeps the raw information captured at collection time. + */ + @Getter + private final Map recordedClassLocations = new HashMap<>(); + + /** Registry of the VertexInfo instances created for each vertex name. */ + private final Map vertexRegistry = new HashMap<>(); + + /** + * Creates a dependency collector that writes into the supplied graphs. + * + * @param classReferencesGraph the graph receiving class dependencies + * @param packageReferencesGraph the graph receiving cross-package dependencies + */ public GraphDependencyCollector( - Graph classReferencesGraph, - Graph packageReferencesGraph) { + Graph classReferencesGraph, + Graph packageReferencesGraph) { this.classReferencesGraph = classReferencesGraph; this.packageReferencesGraph = packageReferencesGraph; } + /** + * Adds or increments a class dependency and records its corresponding package relationship. + * Dependencies from a class to itself are ignored. + * + * @param fromClassFqn the fully qualified source class name + * @param toClassFqn the fully qualified target class name + */ @Override public void addClassDependency(String fromClassFqn, String toClassFqn) { if (fromClassFqn.equals(toClassFqn)) { return; } - classReferencesGraph.addVertex(fromClassFqn); - classReferencesGraph.addVertex(toClassFqn); + VertexInfo fromVertex = getOrCreateVertex(fromClassFqn); + VertexInfo toVertex = getOrCreateVertex(toClassFqn); + classReferencesGraph.addVertex(fromVertex); + classReferencesGraph.addVertex(toVertex); DefaultWeightedEdge classRelationship; - if (!classReferencesGraph.containsEdge(fromClassFqn, toClassFqn)) { - classRelationship = classReferencesGraph.addEdge(fromClassFqn, toClassFqn); + if (!classReferencesGraph.containsEdge(fromVertex, toVertex)) { + classRelationship = classReferencesGraph.addEdge(fromVertex, toVertex); } else { - classRelationship = classReferencesGraph.getEdge(fromClassFqn, toClassFqn); + classRelationship = classReferencesGraph.getEdge(fromVertex, toVertex); classReferencesGraph.setEdgeWeight( classRelationship, classReferencesGraph.getEdgeWeight(classRelationship) + 1); } @@ -63,6 +91,13 @@ public void addClassDependency(String fromClassFqn, String toClassFqn) { } } + /** + * Adds or increments the package dependency implied by two class names. + * + * @param fromClassFqn the fully qualified source class name + * @param toClassFqn the fully qualified target class name + * @return the package edge, or {@code null} for equal or empty endpoint packages + */ @Override public DefaultWeightedEdge addPackageDependency(String fromClassFqn, String toClassFqn) { String fromPackageName = getPackageFromFqn(fromClassFqn); @@ -80,14 +115,16 @@ public DefaultWeightedEdge addPackageDependency(String fromClassFqn, String toCl return null; } - packageReferencesGraph.addVertex(fromPackageName); - packageReferencesGraph.addVertex(toPackageName); + VertexInfo fromPackageVertex = getOrCreateVertex(fromPackageName); + VertexInfo toPackageVertex = getOrCreateVertex(toPackageName); + packageReferencesGraph.addVertex(fromPackageVertex); + packageReferencesGraph.addVertex(toPackageVertex); DefaultWeightedEdge packageEdge; - if (!packageReferencesGraph.containsEdge(fromPackageName, toPackageName)) { - packageEdge = packageReferencesGraph.addEdge(fromPackageName, toPackageName); + if (!packageReferencesGraph.containsEdge(fromPackageVertex, toPackageVertex)) { + packageEdge = packageReferencesGraph.addEdge(fromPackageVertex, toPackageVertex); } else { - packageEdge = packageReferencesGraph.getEdge(fromPackageName, toPackageName); + packageEdge = packageReferencesGraph.getEdge(fromPackageVertex, toPackageVertex); packageReferencesGraph.setEdgeWeight(packageEdge, packageReferencesGraph.getEdgeWeight(packageEdge) + 1); } @@ -102,9 +139,26 @@ protected String getPackageFromFqn(String fqn) { return fqn.substring(0, lastIndex); } + /** + * Returns the registered {@link VertexInfo} for the given class or package + * name, creating and registering one (with the parent namespace derived + * from the name) when absent. + */ + private VertexInfo getOrCreateVertex(String name) { + return vertexRegistry.computeIfAbsent(name, VertexInfo::forName); + } + + /** + * Records the raw source location reported by a visitor for later inspection. + * + * @param classFqn the fully qualified class name + * @param sourceFilePath the visitor-reported source location + */ @Override public void recordClassLocation(String classFqn, String sourceFilePath) { - // This will be handled by JavaVisitor which maintains the mapping + // Capture the location reported during collection; repository-relative + // hydration of the VertexInfo vertices happens after the walk. + recordedClassLocations.put(classFqn, sourceFilePath); } @Override @@ -112,8 +166,13 @@ public void registerPackage(String packageName) { packagesInCodebase.add(packageName); } + /** + * Registers a class vertex with its derived parent namespace, even if it has no dependencies. + * + * @param classFqn the fully qualified class name + */ @Override public void registerClassVertex(String classFqn) { - classReferencesGraph.addVertex(classFqn); + classReferencesGraph.addVertex(getOrCreateVertex(classFqn)); } } diff --git a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/VertexInfo.java b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/VertexInfo.java new file mode 100644 index 00000000..3d1ae341 --- /dev/null +++ b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/VertexInfo.java @@ -0,0 +1,168 @@ +package org.hjug.graphbuilder; + +/** + * Rich graph vertex captured when graph dependency information is collected. + * + *

Replaces the bare class/package name String that previously served as the + * graph vertex so the renderer can build a composed codebase map from the + * graph image. Carries: + * + *

    + *
  • {@link #getName()} — the vertex name: the fully qualified class name + * (class graph) or the package name (package graph).
  • + *
  • {@link #getLocationOnDisk()} — the location on disk of the vertex + * relative to the repository root (source file path for a class; + * member-file directory for a package). Populated by hydration after the + * visitor walk; may be {@code null} when no location is known.
  • + *
  • {@link #getParentNamespace()} — the parent namespace: the parent + * package if this vertex is a package, or the enclosing construct + * (package or class) if the vertex is not a package and contains code. + * Derived from the name at creation time.
  • + *
+ * + *

Identity is {@code name}-based ({@code equals}/{@code hashCode}) so + * JGraphT vertex deduplication behaves exactly like the previous String + * vertices and code can probe graphs with cheap {@link #forName(String)} + * lookups. {@code toString()} returns the name so edge rendering + * ({@code "(source : target)"}) is unchanged. + */ +public class VertexInfo { + + private final String name; + private final String parentNamespace; + private String locationOnDisk; + + /** Creates a vertex with the supplied identity and parent namespace, leaving its disk location unset. */ + private VertexInfo(String name, String parentNamespace) { + this.name = name; + this.parentNamespace = parentNamespace; + } + + /** + * Creates a vertex for the given class or package name with the parent + * namespace derived from the name. + * + * @param name the class FQN or package name; must not be null or empty + * @return a VertexInfo whose parent namespace is the enclosing package + * (for a top-level class or a package) or the enclosing class (for + * a nested, anonymous, or synthetic class); {@code null} when no + * enclosing construct can be derived + * @throws NullPointerException if {@code name} is null + * @throws IllegalArgumentException if {@code name} is empty + */ + public static VertexInfo forName(String name) { + if (name == null) { + throw new NullPointerException("name must not be null"); + } + if (name.isEmpty()) { + throw new IllegalArgumentException("name must not be empty"); + } + return new VertexInfo(name, deriveParentNamespace(name)); + } + + /** + * Derives the enclosing construct of a vertex from its name. + * + *

Rules: + *

    + *
  • Kotlin anonymous vertex ({@code pkg.Outer.}, trailing + * segment starts with {@code <}): the text before the last dot; a + * packageless literal {@code } has no parent.
  • + *
  • Nested/anonymous/synthetic class ({@code $} in the simple name): + * the enclosing class (text before the last {@code $}).
  • + *
  • Top-level class or package: the text before the last dot (the + * containing package / parent package); {@code null} when the name + * has no dot (default package).
  • + *
+ * + * @param name the class FQN or package name + * @return the parent namespace, or {@code null} when none can be derived + */ + static String deriveParentNamespace(String name) { + int lastDot = name.lastIndexOf('.'); + String simpleName = lastDot >= 0 ? name.substring(lastDot + 1) : name; + + if (simpleName.startsWith("<")) { + // Kotlin anonymous: "pkg.Outer." -> "pkg.Outer"; + // packageless "" has no enclosing construct + return lastDot > 0 ? name.substring(0, lastDot) : null; + } + + int lastDollar = simpleName.lastIndexOf('$'); + if (lastDollar >= 0) { + // nested / anonymous / synthetic: enclosing class before the last $ + String enclosingSimple = simpleName.substring(0, lastDollar); + if (enclosingSimple.isEmpty()) { + return null; + } + return lastDot >= 0 ? name.substring(0, lastDot + 1) + enclosingSimple : enclosingSimple; + } + + // top-level class or package: containing / parent package + return lastDot >= 0 ? name.substring(0, lastDot) : null; + } + + /** Returns the vertex name (class FQN or package name). */ + public String getName() { + return name; + } + + /** + * Returns the location on disk of this vertex relative to the repository + * root, or {@code null} when not (yet) known. + */ + public String getLocationOnDisk() { + return locationOnDisk; + } + + /** Sets the repository-relative location on disk (used during hydration). */ + void setLocationOnDisk(String locationOnDisk) { + this.locationOnDisk = locationOnDisk; + } + + /** + * Returns the parent namespace: the parent package if this vertex is a + * package, or the enclosing construct (package or class) if the vertex is + * not a package and contains code; {@code null} when none can be derived. + */ + public String getParentNamespace() { + return parentNamespace; + } + + /** + * Compares vertex names; parent namespaces and hydrated disk locations do not affect identity. + * + * @param o the object to compare + * @return whether the other object is a vertex with the same name + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof VertexInfo)) { + return false; + } + return name.equals(((VertexInfo) o).name); + } + + /** + * Returns a name-based hash that remains stable when the disk location is hydrated. + * + * @return the vertex name's hash code + */ + @Override + public int hashCode() { + return name.hashCode(); + } + + /** + * Returns the vertex name for graph edge rendering. + * + * @return the fully qualified class name or package name + */ + @Override + public String toString() { + return name; + } +} diff --git a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/VertexInfoHydrator.java b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/VertexInfoHydrator.java new file mode 100644 index 00000000..06e11b86 --- /dev/null +++ b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/VertexInfoHydrator.java @@ -0,0 +1,155 @@ +package org.hjug.graphbuilder; + +import java.util.HashMap; +import java.util.Map; +import org.jgrapht.Graph; +import org.jgrapht.graph.DefaultWeightedEdge; + +/** + * Populates {@link VertexInfo#getLocationOnDisk()} on class- and package-graph + * vertices after the visitor walk, using the canonical repository-relative + * class-to-source-file mapping maintained by the visitors. + * + *

Class vertices get the repository-relative path of the source file that + * declares them; nested ({@code Outer$Inner}), anonymous/synthetic + * ({@code Outer$1}/{@code Outer$}) and Kotlin ({@code pkg.Outer.}) + * vertices fall back to the mapping entry of their enclosing class. Package + * vertices get the directory of the shortest mapped direct-member + * source file (subpackage members do not contribute). + * The parent namespace is already derived from the name at vertex creation + * ({@link VertexInfo#forName(String)}) and is left untouched. + */ +public final class VertexInfoHydrator { + + /** Prevents instantiation of this utility class. */ + private VertexInfoHydrator() {} + + /** + * Hydrates every vertex of the supplied class and package graphs. + * + * @param classGraph the class reference graph + * @param packageGraph the package reference graph + * @param classToSourceFilePathMapping repository-relative class FQN to source file path mapping + */ + public static void hydrate( + Graph classGraph, + Graph packageGraph, + Map classToSourceFilePathMapping) { + if (classGraph != null) { + for (VertexInfo vertex : classGraph.vertexSet()) { + vertex.setLocationOnDisk(resolveClassLocation(vertex.getName(), classToSourceFilePathMapping)); + } + } + if (packageGraph != null) { + // index of package name -> shortest mapped direct-member path, built once + // per hydration so every package vertex reuses it + Map packagePathIndex = buildPackagePathIndex(classToSourceFilePathMapping); + for (VertexInfo vertex : packageGraph.vertexSet()) { + vertex.setLocationOnDisk(resolvePackageLocation(vertex.getName(), packagePathIndex)); + } + } + } + + /** + * Resolves a class source path, falling back to its outer Java class or enclosing Kotlin construct. + * + * @param fqn the fully qualified class name + * @param mapping the repository-relative class-to-source-file mapping + * @return the mapped source path, or {@code null} if neither the class nor its enclosing class is mapped + */ + private static String resolveClassLocation(String fqn, Map mapping) { + String direct = mapping.get(fqn); + if (direct != null) { + return direct; + } + + // nested / anonymous / synthetic classes (Outer$Inner, Outer$1, Outer$) + // share the enclosing (outer) class's source file + int dollarIndex = fqn.indexOf('$'); + if (dollarIndex > 0) { + String enclosing = mapping.get(fqn.substring(0, dollarIndex)); + if (enclosing != null) { + return enclosing; + } + } + + // Kotlin anonymous (pkg.Outer.) shares the enclosing class's source file + int lastDot = fqn.lastIndexOf('.'); + if (lastDot > 0 && fqn.charAt(lastDot + 1) == '<') { + return mapping.get(fqn.substring(0, lastDot)); + } + + return null; + } + + /** + * Builds the package name to shortest mapped direct-member path index used + * for package-location resolution. Only direct members of a package + * contribute: a nested {@code $} class name belongs to its outer class's + * package, and a Kotlin {@code pkg.Outer.} member belongs to + * {@code pkg}; a class in a subpackage never contributes to its ancestor + * packages. + * + * @param mapping repository-relative class FQN to source file path mapping + * @return package name to shortest direct-member path; ties resolve to the + * lexicographically first FQN for determinism + */ + private static Map buildPackagePathIndex(Map mapping) { + Map index = new HashMap<>(); + for (String fqn : new java.util.TreeSet<>(mapping.keySet())) { + String packageName = directMemberPackageName(fqn); + if (packageName == null) { + continue; + } + String candidate = mapping.get(fqn); + String current = index.get(packageName); + if (candidate != null && (current == null || candidate.length() < current.length())) { + index.put(packageName, candidate); + } + } + return index; + } + + /** + * Returns the name of the package of which the given class FQN is a direct + * member, or {@code null} when it is a subpackage member or has no package. + * The remainder after the package may contain {@code $} (nested classes) + * or a trailing Kotlin {@code .} segment, but no further + * package dots. + */ + private static String directMemberPackageName(String fqn) { + int lastDot = fqn.lastIndexOf('.'); + if (lastDot < 0) { + return null; // packageless class: no package vertex to locate + } + String packageName; + String remainder; + if (fqn.charAt(lastDot + 1) == '<') { + // Kotlin anonymous (pkg.Outer.): member of the package + // enclosing the anonymous's outer class + int outerDot = fqn.lastIndexOf('.', lastDot - 1); + if (outerDot < 0) { + return null; // packageless outer class + } + packageName = fqn.substring(0, outerDot); + remainder = fqn.substring(outerDot + 1, lastDot); + } else { + packageName = fqn.substring(0, lastDot); + remainder = fqn.substring(lastDot + 1); + } + if (remainder.indexOf('.') >= 0) { + return null; // lives in a subpackage + } + return packageName; + } + + /** Returns the directory of the package's selected direct-member source file, or {@code null} if unmapped. */ + private static String resolvePackageLocation(String packageName, Map packagePathIndex) { + String memberPath = packagePathIndex.get(packageName); + if (memberPath == null) { + return null; + } + int lastSlash = memberPath.lastIndexOf('/'); + return lastSlash >= 0 ? memberPath.substring(0, lastSlash) : memberPath; + } +} diff --git a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/graphbuilder/JavaSourceFileGraphBuilder.java b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/graphbuilder/JavaSourceFileGraphBuilder.java index 611071ab..32236897 100644 --- a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/graphbuilder/JavaSourceFileGraphBuilder.java +++ b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/graphbuilder/JavaSourceFileGraphBuilder.java @@ -16,6 +16,8 @@ import org.hjug.graphbuilder.CodebaseGraphDTO; import org.hjug.graphbuilder.GraphBuilderConfig; import org.hjug.graphbuilder.GraphDependencyCollector; +import org.hjug.graphbuilder.VertexInfo; +import org.hjug.graphbuilder.VertexInfoHydrator; import org.hjug.graphbuilder.metrics.ClassMetrics; import org.hjug.graphbuilder.metrics.DisharmonyDetector; import org.hjug.graphbuilder.metrics.DisharmonyDetector.ClassDisharmony; @@ -48,9 +50,9 @@ public CodebaseGraphDTO buildGraph(String repositoryPath, String repositoryRoot, JavaParser javaParser = createJavaParser(); ExecutionContext ctx = new InMemoryExecutionContext(e -> log.warn("OpenRewrite parse/visit error", e)); - final Graph classReferencesGraph = + final Graph classReferencesGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - final Graph packageReferencesGraph = + final Graph packageReferencesGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); final GraphDependencyCollector dependencyCollector = @@ -118,8 +120,8 @@ static JavaParser createJavaParser() { /** Finalizes collected metrics and assembles the Java codebase graph result. */ static CodebaseGraphDTO finalizeDto( - Graph classReferencesGraph, - Graph packageReferencesGraph, + Graph classReferencesGraph, + Graph packageReferencesGraph, GraphDependencyCollector dependencyCollector, Map classToSourceFilePathMapping, GraphMetricsCollector metricsCollector) { @@ -127,6 +129,10 @@ static CodebaseGraphDTO finalizeDto( removeClassesNotInCodebase(dependencyCollector.getPackagesInCodebase(), classReferencesGraph); removePackagesNotInCodebase(dependencyCollector.getPackagesInCodebase(), packageReferencesGraph); + // Populate VertexInfo locations (repo-relative source paths / package + // directories) from the canonical mapping before the DTO leaves the builder + VertexInfoHydrator.hydrate(classReferencesGraph, packageReferencesGraph, classToSourceFilePathMapping); + dependencyCollector .getClassRelationshipsInPackageRelationship() .keySet() @@ -152,11 +158,11 @@ static CodebaseGraphDTO finalizeDto( /** Removes graph vertices whose packages were not discovered in the analyzed codebase. */ static void removeClassesNotInCodebase( - Set packagesInCodebase, Graph classReferencesGraph) { - Set classesToRemove = new HashSet<>(); - for (String classFqn : classReferencesGraph.vertexSet()) { - if (!packagesInCodebase.contains(getPackage(classFqn))) { - classesToRemove.add(classFqn); + Set packagesInCodebase, Graph classReferencesGraph) { + Set classesToRemove = new HashSet<>(); + for (VertexInfo classVertex : classReferencesGraph.vertexSet()) { + if (!packagesInCodebase.contains(getPackage(classVertex.getName()))) { + classesToRemove.add(classVertex); } } classReferencesGraph.removeAllVertices(classesToRemove); @@ -164,11 +170,11 @@ static void removeClassesNotInCodebase( /** Removes package vertices that were not discovered in the analyzed codebase. */ static void removePackagesNotInCodebase( - Set packagesInCodebase, Graph packageReferencesGraph) { - Set packagesToRemove = new HashSet<>(); - for (String aPackage : packageReferencesGraph.vertexSet()) { - if (!packagesInCodebase.contains(aPackage)) { - packagesToRemove.add(aPackage); + Set packagesInCodebase, Graph packageReferencesGraph) { + Set packagesToRemove = new HashSet<>(); + for (VertexInfo packageVertex : packageReferencesGraph.vertexSet()) { + if (!packagesInCodebase.contains(packageVertex.getName())) { + packagesToRemove.add(packageVertex); } } packageReferencesGraph.removeAllVertices(packagesToRemove); diff --git a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/graphbuilder/KotlinSourceFileGraphBuilder.java b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/graphbuilder/KotlinSourceFileGraphBuilder.java index ee3826ae..bc80e398 100644 --- a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/graphbuilder/KotlinSourceFileGraphBuilder.java +++ b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/graphbuilder/KotlinSourceFileGraphBuilder.java @@ -12,6 +12,7 @@ import org.hjug.graphbuilder.CodebaseGraphDTO; import org.hjug.graphbuilder.GraphBuilderConfig; import org.hjug.graphbuilder.GraphDependencyCollector; +import org.hjug.graphbuilder.VertexInfo; import org.hjug.graphbuilder.metrics.GraphMetricsCollector; import org.hjug.graphbuilder.metrics.KotlinMetricsCollectingVisitor; import org.hjug.graphbuilder.visitor.KotlinDependencyVisitor; @@ -35,6 +36,16 @@ @Slf4j public class KotlinSourceFileGraphBuilder implements SourceFileGraphBuilder { + /** + * Builds dependency graphs and metrics from Kotlin source files, honoring configured test exclusions. + * Recoverable partial parse trees are visited before the resulting graph metadata is finalized. + * + * @param repositoryPath the source directory to scan + * @param repositoryRoot the repository root used to canonicalize source locations + * @param config the parser and source-exclusion settings + * @return the Kotlin codebase graph with hydrated vertex locations + * @throws IOException if source files cannot be read + */ @Override public CodebaseGraphDTO buildGraph(String repositoryPath, String repositoryRoot, GraphBuilderConfig config) throws IOException { @@ -57,9 +68,9 @@ public CodebaseGraphDTO buildGraph(String repositoryPath, String repositoryRoot, ExecutionContext ctx = new InMemoryExecutionContext(e -> log.warn("OpenRewrite parse/visit error", e)); - final Graph classReferencesGraph = + final Graph classReferencesGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - final Graph packageReferencesGraph = + final Graph packageReferencesGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); final GraphDependencyCollector dependencyCollector = diff --git a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/metrics/GraphMetricsCollector.java b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/metrics/GraphMetricsCollector.java index 7960b4fb..6e2d70a2 100644 --- a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/metrics/GraphMetricsCollector.java +++ b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/metrics/GraphMetricsCollector.java @@ -6,14 +6,15 @@ import java.util.Set; import lombok.Getter; import org.hjug.graphbuilder.DependencyCollector; +import org.hjug.graphbuilder.VertexInfo; import org.jgrapht.Graph; import org.jgrapht.graph.DefaultWeightedEdge; @Getter public class GraphMetricsCollector implements DependencyCollector { - private final Graph classGraph; - private final Graph packageGraph; + private final Graph classGraph; + private final Graph packageGraph; private final Map classMetrics = new HashMap<>(); private final Map classToSourceFileMapping = new HashMap<>(); /** @@ -29,24 +30,38 @@ public class GraphMetricsCollector implements DependencyCollector { /** Maps callee full-qualified method signature → set of caller class FQNs (CC). */ private final Map> calleeToCallerClasses = new HashMap<>(); + /** + * Creates a metrics collector backed by the supplied dependency graphs. + * + * @param classGraph the graph receiving class dependencies + * @param packageGraph the graph receiving package dependencies + */ public GraphMetricsCollector( - Graph classGraph, Graph packageGraph) { + Graph classGraph, Graph packageGraph) { this.classGraph = classGraph; this.packageGraph = packageGraph; } + /** + * Adds or increments a class edge and records the target dependency in the source class metrics. + * + * @param fromClass the fully qualified source class name + * @param toClass the fully qualified target class name + */ @Override public void addClassDependency(String fromClass, String toClass) { - if (!classGraph.containsVertex(fromClass)) { - classGraph.addVertex(fromClass); + VertexInfo fromVertex = VertexInfo.forName(fromClass); + VertexInfo toVertex = VertexInfo.forName(toClass); + if (!classGraph.containsVertex(fromVertex)) { + classGraph.addVertex(fromVertex); } - if (!classGraph.containsVertex(toClass)) { - classGraph.addVertex(toClass); + if (!classGraph.containsVertex(toVertex)) { + classGraph.addVertex(toVertex); } - DefaultWeightedEdge edge = classGraph.getEdge(fromClass, toClass); + DefaultWeightedEdge edge = classGraph.getEdge(fromVertex, toVertex); if (edge == null) { - edge = classGraph.addEdge(fromClass, toClass); + edge = classGraph.addEdge(fromVertex, toVertex); if (edge != null) { classGraph.setEdgeWeight(edge, 1.0); } @@ -58,18 +73,27 @@ public void addClassDependency(String fromClass, String toClass) { getOrCreateClassMetrics(fromClass).addDependency(toClass); } + /** + * Adds or increments a dependency between the supplied package names. + * + * @param fromPackage the source package name + * @param toPackage the target package name + * @return the newly created or existing package edge + */ @Override public DefaultWeightedEdge addPackageDependency(String fromPackage, String toPackage) { - if (!packageGraph.containsVertex(fromPackage)) { - packageGraph.addVertex(fromPackage); + VertexInfo fromPackageVertex = VertexInfo.forName(fromPackage); + VertexInfo toPackageVertex = VertexInfo.forName(toPackage); + if (!packageGraph.containsVertex(fromPackageVertex)) { + packageGraph.addVertex(fromPackageVertex); } - if (!packageGraph.containsVertex(toPackage)) { - packageGraph.addVertex(toPackage); + if (!packageGraph.containsVertex(toPackageVertex)) { + packageGraph.addVertex(toPackageVertex); } - DefaultWeightedEdge edge = packageGraph.getEdge(fromPackage, toPackage); + DefaultWeightedEdge edge = packageGraph.getEdge(fromPackageVertex, toPackageVertex); if (edge == null) { - edge = packageGraph.addEdge(fromPackage, toPackage); + edge = packageGraph.addEdge(fromPackageVertex, toPackageVertex); if (edge != null) { packageGraph.setEdgeWeight(edge, 1.0); } @@ -86,20 +110,36 @@ public void recordClassLocation(String classFqn, String sourceFilePath) { classToSourceFileMapping.put(classFqn, sourceFilePath); } + /** + * Ensures a package vertex exists even when no dependency edge has been recorded for it. + * + * @param packageName the package to register + */ @Override public void registerPackage(String packageName) { - if (!packageGraph.containsVertex(packageName)) { - packageGraph.addVertex(packageName); + VertexInfo packageVertex = VertexInfo.forName(packageName); + if (!packageGraph.containsVertex(packageVertex)) { + packageGraph.addVertex(packageVertex); } } + /** + * Ensures a class vertex exists independently of its dependency edges. + * + * @param classFqn the fully qualified class name + */ @Override public void registerClassVertex(String classFqn) { - classGraph.addVertex(classFqn); + classGraph.addVertex(VertexInfo.forName(classFqn)); } + /** + * Returns the names of all vertices currently present in the package graph. + * + * @return a set of package names + */ public Set getPackagesInCodebase() { - return packageGraph.vertexSet(); + return packageGraph.vertexSet().stream().map(VertexInfo::getName).collect(java.util.stream.Collectors.toSet()); } public void recordClassMetric(String className, String metricName, Object value) { diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/CompositeGraphBuilderJavaOnlyTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/CompositeGraphBuilderJavaOnlyTest.java index 175dc062..e448a713 100644 --- a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/CompositeGraphBuilderJavaOnlyTest.java +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/CompositeGraphBuilderJavaOnlyTest.java @@ -83,6 +83,7 @@ void analyzeKotlinSwitchHasBeenRemoved() { Arrays.stream(methods).map(Method::getName).toArray())); } + /** Verifies that the default configuration includes Kotlin declarations and their source mappings. */ @DisplayName("Default config parses Kotlin sources unconditionally (.kt entries appear)") @Test void defaultConfigParsesKotlinSources() throws IOException { @@ -95,12 +96,12 @@ void defaultConfigParsesKotlinSources() throws IOException { CompositeGraphBuilder compositeGraphBuilder = new CompositeGraphBuilder(); CodebaseGraphDTO dto = compositeGraphBuilder.getCodebaseGraphDTO(srcDirectory.getAbsolutePath(), config); - Graph classGraph = dto.getClassReferencesGraph(); + Graph classGraph = dto.getClassReferencesGraph(); assertNotNull(classGraph); // KConsumer is declared only in a .kt file; its presence proves Kotlin parsing ran. assertTrue( - classGraph.containsVertex("com.ideacrest.parser.mixedclasses.KConsumer"), + classGraph.containsVertex(VertexInfo.forName("com.ideacrest.parser.mixedclasses.KConsumer")), "KConsumer must be a vertex when Kotlin analysis runs unconditionally, vertices: " + classGraph.vertexSet()); diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/CompositeGraphBuilderReconciliationTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/CompositeGraphBuilderReconciliationTest.java index 278ceefa..1444d235 100644 --- a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/CompositeGraphBuilderReconciliationTest.java +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/CompositeGraphBuilderReconciliationTest.java @@ -22,24 +22,30 @@ void reconcileUnattributedVertices_uniqueMatch_redirectsAndSumsWeights() { // - Fabricated Java reference: com.other.pkg.GameSettings (no source mapping) // - Caller: com.other.pkg.SomeClass -> fabricated GameSettings (weight 3) // - Another caller: com.third.pkg.OtherClass -> canonical GameSettings (weight 2) - Graph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - classGraph.addVertex("com.almasb.fxgl.app.GameSettings"); // canonical - classGraph.addVertex("com.other.pkg.GameSettings"); // fabricated - classGraph.addVertex("com.other.pkg.SomeClass"); - classGraph.addVertex("com.third.pkg.OtherClass"); + Graph classGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + classGraph.addVertex(VertexInfo.forName("com.almasb.fxgl.app.GameSettings")); // canonical + classGraph.addVertex(VertexInfo.forName("com.other.pkg.GameSettings")); // fabricated + classGraph.addVertex(VertexInfo.forName("com.other.pkg.SomeClass")); + classGraph.addVertex(VertexInfo.forName("com.third.pkg.OtherClass")); - DefaultWeightedEdge e1 = classGraph.addEdge("com.other.pkg.SomeClass", "com.other.pkg.GameSettings"); + DefaultWeightedEdge e1 = classGraph.addEdge( + VertexInfo.forName("com.other.pkg.SomeClass"), VertexInfo.forName("com.other.pkg.GameSettings")); classGraph.setEdgeWeight(e1, 3); - DefaultWeightedEdge e2 = classGraph.addEdge("com.third.pkg.OtherClass", "com.almasb.fxgl.app.GameSettings"); + DefaultWeightedEdge e2 = classGraph.addEdge( + VertexInfo.forName("com.third.pkg.OtherClass"), VertexInfo.forName("com.almasb.fxgl.app.GameSettings")); classGraph.setEdgeWeight(e2, 2); // Package graph - Graph packageGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - packageGraph.addVertex("com.other.pkg"); - packageGraph.addVertex("com.third.pkg"); - packageGraph.addVertex("com.almasb.fxgl.app"); - packageGraph.addEdge("com.other.pkg", "com.other.pkg"); // self-edge from fabricated (ignored by collector) - packageGraph.addEdge("com.third.pkg", "com.almasb.fxgl.app"); + Graph packageGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + packageGraph.addVertex(VertexInfo.forName("com.other.pkg")); + packageGraph.addVertex(VertexInfo.forName("com.third.pkg")); + packageGraph.addVertex(VertexInfo.forName("com.almasb.fxgl.app")); + packageGraph.addEdge( + VertexInfo.forName("com.other.pkg"), + VertexInfo.forName("com.other.pkg")); // self-edge from fabricated (ignored by collector) + packageGraph.addEdge(VertexInfo.forName("com.third.pkg"), VertexInfo.forName("com.almasb.fxgl.app")); // Source path mapping has the canonical FQN AND the caller classes (they're in the codebase) Map sourcePathMapping = new HashMap<>(); @@ -54,37 +60,46 @@ void reconcileUnattributedVertices_uniqueMatch_redirectsAndSumsWeights() { // Assertions // 1. Fabricated vertex should be removed assertFalse( - classGraph.containsVertex("com.other.pkg.GameSettings"), + classGraph.containsVertex(VertexInfo.forName("com.other.pkg.GameSettings")), "Fabricated vertex should be removed after reconciliation"); // 2. Canonical vertex should still exist - assertTrue(classGraph.containsVertex("com.almasb.fxgl.app.GameSettings"), "Canonical vertex should remain"); + assertTrue( + classGraph.containsVertex(VertexInfo.forName("com.almasb.fxgl.app.GameSettings")), + "Canonical vertex should remain"); // 3. Edge from SomeClass should now point to canonical with its original weight assertTrue( - classGraph.containsEdge("com.other.pkg.SomeClass", "com.almasb.fxgl.app.GameSettings"), + classGraph.containsEdge( + VertexInfo.forName("com.other.pkg.SomeClass"), + VertexInfo.forName("com.almasb.fxgl.app.GameSettings")), "Edge should be redirected to canonical vertex"); - DefaultWeightedEdge redirectedEdge = - classGraph.getEdge("com.other.pkg.SomeClass", "com.almasb.fxgl.app.GameSettings"); + DefaultWeightedEdge redirectedEdge = classGraph.getEdge( + VertexInfo.forName("com.other.pkg.SomeClass"), VertexInfo.forName("com.almasb.fxgl.app.GameSettings")); assertEquals( 3.0, classGraph.getEdgeWeight(redirectedEdge), "Edge weight should be preserved from fabricated edge"); // 4. The pre-existing edge from OtherClass to canonical should remain with its weight assertTrue( - classGraph.containsEdge("com.third.pkg.OtherClass", "com.almasb.fxgl.app.GameSettings"), + classGraph.containsEdge( + VertexInfo.forName("com.third.pkg.OtherClass"), + VertexInfo.forName("com.almasb.fxgl.app.GameSettings")), "Pre-existing edge to canonical should remain"); - DefaultWeightedEdge existingEdge = - classGraph.getEdge("com.third.pkg.OtherClass", "com.almasb.fxgl.app.GameSettings"); + DefaultWeightedEdge existingEdge = classGraph.getEdge( + VertexInfo.forName("com.third.pkg.OtherClass"), VertexInfo.forName("com.almasb.fxgl.app.GameSettings")); assertEquals(2.0, classGraph.getEdgeWeight(existingEdge), "Pre-existing edge weight should be preserved"); // 5. Original edge to fabricated should be gone assertFalse( - classGraph.containsEdge("com.other.pkg.SomeClass", "com.other.pkg.GameSettings"), + classGraph.containsEdge( + VertexInfo.forName("com.other.pkg.SomeClass"), + VertexInfo.forName("com.other.pkg.GameSettings")), "Original edge to fabricated vertex should be removed"); // 6. Package graph should have the real cross-package edge assertTrue( - packageGraph.containsEdge("com.other.pkg", "com.almasb.fxgl.app"), + packageGraph.containsEdge( + VertexInfo.forName("com.other.pkg"), VertexInfo.forName("com.almasb.fxgl.app")), "Package graph should have the real cross-package edge"); } @@ -94,18 +109,22 @@ void reconcileUnattributedVertices_uniqueMatch_redirectsAndSumsWeights() { */ @Test void reconcileUnattributedVertices_sameSourceMultipleEdges_sumsWeights() { - Graph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - classGraph.addVertex("com.almasb.fxgl.app.GameSettings"); // canonical - classGraph.addVertex("com.other.pkg.GameSettings"); // fabricated - classGraph.addVertex("com.other.pkg.SomeClass"); + Graph classGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + classGraph.addVertex(VertexInfo.forName("com.almasb.fxgl.app.GameSettings")); // canonical + classGraph.addVertex(VertexInfo.forName("com.other.pkg.GameSettings")); // fabricated + classGraph.addVertex(VertexInfo.forName("com.other.pkg.SomeClass")); // Two edges from the SAME source: one to fabricated, one to canonical - DefaultWeightedEdge e1 = classGraph.addEdge("com.other.pkg.SomeClass", "com.other.pkg.GameSettings"); + DefaultWeightedEdge e1 = classGraph.addEdge( + VertexInfo.forName("com.other.pkg.SomeClass"), VertexInfo.forName("com.other.pkg.GameSettings")); classGraph.setEdgeWeight(e1, 3); - DefaultWeightedEdge e2 = classGraph.addEdge("com.other.pkg.SomeClass", "com.almasb.fxgl.app.GameSettings"); + DefaultWeightedEdge e2 = classGraph.addEdge( + VertexInfo.forName("com.other.pkg.SomeClass"), VertexInfo.forName("com.almasb.fxgl.app.GameSettings")); classGraph.setEdgeWeight(e2, 2); - Graph packageGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + Graph packageGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); Map sourcePathMapping = new HashMap<>(); sourcePathMapping.put( @@ -115,12 +134,13 @@ void reconcileUnattributedVertices_sameSourceMultipleEdges_sumsWeights() { CompositeGraphBuilder.reconcileUnattributedVertices(classGraph, packageGraph, sourcePathMapping); // Fabricated vertex should be removed - assertFalse(classGraph.containsVertex("com.other.pkg.GameSettings")); + assertFalse(classGraph.containsVertex(VertexInfo.forName("com.other.pkg.GameSettings"))); // The two edges from SomeClass should be merged into one with summed weight (3 + 2 = 5) - assertTrue(classGraph.containsEdge("com.other.pkg.SomeClass", "com.almasb.fxgl.app.GameSettings")); - DefaultWeightedEdge mergedEdge = - classGraph.getEdge("com.other.pkg.SomeClass", "com.almasb.fxgl.app.GameSettings"); + assertTrue(classGraph.containsEdge( + VertexInfo.forName("com.other.pkg.SomeClass"), VertexInfo.forName("com.almasb.fxgl.app.GameSettings"))); + DefaultWeightedEdge mergedEdge = classGraph.getEdge( + VertexInfo.forName("com.other.pkg.SomeClass"), VertexInfo.forName("com.almasb.fxgl.app.GameSettings")); assertEquals(5.0, classGraph.getEdgeWeight(mergedEdge), "Edge weights from same source should be summed"); } @@ -130,14 +150,16 @@ void reconcileUnattributedVertices_sameSourceMultipleEdges_sumsWeights() { */ @Test void reconcileUnattributedVertices_ambiguousMatch_leavesVertexUntouched() { - Graph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - classGraph.addVertex("com.pkg1.Node"); // canonical 1 - classGraph.addVertex("com.pkg2.Node"); // canonical 2 - classGraph.addVertex("com.caller.Node"); // fabricated - SAME simple name "Node" - classGraph.addVertex("com.caller.Caller"); + Graph classGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + classGraph.addVertex(VertexInfo.forName("com.pkg1.Node")); // canonical 1 + classGraph.addVertex(VertexInfo.forName("com.pkg2.Node")); // canonical 2 + classGraph.addVertex(VertexInfo.forName("com.caller.Node")); // fabricated - SAME simple name "Node" + classGraph.addVertex(VertexInfo.forName("com.caller.Caller")); - classGraph.addEdge("com.caller.Caller", "com.caller.Node"); - classGraph.setEdgeWeight(classGraph.getEdge("com.caller.Caller", "com.caller.Node"), 1); + classGraph.addEdge(VertexInfo.forName("com.caller.Caller"), VertexInfo.forName("com.caller.Node")); + classGraph.setEdgeWeight( + classGraph.getEdge(VertexInfo.forName("com.caller.Caller"), VertexInfo.forName("com.caller.Node")), 1); Map sourcePathMapping = new HashMap<>(); sourcePathMapping.put("com.pkg1.Node", "/src/main/java/com/pkg1/Node.java"); @@ -148,7 +170,7 @@ void reconcileUnattributedVertices_ambiguousMatch_leavesVertexUntouched() { // Fabricated vertex should remain (ambiguous - multiple candidates with same simple name) assertTrue( - classGraph.containsVertex("com.caller.Node"), + classGraph.containsVertex(VertexInfo.forName("com.caller.Node")), "Fabricated vertex should remain when simple name is ambiguous"); } @@ -162,11 +184,14 @@ void reconcileUnattributedVertices_ambiguousMatch_leavesVertexUntouched() { */ @Test void reconcileUnattributedVertices_zeroMatch_removesExternalClass() { - Graph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - classGraph.addVertex("com.myapp.MyClass"); - classGraph.addVertex("com.myapp.Button"); // fabricated - external JavaFX class attributed to caller's package - classGraph.addEdge("com.myapp.MyClass", "com.myapp.Button"); - classGraph.setEdgeWeight(classGraph.getEdge("com.myapp.MyClass", "com.myapp.Button"), 1); + Graph classGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + classGraph.addVertex(VertexInfo.forName("com.myapp.MyClass")); + classGraph.addVertex(VertexInfo.forName( + "com.myapp.Button")); // fabricated - external JavaFX class attributed to caller's package + classGraph.addEdge(VertexInfo.forName("com.myapp.MyClass"), VertexInfo.forName("com.myapp.Button")); + classGraph.setEdgeWeight( + classGraph.getEdge(VertexInfo.forName("com.myapp.MyClass"), VertexInfo.forName("com.myapp.Button")), 1); Map sourcePathMapping = new HashMap<>(); sourcePathMapping.put("com.myapp.MyClass", "/src/main/java/com/myapp/MyClass.java"); @@ -176,12 +201,13 @@ void reconcileUnattributedVertices_zeroMatch_removesExternalClass() { // Fabricated external class should be REMOVED (not left untouched) assertFalse( - classGraph.containsVertex("com.myapp.Button"), + classGraph.containsVertex(VertexInfo.forName("com.myapp.Button")), "External class vertex should be removed when no real declaration exists"); // Edge to external class should also be removed assertFalse( - classGraph.containsEdge("com.myapp.MyClass", "com.myapp.Button"), + classGraph.containsEdge( + VertexInfo.forName("com.myapp.MyClass"), VertexInfo.forName("com.myapp.Button")), "Edge to external class should be removed"); } @@ -191,11 +217,15 @@ void reconcileUnattributedVertices_zeroMatch_removesExternalClass() { */ @Test void reconcileUnattributedVertices_zeroMatch_realExternalPackage_removesExternalClass() { - Graph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - classGraph.addVertex("com.myapp.MyClass"); - classGraph.addVertex("javafx.scene.control.Button"); // external class with real package - classGraph.addEdge("com.myapp.MyClass", "javafx.scene.control.Button"); - classGraph.setEdgeWeight(classGraph.getEdge("com.myapp.MyClass", "javafx.scene.control.Button"), 1); + Graph classGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + classGraph.addVertex(VertexInfo.forName("com.myapp.MyClass")); + classGraph.addVertex(VertexInfo.forName("javafx.scene.control.Button")); // external class with real package + classGraph.addEdge(VertexInfo.forName("com.myapp.MyClass"), VertexInfo.forName("javafx.scene.control.Button")); + classGraph.setEdgeWeight( + classGraph.getEdge( + VertexInfo.forName("com.myapp.MyClass"), VertexInfo.forName("javafx.scene.control.Button")), + 1); Map sourcePathMapping = new HashMap<>(); sourcePathMapping.put("com.myapp.MyClass", "/src/main/java/com/myapp/MyClass.java"); @@ -205,7 +235,7 @@ void reconcileUnattributedVertices_zeroMatch_realExternalPackage_removesExternal // External class with real package should also be removed assertFalse( - classGraph.containsVertex("javafx.scene.control.Button"), + classGraph.containsVertex(VertexInfo.forName("javafx.scene.control.Button")), "External class with real package should be removed when no real declaration exists"); } @@ -215,14 +245,19 @@ void reconcileUnattributedVertices_zeroMatch_realExternalPackage_removesExternal */ @Test void reconcileUnattributedVertices_noPackageMatch_leavesAmbiguousUntouched() { - Graph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - classGraph.addVertex("com.pkg1.Node"); // canonical 1 - classGraph.addVertex("com.pkg2.Node"); // canonical 2 - classGraph.addVertex("com.unrelated.Node"); // fabricated - same simple name "Node", NO package match - classGraph.addVertex("com.unrelated.Caller"); - - classGraph.addEdge("com.unrelated.Caller", "com.unrelated.Node"); - classGraph.setEdgeWeight(classGraph.getEdge("com.unrelated.Caller", "com.unrelated.Node"), 1); + Graph classGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + classGraph.addVertex(VertexInfo.forName("com.pkg1.Node")); // canonical 1 + classGraph.addVertex(VertexInfo.forName("com.pkg2.Node")); // canonical 2 + classGraph.addVertex( + VertexInfo.forName("com.unrelated.Node")); // fabricated - same simple name "Node", NO package match + classGraph.addVertex(VertexInfo.forName("com.unrelated.Caller")); + + classGraph.addEdge(VertexInfo.forName("com.unrelated.Caller"), VertexInfo.forName("com.unrelated.Node")); + classGraph.setEdgeWeight( + classGraph.getEdge( + VertexInfo.forName("com.unrelated.Caller"), VertexInfo.forName("com.unrelated.Node")), + 1); Map sourcePathMapping = new HashMap<>(); sourcePathMapping.put("com.pkg1.Node", "/src/main/java/com/pkg1/Node.java"); @@ -233,7 +268,7 @@ void reconcileUnattributedVertices_noPackageMatch_leavesAmbiguousUntouched() { // Fabricated vertex should remain (no package match, ambiguous) assertTrue( - classGraph.containsVertex("com.unrelated.Node"), + classGraph.containsVertex(VertexInfo.forName("com.unrelated.Node")), "Fabricated vertex should remain when no package match exists"); } @@ -253,17 +288,20 @@ void reconcileUnattributedVertices_incomingEdgeFromCanonical_guardsAgainstSelfLo // - Canonical Kotlin class: b.Bar (has source mapping) // - Fabricated Java reference: a.Bar (no source mapping) // - Caller: b.Bar -> fabricated a.Bar (weight 3) - Graph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - classGraph.addVertex("b.Bar"); // canonical - classGraph.addVertex("a.Bar"); // fabricated - classGraph.addVertex("b.Bar"); // same as canonical, but we add edge from canonical to fabricated + Graph classGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + classGraph.addVertex(VertexInfo.forName("b.Bar")); // canonical + classGraph.addVertex(VertexInfo.forName("a.Bar")); // fabricated + classGraph.addVertex( + VertexInfo.forName("b.Bar")); // same as canonical, but we add edge from canonical to fabricated - DefaultWeightedEdge e1 = classGraph.addEdge("b.Bar", "a.Bar"); + DefaultWeightedEdge e1 = classGraph.addEdge(VertexInfo.forName("b.Bar"), VertexInfo.forName("a.Bar")); classGraph.setEdgeWeight(e1, 3); - Graph packageGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - packageGraph.addVertex("a"); - packageGraph.addVertex("b"); + Graph packageGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + packageGraph.addVertex(VertexInfo.forName("a")); + packageGraph.addVertex(VertexInfo.forName("b")); Map sourcePathMapping = new HashMap<>(); sourcePathMapping.put("b.Bar", "/src/main/kotlin/b/Bar.kt"); @@ -271,14 +309,14 @@ void reconcileUnattributedVertices_incomingEdgeFromCanonical_guardsAgainstSelfLo CompositeGraphBuilder.reconcileUnattributedVertices(classGraph, packageGraph, sourcePathMapping); // Fabricated vertex should be removed - assertFalse(classGraph.containsVertex("a.Bar"), "Fabricated vertex should be removed"); + assertFalse(classGraph.containsVertex(VertexInfo.forName("a.Bar")), "Fabricated vertex should be removed"); // Canonical vertex should exist - assertTrue(classGraph.containsVertex("b.Bar"), "Canonical vertex should remain"); + assertTrue(classGraph.containsVertex(VertexInfo.forName("b.Bar")), "Canonical vertex should remain"); // NO self-loop should exist on canonical vertex assertFalse( - classGraph.containsEdge("b.Bar", "b.Bar"), + classGraph.containsEdge(VertexInfo.forName("b.Bar"), VertexInfo.forName("b.Bar")), "Self-loop should NOT be created when incoming edge source equals canonical vertex"); } @@ -292,16 +330,18 @@ void reconcileUnattributedVertices_incomingEdgeFromCanonical_guardsAgainstSelfLo */ @Test void reconcileUnattributedVertices_outgoingEdgeToCanonical_guardsAgainstSelfLoop() { - Graph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - classGraph.addVertex("b.Bar"); // canonical - classGraph.addVertex("a.Bar"); // fabricated + Graph classGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + classGraph.addVertex(VertexInfo.forName("b.Bar")); // canonical + classGraph.addVertex(VertexInfo.forName("a.Bar")); // fabricated - DefaultWeightedEdge e1 = classGraph.addEdge("a.Bar", "b.Bar"); + DefaultWeightedEdge e1 = classGraph.addEdge(VertexInfo.forName("a.Bar"), VertexInfo.forName("b.Bar")); classGraph.setEdgeWeight(e1, 3); - Graph packageGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - packageGraph.addVertex("a"); - packageGraph.addVertex("b"); + Graph packageGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + packageGraph.addVertex(VertexInfo.forName("a")); + packageGraph.addVertex(VertexInfo.forName("b")); Map sourcePathMapping = new HashMap<>(); sourcePathMapping.put("b.Bar", "/src/main/kotlin/b/Bar.kt"); @@ -309,14 +349,14 @@ void reconcileUnattributedVertices_outgoingEdgeToCanonical_guardsAgainstSelfLoop CompositeGraphBuilder.reconcileUnattributedVertices(classGraph, packageGraph, sourcePathMapping); // Fabricated vertex should be removed - assertFalse(classGraph.containsVertex("a.Bar"), "Fabricated vertex should be removed"); + assertFalse(classGraph.containsVertex(VertexInfo.forName("a.Bar")), "Fabricated vertex should be removed"); // Canonical vertex should exist - assertTrue(classGraph.containsVertex("b.Bar"), "Canonical vertex should remain"); + assertTrue(classGraph.containsVertex(VertexInfo.forName("b.Bar")), "Canonical vertex should remain"); // NO self-loop should exist on canonical vertex assertFalse( - classGraph.containsEdge("b.Bar", "b.Bar"), + classGraph.containsEdge(VertexInfo.forName("b.Bar"), VertexInfo.forName("b.Bar")), "Self-loop should NOT be created when outgoing edge target equals canonical vertex"); } @@ -343,14 +383,17 @@ void reconcileUnattributedVertices_outgoingEdgeToCanonical_guardsAgainstSelfLoop */ @Test void reconcileUnattributedVertices_realDeclarationWithDuplicateSimpleName_neverContracted() { - Graph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - classGraph.addVertex("com.pkg1.Target"); // real declaration (in mapping) - classGraph.addVertex("com.pkg2.Target"); // same-named real declaration (in mapping) - classGraph.addVertex("com.pkg1.Caller"); // real caller (in mapping) + Graph classGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + classGraph.addVertex(VertexInfo.forName("com.pkg1.Target")); // real declaration (in mapping) + classGraph.addVertex(VertexInfo.forName("com.pkg2.Target")); // same-named real declaration (in mapping) + classGraph.addVertex(VertexInfo.forName("com.pkg1.Caller")); // real caller (in mapping) - DefaultWeightedEdge e1 = classGraph.addEdge("com.pkg1.Caller", "com.pkg1.Target"); + DefaultWeightedEdge e1 = + classGraph.addEdge(VertexInfo.forName("com.pkg1.Caller"), VertexInfo.forName("com.pkg1.Target")); classGraph.setEdgeWeight(e1, 2); - DefaultWeightedEdge e2 = classGraph.addEdge("com.pkg1.Target", "com.pkg2.Target"); + DefaultWeightedEdge e2 = + classGraph.addEdge(VertexInfo.forName("com.pkg1.Target"), VertexInfo.forName("com.pkg2.Target")); classGraph.setEdgeWeight(e2, 3); Map sourcePathMapping = new HashMap<>(); @@ -362,30 +405,38 @@ void reconcileUnattributedVertices_realDeclarationWithDuplicateSimpleName_neverC classGraph, new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class), sourcePathMapping); // Both real declarations must remain despite the shared simple name - assertTrue(classGraph.containsVertex("com.pkg1.Target"), "Real declaration com.pkg1.Target must remain"); - assertTrue(classGraph.containsVertex("com.pkg2.Target"), "Real declaration com.pkg2.Target must remain"); + assertTrue( + classGraph.containsVertex(VertexInfo.forName("com.pkg1.Target")), + "Real declaration com.pkg1.Target must remain"); + assertTrue( + classGraph.containsVertex(VertexInfo.forName("com.pkg2.Target")), + "Real declaration com.pkg2.Target must remain"); assertEquals(3, classGraph.vertexSet().size(), "No vertex may be contracted or removed"); // Every edge intact with its original weight — nothing redirected - assertTrue(classGraph.containsEdge("com.pkg1.Caller", "com.pkg1.Target")); + assertTrue( + classGraph.containsEdge(VertexInfo.forName("com.pkg1.Caller"), VertexInfo.forName("com.pkg1.Target"))); assertEquals( 2.0, - classGraph.getEdgeWeight(classGraph.getEdge("com.pkg1.Caller", "com.pkg1.Target")), + classGraph.getEdgeWeight(classGraph.getEdge( + VertexInfo.forName("com.pkg1.Caller"), VertexInfo.forName("com.pkg1.Target"))), "Edge to real declaration must keep its weight"); - assertTrue(classGraph.containsEdge("com.pkg1.Target", "com.pkg2.Target")); + assertTrue( + classGraph.containsEdge(VertexInfo.forName("com.pkg1.Target"), VertexInfo.forName("com.pkg2.Target"))); assertEquals( 3.0, - classGraph.getEdgeWeight(classGraph.getEdge("com.pkg1.Target", "com.pkg2.Target")), + classGraph.getEdgeWeight(classGraph.getEdge( + VertexInfo.forName("com.pkg1.Target"), VertexInfo.forName("com.pkg2.Target"))), "Edge between real declarations must keep its weight"); // Contraction would have collapsed com.pkg1.Target into com.pkg2.Target, // turning its incoming edges into edges on com.pkg2.Target (and its // outgoing edge into a self-loop). Neither may exist. assertFalse( - classGraph.containsEdge("com.pkg2.Target", "com.pkg2.Target"), + classGraph.containsEdge(VertexInfo.forName("com.pkg2.Target"), VertexInfo.forName("com.pkg2.Target")), "No self-loop may appear from contracting a real declaration into its same-named sibling"); assertFalse( - classGraph.containsEdge("com.pkg1.Caller", "com.pkg2.Target"), + classGraph.containsEdge(VertexInfo.forName("com.pkg1.Caller"), VertexInfo.forName("com.pkg2.Target")), "Caller edge must not be redirected to the other same-named class"); } @@ -402,13 +453,17 @@ void reconcileUnattributedVertices_realDeclarationWithDuplicateSimpleName_neverC */ @Test void reconcileUnattributedVertices_javaAnonymousInnerClass_preservedViaEnclosingSourcePath() { - Graph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - classGraph.addVertex("com.example.Outer"); // real declaration - classGraph.addVertex("com.example.Outer$1"); // anonymous inner class (no source mapping initially) - classGraph.addVertex("com.example.Caller"); // real caller + Graph classGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + classGraph.addVertex(VertexInfo.forName("com.example.Outer")); // real declaration + classGraph.addVertex( + VertexInfo.forName("com.example.Outer$1")); // anonymous inner class (no source mapping initially) + classGraph.addVertex(VertexInfo.forName("com.example.Caller")); // real caller - classGraph.addEdge("com.example.Caller", "com.example.Outer"); - classGraph.addEdge("com.example.Outer", "com.example.Outer$1"); // Outer depends on its anonymous class + classGraph.addEdge(VertexInfo.forName("com.example.Caller"), VertexInfo.forName("com.example.Outer")); + classGraph.addEdge( + VertexInfo.forName("com.example.Outer"), + VertexInfo.forName("com.example.Outer$1")); // Outer depends on its anonymous class Map sourcePathMapping = new HashMap<>(); sourcePathMapping.put("com.example.Outer", "/src/main/java/com/example/Outer.java"); @@ -422,12 +477,13 @@ void reconcileUnattributedVertices_javaAnonymousInnerClass_preservedViaEnclosing // Anonymous class should be preserved (mapped to enclosing class's source path) assertTrue( - classGraph.containsVertex("com.example.Outer$1"), + classGraph.containsVertex(VertexInfo.forName("com.example.Outer$1")), "Anonymous inner class should be preserved via enclosing class source mapping"); // Edge from Outer to anonymous should remain assertTrue( - classGraph.containsEdge("com.example.Outer", "com.example.Outer$1"), + classGraph.containsEdge( + VertexInfo.forName("com.example.Outer"), VertexInfo.forName("com.example.Outer$1")), "Edge to anonymous inner class should remain"); } @@ -437,9 +493,10 @@ void reconcileUnattributedVertices_javaAnonymousInnerClass_preservedViaEnclosing */ @Test void reconcileUnattributedVertices_javaSyntheticClass_preservedViaEnclosingSourcePath() { - Graph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - classGraph.addVertex("com.example.Outer"); - classGraph.addVertex("com.example.Outer$"); // synthetic class + Graph classGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + classGraph.addVertex(VertexInfo.forName("com.example.Outer")); + classGraph.addVertex(VertexInfo.forName("com.example.Outer$")); // synthetic class Map sourcePathMapping = new HashMap<>(); sourcePathMapping.put("com.example.Outer", "/src/main/java/com/example/Outer.java"); @@ -450,7 +507,7 @@ void reconcileUnattributedVertices_javaSyntheticClass_preservedViaEnclosingSourc classGraph, new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class), sourcePathMapping); assertTrue( - classGraph.containsVertex("com.example.Outer$"), + classGraph.containsVertex(VertexInfo.forName("com.example.Outer$")), "Synthetic class should be preserved via enclosing class source mapping"); } @@ -460,9 +517,10 @@ void reconcileUnattributedVertices_javaSyntheticClass_preservedViaEnclosingSourc */ @Test void reconcileUnattributedVertices_kotlinAnonymousObject_preservedViaEnclosingSourcePath() { - Graph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - classGraph.addVertex("com.example.Outer"); - classGraph.addVertex("com.example.Outer."); // Kotlin anonymous object + Graph classGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + classGraph.addVertex(VertexInfo.forName("com.example.Outer")); + classGraph.addVertex(VertexInfo.forName("com.example.Outer.")); // Kotlin anonymous object Map sourcePathMapping = new HashMap<>(); sourcePathMapping.put("com.example.Outer", "/src/main/kotlin/com/example/Outer.kt"); @@ -473,7 +531,7 @@ void reconcileUnattributedVertices_kotlinAnonymousObject_preservedViaEnclosingSo classGraph, new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class), sourcePathMapping); assertTrue( - classGraph.containsVertex("com.example.Outer."), + classGraph.containsVertex(VertexInfo.forName("com.example.Outer.")), "Kotlin anonymous object should be preserved via enclosing class source mapping"); } @@ -484,8 +542,9 @@ void reconcileUnattributedVertices_kotlinAnonymousObject_preservedViaEnclosingSo */ @Test void reconcileUnattributedVertices_topLevelKotlinAnonymous_noEnclosingClass_notMapped() { - Graph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - classGraph.addVertex(""); // Top-level Kotlin anonymous, no enclosing class + Graph classGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + classGraph.addVertex(VertexInfo.forName("")); // Top-level Kotlin anonymous, no enclosing class Map sourcePathMapping = new HashMap<>(); // No enclosing class in mapping @@ -498,7 +557,7 @@ void reconcileUnattributedVertices_topLevelKotlinAnonymous_noEnclosingClass_notM // Top-level has no enclosing class to map to, so it's not in source mapping // and would be removed as external (zero match) assertFalse( - classGraph.containsVertex(""), + classGraph.containsVertex(VertexInfo.forName("")), "Top-level Kotlin anonymous with no enclosing class should be removed as external"); } @@ -519,16 +578,16 @@ void mergeClassRelationships_usesMergedGraphEdges() { CodebaseGraphDTO merged = CompositeGraphBuilder.merge(javaDto, kotlinDto); // Verify merged package graph has both package vertices and the cross-package edge - Graph mergedPkgGraph = merged.getPackageReferencesGraph(); - assertTrue(mergedPkgGraph.containsVertex("com.java")); - assertTrue(mergedPkgGraph.containsVertex("com.kotlin")); + Graph mergedPkgGraph = merged.getPackageReferencesGraph(); + assertTrue(mergedPkgGraph.containsVertex(VertexInfo.forName("com.java"))); + assertTrue(mergedPkgGraph.containsVertex(VertexInfo.forName("com.kotlin"))); // Verify merged class graph has all four class vertices - Graph mergedClassGraph = merged.getClassReferencesGraph(); - assertTrue(mergedClassGraph.containsVertex("com.java.A")); - assertTrue(mergedClassGraph.containsVertex("com.java.B")); - assertTrue(mergedClassGraph.containsVertex("com.kotlin.C")); - assertTrue(mergedClassGraph.containsVertex("com.kotlin.D")); + Graph mergedClassGraph = merged.getClassReferencesGraph(); + assertTrue(mergedClassGraph.containsVertex(VertexInfo.forName("com.java.A"))); + assertTrue(mergedClassGraph.containsVertex(VertexInfo.forName("com.java.B"))); + assertTrue(mergedClassGraph.containsVertex(VertexInfo.forName("com.kotlin.C"))); + assertTrue(mergedClassGraph.containsVertex(VertexInfo.forName("com.kotlin.D"))); // Verify class relationship map keys are edges from MERGED package graph Map> relMap = merged.getClassRelationshipsInPackageRelationship(); @@ -563,28 +622,36 @@ void mergeClassRelationships_preservesEndpoints() { CodebaseGraphDTO merged = CompositeGraphBuilder.merge(javaDto, kotlinDto); - Graph mergedClassGraph = merged.getClassReferencesGraph(); + Graph mergedClassGraph = merged.getClassReferencesGraph(); Map> relMap = merged.getClassRelationshipsInPackageRelationship(); // Verify Java relationship endpoints preserved - DefaultWeightedEdge javaPkgEdge = merged.getPackageReferencesGraph().getEdge("com.pkg1", "com.pkg2"); + DefaultWeightedEdge javaPkgEdge = merged.getPackageReferencesGraph() + .getEdge(VertexInfo.forName("com.pkg1"), VertexInfo.forName("com.pkg2")); assertNotNull(javaPkgEdge); Set javaClassEdges = relMap.get(javaPkgEdge); assertNotNull(javaClassEdges); assertEquals(1, javaClassEdges.size()); DefaultWeightedEdge javaClassEdge = javaClassEdges.iterator().next(); - assertEquals("com.pkg1.Source", mergedClassGraph.getEdgeSource(javaClassEdge)); - assertEquals("com.pkg2.Target", mergedClassGraph.getEdgeTarget(javaClassEdge)); + assertEquals( + "com.pkg1.Source", mergedClassGraph.getEdgeSource(javaClassEdge).getName()); + assertEquals( + "com.pkg2.Target", mergedClassGraph.getEdgeTarget(javaClassEdge).getName()); // Verify Kotlin relationship endpoints preserved - DefaultWeightedEdge kotlinPkgEdge = merged.getPackageReferencesGraph().getEdge("com.pkg3", "com.pkg4"); + DefaultWeightedEdge kotlinPkgEdge = merged.getPackageReferencesGraph() + .getEdge(VertexInfo.forName("com.pkg3"), VertexInfo.forName("com.pkg4")); assertNotNull(kotlinPkgEdge); Set kotlinClassEdges = relMap.get(kotlinPkgEdge); assertNotNull(kotlinClassEdges); assertEquals(1, kotlinClassEdges.size()); DefaultWeightedEdge kotlinClassEdge = kotlinClassEdges.iterator().next(); - assertEquals("com.pkg3.Source", mergedClassGraph.getEdgeSource(kotlinClassEdge)); - assertEquals("com.pkg4.Target", mergedClassGraph.getEdgeTarget(kotlinClassEdge)); + assertEquals( + "com.pkg3.Source", + mergedClassGraph.getEdgeSource(kotlinClassEdge).getName()); + assertEquals( + "com.pkg4.Target", + mergedClassGraph.getEdgeTarget(kotlinClassEdge).getName()); } /** @@ -601,11 +668,12 @@ void mergeClassRelationships_sumsWeightsForSameRelationship() { CodebaseGraphDTO merged = CompositeGraphBuilder.merge(javaDto, kotlinDto); - Graph mergedClassGraph = merged.getClassReferencesGraph(); - Graph mergedPkgGraph = merged.getPackageReferencesGraph(); + Graph mergedClassGraph = merged.getClassReferencesGraph(); + Graph mergedPkgGraph = merged.getPackageReferencesGraph(); // Class edge weight should be 2 + 3 = 5 - DefaultWeightedEdge mergedClassEdge = mergedClassGraph.getEdge("com.shared.A", "com.shared.B"); + DefaultWeightedEdge mergedClassEdge = + mergedClassGraph.getEdge(VertexInfo.forName("com.shared.A"), VertexInfo.forName("com.shared.B")); assertNotNull(mergedClassEdge); assertEquals(5.0, mergedClassGraph.getEdgeWeight(mergedClassEdge)); @@ -613,7 +681,8 @@ void mergeClassRelationships_sumsWeightsForSameRelationship() { // (addPackageEdgeIfCrossPackage only adds for cross-package, so self-edge not added by mergeGraph) // The package self-edge comes from the dependency collector, not mergeGraph. // In mergeGraph, self-edges from source graphs ARE copied. - DefaultWeightedEdge mergedPkgEdge = mergedPkgGraph.getEdge("com.shared", "com.shared"); + DefaultWeightedEdge mergedPkgEdge = + mergedPkgGraph.getEdge(VertexInfo.forName("com.shared"), VertexInfo.forName("com.shared")); assertNotNull(mergedPkgEdge, "mergeGraph must copy the intra-package self-edge"); assertEquals(5.0, mergedPkgGraph.getEdgeWeight(mergedPkgEdge)); } @@ -627,19 +696,22 @@ void mergeClassRelationships_reconciliationRebuildsRelationshipMap() { // Java DTO with a fabricated cross-language reference: // Java class com.java.Caller references Kotlin class com.kotlin.Target // but Java parser fabricates com.java.Target (wrong package) - Graph javaClassGraph = + Graph javaClassGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - javaClassGraph.addVertex("com.java.Caller"); - javaClassGraph.addVertex("com.java.Target"); // fabricated - DefaultWeightedEdge javaEdge = javaClassGraph.addEdge("com.java.Caller", "com.java.Target"); + javaClassGraph.addVertex(VertexInfo.forName("com.java.Caller")); + javaClassGraph.addVertex(VertexInfo.forName("com.java.Target")); // fabricated + DefaultWeightedEdge javaEdge = + javaClassGraph.addEdge(VertexInfo.forName("com.java.Caller"), VertexInfo.forName("com.java.Target")); javaClassGraph.setEdgeWeight(javaEdge, 2); - Graph javaPkgGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - javaPkgGraph.addVertex("com.java"); - javaPkgGraph.addEdge("com.java", "com.java"); // self-edge + Graph javaPkgGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + javaPkgGraph.addVertex(VertexInfo.forName("com.java")); + javaPkgGraph.addEdge(VertexInfo.forName("com.java"), VertexInfo.forName("com.java")); // self-edge Map> javaClassRels = new HashMap<>(); - javaClassRels.put(javaPkgGraph.getEdge("com.java", "com.java"), Set.of(javaEdge)); + javaClassRels.put( + javaPkgGraph.getEdge(VertexInfo.forName("com.java"), VertexInfo.forName("com.java")), Set.of(javaEdge)); Map javaSourceMapping = new HashMap<>(); javaSourceMapping.put("com.java.Caller", "/src/main/java/com/java/Caller.java"); @@ -648,20 +720,23 @@ void mergeClassRelationships_reconciliationRebuildsRelationshipMap() { javaClassGraph, javaPkgGraph, javaClassRels, javaSourceMapping, new ArrayList<>(), new ArrayList<>()); // Kotlin DTO with the REAL target class - Graph kotlinClassGraph = + Graph kotlinClassGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - kotlinClassGraph.addVertex("com.kotlin.Target"); // real canonical - kotlinClassGraph.addVertex("com.kotlin.Other"); - DefaultWeightedEdge kotlinEdge = kotlinClassGraph.addEdge("com.kotlin.Other", "com.kotlin.Target"); + kotlinClassGraph.addVertex(VertexInfo.forName("com.kotlin.Target")); // real canonical + kotlinClassGraph.addVertex(VertexInfo.forName("com.kotlin.Other")); + DefaultWeightedEdge kotlinEdge = kotlinClassGraph.addEdge( + VertexInfo.forName("com.kotlin.Other"), VertexInfo.forName("com.kotlin.Target")); kotlinClassGraph.setEdgeWeight(kotlinEdge, 3); - Graph kotlinPkgGraph = + Graph kotlinPkgGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - kotlinPkgGraph.addVertex("com.kotlin"); - kotlinPkgGraph.addEdge("com.kotlin", "com.kotlin"); + kotlinPkgGraph.addVertex(VertexInfo.forName("com.kotlin")); + kotlinPkgGraph.addEdge(VertexInfo.forName("com.kotlin"), VertexInfo.forName("com.kotlin")); Map> kotlinClassRels = new HashMap<>(); - kotlinClassRels.put(kotlinPkgGraph.getEdge("com.kotlin", "com.kotlin"), Set.of(kotlinEdge)); + kotlinClassRels.put( + kotlinPkgGraph.getEdge(VertexInfo.forName("com.kotlin"), VertexInfo.forName("com.kotlin")), + Set.of(kotlinEdge)); Map kotlinSourceMapping = new HashMap<>(); kotlinSourceMapping.put("com.kotlin.Target", "/src/main/kotlin/com/kotlin/Target.kt"); @@ -678,26 +753,30 @@ void mergeClassRelationships_reconciliationRebuildsRelationshipMap() { // Merge - reconciliation should contract com.java.Target into com.kotlin.Target CodebaseGraphDTO merged = CompositeGraphBuilder.merge(javaDto, kotlinDto); - Graph mergedClassGraph = merged.getClassReferencesGraph(); - Graph mergedPkgGraph = merged.getPackageReferencesGraph(); + Graph mergedClassGraph = merged.getClassReferencesGraph(); + Graph mergedPkgGraph = merged.getPackageReferencesGraph(); Map> relMap = merged.getClassRelationshipsInPackageRelationship(); // Fabricated vertex should be gone assertFalse( - mergedClassGraph.containsVertex("com.java.Target"), + mergedClassGraph.containsVertex(VertexInfo.forName("com.java.Target")), "Fabricated vertex should be removed by reconciliation"); // Canonical vertex should exist - assertTrue(mergedClassGraph.containsVertex("com.kotlin.Target"), "Canonical vertex should remain"); + assertTrue( + mergedClassGraph.containsVertex(VertexInfo.forName("com.kotlin.Target")), + "Canonical vertex should remain"); // Edge from Caller should now point to canonical assertTrue( - mergedClassGraph.containsEdge("com.java.Caller", "com.kotlin.Target"), + mergedClassGraph.containsEdge( + VertexInfo.forName("com.java.Caller"), VertexInfo.forName("com.kotlin.Target")), "Edge should be redirected to canonical after reconciliation"); // Verify relationship map was rebuilt after reconciliation // The package edge com.java -> com.kotlin should exist (cross-package) - DefaultWeightedEdge crossPkgEdge = mergedPkgGraph.getEdge("com.java", "com.kotlin"); + DefaultWeightedEdge crossPkgEdge = + mergedPkgGraph.getEdge(VertexInfo.forName("com.java"), VertexInfo.forName("com.kotlin")); assertNotNull(crossPkgEdge, "Cross-package edge should be created by reconciliation"); // The relationship map should have the cross-package edge as key @@ -710,8 +789,12 @@ void mergeClassRelationships_reconciliationRebuildsRelationshipMap() { DefaultWeightedEdge redirectedEdge = classEdges.iterator().next(); assertTrue(mergedClassGraph.containsEdge(redirectedEdge), "Redirected edge must belong to merged class graph"); - assertEquals("com.java.Caller", mergedClassGraph.getEdgeSource(redirectedEdge)); - assertEquals("com.kotlin.Target", mergedClassGraph.getEdgeTarget(redirectedEdge)); + assertEquals( + "com.java.Caller", + mergedClassGraph.getEdgeSource(redirectedEdge).getName()); + assertEquals( + "com.kotlin.Target", + mergedClassGraph.getEdgeTarget(redirectedEdge).getName()); } /** @@ -723,23 +806,28 @@ void mergeClassRelationships_reconciliationRebuildsRelationshipMap() { @Test void merge_javaAnonymousInnerClass_preservedThroughMergePipeline() { // Java DTO: Outer class with anonymous inner class Outer$1 - Graph javaClassGraph = + Graph javaClassGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - javaClassGraph.addVertex("com.example.Outer"); - javaClassGraph.addVertex("com.example.Outer$1"); // anonymous inner class - javaClassGraph.addVertex("com.example.Caller"); + javaClassGraph.addVertex(VertexInfo.forName("com.example.Outer")); + javaClassGraph.addVertex(VertexInfo.forName("com.example.Outer$1")); // anonymous inner class + javaClassGraph.addVertex(VertexInfo.forName("com.example.Caller")); - DefaultWeightedEdge e1 = javaClassGraph.addEdge("com.example.Caller", "com.example.Outer"); + DefaultWeightedEdge e1 = javaClassGraph.addEdge( + VertexInfo.forName("com.example.Caller"), VertexInfo.forName("com.example.Outer")); javaClassGraph.setEdgeWeight(e1, 1); - DefaultWeightedEdge e2 = javaClassGraph.addEdge("com.example.Outer", "com.example.Outer$1"); + DefaultWeightedEdge e2 = javaClassGraph.addEdge( + VertexInfo.forName("com.example.Outer"), VertexInfo.forName("com.example.Outer$1")); javaClassGraph.setEdgeWeight(e2, 1); - Graph javaPkgGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - javaPkgGraph.addVertex("com.example"); - javaPkgGraph.addEdge("com.example", "com.example"); + Graph javaPkgGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + javaPkgGraph.addVertex(VertexInfo.forName("com.example")); + javaPkgGraph.addEdge(VertexInfo.forName("com.example"), VertexInfo.forName("com.example")); Map> javaClassRels = new HashMap<>(); - javaClassRels.put(javaPkgGraph.getEdge("com.example", "com.example"), Set.of(e1, e2)); + javaClassRels.put( + javaPkgGraph.getEdge(VertexInfo.forName("com.example"), VertexInfo.forName("com.example")), + Set.of(e1, e2)); Map javaSourceMapping = new HashMap<>(); javaSourceMapping.put("com.example.Outer", "/src/main/java/com/example/Outer.java"); @@ -750,13 +838,13 @@ void merge_javaAnonymousInnerClass_preservedThroughMergePipeline() { javaClassGraph, javaPkgGraph, javaClassRels, javaSourceMapping, new ArrayList<>(), new ArrayList<>()); // Kotlin DTO: just another class in a different package - Graph kotlinClassGraph = + Graph kotlinClassGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - kotlinClassGraph.addVertex("com.other.KotlinClass"); + kotlinClassGraph.addVertex(VertexInfo.forName("com.other.KotlinClass")); - Graph kotlinPkgGraph = + Graph kotlinPkgGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - kotlinPkgGraph.addVertex("com.other"); + kotlinPkgGraph.addVertex(VertexInfo.forName("com.other")); Map> kotlinClassRels = new HashMap<>(); @@ -774,16 +862,17 @@ void merge_javaAnonymousInnerClass_preservedThroughMergePipeline() { // Merge - anonymous class mapping should happen before reconciliation CodebaseGraphDTO merged = CompositeGraphBuilder.merge(javaDto, kotlinDto); - Graph mergedClassGraph = merged.getClassReferencesGraph(); + Graph mergedClassGraph = merged.getClassReferencesGraph(); // Anonymous class should be preserved (mapped to Outer's source path during merge) assertTrue( - mergedClassGraph.containsVertex("com.example.Outer$1"), + mergedClassGraph.containsVertex(VertexInfo.forName("com.example.Outer$1")), "Anonymous inner class should be preserved through merge pipeline"); // Edge to anonymous class should remain assertTrue( - mergedClassGraph.containsEdge("com.example.Outer", "com.example.Outer$1"), + mergedClassGraph.containsEdge( + VertexInfo.forName("com.example.Outer"), VertexInfo.forName("com.example.Outer$1")), "Edge to anonymous inner class should remain after merge"); } @@ -793,16 +882,18 @@ void merge_javaAnonymousInnerClass_preservedThroughMergePipeline() { private static CodebaseGraphDTO createDto( String classSource, String classTarget, double weight, String pkgSource, String pkgTarget) { - Graph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - classGraph.addVertex(classSource); - classGraph.addVertex(classTarget); - DefaultWeightedEdge classEdge = classGraph.addEdge(classSource, classTarget); + Graph classGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + classGraph.addVertex(VertexInfo.forName(classSource)); + classGraph.addVertex(VertexInfo.forName(classTarget)); + DefaultWeightedEdge classEdge = + classGraph.addEdge(VertexInfo.forName(classSource), VertexInfo.forName(classTarget)); classGraph.setEdgeWeight(classEdge, weight); - Graph pkgGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - pkgGraph.addVertex(pkgSource); - pkgGraph.addVertex(pkgTarget); - DefaultWeightedEdge pkgEdge = pkgGraph.addEdge(pkgSource, pkgTarget); + Graph pkgGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + pkgGraph.addVertex(VertexInfo.forName(pkgSource)); + pkgGraph.addVertex(VertexInfo.forName(pkgTarget)); + DefaultWeightedEdge pkgEdge = pkgGraph.addEdge(VertexInfo.forName(pkgSource), VertexInfo.forName(pkgTarget)); pkgGraph.setEdgeWeight(pkgEdge, weight); Map> classRels = new HashMap<>(); diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/CompositeGraphBuilderTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/CompositeGraphBuilderTest.java index d78e4adf..cdbd4abc 100644 --- a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/CompositeGraphBuilderTest.java +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/CompositeGraphBuilderTest.java @@ -15,6 +15,7 @@ */ class CompositeGraphBuilderTest { + /** Verifies that a mixed source directory produces one graph containing Java and Kotlin dependencies. */ @DisplayName("A directory with both Java and Kotlin source files produces a single graph with cross-language edges") @Test void parseMixedSourceDirectoryTest() throws IOException { @@ -22,29 +23,37 @@ void parseMixedSourceDirectoryTest() throws IOException { CompositeGraphBuilder compositeGraphBuilder = new CompositeGraphBuilder(); CodebaseGraphDTO dto = compositeGraphBuilder.getCodebaseGraphDTO(srcDirectory.getAbsolutePath(), false, ""); - Graph classReferencesGraph = dto.getClassReferencesGraph(); + Graph classReferencesGraph = dto.getClassReferencesGraph(); assertNotNull(classReferencesGraph); // All 4 classes should be vertices assertEquals(4, classReferencesGraph.vertexSet().size()); - assertTrue(classReferencesGraph.containsVertex("com.ideacrest.parser.mixedclasses.JavaClass")); - assertTrue(classReferencesGraph.containsVertex("com.ideacrest.parser.mixedclasses.KotlinClass")); - assertTrue(classReferencesGraph.containsVertex("com.ideacrest.parser.mixedclasses.SharedTarget")); - assertTrue(classReferencesGraph.containsVertex("com.ideacrest.parser.mixedclasses.KConsumer")); + assertTrue( + classReferencesGraph.containsVertex(VertexInfo.forName("com.ideacrest.parser.mixedclasses.JavaClass"))); + assertTrue(classReferencesGraph.containsVertex( + VertexInfo.forName("com.ideacrest.parser.mixedclasses.KotlinClass"))); + assertTrue(classReferencesGraph.containsVertex( + VertexInfo.forName("com.ideacrest.parser.mixedclasses.SharedTarget"))); + assertTrue( + classReferencesGraph.containsVertex(VertexInfo.forName("com.ideacrest.parser.mixedclasses.KConsumer"))); // Java -> Kotlin cross-language edge assertTrue(classReferencesGraph.containsEdge( - "com.ideacrest.parser.mixedclasses.JavaClass", "com.ideacrest.parser.mixedclasses.KotlinClass")); + VertexInfo.forName("com.ideacrest.parser.mixedclasses.JavaClass"), + VertexInfo.forName("com.ideacrest.parser.mixedclasses.KotlinClass"))); // Kotlin -> Java cross-language edge assertTrue(classReferencesGraph.containsEdge( - "com.ideacrest.parser.mixedclasses.KotlinClass", "com.ideacrest.parser.mixedclasses.JavaClass")); + VertexInfo.forName("com.ideacrest.parser.mixedclasses.KotlinClass"), + VertexInfo.forName("com.ideacrest.parser.mixedclasses.JavaClass"))); // Kotlin -> Java shared target edge assertTrue(classReferencesGraph.containsEdge( - "com.ideacrest.parser.mixedclasses.KConsumer", "com.ideacrest.parser.mixedclasses.SharedTarget")); + VertexInfo.forName("com.ideacrest.parser.mixedclasses.KConsumer"), + VertexInfo.forName("com.ideacrest.parser.mixedclasses.SharedTarget"))); } + /** Verifies that cross-package Java references resolve to canonical Kotlin vertices and package edges. */ @DisplayName("Cross-package Java-to-Kotlin reference is reconciled to the canonical Kotlin FQN") @Test void parseMixedSourceDirectoryCrossPackageTest() throws IOException { @@ -52,23 +61,25 @@ void parseMixedSourceDirectoryCrossPackageTest() throws IOException { CompositeGraphBuilder compositeGraphBuilder = new CompositeGraphBuilder(); CodebaseGraphDTO dto = compositeGraphBuilder.getCodebaseGraphDTO(srcDirectory.getAbsolutePath(), false, ""); - Graph classReferencesGraph = dto.getClassReferencesGraph(); + Graph classReferencesGraph = dto.getClassReferencesGraph(); assertNotNull(classReferencesGraph); // The canonical Kotlin class FQN should be present assertTrue( - classReferencesGraph.containsVertex("com.almasb.fxgl.app.GameSettings"), + classReferencesGraph.containsVertex(VertexInfo.forName("com.almasb.fxgl.app.GameSettings")), "Canonical Kotlin class should be in the graph"); // The fabricated vertex (caller's package + simple name) should NOT be present assertFalse( - classReferencesGraph.containsVertex("com.ideacrest.parser.mixedclasses.GameSettings"), + classReferencesGraph.containsVertex( + VertexInfo.forName("com.ideacrest.parser.mixedclasses.GameSettings")), "Fabricated cross-package vertex should be reconciled away"); // The Java class should have an edge to the CANONICAL Kotlin FQN assertTrue( classReferencesGraph.containsEdge( - "com.ideacrest.parser.mixedclasses.JavaClass", "com.almasb.fxgl.app.GameSettings"), + VertexInfo.forName("com.ideacrest.parser.mixedclasses.JavaClass"), + VertexInfo.forName("com.almasb.fxgl.app.GameSettings")), "Java class should reference the canonical Kotlin FQN after reconciliation"); // Source path mapping should have the canonical FQN @@ -77,9 +88,11 @@ void parseMixedSourceDirectoryCrossPackageTest() throws IOException { "Source path mapping should have the canonical Kotlin FQN"); // Package graph should have the cross-package edge - Graph packageReferencesGraph = dto.getPackageReferencesGraph(); + Graph packageReferencesGraph = dto.getPackageReferencesGraph(); assertTrue( - packageReferencesGraph.containsEdge("com.ideacrest.parser.mixedclasses", "com.almasb.fxgl.app"), + packageReferencesGraph.containsEdge( + VertexInfo.forName("com.ideacrest.parser.mixedclasses"), + VertexInfo.forName("com.almasb.fxgl.app")), "Package graph should have the cross-package edge after reconciliation"); } } diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/GraphDependencyCollectorTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/GraphDependencyCollectorTest.java index dbcf04cf..3fe10325 100644 --- a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/GraphDependencyCollectorTest.java +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/GraphDependencyCollectorTest.java @@ -13,10 +13,14 @@ /** * Unit tests for {@link GraphDependencyCollector}. * - *

Anonymous/synthetic classes (Java {@code Outer$1}/{@code Outer$2}/{@code Outer$}, the literal + *

Vertices are {@link VertexInfo} instances: the collector captures the + * information needed to populate a VertexInfo instead of using only the class + * or package name as the vertex. + * + *

Anonymous/synthetic classes (Java {@code Outer$1}/{code Outer$2}/{@code Outer$}, the literal * Kotlin {@code ""} string, Kotlin synthetic {@code $N} classes) are first-class * graph members: they can contain antipatterns and so are rendered with {@code $} as the - * enclosing-class separator. The collector therefore no longer sieves them out — only a self-edge + * enclosing-class separator. The collector therefore does not sieve them out — only a self-edge * ({@code from == to}) is suppressed. Render-time sink filtering (vertices with no outgoing edges) * lives in {@link org.hjug.refactorfirst.report.HtmlReport}. * @@ -24,128 +28,200 @@ */ class GraphDependencyCollectorTest { - private static Graph newClassGraph() { + /** Creates an empty directed weighted graph for dependency collection tests. */ + private static Graph newClassGraph() { return new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); } + /** Asserts that a graph contains a vertex matching the supplied class or package name. */ + private static void assertHasVertex(Graph graph, String name) { + VertexInfo vertex = VertexInfo.forName(name); + assertTrue(graph.containsVertex(vertex), "expected vertex " + name); + } + + /** Verifies that a Java anonymous source class participates in class dependencies. */ @DisplayName("addClassDependency creates vertex + edge for a Java anonymous source") @Test void addClassDependency_javaAnonymousSource_addsVertex() { - Graph classGraph = newClassGraph(); - Graph pkgGraph = newClassGraph(); + Graph classGraph = newClassGraph(); + Graph pkgGraph = newClassGraph(); GraphDependencyCollector collector = new GraphDependencyCollector(classGraph, pkgGraph); collector.addClassDependency("com.foo.Outer$1", "com.foo.Real"); - assertTrue(classGraph.containsVertex("com.foo.Outer$1"), "Java anonymous source must be a vertex"); - assertTrue(classGraph.containsVertex("com.foo.Real")); - assertTrue(classGraph.containsEdge("com.foo.Outer$1", "com.foo.Real")); + assertHasVertex(classGraph, "com.foo.Outer$1"); + assertHasVertex(classGraph, "com.foo.Real"); + assertTrue(classGraph.containsEdge(VertexInfo.forName("com.foo.Outer$1"), VertexInfo.forName("com.foo.Real"))); // same package -> no package edge, but the real package vertex is not required here either assertTrue(pkgGraph.edgeSet().isEmpty()); } + /** Verifies that an anonymous target class contributes both class and cross-package dependencies. */ @DisplayName("addClassDependency creates vertex + edge for a Java anonymous target across packages") @Test void addClassDependency_javaAnonymousTarget_addsVertex() { - Graph classGraph = newClassGraph(); - Graph pkgGraph = newClassGraph(); + Graph classGraph = newClassGraph(); + Graph pkgGraph = newClassGraph(); GraphDependencyCollector collector = new GraphDependencyCollector(classGraph, pkgGraph); collector.addClassDependency("com.foo.Real", "com.bar.Outer$1"); - assertTrue(classGraph.containsVertex("com.foo.Real")); - assertTrue(classGraph.containsVertex("com.bar.Outer$1"), "Java anonymous target must be a vertex"); - assertTrue(classGraph.containsEdge("com.foo.Real", "com.bar.Outer$1")); + assertHasVertex(classGraph, "com.foo.Real"); + assertHasVertex(classGraph, "com.bar.Outer$1"); + assertTrue(classGraph.containsEdge(VertexInfo.forName("com.foo.Real"), VertexInfo.forName("com.bar.Outer$1"))); // Java anonymous classes have a real package; cross-package must populate package graph - assertTrue(pkgGraph.containsVertex("com.foo")); - assertTrue(pkgGraph.containsVertex("com.bar")); + assertHasVertex(pkgGraph, "com.foo"); + assertHasVertex(pkgGraph, "com.bar"); assertEquals(1, pkgGraph.edgeSet().size()); } + /** Verifies that dependency endpoints retain their names and derived enclosing namespaces. */ + @DisplayName("addClassDependency captures VertexInfo with parent namespace instead of a bare name") + @Test + void addClassDependency_capturesVertexInfoWithParentNamespace() { + Graph classGraph = newClassGraph(); + Graph pkgGraph = newClassGraph(); + GraphDependencyCollector collector = new GraphDependencyCollector(classGraph, pkgGraph); + + collector.addClassDependency("com.foo.Outer$Inner", "com.bar.Baz"); + + VertexInfo source = classGraph.getEdgeSource( + classGraph.getEdge(VertexInfo.forName("com.foo.Outer$Inner"), VertexInfo.forName("com.bar.Baz"))); + VertexInfo target = classGraph.getEdgeTarget( + classGraph.getEdge(VertexInfo.forName("com.foo.Outer$Inner"), VertexInfo.forName("com.bar.Baz"))); + assertEquals("com.foo.Outer$Inner", source.getName()); + assertEquals("com.foo.Outer", source.getParentNamespace()); + assertEquals("com.bar.Baz", target.getName()); + assertEquals("com.bar", target.getParentNamespace()); + } + + /** Verifies that package dependency vertices identify their parent packages. */ + @DisplayName("package vertices carry their parent package as the parent namespace") + @Test + void addClassDependency_packageVerticesHaveParentPackageNamespace() { + Graph classGraph = newClassGraph(); + Graph pkgGraph = newClassGraph(); + GraphDependencyCollector collector = new GraphDependencyCollector(classGraph, pkgGraph); + + collector.addClassDependency("com.foo.A", "com.bar.B"); + + VertexInfo fromPackage = + pkgGraph.getEdgeSource(pkgGraph.edgeSet().iterator().next()); + VertexInfo toPackage = + pkgGraph.getEdgeTarget(pkgGraph.edgeSet().iterator().next()); + assertEquals("com.foo", fromPackage.getName()); + assertEquals("com", fromPackage.getParentNamespace()); + assertEquals("com.bar", toPackage.getName()); + assertEquals("com", toPackage.getParentNamespace()); + } + + /** Verifies that a packageless Kotlin anonymous source creates a class edge without a package edge. */ @DisplayName("addClassDependency makes the Kotlin literal a vertex and edge") @Test void addClassDependency_kotlinAnonymousSource_addsVertex() { - Graph classGraph = newClassGraph(); - Graph pkgGraph = newClassGraph(); + Graph classGraph = newClassGraph(); + Graph pkgGraph = newClassGraph(); GraphDependencyCollector collector = new GraphDependencyCollector(classGraph, pkgGraph); collector.addClassDependency("", "com.bar.X"); - assertTrue(classGraph.containsVertex(""), "Kotlin must be a vertex"); - assertTrue(classGraph.containsVertex("com.bar.X")); - assertTrue(classGraph.containsEdge("", "com.bar.X")); + assertHasVertex(classGraph, ""); + assertHasVertex(classGraph, "com.bar.X"); + assertTrue(classGraph.containsEdge(VertexInfo.forName(""), VertexInfo.forName("com.bar.X"))); // package graph: the source has no package, so no package edge is created // and neither an "" vertex nor the cross-package target vertex should appear. assertFalse( - pkgGraph.containsVertex(""), + pkgGraph.vertexSet().stream().anyMatch(v -> v.getName().isEmpty()), " must not pollute the package graph with an empty package vertex"); assertTrue(pkgGraph.edgeSet().isEmpty(), "no package edge should be created when the source package is empty"); } + /** Verifies that a packageless anonymous source creates neither an empty package vertex nor an edge. */ @DisplayName("addPackageDependency with a Kotlin source does not create an empty package vertex") @Test void addPackageDependency_kotlinAnonymousSource_doesNotPollutePackageGraph() { - Graph classGraph = newClassGraph(); - Graph pkgGraph = newClassGraph(); + Graph classGraph = newClassGraph(); + Graph pkgGraph = newClassGraph(); GraphDependencyCollector collector = new GraphDependencyCollector(classGraph, pkgGraph); DefaultWeightedEdge edge = collector.addPackageDependency("", "com.bar.Other"); // getPackageFromFqn("") returns ""; "".equals("com.bar") is false, so a // package edge "" -> "com.bar" would otherwise be created. Assert it is not. - assertFalse(pkgGraph.containsVertex(""), "must not register the empty string as a package vertex"); + assertFalse( + pkgGraph.vertexSet().stream().anyMatch(v -> v.getName().isEmpty()), + "must not register the empty string as a package vertex"); // No edge should be created because the source package ("") is degenerate; collector must // treat an empty source package as a no-op. assertNull(edge, "addPackageDependency must signal a no-op when the source package is empty"); assertTrue(pkgGraph.edgeSet().isEmpty()); } + /** Verifies that explicit registration retains anonymous and synthetic class vertices. */ @DisplayName("registerClassVertex registers Java anonymous and Kotlin FQNs") @Test void registerClassVertex_syntheticFqn_registered() { - Graph classGraph = newClassGraph(); - Graph pkgGraph = newClassGraph(); + Graph classGraph = newClassGraph(); + Graph pkgGraph = newClassGraph(); GraphDependencyCollector collector = new GraphDependencyCollector(classGraph, pkgGraph); collector.registerClassVertex(""); collector.registerClassVertex("com.foo.Outer$1"); collector.registerClassVertex("com.foo.Outer$2"); - assertTrue(classGraph.containsVertex("")); - assertTrue(classGraph.containsVertex("com.foo.Outer$1")); - assertTrue(classGraph.containsVertex("com.foo.Outer$2")); + assertHasVertex(classGraph, ""); + assertHasVertex(classGraph, "com.foo.Outer$1"); + assertHasVertex(classGraph, "com.foo.Outer$2"); assertEquals(3, classGraph.vertexSet().size()); } + /** Verifies that explicit registration adds a named class without requiring dependencies. */ @DisplayName("registerClassVertex registers real FQNs (control)") @Test void registerClassVertex_realFqn_registered() { - Graph classGraph = newClassGraph(); - Graph pkgGraph = newClassGraph(); + Graph classGraph = newClassGraph(); + Graph pkgGraph = newClassGraph(); GraphDependencyCollector collector = new GraphDependencyCollector(classGraph, pkgGraph); collector.registerClassVertex("com.foo.Real"); - assertTrue(classGraph.containsVertex("com.foo.Real")); + assertHasVertex(classGraph, "com.foo.Real"); assertEquals(1, classGraph.vertexSet().size()); } + /** Verifies that the collector preserves the raw source location reported for a class. */ + @DisplayName("recordClassLocation captures the location reported during collection") + @Test + void recordClassLocation_capturesLocation() { + Graph classGraph = newClassGraph(); + Graph pkgGraph = newClassGraph(); + GraphDependencyCollector collector = new GraphDependencyCollector(classGraph, pkgGraph); + + collector.registerClassVertex("com.foo.Real"); + collector.recordClassLocation("com.foo.Real", "file:///repo/src/main/java/com/foo/Real.java"); + + assertEquals( + "file:///repo/src/main/java/com/foo/Real.java", + collector.getRecordedClassLocations().get("com.foo.Real")); + } + + /** Verifies that a cross-package class dependency populates both graphs and their relationship mapping. */ @DisplayName("addClassDependency happy path still populates class + package graphs") @Test void addClassDependency_realCrossPackageDeps_populatesBothGraphs() { - Graph classGraph = newClassGraph(); - Graph pkgGraph = newClassGraph(); + Graph classGraph = newClassGraph(); + Graph pkgGraph = newClassGraph(); GraphDependencyCollector collector = new GraphDependencyCollector(classGraph, pkgGraph); collector.addClassDependency("com.foo.A", "com.bar.B"); - assertTrue(classGraph.containsVertex("com.foo.A")); - assertTrue(classGraph.containsVertex("com.bar.B")); - assertTrue(classGraph.containsEdge("com.foo.A", "com.bar.B")); + assertHasVertex(classGraph, "com.foo.A"); + assertHasVertex(classGraph, "com.bar.B"); + assertTrue(classGraph.containsEdge(VertexInfo.forName("com.foo.A"), VertexInfo.forName("com.bar.B"))); assertEquals(1, classGraph.edgeSet().size()); - assertTrue(pkgGraph.containsVertex("com.foo")); - assertTrue(pkgGraph.containsVertex("com.bar")); + assertHasVertex(pkgGraph, "com.foo"); + assertHasVertex(pkgGraph, "com.bar"); assertEquals(1, pkgGraph.edgeSet().size()); Map> relationships = @@ -153,16 +229,17 @@ void addClassDependency_realCrossPackageDeps_populatesBothGraphs() { assertEquals(1, relationships.size(), "cross-package dependency must be tracked"); } + /** Verifies that a dependency within one package creates only the class relationship. */ @DisplayName("addClassDependency within the same package still records a class edge and no package edge") @Test void addClassDependency_realSamePackageDep_recordsClassEdgeOnly() { - Graph classGraph = newClassGraph(); - Graph pkgGraph = newClassGraph(); + Graph classGraph = newClassGraph(); + Graph pkgGraph = newClassGraph(); GraphDependencyCollector collector = new GraphDependencyCollector(classGraph, pkgGraph); collector.addClassDependency("com.foo.A", "com.foo.B"); - assertTrue(classGraph.containsEdge("com.foo.A", "com.foo.B")); + assertTrue(classGraph.containsEdge(VertexInfo.forName("com.foo.A"), VertexInfo.forName("com.foo.B"))); assertTrue(pkgGraph.edgeSet().isEmpty(), "same-package dependency must not create a package edge"); assertTrue(collector.getClassRelationshipsInPackageRelationship().isEmpty()); } diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/Java25AnalysisIntegrationTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/Java25AnalysisIntegrationTest.java index c5c418e0..1d8fb43d 100644 --- a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/Java25AnalysisIntegrationTest.java +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/Java25AnalysisIntegrationTest.java @@ -28,6 +28,7 @@ class Java25AnalysisIntegrationTest { private static final String FIXTURE_DIR = "src/test/resources/java25SrcDirectory"; + /** Verifies that a Java 25 flexible constructor fixture appears in the analyzed class graph. */ @DisplayName("Java 25 source using flexible constructor bodies is parsed and appears in the graph") @Test void java25SourceIsAnalyzed() throws IOException { @@ -37,7 +38,8 @@ void java25SourceIsAnalyzed() throws IOException { CodebaseGraphDTO dto = new CompositeGraphBuilder().getCodebaseGraphDTO(srcDirectory.getAbsolutePath(), config); assertTrue( - dto.getClassReferencesGraph().containsVertex("com.example.java25.FlexibleConstructorBody"), + dto.getClassReferencesGraph() + .containsVertex(VertexInfo.forName("com.example.java25.FlexibleConstructorBody")), "Java 25 fixture class must be a vertex in the class graph. Vertices: " + dto.getClassReferencesGraph().vertexSet()); assertTrue( diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/JavaGraphBuilderTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/JavaGraphBuilderTest.java index 7d1818b6..5a041997 100644 --- a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/JavaGraphBuilderTest.java +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/JavaGraphBuilderTest.java @@ -31,52 +31,103 @@ void parseSourceDirectoryEmptyTest() { IllegalArgumentException.class, () -> compositeGraphBuilder.getCodebaseGraphDTO(null, false, "")); } + /** Verifies that Java class vertices expose repository-relative source paths and parent namespaces. */ + @DisplayName("Vertices are VertexInfo instances hydrated with repo-relative location and parent namespace") + @Test + void parseSourceDirectoryVerticesCarryVertexInfo() throws IOException { + File srcDirectory = new File("src/test/resources/javaSrcDirectory"); + CodebaseGraphDTO dto = compositeGraphBuilder.getCodebaseGraphDTO(srcDirectory.getAbsolutePath(), false, ""); + + Graph classGraph = dto.getClassReferencesGraph(); + VertexInfo classA = classGraph.vertexSet().stream() + .filter(v -> v.getName().equals("com.ideacrest.parser.testclasses.A")) + .findFirst() + .orElseThrow(); + assertEquals("com.ideacrest.parser.testclasses", classA.getParentNamespace()); + assertEquals("com/ideacrest/parser/testclasses/A.java", classA.getLocationOnDisk()); + + Graph packageGraph = dto.getPackageReferencesGraph(); + // single-package fixture: no cross-package edges, so no package vertices exist + assertTrue(packageGraph.vertexSet().isEmpty()); + assertTrue(packageGraph.edgeSet().isEmpty()); + } + + /** Verifies that cross-package analysis hydrates package directories and parent namespaces. */ + @DisplayName("Cross-package builds hydrate package vertices with parent package and member-file directory") + @Test + void crossPackageBuildHydratesPackageVertices() throws IOException { + File srcDirectory = new File("src/test/resources/mixedSrcDirectoryCrossPackage"); + CodebaseGraphDTO dto = compositeGraphBuilder.getCodebaseGraphDTO(srcDirectory.getAbsolutePath(), false, ""); + + Graph packageGraph = dto.getPackageReferencesGraph(); + VertexInfo fxglAppPackage = packageGraph.vertexSet().stream() + .filter(v -> v.getName().equals("com.almasb.fxgl.app")) + .findFirst() + .orElseThrow(); + assertEquals("com.almasb.fxgl", fxglAppPackage.getParentNamespace()); + assertNotNull(fxglAppPackage.getLocationOnDisk(), "package location derived from a member file"); + assertTrue( + fxglAppPackage.getLocationOnDisk().endsWith("com/almasb/fxgl/app"), + "expected the package directory, got: " + fxglAppPackage.getLocationOnDisk()); + } + + /** Verifies the Java fixture graph contains the expected classes, dependencies, and edge weights. */ @DisplayName("Given a valid source directory input parameter return a valid graph.") @Test void parseSourceDirectoryTest() throws IOException { File srcDirectory = new File("src/test/resources/javaSrcDirectory"); CodebaseGraphDTO dto = compositeGraphBuilder.getCodebaseGraphDTO(srcDirectory.getAbsolutePath(), false, ""); - Graph classReferencesGraph = dto.getClassReferencesGraph(); + Graph classReferencesGraph = dto.getClassReferencesGraph(); assertNotNull(classReferencesGraph); assertEquals(5, classReferencesGraph.vertexSet().size()); assertEquals(7, classReferencesGraph.edgeSet().size()); - assertTrue(classReferencesGraph.containsVertex("com.ideacrest.parser.testclasses.A")); - assertTrue(classReferencesGraph.containsVertex("com.ideacrest.parser.testclasses.B")); - assertTrue(classReferencesGraph.containsVertex("com.ideacrest.parser.testclasses.C")); - assertTrue(classReferencesGraph.containsVertex("com.ideacrest.parser.testclasses.D")); - assertTrue(classReferencesGraph.containsVertex("com.ideacrest.parser.testclasses.E")); + assertTrue(classReferencesGraph.containsVertex(VertexInfo.forName("com.ideacrest.parser.testclasses.A"))); + assertTrue(classReferencesGraph.containsVertex(VertexInfo.forName("com.ideacrest.parser.testclasses.B"))); + assertTrue(classReferencesGraph.containsVertex(VertexInfo.forName("com.ideacrest.parser.testclasses.C"))); + assertTrue(classReferencesGraph.containsVertex(VertexInfo.forName("com.ideacrest.parser.testclasses.D"))); + assertTrue(classReferencesGraph.containsVertex(VertexInfo.forName("com.ideacrest.parser.testclasses.E"))); assertTrue(classReferencesGraph.containsEdge( - "com.ideacrest.parser.testclasses.A", "com.ideacrest.parser.testclasses.B")); + VertexInfo.forName("com.ideacrest.parser.testclasses.A"), + VertexInfo.forName("com.ideacrest.parser.testclasses.B"))); assertTrue(classReferencesGraph.containsEdge( - "com.ideacrest.parser.testclasses.B", "com.ideacrest.parser.testclasses.C")); + VertexInfo.forName("com.ideacrest.parser.testclasses.B"), + VertexInfo.forName("com.ideacrest.parser.testclasses.C"))); assertTrue(classReferencesGraph.containsEdge( - "com.ideacrest.parser.testclasses.C", "com.ideacrest.parser.testclasses.A")); + VertexInfo.forName("com.ideacrest.parser.testclasses.C"), + VertexInfo.forName("com.ideacrest.parser.testclasses.A"))); assertTrue(classReferencesGraph.containsEdge( - "com.ideacrest.parser.testclasses.C", "com.ideacrest.parser.testclasses.E")); + VertexInfo.forName("com.ideacrest.parser.testclasses.C"), + VertexInfo.forName("com.ideacrest.parser.testclasses.E"))); assertTrue(classReferencesGraph.containsEdge( - "com.ideacrest.parser.testclasses.D", "com.ideacrest.parser.testclasses.A")); + VertexInfo.forName("com.ideacrest.parser.testclasses.D"), + VertexInfo.forName("com.ideacrest.parser.testclasses.A"))); assertTrue(classReferencesGraph.containsEdge( - "com.ideacrest.parser.testclasses.D", "com.ideacrest.parser.testclasses.C")); + VertexInfo.forName("com.ideacrest.parser.testclasses.D"), + VertexInfo.forName("com.ideacrest.parser.testclasses.C"))); assertTrue(classReferencesGraph.containsEdge( - "com.ideacrest.parser.testclasses.E", "com.ideacrest.parser.testclasses.D")); + VertexInfo.forName("com.ideacrest.parser.testclasses.E"), + VertexInfo.forName("com.ideacrest.parser.testclasses.D"))); // confirm edge weight calculations assertEquals( 1, getEdgeWeight( classReferencesGraph, - "com.ideacrest.parser.testclasses.A", - "com.ideacrest.parser.testclasses.B")); + VertexInfo.forName("com.ideacrest.parser.testclasses.A"), + VertexInfo.forName("com.ideacrest.parser.testclasses.B"))); assertEquals( 2, getEdgeWeight( classReferencesGraph, - "com.ideacrest.parser.testclasses.E", - "com.ideacrest.parser.testclasses.D")); + VertexInfo.forName("com.ideacrest.parser.testclasses.E"), + VertexInfo.forName("com.ideacrest.parser.testclasses.D"))); } + /** Returns the weight of the dependency between the supplied source and target vertices. */ private static double getEdgeWeight( - Graph classReferencesGraph, String sourceVertex, String targetVertex) { + Graph classReferencesGraph, + VertexInfo sourceVertex, + VertexInfo targetVertex) { return classReferencesGraph.getEdgeWeight(classReferencesGraph.getEdge(sourceVertex, targetVertex)); } } diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/KotlinGraphBuilderTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/KotlinGraphBuilderTest.java index 9de69c49..992d3750 100644 --- a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/KotlinGraphBuilderTest.java +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/KotlinGraphBuilderTest.java @@ -21,79 +21,90 @@ class KotlinGraphBuilderTest { private final CompositeGraphBuilder compositeGraphBuilder = new CompositeGraphBuilder(); + /** Verifies the Kotlin fixture graph contains the expected classes, dependencies, and edge weights. */ @DisplayName("Given a valid Kotlin source directory input parameter return a valid graph.") @Test void parseKotlinSourceDirectoryTest() throws IOException { File srcDirectory = new File("src/test/resources/kotlinSrcDirectory"); CodebaseGraphDTO dto = compositeGraphBuilder.getCodebaseGraphDTO(srcDirectory.getAbsolutePath(), false, ""); - Graph classReferencesGraph = dto.getClassReferencesGraph(); + Graph classReferencesGraph = dto.getClassReferencesGraph(); assertNotNull(classReferencesGraph); assertEquals(5, classReferencesGraph.vertexSet().size()); assertEquals(7, classReferencesGraph.edgeSet().size()); - assertTrue(classReferencesGraph.containsVertex("com.ideacrest.parser.testclasses.A")); - assertTrue(classReferencesGraph.containsVertex("com.ideacrest.parser.testclasses.B")); - assertTrue(classReferencesGraph.containsVertex("com.ideacrest.parser.testclasses.C")); - assertTrue(classReferencesGraph.containsVertex("com.ideacrest.parser.testclasses.D")); - assertTrue(classReferencesGraph.containsVertex("com.ideacrest.parser.testclasses.E")); + assertTrue(classReferencesGraph.containsVertex(VertexInfo.forName("com.ideacrest.parser.testclasses.A"))); + assertTrue(classReferencesGraph.containsVertex(VertexInfo.forName("com.ideacrest.parser.testclasses.B"))); + assertTrue(classReferencesGraph.containsVertex(VertexInfo.forName("com.ideacrest.parser.testclasses.C"))); + assertTrue(classReferencesGraph.containsVertex(VertexInfo.forName("com.ideacrest.parser.testclasses.D"))); + assertTrue(classReferencesGraph.containsVertex(VertexInfo.forName("com.ideacrest.parser.testclasses.E"))); assertTrue(classReferencesGraph.containsEdge( - "com.ideacrest.parser.testclasses.A", "com.ideacrest.parser.testclasses.B")); + VertexInfo.forName("com.ideacrest.parser.testclasses.A"), + VertexInfo.forName("com.ideacrest.parser.testclasses.B"))); assertTrue(classReferencesGraph.containsEdge( - "com.ideacrest.parser.testclasses.B", "com.ideacrest.parser.testclasses.C")); + VertexInfo.forName("com.ideacrest.parser.testclasses.B"), + VertexInfo.forName("com.ideacrest.parser.testclasses.C"))); assertTrue(classReferencesGraph.containsEdge( - "com.ideacrest.parser.testclasses.C", "com.ideacrest.parser.testclasses.A")); + VertexInfo.forName("com.ideacrest.parser.testclasses.C"), + VertexInfo.forName("com.ideacrest.parser.testclasses.A"))); assertTrue(classReferencesGraph.containsEdge( - "com.ideacrest.parser.testclasses.C", "com.ideacrest.parser.testclasses.E")); + VertexInfo.forName("com.ideacrest.parser.testclasses.C"), + VertexInfo.forName("com.ideacrest.parser.testclasses.E"))); assertTrue(classReferencesGraph.containsEdge( - "com.ideacrest.parser.testclasses.D", "com.ideacrest.parser.testclasses.A")); + VertexInfo.forName("com.ideacrest.parser.testclasses.D"), + VertexInfo.forName("com.ideacrest.parser.testclasses.A"))); assertTrue(classReferencesGraph.containsEdge( - "com.ideacrest.parser.testclasses.D", "com.ideacrest.parser.testclasses.C")); + VertexInfo.forName("com.ideacrest.parser.testclasses.D"), + VertexInfo.forName("com.ideacrest.parser.testclasses.C"))); assertTrue(classReferencesGraph.containsEdge( - "com.ideacrest.parser.testclasses.E", "com.ideacrest.parser.testclasses.D")); + VertexInfo.forName("com.ideacrest.parser.testclasses.E"), + VertexInfo.forName("com.ideacrest.parser.testclasses.D"))); // confirm edge weight calculations assertEquals( 1, getEdgeWeight( classReferencesGraph, - "com.ideacrest.parser.testclasses.A", - "com.ideacrest.parser.testclasses.B")); + VertexInfo.forName("com.ideacrest.parser.testclasses.A"), + VertexInfo.forName("com.ideacrest.parser.testclasses.B"))); assertEquals( 2, getEdgeWeight( classReferencesGraph, - "com.ideacrest.parser.testclasses.E", - "com.ideacrest.parser.testclasses.D")); + VertexInfo.forName("com.ideacrest.parser.testclasses.E"), + VertexInfo.forName("com.ideacrest.parser.testclasses.D"))); } + /** Verifies that a Kotlin class-header dependency contributes exactly one unit of edge weight. */ @DisplayName("Kotlin class-header dependencies are recorded exactly once.") @Test void classHeaderDependencyHasWeightOne() throws IOException { File srcDirectory = new File("src/test/resources/kotlinClassHeaderSrcDirectory"); CodebaseGraphDTO dto = compositeGraphBuilder.getCodebaseGraphDTO(srcDirectory.getAbsolutePath(), false, ""); - Graph classReferencesGraph = dto.getClassReferencesGraph(); + Graph classReferencesGraph = dto.getClassReferencesGraph(); String source = "com.ideacrest.parser.classheader.HeaderImplementation"; String target = "com.ideacrest.parser.classheader.HeaderContract"; - assertTrue(classReferencesGraph.containsVertex(source)); - assertTrue(classReferencesGraph.containsVertex(target)); - assertTrue(classReferencesGraph.containsEdge(source, target)); - assertEquals(1.0, getEdgeWeight(classReferencesGraph, source, target)); + assertTrue(classReferencesGraph.containsVertex(VertexInfo.forName(source))); + assertTrue(classReferencesGraph.containsVertex(VertexInfo.forName(target))); + assertTrue(classReferencesGraph.containsEdge(VertexInfo.forName(source), VertexInfo.forName(target))); + assertEquals(1.0, getEdgeWeight(classReferencesGraph, VertexInfo.forName(source), VertexInfo.forName(target))); } + /** Verifies that Kotlin callable references create dependencies on their declaring classes. */ @DisplayName("Kotlin callable references produce edges between caller and target's declaring class.") @Test void parseKotlinCallableReferenceTest() throws IOException { File srcDirectory = new File("src/test/resources/kotlinCallableRefSrcDirectory"); CodebaseGraphDTO dto = compositeGraphBuilder.getCodebaseGraphDTO(srcDirectory.getAbsolutePath(), false, ""); - Graph classReferencesGraph = dto.getClassReferencesGraph(); + Graph classReferencesGraph = dto.getClassReferencesGraph(); assertNotNull(classReferencesGraph); // Both classes should appear as vertices assertTrue( - classReferencesGraph.containsVertex("com.ideacrest.parser.callref.CallableRefTarget"), + classReferencesGraph.containsVertex( + VertexInfo.forName("com.ideacrest.parser.callref.CallableRefTarget")), "CallableRefTarget should be a graph vertex, vertices: " + classReferencesGraph.vertexSet()); assertTrue( - classReferencesGraph.containsVertex("com.ideacrest.parser.callref.CallableRefUser"), + classReferencesGraph.containsVertex(VertexInfo.forName("com.ideacrest.parser.callref.CallableRefUser")), "CallableRefUser should be a graph vertex, vertices: " + classReferencesGraph.vertexSet()); // The two callable references (`CallableRefTarget::alpha` and @@ -102,7 +113,8 @@ void parseKotlinCallableReferenceTest() throws IOException { // The implementation weight-merges duplicates, so we just require // the edge exists and its weight is at least 1. boolean edgeExists = classReferencesGraph.containsEdge( - "com.ideacrest.parser.callref.CallableRefUser", "com.ideacrest.parser.callref.CallableRefTarget"); + VertexInfo.forName("com.ideacrest.parser.callref.CallableRefUser"), + VertexInfo.forName("com.ideacrest.parser.callref.CallableRefTarget")); assertTrue( edgeExists, "CallableRefUser -> CallableRefTarget edge should exist (callable references + .call() invocation), edges: " @@ -111,15 +123,18 @@ void parseKotlinCallableReferenceTest() throws IOException { if (edgeExists) { double weight = getEdgeWeight( classReferencesGraph, - "com.ideacrest.parser.callref.CallableRefUser", - "com.ideacrest.parser.callref.CallableRefTarget"); + VertexInfo.forName("com.ideacrest.parser.callref.CallableRefUser"), + VertexInfo.forName("com.ideacrest.parser.callref.CallableRefTarget")); assertTrue( weight >= 1.0, "CallableRefUser -> CallableRefTarget edge weight should be >= 1, was: " + weight); } } + /** Returns the weight of the dependency between the supplied source and target vertices. */ private static double getEdgeWeight( - Graph classReferencesGraph, String sourceVertex, String targetVertex) { + Graph classReferencesGraph, + VertexInfo sourceVertex, + VertexInfo targetVertex) { return classReferencesGraph.getEdgeWeight(classReferencesGraph.getEdge(sourceVertex, targetVertex)); } } diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/TypeParameterReferenceTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/TypeParameterReferenceTest.java index 36330393..e1d7eabc 100644 --- a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/TypeParameterReferenceTest.java +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/TypeParameterReferenceTest.java @@ -39,6 +39,7 @@ */ class TypeParameterReferenceTest { + /** Verifies that Kotlin type-parameter bounds create class edges while a top-level typealias adds no vertex. */ @DisplayName("Kotlin class/method/property type-parameter bounds produce edges to the bound class; " + "top-level typealias does not break the visitor") @Test @@ -46,7 +47,7 @@ void detectTypeParameterBoundEdges() throws IOException { File srcDirectory = new File("src/test/resources/kotlinTypeParamSrcDirectory"); CompositeGraphBuilder compositeGraphBuilder = new CompositeGraphBuilder(); CodebaseGraphDTO dto = compositeGraphBuilder.getCodebaseGraphDTO(srcDirectory.getAbsolutePath(), false, ""); - Graph classReferencesGraph = dto.getClassReferencesGraph(); + Graph classReferencesGraph = dto.getClassReferencesGraph(); assertNotNull(classReferencesGraph); System.out.println("Vertices: " + classReferencesGraph.vertexSet()); @@ -54,18 +55,20 @@ void detectTypeParameterBoundEdges() throws IOException { // The bound class appears as a vertex. assertTrue( - classReferencesGraph.containsVertex("com.ideacrest.parser.typeparams.MetaClassA"), + classReferencesGraph.containsVertex(VertexInfo.forName("com.ideacrest.parser.typeparams.MetaClassA")), "MetaClassA should be a vertex"); // GenericHolder — class-level type param bound, // method type-param bounds, and a property of declared type // MetaClassA — all funnel into the GenericHolder -> MetaClassA edge. assertTrue( - classReferencesGraph.containsVertex("com.ideacrest.parser.typeparams.GenericHolder"), + classReferencesGraph.containsVertex( + VertexInfo.forName("com.ideacrest.parser.typeparams.GenericHolder")), "GenericHolder should be a vertex"); assertTrue( classReferencesGraph.containsEdge( - "com.ideacrest.parser.typeparams.GenericHolder", "com.ideacrest.parser.typeparams.MetaClassA"), + VertexInfo.forName("com.ideacrest.parser.typeparams.GenericHolder"), + VertexInfo.forName("com.ideacrest.parser.typeparams.MetaClassA")), "GenericHolder -> MetaClassA edge should exist (class/method/property type-parameter bounds)"); // The top-level typealias MetaList = List has no class diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/VertexInfoHydratorTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/VertexInfoHydratorTest.java new file mode 100644 index 00000000..34b6a7bd --- /dev/null +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/VertexInfoHydratorTest.java @@ -0,0 +1,214 @@ +package org.hjug.graphbuilder; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.HashMap; +import java.util.Map; +import org.jgrapht.Graph; +import org.jgrapht.graph.DefaultDirectedWeightedGraph; +import org.jgrapht.graph.DefaultWeightedEdge; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Given-When-Then unit tests for {@link VertexInfoHydrator}, which populates + * {@link VertexInfo#getLocationOnDisk()} on class- and package-graph vertices + * from the canonical repository-relative class-to-source-file mapping after + * the visitor walk. + */ +class VertexInfoHydratorTest { + + /** Creates an empty graph for testing class and package location hydration. */ + private static Graph newGraph() { + return new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + } + + /** Returns the stored graph vertex with the supplied name so its hydrated metadata can be inspected. */ + private static VertexInfo vertex(Graph graph, String name) { + return graph.vertexSet().stream() + .filter(v -> v.getName().equals(name)) + .findFirst() + .orElseThrow(); + } + + /** Verifies that a directly mapped class receives its repository-relative source path. */ + @DisplayName("Hydration sets the repo-relative source path for mapped classes") + @Test + void hydrate_mappedClass_getsLocationOnDisk() { + Graph classGraph = newGraph(); + classGraph.addVertex(VertexInfo.forName("com.foo.Bar")); + Graph packageGraph = newGraph(); + Map mapping = new HashMap<>(); + mapping.put("com.foo.Bar", "src/main/java/com/foo/Bar.java"); + + VertexInfoHydrator.hydrate(classGraph, packageGraph, mapping); + + assertEquals( + "src/main/java/com/foo/Bar.java", + vertex(classGraph, "com.foo.Bar").getLocationOnDisk()); + } + + /** Verifies that a nested class inherits its outer class's mapped source location. */ + @DisplayName("Hydration resolves a nested class through its enclosing class's mapping") + @Test + void hydrate_nestedClass_fallsBackToEnclosingClassMapping() { + Graph classGraph = newGraph(); + classGraph.addVertex(VertexInfo.forName("com.foo.Outer$Inner")); + Graph packageGraph = newGraph(); + Map mapping = new HashMap<>(); + mapping.put("com.foo.Outer", "src/main/java/com/foo/Outer.java"); + + VertexInfoHydrator.hydrate(classGraph, packageGraph, mapping); + + assertEquals( + "src/main/java/com/foo/Outer.java", + vertex(classGraph, "com.foo.Outer$Inner").getLocationOnDisk()); + } + + /** Verifies that a Kotlin anonymous vertex uses its enclosing class's source location. */ + @DisplayName("Hydration resolves a Kotlin vertex through its enclosing mapping") + @Test + void hydrate_kotlinAnonymous_fallsBackToEnclosingMapping() { + Graph classGraph = newGraph(); + classGraph.addVertex(VertexInfo.forName("dev.Control.")); + Graph packageGraph = newGraph(); + Map mapping = new HashMap<>(); + mapping.put("dev.Control", "src/main/kotlin/dev/Control.kt"); + + VertexInfoHydrator.hydrate(classGraph, packageGraph, mapping); + + assertEquals( + "src/main/kotlin/dev/Control.kt", + vertex(classGraph, "dev.Control.").getLocationOnDisk()); + } + + /** Verifies that an unmapped class keeps its parent namespace and an unknown disk location. */ + @DisplayName("Hydration leaves an unmapped class location null but keeps the derived parent namespace") + @Test + void hydrate_unmappedClass_locationStaysNullAndParentNamespaceIsKept() { + Graph classGraph = newGraph(); + classGraph.addVertex(VertexInfo.forName("com.foo.Unmapped")); + Graph packageGraph = newGraph(); + Map mapping = new HashMap<>(); + + VertexInfoHydrator.hydrate(classGraph, packageGraph, mapping); + + assertNull(vertex(classGraph, "com.foo.Unmapped").getLocationOnDisk()); + assertEquals("com.foo", vertex(classGraph, "com.foo.Unmapped").getParentNamespace()); + } + + /** Verifies that a package location is its direct member's directory and its parent namespace is retained. */ + @DisplayName("Hydration sets a package vertex's location to a member file's directory") + @Test + void hydrate_packageVertex_getsMemberFileDirectory() { + Graph classGraph = newGraph(); + Graph packageGraph = newGraph(); + packageGraph.addVertex(VertexInfo.forName("com.foo")); + Map mapping = new HashMap<>(); + mapping.put("com.foo.Bar", "src/main/java/com/foo/Bar.java"); + mapping.put("com.foo.Baz", "src/main/java/com/foo/Baz.java"); + + VertexInfoHydrator.hydrate(classGraph, packageGraph, mapping); + + assertEquals("src/main/java/com/foo", vertex(packageGraph, "com.foo").getLocationOnDisk()); + assertEquals("com", vertex(packageGraph, "com.foo").getParentNamespace()); + } + + /** Verifies that a package without mapped members has no disk location. */ + @DisplayName("Hydration leaves a package without mapped members location null") + @Test + void hydrate_packageWithoutMembers_locationStaysNull() { + Graph classGraph = newGraph(); + Graph packageGraph = newGraph(); + packageGraph.addVertex(VertexInfo.forName("com.bare")); + Map mapping = new HashMap<>(); + + VertexInfoHydrator.hydrate(classGraph, packageGraph, mapping); + + assertNull(vertex(packageGraph, "com.bare").getLocationOnDisk()); + } + + /** Verifies that subpackage members cannot supply a parent package location. */ + @DisplayName("A package whose only mapped members live in subpackages keeps a null location") + @Test + void hydrate_packageWithOnlySubpackageMembers_locationStaysNull() { + Graph classGraph = newGraph(); + Graph packageGraph = newGraph(); + packageGraph.addVertex(VertexInfo.forName("com.foo")); + Map mapping = new HashMap<>(); + mapping.put("com.foo.bar.Baz", "src/main/java/com/foo/bar/Baz.java"); + + VertexInfoHydrator.hydrate(classGraph, packageGraph, mapping); + + assertNull( + vertex(packageGraph, "com.foo").getLocationOnDisk(), + "a subpackage member's directory must not be used as the parent package's location"); + } + + /** Verifies that a nested Java class mapping can locate its enclosing package's directory. */ + @DisplayName("A nested $ class is a direct member of its outer package for location resolution") + @Test + void hydrate_packageVertex_nestedDollarClassCountsAsDirectMember() { + Graph classGraph = newGraph(); + Graph packageGraph = newGraph(); + packageGraph.addVertex(VertexInfo.forName("com.foo")); + Map mapping = new HashMap<>(); + mapping.put("com.foo.Outer$Inner", "src/main/java/com/foo/Outer.java"); + + VertexInfoHydrator.hydrate(classGraph, packageGraph, mapping); + + assertEquals("src/main/java/com/foo", vertex(packageGraph, "com.foo").getLocationOnDisk()); + } + + /** Verifies that a Kotlin anonymous class mapping contributes to its enclosing package location. */ + @DisplayName("A Kotlin member is a direct member of its package for location resolution") + @Test + void hydrate_packageVertex_kotlinAnonymousCountsAsDirectMember() { + Graph classGraph = newGraph(); + Graph packageGraph = newGraph(); + packageGraph.addVertex(VertexInfo.forName("com.foo")); + Map mapping = new HashMap<>(); + mapping.put("com.foo.Outer.", "src/main/kotlin/com/foo/Outer.kt"); + + VertexInfoHydrator.hydrate(classGraph, packageGraph, mapping); + + assertEquals("src/main/kotlin/com/foo", vertex(packageGraph, "com.foo").getLocationOnDisk()); + } + + /** Verifies that package hydration selects the shortest direct-member path and ignores subpackage paths. */ + @DisplayName("A package with multiple direct members resolves to the shortest mapped member path") + @Test + void hydrate_packageVertex_selectsShortestDirectMemberPath() { + Graph classGraph = newGraph(); + Graph packageGraph = newGraph(); + packageGraph.addVertex(VertexInfo.forName("com.foo")); + Map mapping = new HashMap<>(); + mapping.put("com.foo.Bar", "module-a/src/main/java/com/foo/Bar.java"); + mapping.put("com.foo.Baz", "src/com/foo/Baz.java"); + mapping.put("com.foo.qux.Quux", "src/com/foo/qux/Quux.java"); + + VertexInfoHydrator.hydrate(classGraph, packageGraph, mapping); + + assertEquals("src/com/foo", vertex(packageGraph, "com.foo").getLocationOnDisk()); + } + + /** Verifies that repeated hydration with the same mapping preserves class and package locations. */ + @DisplayName("Hydration is idempotent: re-hydrating does not change populated locations") + @Test + void hydrate_twice_isIdempotent() { + Graph classGraph = newGraph(); + classGraph.addVertex(VertexInfo.forName("com.foo.Bar")); + Graph packageGraph = newGraph(); + packageGraph.addVertex(VertexInfo.forName("com.foo")); + Map mapping = new HashMap<>(); + mapping.put("com.foo.Bar", "src/main/java/com/foo/Bar.java"); + + VertexInfoHydrator.hydrate(classGraph, packageGraph, mapping); + VertexInfoHydrator.hydrate(classGraph, packageGraph, mapping); + + assertEquals( + "src/main/java/com/foo/Bar.java", + vertex(classGraph, "com.foo.Bar").getLocationOnDisk()); + assertEquals("src/main/java/com/foo", vertex(packageGraph, "com.foo").getLocationOnDisk()); + } +} diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/VertexInfoTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/VertexInfoTest.java new file mode 100644 index 00000000..9dbfb3fd --- /dev/null +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/VertexInfoTest.java @@ -0,0 +1,132 @@ +package org.hjug.graphbuilder; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link VertexInfo}. + * + *

VertexInfo is the graph vertex type: it carries the vertex name plus the + * information needed to build a composed codebase map — the location on disk + * relative to the repository root and the parent namespace (parent package for + * package vertices, enclosing package/class for class vertices). + * + *

Identity is name-based so JGraphT vertex deduplication behaves exactly like + * the previous String vertices, and {@code toString()} returns the name so + * edge rendering ({@code "(source : target)"}) is unchanged. + */ +class VertexInfoTest { + + /** Verifies that a top-level class derives its package namespace and starts with no disk location. */ + @DisplayName("forName captures the vertex name and derives the enclosing package for a top-level class") + @Test + void forName_topLevelClass_parentNamespaceIsEnclosingPackage() { + VertexInfo vertex = VertexInfo.forName("com.foo.Bar"); + + assertEquals("com.foo.Bar", vertex.getName()); + assertEquals("com.foo", vertex.getParentNamespace()); + assertNull(vertex.getLocationOnDisk(), "location is hydrated later, not at creation"); + } + + /** Verifies that a nested class identifies its enclosing class as its parent namespace. */ + @DisplayName("forName derives the enclosing class for a nested class") + @Test + void forName_nestedClass_parentNamespaceIsEnclosingClass() { + VertexInfo vertex = VertexInfo.forName("com.foo.Outer$Inner"); + + assertEquals("com.foo.Outer", vertex.getParentNamespace()); + } + + /** Verifies parent derivation for anonymous, synthetic, and multiply nested Java classes. */ + @DisplayName("forName derives the enclosing class for Java anonymous and synthetic classes") + @Test + void forName_anonymousAndSyntheticClasses_parentNamespaceIsEnclosingClass() { + assertEquals("com.foo.Outer", VertexInfo.forName("com.foo.Outer$1").getParentNamespace()); + assertEquals("com.foo.Outer", VertexInfo.forName("com.foo.Outer$").getParentNamespace()); + assertEquals( + "com.foo.Outer$Inner", + VertexInfo.forName("com.foo.Outer$Inner$2").getParentNamespace()); + } + + /** Verifies that a Kotlin anonymous vertex identifies its enclosing construct. */ + @DisplayName("forName derives the enclosing class for a Kotlin vertex") + @Test + void forName_kotlinAnonymous_parentNamespaceIsEnclosingConstruct() { + VertexInfo vertex = VertexInfo.forName("dev.DeveloperWASDControl."); + + assertEquals("dev.DeveloperWASDControl", vertex.getParentNamespace()); + } + + /** Verifies that unqualified classes and standalone anonymous vertices have no parent namespace. */ + @DisplayName("forName yields no parent namespace for packageless and top-level anonymous vertices") + @Test + void forName_packagelessVertex_parentNamespaceIsNull() { + assertNull(VertexInfo.forName("Bar").getParentNamespace()); + assertNull(VertexInfo.forName("").getParentNamespace()); + } + + /** Verifies that package vertices derive parent packages and top-level packages have no parent. */ + @DisplayName("forName derives the parent package for a package vertex") + @Test + void forName_packageVertex_parentNamespaceIsParentPackage() { + VertexInfo vertex = VertexInfo.forName("com.foo"); + + assertEquals("com", vertex.getParentNamespace()); + assertNull(VertexInfo.forName("com").getParentNamespace(), "top-level package has no parent"); + } + + /** Verifies that vertex identity depends only on the name and is unaffected by location hydration. */ + @DisplayName("VertexInfo equality and hashing are name-based only") + @Test + void equalsAndHashCode_considerOnlyTheName() { + VertexInfo bare = VertexInfo.forName("com.foo.Bar"); + VertexInfo hydrated = VertexInfo.forName("com.foo.Bar"); + hydrated.setLocationOnDisk("src/main/java/com/foo/Bar.java"); + + assertEquals(bare, hydrated); + assertEquals(bare.hashCode(), hydrated.hashCode()); + assertNotEquals(bare, VertexInfo.forName("com.foo.Baz")); + } + + /** Verifies that a fresh vertex probe finds a graph vertex with the same name. */ + @DisplayName("A VertexInfo probe created by forName is found in a graph by name") + @Test + void probe_matchesGraphVertexWithSameName() { + org.jgrapht.Graph graph = + new org.jgrapht.graph.DefaultDirectedWeightedGraph<>(org.jgrapht.graph.DefaultWeightedEdge.class); + VertexInfo collected = VertexInfo.forName("com.foo.Bar"); + graph.addVertex(collected); + + assertTrue(graph.containsVertex(VertexInfo.forName("com.foo.Bar"))); + assertTrue(graph.vertexSet().contains(collected)); + } + + /** Verifies that string conversion preserves named and anonymous vertex names for edge rendering. */ + @DisplayName("toString returns the vertex name so edge rendering is unchanged") + @Test + void toString_returnsName() { + assertEquals("com.foo.Bar", VertexInfo.forName("com.foo.Bar").toString()); + assertEquals("", VertexInfo.forName("").toString()); + } + + /** Verifies that hydration can assign a repository-relative disk location. */ + @DisplayName("locationOnDisk can be set during hydration and read back") + @Test + void locationOnDisk_canBeHydrated() { + VertexInfo vertex = VertexInfo.forName("com.foo.Bar"); + + vertex.setLocationOnDisk("src/main/java/com/foo/Bar.java"); + + assertEquals("src/main/java/com/foo/Bar.java", vertex.getLocationOnDisk()); + } + + /** Verifies that vertex creation rejects null and empty names with the expected exceptions. */ + @DisplayName("forName rejects null and empty names") + @Test + void forName_rejectsNullAndEmptyNames() { + assertThrows(NullPointerException.class, () -> VertexInfo.forName(null)); + assertThrows(IllegalArgumentException.class, () -> VertexInfo.forName("")); + } +} diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/graphbuilder/JavaSourceFileGraphBuilderPrunesClassesNotInCodebaseTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/graphbuilder/JavaSourceFileGraphBuilderPrunesClassesNotInCodebaseTest.java index 6333a6fc..1ea17af1 100644 --- a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/graphbuilder/JavaSourceFileGraphBuilderPrunesClassesNotInCodebaseTest.java +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/graphbuilder/JavaSourceFileGraphBuilderPrunesClassesNotInCodebaseTest.java @@ -9,6 +9,7 @@ import java.util.Set; import org.hjug.graphbuilder.CodebaseGraphDTO; import org.hjug.graphbuilder.CompositeGraphBuilder; +import org.hjug.graphbuilder.VertexInfo; import org.jgrapht.Graph; import org.jgrapht.graph.DefaultWeightedEdge; import org.junit.jupiter.api.DisplayName; @@ -24,55 +25,61 @@ */ class JavaSourceFileGraphBuilderPrunesClassesNotInCodebaseTest { + /** Verifies that pruning external classes also removes their incident dependency edges. */ @Test void removeClassesNotInCodebase() throws IOException { File srcDirectory = new File("src/test/resources/javaSrcDirectory"); CompositeGraphBuilder compositeGraphBuilder = new CompositeGraphBuilder(); CodebaseGraphDTO dto = compositeGraphBuilder.getCodebaseGraphDTO(srcDirectory.getAbsolutePath(), false, ""); - Graph classReferencesGraph = dto.getClassReferencesGraph(); - classReferencesGraph.addVertex("org.favioriteoss.FunClass"); - classReferencesGraph.addVertex("org.favioriteoss.AnotherFunClass"); + Graph classReferencesGraph = dto.getClassReferencesGraph(); + classReferencesGraph.addVertex(VertexInfo.forName("org.favioriteoss.FunClass")); + classReferencesGraph.addVertex(VertexInfo.forName("org.favioriteoss.AnotherFunClass")); - DefaultWeightedEdge edge1 = - classReferencesGraph.addEdge("com.ideacrest.parser.testclasses.A", "org.favioriteoss.FunClass"); - DefaultWeightedEdge edge2 = - classReferencesGraph.addEdge("com.ideacrest.parser.testclasses.A", "org.favioriteoss.AnotherFunClass"); + DefaultWeightedEdge edge1 = classReferencesGraph.addEdge( + VertexInfo.forName("com.ideacrest.parser.testclasses.A"), + VertexInfo.forName("org.favioriteoss.FunClass")); + DefaultWeightedEdge edge2 = classReferencesGraph.addEdge( + VertexInfo.forName("com.ideacrest.parser.testclasses.A"), + VertexInfo.forName("org.favioriteoss.AnotherFunClass")); - assertTrue(classReferencesGraph.containsVertex("org.favioriteoss.FunClass")); - assertTrue(classReferencesGraph.containsVertex("org.favioriteoss.AnotherFunClass")); + assertTrue(classReferencesGraph.containsVertex(VertexInfo.forName("org.favioriteoss.FunClass"))); + assertTrue(classReferencesGraph.containsVertex(VertexInfo.forName("org.favioriteoss.AnotherFunClass"))); Set packagesInCodebase = new HashSet<>(); packagesInCodebase.add("com.ideacrest.parser.testclasses"); JavaSourceFileGraphBuilder.removeClassesNotInCodebase(packagesInCodebase, classReferencesGraph); - assertFalse(classReferencesGraph.containsVertex("org.favioriteoss.FunClass")); - assertFalse(classReferencesGraph.containsVertex("org.favioriteoss.AnotherFunClass")); + assertFalse(classReferencesGraph.containsVertex(VertexInfo.forName("org.favioriteoss.FunClass"))); + assertFalse(classReferencesGraph.containsVertex(VertexInfo.forName("org.favioriteoss.AnotherFunClass"))); assertFalse(classReferencesGraph.containsEdge(edge1)); assertFalse(classReferencesGraph.containsEdge(edge2)); } + /** Verifies that package pruning removes vertices outside the set of codebase packages. */ @Test @DisplayName("removePackagesNotInCodebase drops packages outside the codebase package set") void removePackagesNotInCodebase() throws IOException { File srcDirectory = new File("src/test/resources/javaSrcDirectory"); CompositeGraphBuilder compositeGraphBuilder = new CompositeGraphBuilder(); CodebaseGraphDTO dto = compositeGraphBuilder.getCodebaseGraphDTO(srcDirectory.getAbsolutePath(), false, ""); - Graph packageReferencesGraph = dto.getPackageReferencesGraph(); - packageReferencesGraph.addVertex("org.favioriteoss"); + Graph packageReferencesGraph = dto.getPackageReferencesGraph(); + packageReferencesGraph.addVertex(VertexInfo.forName("org.favioriteoss")); - assertTrue(packageReferencesGraph.containsVertex("org.favioriteoss")); + assertTrue(packageReferencesGraph.containsVertex(VertexInfo.forName("org.favioriteoss"))); - Set packagesInCodebase = - new HashSet<>(dto.getPackageReferencesGraph().vertexSet()); + Set packagesInCodebase = dto.getPackageReferencesGraph().vertexSet().stream() + .map(VertexInfo::getName) + .collect(java.util.stream.Collectors.toCollection(HashSet::new)); // remove the rogue package from the "in codebase" set packagesInCodebase.remove("org.favioriteoss"); JavaSourceFileGraphBuilder.removePackagesNotInCodebase(packagesInCodebase, packageReferencesGraph); - assertFalse(packageReferencesGraph.containsVertex("org.favioriteoss")); + assertFalse(packageReferencesGraph.containsVertex(VertexInfo.forName("org.favioriteoss"))); } + /** Verifies that JavaFX library references are excluded from the final codebase graph. */ @Test @DisplayName("External JavaFX classes referenced in source are pruned from class graph") void externalJavaFXClassesArePruned(@TempDir File tempDir) throws IOException { @@ -94,13 +101,13 @@ public class MyApp { CodebaseGraphDTO dto = builder.getCodebaseGraphDTO(tempDir.getAbsolutePath(), false, ""); // JavaFX classes should NOT be in the class graph - assertFalse(dto.getClassReferencesGraph().containsVertex("javafx.scene.control.Button")); - assertFalse(dto.getClassReferencesGraph().containsVertex("javafx.scene.layout.Pane")); + assertFalse(dto.getClassReferencesGraph().containsVertex(VertexInfo.forName("javafx.scene.control.Button"))); + assertFalse(dto.getClassReferencesGraph().containsVertex(VertexInfo.forName("javafx.scene.layout.Pane"))); // Fabricated versions (attributed to caller's package) should also be removed - assertFalse(dto.getClassReferencesGraph().containsVertex("com.myapp.Button")); - assertFalse(dto.getClassReferencesGraph().containsVertex("com.myapp.Pane")); + assertFalse(dto.getClassReferencesGraph().containsVertex(VertexInfo.forName("com.myapp.Button"))); + assertFalse(dto.getClassReferencesGraph().containsVertex(VertexInfo.forName("com.myapp.Pane"))); // Only the actual codebase class should remain - assertTrue(dto.getClassReferencesGraph().containsVertex("com.myapp.MyApp")); + assertTrue(dto.getClassReferencesGraph().containsVertex(VertexInfo.forName("com.myapp.MyApp"))); } } diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/graphbuilder/KotlinSourceFileGraphBuilderPartialParseTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/graphbuilder/KotlinSourceFileGraphBuilderPartialParseTest.java index 01cd7f6f..5ab6305d 100644 --- a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/graphbuilder/KotlinSourceFileGraphBuilderPartialParseTest.java +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/graphbuilder/KotlinSourceFileGraphBuilderPartialParseTest.java @@ -8,6 +8,7 @@ import java.util.Map; import org.hjug.graphbuilder.CodebaseGraphDTO; import org.hjug.graphbuilder.GraphBuilderConfig; +import org.hjug.graphbuilder.VertexInfo; import org.jgrapht.Graph; import org.jgrapht.graph.DefaultWeightedEdge; import org.junit.jupiter.api.DisplayName; @@ -23,6 +24,7 @@ class KotlinSourceFileGraphBuilderPartialParseTest { @TempDir Path tempDir; + /** Verifies that the license-header fixture retains class vertices and source paths during parse recovery. */ @DisplayName("Kotlin file with license header parse error still registers classes from partial parse tree") @Test void kotlinFileWithLicenseHeaderParseError_registersClassesFromPartialTree() throws IOException { @@ -59,13 +61,13 @@ class ReadOnlyGameSettings internal constructor( CodebaseGraphDTO dto = builder.buildGraph(tempDir.toString(), "", config); // The classes should be registered despite the ParseError - Graph classGraph = dto.getClassReferencesGraph(); + Graph classGraph = dto.getClassReferencesGraph(); assertTrue( - classGraph.containsVertex("com.example.parseerror.GameSettings"), + classGraph.containsVertex(VertexInfo.forName("com.example.parseerror.GameSettings")), "GameSettings class should be registered despite ParseError"); assertTrue( - classGraph.containsVertex("com.example.parseerror.ReadOnlyGameSettings"), + classGraph.containsVertex(VertexInfo.forName("com.example.parseerror.ReadOnlyGameSettings")), "ReadOnlyGameSettings class should be registered despite ParseError"); // Verify the source path mapping is correct @@ -84,6 +86,7 @@ class ReadOnlyGameSettings internal constructor( "Source path should point to Settings.kt, got: " + gameSettingsPath); } + /** Verifies that an ordinary Kotlin source file registers its class vertex. */ @DisplayName("Kotlin file without parse error still works normally") @Test void kotlinFileWithoutParseError_worksNormally() throws IOException { @@ -106,11 +109,14 @@ class NormalClass { CodebaseGraphDTO dto = builder.buildGraph(tempDir.toString(), "", config); - Graph classGraph = dto.getClassReferencesGraph(); + Graph classGraph = dto.getClassReferencesGraph(); - assertTrue(classGraph.containsVertex("com.example.normal.NormalClass"), "NormalClass should be registered"); + assertTrue( + classGraph.containsVertex(VertexInfo.forName("com.example.normal.NormalClass")), + "NormalClass should be registered"); } + /** Verifies that an unrecoverable Kotlin input returns a DTO without throwing. */ @DisplayName("ParseError without partial tree is handled gracefully") @Test void parseErrorWithoutPartialTree_handledGracefully() throws IOException { @@ -136,12 +142,13 @@ fun incomplete() { CodebaseGraphDTO dto = builder.buildGraph(tempDir.toString(), "", config); // The graph might be empty or have no vertices for this file, but shouldn't crash - Graph classGraph = dto.getClassReferencesGraph(); + Graph classGraph = dto.getClassReferencesGraph(); // Just verify it doesn't throw and returns a valid DTO assertNotNull(dto); assertNotNull(classGraph); } + /** Verifies that the parse-recovery fixture retains all classes declared in the source file. */ @DisplayName("Multiple classes in file with ParseError are all registered") @Test void multipleClassesInFileWithParseError_allRegistered() throws IOException { @@ -172,12 +179,17 @@ data class ThirdDataClass(val value: String) CodebaseGraphDTO dto = builder.buildGraph(tempDir.toString(), "", config); - Graph classGraph = dto.getClassReferencesGraph(); + Graph classGraph = dto.getClassReferencesGraph(); - assertTrue(classGraph.containsVertex("com.example.multi.FirstClass"), "FirstClass should be registered"); - assertTrue(classGraph.containsVertex("com.example.multi.SecondClass"), "SecondClass should be registered"); assertTrue( - classGraph.containsVertex("com.example.multi.ThirdDataClass"), "ThirdDataClass should be registered"); + classGraph.containsVertex(VertexInfo.forName("com.example.multi.FirstClass")), + "FirstClass should be registered"); + assertTrue( + classGraph.containsVertex(VertexInfo.forName("com.example.multi.SecondClass")), + "SecondClass should be registered"); + assertTrue( + classGraph.containsVertex(VertexInfo.forName("com.example.multi.ThirdDataClass")), + "ThirdDataClass should be registered"); // Verify all have source path mappings Map pathMapping = dto.getClassToSourceFilePathMapping(); diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/graphbuilder/SourceFileGraphBuilderTestPathExclusionTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/graphbuilder/SourceFileGraphBuilderTestPathExclusionTest.java index 66e61832..294ea93b 100644 --- a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/graphbuilder/SourceFileGraphBuilderTestPathExclusionTest.java +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/graphbuilder/SourceFileGraphBuilderTestPathExclusionTest.java @@ -7,6 +7,7 @@ import java.nio.file.Path; import org.hjug.graphbuilder.CodebaseGraphDTO; import org.hjug.graphbuilder.GraphBuilderConfig; +import org.hjug.graphbuilder.VertexInfo; import org.jgrapht.Graph; import org.jgrapht.graph.DefaultWeightedEdge; import org.junit.jupiter.api.DisplayName; @@ -31,6 +32,7 @@ void directoryMatchingDoesNotExcludeLongerDirectoryNames() { Path.of("project/src/testFixtures/java/Example.java"), "src/test")); } + /** Verifies that Java test exclusion recognizes a forward-slash directory pattern. */ @DisplayName("Java builder excludes test files with forward-slash testSourceDirectory on Windows-style paths") @Test void javaBuilder_excludesTestsWithForwardSlashPatternOnWindowsPaths(@TempDir Path tempDir) throws IOException { @@ -57,15 +59,18 @@ void javaBuilder_excludesTestsWithForwardSlashPatternOnWindowsPaths(@TempDir Pat .build(); CodebaseGraphDTO dto = builder.buildGraph(tempDir.toString(), "", config); - Graph classGraph = dto.getClassReferencesGraph(); + Graph classGraph = dto.getClassReferencesGraph(); // Main class should be included - assertTrue(classGraph.containsVertex("com.example.Main"), "Main class should be in graph"); + assertTrue(classGraph.containsVertex(VertexInfo.forName("com.example.Main")), "Main class should be in graph"); // Test class should be EXCLUDED (this verifies the fix works) - assertFalse(classGraph.containsVertex("com.example.MainTest"), "MainTest class should be excluded from graph"); + assertFalse( + classGraph.containsVertex(VertexInfo.forName("com.example.MainTest")), + "MainTest class should be excluded from graph"); } + /** Verifies Java test exclusion with a backslash-separated directory pattern. */ @DisplayName("Java builder excludes test files with backslash testSourceDirectory") @Test void javaBuilder_excludesTestsWithBackslashPattern(@TempDir Path tempDir) throws IOException { @@ -89,14 +94,15 @@ void javaBuilder_excludesTestsWithBackslashPattern(@TempDir Path tempDir) throws .build(); CodebaseGraphDTO dto = builder.buildGraph(tempDir.toString(), "", config); - Graph classGraph = dto.getClassReferencesGraph(); + Graph classGraph = dto.getClassReferencesGraph(); - assertTrue(classGraph.containsVertex("com.example.Main"), "Main class should be in graph"); + assertTrue(classGraph.containsVertex(VertexInfo.forName("com.example.Main")), "Main class should be in graph"); assertFalse( - classGraph.containsVertex("com.example.MainTest"), + classGraph.containsVertex(VertexInfo.forName("com.example.MainTest")), "MainTest class should be excluded with backslash pattern"); } + /** Verifies that Kotlin test exclusion recognizes a forward-slash directory pattern. */ @DisplayName("Kotlin builder excludes test files with forward-slash testSourceDirectory on Windows-style paths") @Test void kotlinBuilder_excludesTestsWithForwardSlashPatternOnWindowsPaths(@TempDir Path tempDir) throws IOException { @@ -116,12 +122,15 @@ void kotlinBuilder_excludesTestsWithForwardSlashPatternOnWindowsPaths(@TempDir P .build(); CodebaseGraphDTO dto = builder.buildGraph(tempDir.toString(), "", config); - Graph classGraph = dto.getClassReferencesGraph(); + Graph classGraph = dto.getClassReferencesGraph(); - assertTrue(classGraph.containsVertex("com.example.Main"), "Main class should be in graph"); - assertFalse(classGraph.containsVertex("com.example.MainTest"), "MainTest class should be excluded from graph"); + assertTrue(classGraph.containsVertex(VertexInfo.forName("com.example.Main")), "Main class should be in graph"); + assertFalse( + classGraph.containsVertex(VertexInfo.forName("com.example.MainTest")), + "MainTest class should be excluded from graph"); } + /** Verifies Kotlin test exclusion with a backslash-separated directory pattern. */ @DisplayName("Kotlin builder excludes test files with backslash testSourceDirectory") @Test void kotlinBuilder_excludesTestsWithBackslashPattern(@TempDir Path tempDir) throws IOException { @@ -141,14 +150,15 @@ void kotlinBuilder_excludesTestsWithBackslashPattern(@TempDir Path tempDir) thro .build(); CodebaseGraphDTO dto = builder.buildGraph(tempDir.toString(), "", config); - Graph classGraph = dto.getClassReferencesGraph(); + Graph classGraph = dto.getClassReferencesGraph(); - assertTrue(classGraph.containsVertex("com.example.Main"), "Main class should be in graph"); + assertTrue(classGraph.containsVertex(VertexInfo.forName("com.example.Main")), "Main class should be in graph"); assertFalse( - classGraph.containsVertex("com.example.MainTest"), + classGraph.containsVertex(VertexInfo.forName("com.example.MainTest")), "MainTest class should be excluded with backslash pattern"); } + /** Verifies that Java analysis retains test classes when test exclusion is disabled. */ @DisplayName("Java builder includes test files when excludeTests=false") @Test void javaBuilder_includesTestsWhenExcludeTestsFalse(@TempDir Path tempDir) throws IOException { @@ -171,14 +181,15 @@ void javaBuilder_includesTestsWhenExcludeTestsFalse(@TempDir Path tempDir) throw .build(); CodebaseGraphDTO dto = builder.buildGraph(tempDir.toString(), "", config); - Graph classGraph = dto.getClassReferencesGraph(); + Graph classGraph = dto.getClassReferencesGraph(); - assertTrue(classGraph.containsVertex("com.example.Main"), "Main class should be in graph"); + assertTrue(classGraph.containsVertex(VertexInfo.forName("com.example.Main")), "Main class should be in graph"); assertTrue( - classGraph.containsVertex("com.example.MainTest"), + classGraph.containsVertex(VertexInfo.forName("com.example.MainTest")), "MainTest class should be included when excludeTests=false"); } + /** Verifies that Kotlin analysis retains test classes when test exclusion is disabled. */ @DisplayName("Kotlin builder includes test files when excludeTests=false") @Test void kotlinBuilder_includesTestsWhenExcludeTestsFalse(@TempDir Path tempDir) throws IOException { @@ -198,11 +209,11 @@ void kotlinBuilder_includesTestsWhenExcludeTestsFalse(@TempDir Path tempDir) thr .build(); CodebaseGraphDTO dto = builder.buildGraph(tempDir.toString(), "", config); - Graph classGraph = dto.getClassReferencesGraph(); + Graph classGraph = dto.getClassReferencesGraph(); - assertTrue(classGraph.containsVertex("com.example.Main"), "Main class should be in graph"); + assertTrue(classGraph.containsVertex(VertexInfo.forName("com.example.Main")), "Main class should be in graph"); assertTrue( - classGraph.containsVertex("com.example.MainTest"), + classGraph.containsVertex(VertexInfo.forName("com.example.MainTest")), "MainTest class should be included when excludeTests=false"); } } diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/metrics/KotlinDisharmonyParityTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/metrics/KotlinDisharmonyParityTest.java index 748609fd..cd356199 100644 --- a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/metrics/KotlinDisharmonyParityTest.java +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/metrics/KotlinDisharmonyParityTest.java @@ -10,6 +10,7 @@ import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.hjug.graphbuilder.VertexInfo; import org.hjug.graphbuilder.metrics.DisharmonyDetector.ClassDisharmony; import org.hjug.graphbuilder.metrics.DisharmonyDetector.MethodDisharmony; import org.jgrapht.graph.DefaultDirectedWeightedGraph; @@ -70,9 +71,9 @@ private static GraphMetricsCollector loadDirectory(String directory) throws IOEx .build(); ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace); - DefaultDirectedWeightedGraph classGraph = + DefaultDirectedWeightedGraph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - DefaultDirectedWeightedGraph packageGraph = + DefaultDirectedWeightedGraph packageGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); GraphMetricsCollector metricsCollector = new GraphMetricsCollector(classGraph, packageGraph); diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/metrics/KotlinDisharmonyTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/metrics/KotlinDisharmonyTest.java index 61ec734b..3f0f527e 100644 --- a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/metrics/KotlinDisharmonyTest.java +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/metrics/KotlinDisharmonyTest.java @@ -9,6 +9,7 @@ import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.hjug.graphbuilder.VertexInfo; import org.hjug.graphbuilder.metrics.DisharmonyDetector.ClassDisharmony; import org.jgrapht.graph.DefaultDirectedWeightedGraph; import org.jgrapht.graph.DefaultWeightedEdge; @@ -51,9 +52,9 @@ private GraphMetricsCollector loadFixtures() throws IOException { .build(); ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace); - DefaultDirectedWeightedGraph classGraph = + DefaultDirectedWeightedGraph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - DefaultDirectedWeightedGraph packageGraph = + DefaultDirectedWeightedGraph packageGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); GraphMetricsCollector metricsCollector = new GraphMetricsCollector(classGraph, packageGraph); diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/metrics/KotlinMetricsCollectionTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/metrics/KotlinMetricsCollectionTest.java index 52557fb9..ab78a01b 100644 --- a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/metrics/KotlinMetricsCollectionTest.java +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/metrics/KotlinMetricsCollectionTest.java @@ -8,6 +8,7 @@ import java.nio.file.Path; import java.util.List; import java.util.stream.Collectors; +import org.hjug.graphbuilder.VertexInfo; import org.jgrapht.graph.DefaultDirectedWeightedGraph; import org.jgrapht.graph.DefaultWeightedEdge; import org.junit.jupiter.api.DisplayName; @@ -40,9 +41,9 @@ void collectKotlinClassMetrics() throws IOException { .build(); ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace); - DefaultDirectedWeightedGraph classGraph = + DefaultDirectedWeightedGraph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - DefaultDirectedWeightedGraph packageGraph = + DefaultDirectedWeightedGraph packageGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); GraphMetricsCollector metricsCollector = new GraphMetricsCollector(classGraph, packageGraph); @@ -90,9 +91,9 @@ void detectKotlinGodClass() throws IOException { .build(); ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace); - DefaultDirectedWeightedGraph classGraph = + DefaultDirectedWeightedGraph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - DefaultDirectedWeightedGraph packageGraph = + DefaultDirectedWeightedGraph packageGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); GraphMetricsCollector metricsCollector = new GraphMetricsCollector(classGraph, packageGraph); @@ -138,9 +139,9 @@ void collectKotlinCallableReferenceCount() throws IOException { .build(); ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace); - DefaultDirectedWeightedGraph classGraph = + DefaultDirectedWeightedGraph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - DefaultDirectedWeightedGraph packageGraph = + DefaultDirectedWeightedGraph packageGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); GraphMetricsCollector metricsCollector = new GraphMetricsCollector(classGraph, packageGraph); @@ -186,9 +187,9 @@ void collectTypeParameterFqnsMetrics() throws IOException { .build(); ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace); - DefaultDirectedWeightedGraph classGraph = + DefaultDirectedWeightedGraph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - DefaultDirectedWeightedGraph packageGraph = + DefaultDirectedWeightedGraph packageGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); GraphMetricsCollector metricsCollector = new GraphMetricsCollector(classGraph, packageGraph); diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/metrics/KotlinPropertyMetricsTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/metrics/KotlinPropertyMetricsTest.java index 553e6b38..df775d5a 100644 --- a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/metrics/KotlinPropertyMetricsTest.java +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/metrics/KotlinPropertyMetricsTest.java @@ -8,6 +8,7 @@ import java.nio.file.Path; import java.util.List; import java.util.stream.Collectors; +import org.hjug.graphbuilder.VertexInfo; import org.jgrapht.graph.DefaultDirectedWeightedGraph; import org.jgrapht.graph.DefaultWeightedEdge; import org.junit.jupiter.api.DisplayName; @@ -37,9 +38,9 @@ void collectKotlinPropertyMetrics() throws IOException { .build(); ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace); - DefaultDirectedWeightedGraph classGraph = + DefaultDirectedWeightedGraph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - DefaultDirectedWeightedGraph packageGraph = + DefaultDirectedWeightedGraph packageGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); GraphMetricsCollector metricsCollector = new GraphMetricsCollector(classGraph, packageGraph); diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/metrics/MetricsCollectionTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/metrics/MetricsCollectionTest.java index dd016121..044b97ef 100644 --- a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/metrics/MetricsCollectionTest.java +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/metrics/MetricsCollectionTest.java @@ -6,6 +6,7 @@ import java.nio.file.Path; import java.util.List; import java.util.stream.Collectors; +import org.hjug.graphbuilder.VertexInfo; import org.jgrapht.graph.DefaultDirectedWeightedGraph; import org.jgrapht.graph.DefaultWeightedEdge; import org.junit.jupiter.api.Assertions; @@ -23,9 +24,9 @@ void collectClassMetrics() throws IOException { JavaParser javaParser = JavaParser.fromJavaVersion().build(); ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace); - DefaultDirectedWeightedGraph classGraph = + DefaultDirectedWeightedGraph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - DefaultDirectedWeightedGraph packageGraph = + DefaultDirectedWeightedGraph packageGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); GraphMetricsCollector metricsCollector = new GraphMetricsCollector(classGraph, packageGraph); @@ -70,9 +71,9 @@ void collectMethodMetrics() throws IOException { JavaParser javaParser = JavaParser.fromJavaVersion().build(); ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace); - DefaultDirectedWeightedGraph classGraph = + DefaultDirectedWeightedGraph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - DefaultDirectedWeightedGraph packageGraph = + DefaultDirectedWeightedGraph packageGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); GraphMetricsCollector metricsCollector = new GraphMetricsCollector(classGraph, packageGraph); @@ -117,9 +118,9 @@ void detectGodClass() throws IOException { JavaParser javaParser = JavaParser.fromJavaVersion().build(); ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace); - DefaultDirectedWeightedGraph classGraph = + DefaultDirectedWeightedGraph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - DefaultDirectedWeightedGraph packageGraph = + DefaultDirectedWeightedGraph packageGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); GraphMetricsCollector metricsCollector = new GraphMetricsCollector(classGraph, packageGraph); @@ -177,9 +178,9 @@ void detectDataClass() throws IOException { JavaParser javaParser = JavaParser.fromJavaVersion().build(); ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace); - DefaultDirectedWeightedGraph classGraph = + DefaultDirectedWeightedGraph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - DefaultDirectedWeightedGraph packageGraph = + DefaultDirectedWeightedGraph packageGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); GraphMetricsCollector metricsCollector = new GraphMetricsCollector(classGraph, packageGraph); @@ -230,9 +231,9 @@ void detectBrainMethod() throws IOException { JavaParser javaParser = JavaParser.fromJavaVersion().build(); ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace); - DefaultDirectedWeightedGraph classGraph = + DefaultDirectedWeightedGraph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - DefaultDirectedWeightedGraph packageGraph = + DefaultDirectedWeightedGraph packageGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); GraphMetricsCollector metricsCollector = new GraphMetricsCollector(classGraph, packageGraph); @@ -266,9 +267,9 @@ void detectBrainClass() throws IOException { JavaParser javaParser = JavaParser.fromJavaVersion().build(); ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace); - DefaultDirectedWeightedGraph classGraph = + DefaultDirectedWeightedGraph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - DefaultDirectedWeightedGraph packageGraph = + DefaultDirectedWeightedGraph packageGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); GraphMetricsCollector metricsCollector = new GraphMetricsCollector(classGraph, packageGraph); @@ -341,9 +342,9 @@ void detectFeatureEnvy() throws IOException { JavaParser javaParser = JavaParser.fromJavaVersion().build(); ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace); - DefaultDirectedWeightedGraph classGraph = + DefaultDirectedWeightedGraph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - DefaultDirectedWeightedGraph packageGraph = + DefaultDirectedWeightedGraph packageGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); GraphMetricsCollector metricsCollector = new GraphMetricsCollector(classGraph, packageGraph); @@ -423,9 +424,9 @@ void detectIntensiveCoupling() throws IOException { JavaParser javaParser = JavaParser.fromJavaVersion().build(); ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace); - DefaultDirectedWeightedGraph classGraph = + DefaultDirectedWeightedGraph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - DefaultDirectedWeightedGraph packageGraph = + DefaultDirectedWeightedGraph packageGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); GraphMetricsCollector metricsCollector = new GraphMetricsCollector(classGraph, packageGraph); @@ -499,9 +500,9 @@ void detectDispersedCoupling() throws IOException { JavaParser javaParser = JavaParser.fromJavaVersion().build(); ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace); - DefaultDirectedWeightedGraph classGraph = + DefaultDirectedWeightedGraph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - DefaultDirectedWeightedGraph packageGraph = + DefaultDirectedWeightedGraph packageGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); GraphMetricsCollector metricsCollector = new GraphMetricsCollector(classGraph, packageGraph); @@ -575,9 +576,9 @@ void detectShotgunSurgery() throws IOException { JavaParser javaParser = JavaParser.fromJavaVersion().build(); ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace); - DefaultDirectedWeightedGraph classGraph = + DefaultDirectedWeightedGraph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - DefaultDirectedWeightedGraph packageGraph = + DefaultDirectedWeightedGraph packageGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); GraphMetricsCollector metricsCollector = new GraphMetricsCollector(classGraph, packageGraph); @@ -647,9 +648,9 @@ void detectRefusedParentBequest() throws IOException { JavaParser javaParser = JavaParser.fromJavaVersion().build(); ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace); - DefaultDirectedWeightedGraph classGraph = + DefaultDirectedWeightedGraph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - DefaultDirectedWeightedGraph packageGraph = + DefaultDirectedWeightedGraph packageGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); GraphMetricsCollector metricsCollector = new GraphMetricsCollector(classGraph, packageGraph); @@ -737,9 +738,9 @@ void detectTraditionBreaker() throws IOException { JavaParser javaParser = JavaParser.fromJavaVersion().build(); ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace); - DefaultDirectedWeightedGraph classGraph = + DefaultDirectedWeightedGraph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - DefaultDirectedWeightedGraph packageGraph = + DefaultDirectedWeightedGraph packageGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); GraphMetricsCollector metricsCollector = new GraphMetricsCollector(classGraph, packageGraph); @@ -820,9 +821,9 @@ void sourceFilePathCapturedForAllClasses() throws IOException { JavaParser javaParser = JavaParser.fromJavaVersion().build(); ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace); - DefaultDirectedWeightedGraph classGraph = + DefaultDirectedWeightedGraph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - DefaultDirectedWeightedGraph packageGraph = + DefaultDirectedWeightedGraph packageGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); GraphMetricsCollector metricsCollector = new GraphMetricsCollector(classGraph, packageGraph); @@ -858,9 +859,9 @@ void javadocMethodReferenceIsNotCountedAsForeignMethodCall() throws IOException JavaParser javaParser = JavaParser.fromJavaVersion().build(); ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace); - DefaultDirectedWeightedGraph classGraph = + DefaultDirectedWeightedGraph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - DefaultDirectedWeightedGraph packageGraph = + DefaultDirectedWeightedGraph packageGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); GraphMetricsCollector metricsCollector = new GraphMetricsCollector(classGraph, packageGraph); @@ -891,9 +892,9 @@ void collectRecordClassMetrics() throws IOException { JavaParser javaParser = JavaParser.fromJavaVersion().build(); ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace); - DefaultDirectedWeightedGraph classGraph = + DefaultDirectedWeightedGraph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - DefaultDirectedWeightedGraph packageGraph = + DefaultDirectedWeightedGraph packageGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); GraphMetricsCollector metricsCollector = new GraphMetricsCollector(classGraph, packageGraph); diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/metrics/MetricsVisitorLogicIdentityTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/metrics/MetricsVisitorLogicIdentityTest.java index 66e88c15..cc4b12e2 100644 --- a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/metrics/MetricsVisitorLogicIdentityTest.java +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/metrics/MetricsVisitorLogicIdentityTest.java @@ -7,6 +7,7 @@ import java.nio.file.Path; import java.util.Collections; import java.util.List; +import org.hjug.graphbuilder.VertexInfo; import org.jgrapht.graph.DefaultDirectedWeightedGraph; import org.jgrapht.graph.DefaultWeightedEdge; import org.junit.jupiter.api.DisplayName; @@ -128,9 +129,9 @@ void visitorMutatedInstanceIsImmutablePostFinalize(@TempDir Path tempDir) throws } private static GraphMetricsCollector newGraphMetricsCollector() { - DefaultDirectedWeightedGraph classGraph = + DefaultDirectedWeightedGraph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - DefaultDirectedWeightedGraph packageGraph = + DefaultDirectedWeightedGraph packageGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); return new GraphMetricsCollector(classGraph, packageGraph); } diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/metrics/SignificantDuplicationTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/metrics/SignificantDuplicationTest.java index 4236fd1a..bd9490a0 100644 --- a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/metrics/SignificantDuplicationTest.java +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/metrics/SignificantDuplicationTest.java @@ -6,6 +6,7 @@ import java.nio.file.Path; import java.util.List; import java.util.stream.Collectors; +import org.hjug.graphbuilder.VertexInfo; import org.jgrapht.graph.DefaultDirectedWeightedGraph; import org.jgrapht.graph.DefaultWeightedEdge; import org.junit.jupiter.api.Assertions; @@ -38,9 +39,9 @@ void setup() throws IOException { JavaParser javaParser = JavaParser.fromJavaVersion().build(); ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace); - DefaultDirectedWeightedGraph classGraph = + DefaultDirectedWeightedGraph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - DefaultDirectedWeightedGraph packageGraph = + DefaultDirectedWeightedGraph packageGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); metricsCollector = new GraphMetricsCollector(classGraph, packageGraph); diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/DependencyVisitorLogicJavaKotlinParityTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/DependencyVisitorLogicJavaKotlinParityTest.java index 87b21742..6e0b3a9f 100644 --- a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/DependencyVisitorLogicJavaKotlinParityTest.java +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/DependencyVisitorLogicJavaKotlinParityTest.java @@ -9,6 +9,7 @@ import java.util.HashMap; import java.util.Map; import org.hjug.graphbuilder.GraphDependencyCollector; +import org.hjug.graphbuilder.VertexInfo; import org.jgrapht.Graph; import org.jgrapht.graph.DefaultDirectedWeightedGraph; import org.jgrapht.graph.DefaultWeightedEdge; @@ -42,6 +43,7 @@ class DependencyVisitorLogicJavaKotlinParityTest { private static final String JAVA_TESTCLASSES = "src/test/java/org/hjug/graphbuilder/visitor/testclasses"; private static final String KOTLIN_SOURCE_PATH_DIR = "src/test/resources/kotlinSourcePathSrcDirectory"; + /** Verifies that Java declarations with missing type attribution can still register class vertices. */ @DisplayName("1. visitCompilationUnit handles null ClassDeclaration type without NPE (Java)") @Test void javaVisitCompilationUnit_nullTypeDoesNotNPE(@TempDir Path tempDir) throws IOException { @@ -74,10 +76,15 @@ void javaVisitCompilationUnit_nullTypeDoesNotNPE(@TempDir Path tempDir) throws I visitor.visit(cu, ctx)); // Verify the visitor completed without exception and registered vertices - assertTrue(collector.getClassReferencesGraph().containsVertex("com.example.nulltype.NullTypeTest") - || collector.getClassReferencesGraph().containsVertex("com.example.nulltype.AnotherClass")); + assertTrue(collector + .getClassReferencesGraph() + .containsVertex(VertexInfo.forName("com.example.nulltype.NullTypeTest")) + || collector + .getClassReferencesGraph() + .containsVertex(VertexInfo.forName("com.example.nulltype.AnotherClass"))); } + /** Verifies that Kotlin declarations with missing type attribution can still register class vertices. */ @DisplayName("2. visitCompilationUnit handles null ClassDeclaration type without NPE (Kotlin)") @Test void kotlinVisitCompilationUnit_nullTypeDoesNotNPE(@TempDir Path tempDir) throws IOException { @@ -111,10 +118,15 @@ void kotlinVisitCompilationUnit_nullTypeDoesNotNPE(@TempDir Path tempDir) throws visitor.visit(cu, ctx)); // Verify the visitor completed without exception and registered vertices - assertTrue(collector.getClassReferencesGraph().containsVertex("com.example.nulltype.NullTypeTest") - || collector.getClassReferencesGraph().containsVertex("com.example.nulltype.AnotherClass")); + assertTrue(collector + .getClassReferencesGraph() + .containsVertex(VertexInfo.forName("com.example.nulltype.NullTypeTest")) + || collector + .getClassReferencesGraph() + .containsVertex(VertexInfo.forName("com.example.nulltype.AnotherClass"))); } + /** Compares normalized project dependency edges and weights from corresponding Java and Kotlin fixtures. */ @DisplayName("3. Identical J-level overrides produce same edges for minimal fixture (Java vs Kotlin)") @Test void identicalMethods_sameEdges() throws IOException { @@ -127,8 +139,8 @@ void identicalMethods_sameEdges() throws IOException { Path kotlinSource = Path.of( KOTLIN_SOURCE_PATH_DIR, "com", "ideacrest", "parser", "kotlin", "sourcepath", "SourcePathSampleKt.kt"); - Graph javaGraph = buildJavaGraph(javaSource); - Graph kotlinGraph = buildKotlinGraph(kotlinSource); + Graph javaGraph = buildJavaGraph(javaSource); + Graph kotlinGraph = buildKotlinGraph(kotlinSource); // Compare only the core project vertices (excluding stdlib differences) compareProjectEdges( @@ -138,9 +150,11 @@ void identicalMethods_sameEdges() throws IOException { "com.ideacrest.parser.kotlin.sourcepath"); } - private Graph buildJavaGraph(Path sourceFile) throws IOException { - Graph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - Graph pkgGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + /** Parses a Java source fixture and returns its collected class dependency graph. */ + private Graph buildJavaGraph(Path sourceFile) throws IOException { + Graph classGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + Graph pkgGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); GraphDependencyCollector collector = new GraphDependencyCollector(classGraph, pkgGraph); JavaParser javaParser = JavaParser.fromJavaVersion().build(); @@ -156,9 +170,11 @@ private Graph buildJavaGraph(Path sourceFile) throw return classGraph; } - private Graph buildKotlinGraph(Path sourceFile) throws IOException { - Graph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - Graph pkgGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + /** Parses a Kotlin source fixture and returns its collected class dependency graph. */ + private Graph buildKotlinGraph(Path sourceFile) throws IOException { + Graph classGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + Graph pkgGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); GraphDependencyCollector collector = new GraphDependencyCollector(classGraph, pkgGraph); KotlinParser kotlinParser = KotlinParser.builder() @@ -191,16 +207,19 @@ private Graph buildKotlinGraph(Path sourceFile) thr return classGraph; } + /** Asserts equality of project edges and weights after normalizing the language-specific package prefixes. */ private void compareProjectEdges( - Graph javaGraph, - Graph kotlinGraph, + Graph javaGraph, + Graph kotlinGraph, String javaPkgPrefix, String kotlinPkgPrefix) { // Extract project-specific vertices var javaVertices = javaGraph.vertexSet().stream() + .map(VertexInfo::getName) .filter(v -> v.startsWith(javaPkgPrefix)) .toList(); var kotlinVertices = kotlinGraph.vertexSet().stream() + .map(VertexInfo::getName) .filter(v -> v.startsWith(kotlinPkgPrefix)) .toList(); @@ -222,8 +241,9 @@ private void compareProjectEdges( "Java and Kotlin project edges and weights should match exactly"); } + /** Collects edge weights keyed by normalized endpoint names when both endpoints occur in the supplied mapping. */ private Map normalizedEdges( - Graph graph, Map normalizedVertices) { + Graph graph, Map normalizedVertices) { Map edges = new HashMap<>(); Map vertexNames = new HashMap<>(); normalizedVertices.forEach((normalized, vertex) -> vertexNames.put(vertex, normalized)); diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaVisitorTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaVisitorTest.java index 0539def1..41dee7a9 100644 --- a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaVisitorTest.java +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaVisitorTest.java @@ -14,6 +14,7 @@ import java.util.stream.Collectors; import org.hjug.graphbuilder.DependencyCollector; import org.hjug.graphbuilder.GraphDependencyCollector; +import org.hjug.graphbuilder.VertexInfo; import org.jgrapht.Graph; import org.jgrapht.graph.DefaultDirectedWeightedGraph; import org.jgrapht.graph.DefaultWeightedEdge; @@ -49,18 +50,22 @@ private static void visitAll(JavaVisitor visitor, String pathS javaParser.parse(list, Path.of(srcDirectory.getAbsolutePath()), ctx).forEach(cu -> visitor.visit(cu, ctx)); } - private static Graph buildAndVisit(String pathString) throws IOException { - Graph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - Graph pkgGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + /** Visits the Java fixtures at the supplied path using directed weighted graphs that permit self-loops. */ + private static Graph buildAndVisit(String pathString) throws IOException { + Graph classGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + Graph pkgGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); GraphDependencyCollector collector = new GraphDependencyCollector(classGraph, pkgGraph); JavaVisitor visitor = new JavaVisitor<>(repoFrom(pathString), "", collector); visitAll(visitor, pathString); return classGraph; } - private static Graph buildAndVisitSimple(String pathString) throws IOException { - Graph classGraph = new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); - Graph pkgGraph = new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); + /** Visits the Java fixtures at the supplied path using directed weighted graphs that reject self-loops. */ + private static Graph buildAndVisitSimple(String pathString) throws IOException { + Graph classGraph = + new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); + Graph pkgGraph = new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); GraphDependencyCollector collector = new GraphDependencyCollector(classGraph, pkgGraph); JavaVisitor visitor = new JavaVisitor<>(repoFrom(pathString), "", collector); visitAll(visitor, pathString); @@ -133,306 +138,352 @@ void recordClassLocationIsCalledForEachInnerClass() throws IOException { .recordClassLocation(eq("org.hjug.graphbuilder.visitor.testclasses.A$StaticInnerClass"), anyString()); } + /** Verifies that the visitor registers the expected class and annotation declarations. */ @Test void visitClasses_capturesAllExpectedVertices() throws IOException { - Graph graph = buildAndVisit(TESTCLASSES); - assertTrue(graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.A")); - assertTrue(graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.B")); - assertTrue(graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.C")); - assertTrue(graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.D")); - assertTrue(graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.E")); - assertTrue(graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.F")); - assertTrue(graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.G")); - assertTrue(graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.MyAnnotation")); + Graph graph = buildAndVisit(TESTCLASSES); + assertTrue(graph.containsVertex(VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.A"))); + assertTrue(graph.containsVertex(VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.B"))); + assertTrue(graph.containsVertex(VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.C"))); + assertTrue(graph.containsVertex(VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.D"))); + assertTrue(graph.containsVertex(VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.E"))); + assertTrue(graph.containsVertex(VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.F"))); + assertTrue(graph.containsVertex(VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.G"))); + assertTrue(graph.containsVertex(VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.MyAnnotation"))); } + /** Verifies that visiting method declarations registers their owning class. */ @Test void visitMethodDeclarations_capturesMethodOwnerVertex() throws IOException { - Graph graph = buildAndVisit(TESTCLASSES); - assertTrue(graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.A")); + Graph graph = buildAndVisit(TESTCLASSES); + assertTrue(graph.containsVertex(VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.A"))); } + /** Verifies that lambda bodies contribute dependency vertices and repeated-use edge weights. */ @Test void visitLambdaBodiesRecursively() throws IOException { - Graph graph = buildAndVisit(LAMBDA); + Graph graph = buildAndVisit(LAMBDA); assertTrue( - graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.lambda.LambdaTestClass"), + graph.containsVertex( + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.lambda.LambdaTestClass")), "LambdaTestClass should be in the graph"); assertTrue( - graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.lambda.HelperClass"), + graph.containsVertex( + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.lambda.HelperClass")), "HelperClass should be captured from lambda body method invocation"); assertTrue( graph.containsEdge( - "org.hjug.graphbuilder.visitor.testclasses.lambda.LambdaTestClass", - "org.hjug.graphbuilder.visitor.testclasses.lambda.HelperClass"), + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.lambda.LambdaTestClass"), + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.lambda.HelperClass")), "Should have edge from LambdaTestClass to HelperClass"); assertTrue( - graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.lambda.DataProcessor"), + graph.containsVertex( + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.lambda.DataProcessor")), "DataProcessor should be captured from new class instantiation in lambda body"); assertTrue( graph.containsEdge( - "org.hjug.graphbuilder.visitor.testclasses.lambda.LambdaTestClass", - "org.hjug.graphbuilder.visitor.testclasses.lambda.DataProcessor"), + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.lambda.LambdaTestClass"), + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.lambda.DataProcessor")), "Should have edge from LambdaTestClass to DataProcessor from lambda body"); assertTrue( - graph.containsVertex("java.lang.StringBuilder"), + graph.containsVertex(VertexInfo.forName("java.lang.StringBuilder")), "StringBuilder should be captured from new class instantiation in lambda body"); assertTrue( graph.containsEdge( - "org.hjug.graphbuilder.visitor.testclasses.lambda.LambdaTestClass", "java.lang.StringBuilder"), + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.lambda.LambdaTestClass"), + VertexInfo.forName("java.lang.StringBuilder")), "Should have edge from LambdaTestClass to StringBuilder from lambda body"); assertTrue( - graph.containsVertex("java.lang.String"), + graph.containsVertex(VertexInfo.forName("java.lang.String")), "String should be captured from method invocations in lambda body"); DefaultWeightedEdge edge = graph.getEdge( - "org.hjug.graphbuilder.visitor.testclasses.lambda.LambdaTestClass", - "org.hjug.graphbuilder.visitor.testclasses.lambda.DataProcessor"); + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.lambda.LambdaTestClass"), + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.lambda.DataProcessor")); assertTrue( graph.getEdgeWeight(edge) >= 2.0, "Edge weight should reflect multiple uses of DataProcessor in lambda bodies"); } + /** Verifies that dependencies within nested lambda bodies are visited recursively. */ @Test void visitNestedLambdaBodiesRecursively() throws IOException { - Graph graph = buildAndVisit(LAMBDA); + Graph graph = buildAndVisit(LAMBDA); assertTrue( - graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.lambda.NestedLambdaTestClass"), + graph.containsVertex( + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.lambda.NestedLambdaTestClass")), "NestedLambdaTestClass should be in the graph"); assertTrue( - graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.lambda.DataProcessor"), + graph.containsVertex( + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.lambda.DataProcessor")), "DataProcessor should be captured from inner nested lambda body"); assertTrue( graph.containsEdge( - "org.hjug.graphbuilder.visitor.testclasses.lambda.NestedLambdaTestClass", - "org.hjug.graphbuilder.visitor.testclasses.lambda.DataProcessor"), + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.lambda.NestedLambdaTestClass"), + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.lambda.DataProcessor")), "Should have edge from NestedLambdaTestClass to DataProcessor from nested lambda"); assertTrue( - graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.lambda.HelperClass"), + graph.containsVertex( + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.lambda.HelperClass")), "HelperClass should be captured from nested lambda method invocation"); assertTrue( graph.containsEdge( - "org.hjug.graphbuilder.visitor.testclasses.lambda.NestedLambdaTestClass", - "org.hjug.graphbuilder.visitor.testclasses.lambda.HelperClass"), + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.lambda.NestedLambdaTestClass"), + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.lambda.HelperClass")), "Should have edge from NestedLambdaTestClass to HelperClass from nested lambda"); DefaultWeightedEdge dataProcessorEdge = graph.getEdge( - "org.hjug.graphbuilder.visitor.testclasses.lambda.NestedLambdaTestClass", - "org.hjug.graphbuilder.visitor.testclasses.lambda.DataProcessor"); + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.lambda.NestedLambdaTestClass"), + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.lambda.DataProcessor")); assertTrue( graph.getEdgeWeight(dataProcessorEdge) >= 2.0, "Edge weight should reflect multiple uses of DataProcessor in nested lambda bodies"); DefaultWeightedEdge helperEdge = graph.getEdge( - "org.hjug.graphbuilder.visitor.testclasses.lambda.NestedLambdaTestClass", - "org.hjug.graphbuilder.visitor.testclasses.lambda.HelperClass"); + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.lambda.NestedLambdaTestClass"), + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.lambda.HelperClass")); assertTrue( graph.getEdgeWeight(helperEdge) >= 2.0, "Edge weight should reflect HelperClass usage in nested lambda blocks"); } + /** Verifies that method invocations contribute the expected dependency edges and weights. */ @Test void visitMethodInvocations() throws IOException { - Graph graph = buildAndVisitSimple(METHOD_INVOCATION); - - assertTrue(graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.methodInvocation.A")); - assertTrue(graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.methodInvocation.B")); - assertTrue(graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.methodInvocation.C")); + Graph graph = buildAndVisitSimple(METHOD_INVOCATION); + + assertTrue(graph.containsVertex( + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.methodInvocation.A"))); + assertTrue(graph.containsVertex( + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.methodInvocation.B"))); + assertTrue(graph.containsVertex( + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.methodInvocation.C"))); assertEquals( 3, graph.getEdgeWeight(graph.getEdge( - "org.hjug.graphbuilder.visitor.testclasses.methodInvocation.A", - "org.hjug.graphbuilder.visitor.testclasses.methodInvocation.B"))); + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.methodInvocation.A"), + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.methodInvocation.B")))); assertEquals( 3, graph.getEdgeWeight(graph.getEdge( - "org.hjug.graphbuilder.visitor.testclasses.methodInvocation.A", - "org.hjug.graphbuilder.visitor.testclasses.methodInvocation.C"))); + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.methodInvocation.A"), + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.methodInvocation.C")))); } + /** Verifies that class instantiations contribute the expected dependency edges and weights. */ @Test void visitNewClass() throws IOException { - Graph graph = buildAndVisitSimple(NEW_CLASS); + Graph graph = buildAndVisitSimple(NEW_CLASS); - assertTrue(graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.newClass.A")); - assertTrue(graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.newClass.B")); - assertTrue(graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.newClass.C")); + assertTrue(graph.containsVertex(VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.newClass.A"))); + assertTrue(graph.containsVertex(VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.newClass.B"))); + assertTrue(graph.containsVertex(VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.newClass.C"))); assertEquals( 6, graph.getEdgeWeight(graph.getEdge( - "org.hjug.graphbuilder.visitor.testclasses.newClass.A", - "org.hjug.graphbuilder.visitor.testclasses.newClass.B"))); + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.newClass.A"), + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.newClass.B")))); assertEquals( 3, graph.getEdgeWeight(graph.getEdge( - "org.hjug.graphbuilder.visitor.testclasses.newClass.A", - "org.hjug.graphbuilder.visitor.testclasses.newClass.C"))); + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.newClass.A"), + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.newClass.C")))); } + /** Verifies that instance initializer bodies contribute dependencies to the enclosing class. */ @Test void visitInstanceInitializerBlocks() throws IOException { - Graph graph = buildAndVisit(INITIALIZERS); + Graph graph = buildAndVisit(INITIALIZERS); assertTrue( - graph.containsVertex( - "org.hjug.graphbuilder.visitor.testclasses.initializers.InitializerBlockTestClass"), + graph.containsVertex(VertexInfo.forName( + "org.hjug.graphbuilder.visitor.testclasses.initializers.InitializerBlockTestClass")), "InitializerBlockTestClass should be in the graph"); assertTrue( - graph.containsVertex("java.util.ArrayList"), + graph.containsVertex(VertexInfo.forName("java.util.ArrayList")), "ArrayList should be captured from instance initializer block"); assertTrue( graph.containsEdge( - "org.hjug.graphbuilder.visitor.testclasses.initializers.InitializerBlockTestClass", - "java.util.ArrayList"), + VertexInfo.forName( + "org.hjug.graphbuilder.visitor.testclasses.initializers.InitializerBlockTestClass"), + VertexInfo.forName("java.util.ArrayList")), "Should have edge from InitializerBlockTestClass to ArrayList from initializer block"); assertTrue( - graph.containsVertex("java.util.HashMap"), + graph.containsVertex(VertexInfo.forName("java.util.HashMap")), "HashMap should be captured from instance initializer block"); assertTrue( graph.containsEdge( - "org.hjug.graphbuilder.visitor.testclasses.initializers.InitializerBlockTestClass", - "java.util.HashMap"), + VertexInfo.forName( + "org.hjug.graphbuilder.visitor.testclasses.initializers.InitializerBlockTestClass"), + VertexInfo.forName("java.util.HashMap")), "Should have edge from InitializerBlockTestClass to HashMap from initializer block"); assertTrue( - graph.containsVertex("java.lang.StringBuilder"), + graph.containsVertex(VertexInfo.forName("java.lang.StringBuilder")), "StringBuilder should be captured from instance initializer block"); assertTrue( graph.containsEdge( - "org.hjug.graphbuilder.visitor.testclasses.initializers.InitializerBlockTestClass", - "java.lang.StringBuilder"), + VertexInfo.forName( + "org.hjug.graphbuilder.visitor.testclasses.initializers.InitializerBlockTestClass"), + VertexInfo.forName("java.lang.StringBuilder")), "Should have edge from InitializerBlockTestClass to StringBuilder from initializer block"); } + /** Verifies that initializer processing captures referenced types and nested class vertices. */ @Test void visitStaticInitializerBlocks() throws IOException { - Graph graph = buildAndVisit(INITIALIZERS); + Graph graph = buildAndVisit(INITIALIZERS); assertTrue( - graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.initializers.ComplexInitializerClass"), + graph.containsVertex(VertexInfo.forName( + "org.hjug.graphbuilder.visitor.testclasses.initializers.ComplexInitializerClass")), "ComplexInitializerClass should be in the graph"); assertTrue( - graph.containsVertex("java.util.concurrent.ConcurrentHashMap"), + graph.containsVertex(VertexInfo.forName("java.util.concurrent.ConcurrentHashMap")), "ConcurrentHashMap should be captured from static initializer block"); assertTrue( graph.containsEdge( - "org.hjug.graphbuilder.visitor.testclasses.initializers.ComplexInitializerClass", - "java.util.concurrent.ConcurrentHashMap"), + VertexInfo.forName( + "org.hjug.graphbuilder.visitor.testclasses.initializers.ComplexInitializerClass"), + VertexInfo.forName("java.util.concurrent.ConcurrentHashMap")), "Should have edge from ComplexInitializerClass to ConcurrentHashMap from static initializer"); assertTrue( - graph.containsVertex("java.util.concurrent.atomic.AtomicInteger"), + graph.containsVertex(VertexInfo.forName("java.util.concurrent.atomic.AtomicInteger")), "AtomicInteger should be captured from static initializer block"); assertTrue( graph.containsEdge( - "org.hjug.graphbuilder.visitor.testclasses.initializers.ComplexInitializerClass", - "java.util.concurrent.atomic.AtomicInteger"), + VertexInfo.forName( + "org.hjug.graphbuilder.visitor.testclasses.initializers.ComplexInitializerClass"), + VertexInfo.forName("java.util.concurrent.atomic.AtomicInteger")), "Should have edge from ComplexInitializerClass to AtomicInteger from static initializer"); assertTrue( graph.containsVertex( - "org.hjug.graphbuilder.visitor.testclasses.initializers.ComplexInitializerClass$DataProcessor"), + VertexInfo.forName( + "org.hjug.graphbuilder.visitor.testclasses.initializers.ComplexInitializerClass$DataProcessor")), "DataProcessor nested class should be captured from instance initializer"); assertTrue( graph.containsVertex( - "org.hjug.graphbuilder.visitor.testclasses.initializers.ComplexInitializerClass$HelperService"), + VertexInfo.forName( + "org.hjug.graphbuilder.visitor.testclasses.initializers.ComplexInitializerClass$HelperService")), "HelperService nested class should be captured from instance initializer"); } + /** Verifies that an instance initializer variable type is attributed to the enclosing class. */ @Test void instanceInitializerVariableTypeIsRecorded() throws IOException { - Graph graph = buildAndVisit(VARIABLE_INITIALIZERS); + Graph graph = buildAndVisit(VARIABLE_INITIALIZERS); assertTrue( graph.containsEdge( - "org.hjug.graphbuilder.visitor.testclasses.variableInitializers.InstanceInitOwner", - "org.hjug.graphbuilder.visitor.testclasses.variableInitializers.InstanceInitDependency"), + VertexInfo.forName( + "org.hjug.graphbuilder.visitor.testclasses.variableInitializers.InstanceInitOwner"), + VertexInfo.forName( + "org.hjug.graphbuilder.visitor.testclasses.variableInitializers.InstanceInitDependency")), "instance initializer variable type should be recorded as a dependency via currentOwnerFqn"); } + /** Verifies the expected exception dependency weight for the try/catch fixture. */ @Test void catchClauseTypeIsCountedOnce() throws IOException { - Graph graph = buildAndVisit(TRY_CATCH); + Graph graph = buildAndVisit(TRY_CATCH); assertEquals( 2.0, getEdgeWeight( graph, - "org.hjug.graphbuilder.visitor.testclasses.tryCatch.TryCatchOwner", - "org.hjug.graphbuilder.visitor.testclasses.tryCatch.CaughtDependency"), + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.tryCatch.TryCatchOwner"), + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.tryCatch.CaughtDependency")), "catch clause exception type is double-processed by visitTry manual loop after super already handled it"); } + /** Verifies that a class referenced only in Javadoc does not create a dependency edge. */ @Test void javadocReferencedClassDoesNotCreateSpuriousDependencyEdge() throws IOException { - Graph graph = buildAndVisit(JAVADOC_TESTCLASSES); + Graph graph = buildAndVisit(JAVADOC_TESTCLASSES); assertFalse( graph.containsEdge( - "org.hjug.graphbuilder.visitor.testclasses.javadoc.JavaDocOwner", - "org.hjug.graphbuilder.visitor.testclasses.javadoc.JavaDocSibling"), + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.javadoc.JavaDocOwner"), + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.javadoc.JavaDocSibling")), "JavaDocSibling is referenced only in Javadoc {@link} and must not create a dependency edge from JavaDocOwner"); } + /** Verifies that a record declaration is registered as a class graph vertex. */ @Test void visitRecordDeclaration_capturesRecordAsVertex() throws IOException { - Graph graph = buildAndVisit(RECORD); + Graph graph = buildAndVisit(RECORD); assertTrue( - graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.record.SimpleRecord"), + graph.containsVertex( + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.record.SimpleRecord")), "SimpleRecord should be in the graph"); } + /** Verifies that record component types contribute dependencies to the record. */ @Test void visitRecordDeclaration_capturesRecordComponentsAsDependencies() throws IOException { - Graph graph = buildAndVisit(RECORD); + Graph graph = buildAndVisit(RECORD); // ComplexRecord has components: String, List, Map, NestedRecord, SimpleRecord[] assertTrue( - graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.record.ComplexRecord"), + graph.containsVertex( + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.record.ComplexRecord")), "ComplexRecord should be in the graph"); assertTrue( - graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.record.ComplexRecord$NestedRecord"), + graph.containsVertex(VertexInfo.forName( + "org.hjug.graphbuilder.visitor.testclasses.record.ComplexRecord$NestedRecord")), "NestedRecord should be in the graph"); assertTrue( - graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.record.SimpleRecord"), + graph.containsVertex( + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.record.SimpleRecord")), "SimpleRecord should be in the graph"); // Check dependencies from ComplexRecord to its component types assertTrue( graph.containsEdge( - "org.hjug.graphbuilder.visitor.testclasses.record.ComplexRecord", "java.lang.String"), + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.record.ComplexRecord"), + VertexInfo.forName("java.lang.String")), "Should have edge from ComplexRecord to String"); assertTrue( - graph.containsEdge("org.hjug.graphbuilder.visitor.testclasses.record.ComplexRecord", "java.util.List"), + graph.containsEdge( + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.record.ComplexRecord"), + VertexInfo.forName("java.util.List")), "Should have edge from ComplexRecord to List"); assertTrue( - graph.containsEdge("org.hjug.graphbuilder.visitor.testclasses.record.ComplexRecord", "java.util.Map"), + graph.containsEdge( + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.record.ComplexRecord"), + VertexInfo.forName("java.util.Map")), "Should have edge from ComplexRecord to Map"); } + /** Verifies that a generic record and its type-parameter bound are registered as vertices. */ @Test void visitGenericRecordDeclaration_capturesTypeParameters() throws IOException { - Graph graph = buildAndVisit(RECORD); + Graph graph = buildAndVisit(RECORD); assertTrue( - graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.record.GenericRecord"), + graph.containsVertex( + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.record.GenericRecord")), "GenericRecord should be in the graph"); assertTrue( - graph.containsVertex("java.lang.Comparable"), + graph.containsVertex(VertexInfo.forName("java.lang.Comparable")), "Should have edge from GenericRecord to Comparable (type bound)"); } + /** Verifies that an annotated record contributes its implemented interface and annotation types. */ @Test void visitAnnotatedRecordDeclaration_capturesAnnotations() throws IOException { - Graph graph = buildAndVisit(RECORD); + Graph graph = buildAndVisit(RECORD); assertTrue( - graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.record.AnnotatedRecord"), + graph.containsVertex( + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.record.AnnotatedRecord")), "AnnotatedRecord should be in the graph"); assertTrue( - graph.containsVertex("java.io.Serializable"), + graph.containsVertex(VertexInfo.forName("java.io.Serializable")), "Should have edge from AnnotatedRecord to Serializable (implements)"); // Deprecated annotation should also be captured assertTrue( - graph.containsVertex("java.lang.Deprecated"), + graph.containsVertex(VertexInfo.forName("java.lang.Deprecated")), "Should have edge from AnnotatedRecord to Deprecated annotation"); } @@ -451,30 +502,38 @@ void visitRecordDeclaration_recordsSourceFileMapping() throws IOException { "NestedRecord should have source file mapping"); } + /** Verifies that anonymous inner classes remain distinct vertices alongside their enclosing class. */ @DisplayName("Anonymous inner classes (Outer$) enter the graph as first-class vertices") @Test void anonymousInnerClasses_enterTheGraphAsVertices() throws IOException { - Graph graph = buildAndVisit(ANONYMOUS); + Graph graph = buildAndVisit(ANONYMOUS); // owner + target (named classes) must still be present assertTrue( - graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.anonymous.AnonymousOwner"), + graph.containsVertex( + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.anonymous.AnonymousOwner")), "AnonymousOwner (named) must be a vertex"); assertTrue( - graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.anonymous.AnonymousTarget"), + graph.containsVertex( + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.anonymous.AnonymousTarget")), "AnonymousTarget (named) must be a vertex"); // anonymous inner classes serialise as Outer$1 / Outer$2 — they ARE first-class graph members now assertTrue( - graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.anonymous.AnonymousOwner$1"), + graph.containsVertex( + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.anonymous.AnonymousOwner$1")), "AnonymousOwner$1 (anonymous inner class) must enter the graph as a vertex"); assertTrue( - graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.anonymous.AnonymousOwner$2"), + graph.containsVertex( + VertexInfo.forName("org.hjug.graphbuilder.visitor.testclasses.anonymous.AnonymousOwner$2")), "AnonymousOwner$2 (anonymous inner class) must enter the graph as a vertex"); } + /** Returns the weight of the dependency between the supplied source and target vertices. */ private static double getEdgeWeight( - Graph classReferencesGraph, String sourceVertex, String targetVertex) { + Graph classReferencesGraph, + VertexInfo sourceVertex, + VertexInfo targetVertex) { return classReferencesGraph.getEdgeWeight(classReferencesGraph.getEdge(sourceVertex, targetVertex)); } } diff --git a/cost-benefit-calculator/src/main/java/org/hjug/cbc/CostBenefitCalculator.java b/cost-benefit-calculator/src/main/java/org/hjug/cbc/CostBenefitCalculator.java index f5492c74..42096a8c 100644 --- a/cost-benefit-calculator/src/main/java/org/hjug/cbc/CostBenefitCalculator.java +++ b/cost-benefit-calculator/src/main/java/org/hjug/cbc/CostBenefitCalculator.java @@ -20,6 +20,7 @@ import org.hjug.git.GitLogReader; import org.hjug.git.ScmLogInfo; import org.hjug.graphbuilder.CodebaseGraphDTO; +import org.hjug.graphbuilder.VertexInfo; import org.hjug.graphbuilder.metrics.DisharmonyDetector.ClassDisharmony; import org.hjug.graphbuilder.metrics.DisharmonyDetector.MethodDisharmony; import org.hjug.graphbuilder.metrics.DisharmonyTypes; @@ -337,12 +338,24 @@ private List getCBOClasses() { return cboClasses; } + /** + * Ranks candidate class edges using cycle impact, removal flags, and package relationships. + * Only edges present in {@code edgeToRemoveCycleCounts} are considered. + * + * @param classGraph the class dependency graph + * @param edgeToRemoveCycleCounts the candidate edges and their cycle counts + * @param dto the codebase graph containing corresponding package relationships + * @param vertexesToRemove the class vertices selected for removal + * @param packageCycles the package cycles used to count each relationship's participation + * @param packageRelationshipDisharmonies the package relationships selected for removal + * @return the ranked class relationships with assigned priorities + */ public List calculateRelationshipCostBenefitValues( - Graph classGraph, + Graph classGraph, Map edgeToRemoveCycleCounts, CodebaseGraphDTO dto, - Set vertexesToRemove, - Map> packageCycles, + Set vertexesToRemove, + Map> packageCycles, List packageRelationshipDisharmonies) { List edgesThatNeedToBeRemoved = new ArrayList<>(); @@ -356,14 +369,14 @@ public List calculateRelationshipCostBenefitValues( continue; } - String edgeSource = classGraph.getEdgeSource(edge); - String edgeTarget = classGraph.getEdgeTarget(edge); + VertexInfo edgeSource = classGraph.getEdgeSource(edge); + VertexInfo edgeTarget = classGraph.getEdgeTarget(edge); boolean sourceNodeShouldBeRemoved = vertexesToRemove.contains(edgeSource); boolean targetNodeShouldBeRemoved = vertexesToRemove.contains(edgeTarget); RankedDisharmony edgeThatNeedsToBeRemoved = new RankedDisharmony( - edgeSource, + edgeSource.getName(), edge, edgeToRemoveCycleCounts.get(edge), (int) classGraph.getEdgeWeight(edge), @@ -401,16 +414,19 @@ public List calculateRelationshipCostBenefitValues( * cycle, not merely cycles that happen to contain one of the endpoints. */ private static int getPackageCycleCount( - String edgeSource, - String edgeTarget, + VertexInfo edgeSource, + VertexInfo edgeTarget, CodebaseGraphDTO dto, - Map> packageCycles) { + Map> packageCycles) { String sourcePackage = toPackageName(edgeSource, dto); String targetPackage = toPackageName(edgeTarget, dto); + if (sourcePackage.isEmpty() || targetPackage.isEmpty()) { + return 0; + } int packageCycleCount = 0; - for (AsSubgraph packageCycle : packageCycles.values()) { - if (packageCycle.containsEdge(sourcePackage, targetPackage)) { + for (AsSubgraph packageCycle : packageCycles.values()) { + if (packageCycle.containsEdge(VertexInfo.forName(sourcePackage), VertexInfo.forName(targetPackage))) { packageCycleCount++; } } @@ -423,10 +439,17 @@ private static int getPackageCycleCount( * relationship disharmonies. */ private static boolean packageRelationshipShouldBeRemoved( - String edgeSource, String edgeTarget, CodebaseGraphDTO dto, Set packageEdgesToRemove) { + VertexInfo edgeSource, + VertexInfo edgeTarget, + CodebaseGraphDTO dto, + Set packageEdgesToRemove) { String sourcePackage = toPackageName(edgeSource, dto); String targetPackage = toPackageName(edgeTarget, dto); - DefaultWeightedEdge packageEdge = dto.getPackageReferencesGraph().getEdge(sourcePackage, targetPackage); + if (sourcePackage.isEmpty() || targetPackage.isEmpty()) { + return false; + } + DefaultWeightedEdge packageEdge = dto.getPackageReferencesGraph() + .getEdge(VertexInfo.forName(sourcePackage), VertexInfo.forName(targetPackage)); return packageEdge != null && packageEdgesToRemove.contains(packageEdge); } @@ -434,14 +457,15 @@ private static boolean packageRelationshipShouldBeRemoved( * The vertex may already be a package name (when classGraph is actually a package graph) or a fully-qualified * class name, in which case the containing package is derived from it. */ - private static String toPackageName(String vertex, CodebaseGraphDTO dto) { + private static String toPackageName(VertexInfo vertex, CodebaseGraphDTO dto) { if (dto.getPackageReferencesGraph().containsVertex(vertex)) { - return vertex; - } else if (vertex.contains(".")) { - return vertex.substring(0, vertex.lastIndexOf('.')); - } else { - return ""; + return vertex.getName(); + } + String name = vertex.getName(); + if (name.contains(".")) { + return name.substring(0, name.lastIndexOf('.')); } + return ""; } static void sortEdgesThatNeedToBeRemoved(List rankedDisharmonies) { diff --git a/cost-benefit-calculator/src/main/java/org/hjug/cbc/CycleRanker.java b/cost-benefit-calculator/src/main/java/org/hjug/cbc/CycleRanker.java index 9a9d912f..9baf856d 100644 --- a/cost-benefit-calculator/src/main/java/org/hjug/cbc/CycleRanker.java +++ b/cost-benefit-calculator/src/main/java/org/hjug/cbc/CycleRanker.java @@ -10,6 +10,7 @@ import org.hjug.graphbuilder.CodebaseGraphDTO; import org.hjug.graphbuilder.CompositeGraphBuilder; import org.hjug.graphbuilder.GraphBuilderConfig; +import org.hjug.graphbuilder.VertexInfo; import org.jgrapht.Graph; import org.jgrapht.graph.AsSubgraph; import org.jgrapht.graph.DefaultWeightedEdge; @@ -63,7 +64,13 @@ public CodebaseGraphDTO generateClassReferencesGraph(boolean excludeTests, Strin return codebaseGraphDTO; } - public List rankCycles(Graph graph) { + /** + * Finds cyclic subgraphs and assigns priorities in descending order of vertex count. + * + * @param graph the dependency graph to analyze + * @return the detected cycles with priorities assigned + */ + public List rankCycles(Graph graph) { List rankedCycles; try { rankedCycles = new ArrayList<>(identifyRankedCycles(graph)); @@ -75,20 +82,25 @@ public List rankCycles(Graph graph) { return rankedCycles; } - private List identifyRankedCycles(Graph classReferencesGraph) + /** Builds cycle records from cyclic subgraphs, retaining vertex names and repository source paths. */ + private List identifyRankedCycles(Graph classReferencesGraph) throws IOException { List rankedCycles = new ArrayList<>(); - CircularReferenceChecker circularReferenceChecker = + CircularReferenceChecker circularReferenceChecker = new CircularReferenceChecker<>(); - Map> cycles = + Map> cycles = circularReferenceChecker.getCycles(classReferencesGraph); cycles.forEach((vertex, subGraph) -> { List cycleNodes = subGraph.vertexSet().stream() - .map(classInCycle -> new CycleNode(classInCycle, getClassRepoPath(classInCycle))) + .map(classInCycle -> + new CycleNode(classInCycle.getName(), getClassRepoPath(classInCycle.getName()))) // .peek(cycleNode -> log.info(cycleNode.toString())) .collect(Collectors.toList()); - rankedCycles.add(new RankedCycle(vertex, subGraph.vertexSet(), subGraph.edgeSet(), cycleNodes)); + Set vertexNames = + subGraph.vertexSet().stream().map(VertexInfo::getName).collect(Collectors.toSet()); + + rankedCycles.add(new RankedCycle(vertex.getName(), vertexNames, subGraph.edgeSet(), cycleNodes)); }); return rankedCycles; diff --git a/cost-benefit-calculator/src/test/java/org/hjug/cbc/CostBenefitCalculatorTest.java b/cost-benefit-calculator/src/test/java/org/hjug/cbc/CostBenefitCalculatorTest.java index f8f88357..e82fa540 100644 --- a/cost-benefit-calculator/src/test/java/org/hjug/cbc/CostBenefitCalculatorTest.java +++ b/cost-benefit-calculator/src/test/java/org/hjug/cbc/CostBenefitCalculatorTest.java @@ -12,6 +12,7 @@ import org.eclipse.jgit.revwalk.RevCommit; import org.hjug.git.ScmLogInfo; import org.hjug.graphbuilder.CodebaseGraphDTO; +import org.hjug.graphbuilder.VertexInfo; import org.hjug.metrics.Disharmony; import org.jetbrains.annotations.NotNull; import org.jgrapht.graph.AsSubgraph; @@ -117,6 +118,7 @@ void testCostBenefitCalculation() throws IOException, GitAPIException, Interrupt Assertions.assertEquals(2, disharmonies.get(1).getPriority().intValue()); } + /** Verifies candidate edge priorities, cycle counts, effort ranks, and endpoint removal flags. */ @Test void calculateRelationshipCostBenefitValues_filtersMissingLogInfoAndAssignsPriority() throws Exception { @@ -125,20 +127,20 @@ void calculateRelationshipCostBenefitValues_filtersMissingLogInfoAndAssignsPrior git.add().addFilepattern(".").call(); git.commit().setMessage("initial commit").call(); - SimpleDirectedWeightedGraph classGraph = + SimpleDirectedWeightedGraph classGraph = new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); - classGraph.addVertex("ClassA"); - classGraph.addVertex("ClassB"); - classGraph.addVertex("ClassC"); - classGraph.addVertex("ClassD"); - classGraph.addVertex("ClassE"); - classGraph.addVertex("ClassF"); + classGraph.addVertex(VertexInfo.forName("ClassA")); + classGraph.addVertex(VertexInfo.forName("ClassB")); + classGraph.addVertex(VertexInfo.forName("ClassC")); + classGraph.addVertex(VertexInfo.forName("ClassD")); + classGraph.addVertex(VertexInfo.forName("ClassE")); + classGraph.addVertex(VertexInfo.forName("ClassF")); - DefaultWeightedEdge edge1 = classGraph.addEdge("ClassA", "ClassB"); + DefaultWeightedEdge edge1 = classGraph.addEdge(VertexInfo.forName("ClassA"), VertexInfo.forName("ClassB")); classGraph.setEdgeWeight(edge1, 4); - DefaultWeightedEdge edge2 = classGraph.addEdge("ClassC", "ClassD"); + DefaultWeightedEdge edge2 = classGraph.addEdge(VertexInfo.forName("ClassC"), VertexInfo.forName("ClassD")); classGraph.setEdgeWeight(edge2, 2); Map edgeSourceNodeInfos = new HashMap<>(); @@ -153,7 +155,8 @@ void calculateRelationshipCostBenefitValues_filtersMissingLogInfoAndAssignsPrior edgeToRemoveCycleCounts.put(edge1, 5); edgeToRemoveCycleCounts.put(edge2, 3); - Set vertexesToRemove = new HashSet<>(Arrays.asList("ClassA", "ClassD")); + Set vertexesToRemove = + new HashSet<>(Arrays.asList(VertexInfo.forName("ClassA"), VertexInfo.forName("ClassD"))); ScmLogInfo scmLogInfo1 = new ScmLogInfo(hudsonPath + "ClassA.java", null, 1, 2, 3); scmLogInfo1.setChangePronenessRank(4); @@ -194,6 +197,7 @@ void calculateRelationshipCostBenefitValues_filtersMissingLogInfoAndAssignsPrior } } + /** Verifies that candidate edges receive sequential priorities when change-proneness ranks differ. */ @Test void calculateRelationshipCostBenefitValues_prefersHigherChangePronenessRank() throws Exception { @@ -202,17 +206,17 @@ void calculateRelationshipCostBenefitValues_prefersHigherChangePronenessRank() t git.add().addFilepattern(".").call(); git.commit().setMessage("initial commit").call(); - SimpleDirectedWeightedGraph classGraph = + SimpleDirectedWeightedGraph classGraph = new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); - classGraph.addVertex("Alpha"); - classGraph.addVertex("Beta"); - classGraph.addVertex("Gamma"); + classGraph.addVertex(VertexInfo.forName("Alpha")); + classGraph.addVertex(VertexInfo.forName("Beta")); + classGraph.addVertex(VertexInfo.forName("Gamma")); - DefaultWeightedEdge edge1 = classGraph.addEdge("Alpha", "Beta"); + DefaultWeightedEdge edge1 = classGraph.addEdge(VertexInfo.forName("Alpha"), VertexInfo.forName("Beta")); classGraph.setEdgeWeight(edge1, 3); - DefaultWeightedEdge edge2 = classGraph.addEdge("Gamma", "Beta"); + DefaultWeightedEdge edge2 = classGraph.addEdge(VertexInfo.forName("Gamma"), VertexInfo.forName("Beta")); classGraph.setEdgeWeight(edge2, 3); Map edgeSourceNodeInfos = new HashMap<>(); @@ -226,7 +230,8 @@ void calculateRelationshipCostBenefitValues_prefersHigherChangePronenessRank() t edgeToRemoveCycleCounts.put(edge1, 4); edgeToRemoveCycleCounts.put(edge2, 4); - Set vertexesToRemove = new HashSet<>(Arrays.asList("Alpha", "Gamma")); + Set vertexesToRemove = + new HashSet<>(Arrays.asList(VertexInfo.forName("Alpha"), VertexInfo.forName("Gamma"))); ScmLogInfo scmLogInfo1 = new ScmLogInfo(faceletsPath + "Alpha.java", null, 2, 3, 1); scmLogInfo1.setChangePronenessRank(2); @@ -254,6 +259,7 @@ void calculateRelationshipCostBenefitValues_prefersHigherChangePronenessRank() t } } + /** Verifies that package cycle counts reflect the relationship itself rather than only its source package. */ @Test void calculateRelationshipCostBenefitValues_packageCycleCountReflectsRelationshipNotJustSourcePackage() throws Exception { @@ -262,32 +268,40 @@ void calculateRelationshipCostBenefitValues_packageCycleCountReflectsRelationshi git.add().addFilepattern(".").call(); git.commit().setMessage("initial commit").call(); - SimpleDirectedWeightedGraph packageGraph = + SimpleDirectedWeightedGraph packageGraph = new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); - packageGraph.addVertex("pkga"); - packageGraph.addVertex("pkgb"); - packageGraph.addVertex("pkgc"); - packageGraph.addVertex("pkgd"); - packageGraph.addEdge("pkga", "pkgb"); - packageGraph.addEdge("pkgb", "pkgc"); - packageGraph.addEdge("pkgc", "pkga"); + packageGraph.addVertex(VertexInfo.forName("pkga")); + packageGraph.addVertex(VertexInfo.forName("pkgb")); + packageGraph.addVertex(VertexInfo.forName("pkgc")); + packageGraph.addVertex(VertexInfo.forName("pkgd")); + packageGraph.addEdge(VertexInfo.forName("pkga"), VertexInfo.forName("pkgb")); + packageGraph.addEdge(VertexInfo.forName("pkgb"), VertexInfo.forName("pkgc")); + packageGraph.addEdge(VertexInfo.forName("pkgc"), VertexInfo.forName("pkga")); // dangling relationship: pkga depends on pkgd, but pkgd is not part of the pkga/pkgb/pkgc cycle - packageGraph.addEdge("pkga", "pkgd"); + packageGraph.addEdge(VertexInfo.forName("pkga"), VertexInfo.forName("pkgd")); - Map> packageCycles = new HashMap<>(); - packageCycles.put("cycle1", new AsSubgraph<>(packageGraph, Set.of("pkga", "pkgb", "pkgc"))); + Map> packageCycles = new HashMap<>(); + packageCycles.put( + VertexInfo.forName("cycle1"), + new AsSubgraph<>( + packageGraph, + Set.of(VertexInfo.forName("pkga"), VertexInfo.forName("pkgb"), VertexInfo.forName("pkgc")))); - SimpleDirectedWeightedGraph classGraph = + SimpleDirectedWeightedGraph classGraph = new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); - classGraph.addVertex("pkga.Foo"); - classGraph.addVertex("pkgb.Bar"); - classGraph.addVertex("pkgc.Baz"); - classGraph.addVertex("pkgd.Qux"); - - DefaultWeightedEdge fooToBar = classGraph.addEdge("pkga.Foo", "pkgb.Bar"); - DefaultWeightedEdge barToBaz = classGraph.addEdge("pkgb.Bar", "pkgc.Baz"); - DefaultWeightedEdge bazToFoo = classGraph.addEdge("pkgc.Baz", "pkga.Foo"); - DefaultWeightedEdge fooToQux = classGraph.addEdge("pkga.Foo", "pkgd.Qux"); + classGraph.addVertex(VertexInfo.forName("pkga.Foo")); + classGraph.addVertex(VertexInfo.forName("pkgb.Bar")); + classGraph.addVertex(VertexInfo.forName("pkgc.Baz")); + classGraph.addVertex(VertexInfo.forName("pkgd.Qux")); + + DefaultWeightedEdge fooToBar = + classGraph.addEdge(VertexInfo.forName("pkga.Foo"), VertexInfo.forName("pkgb.Bar")); + DefaultWeightedEdge barToBaz = + classGraph.addEdge(VertexInfo.forName("pkgb.Bar"), VertexInfo.forName("pkgc.Baz")); + DefaultWeightedEdge bazToFoo = + classGraph.addEdge(VertexInfo.forName("pkgc.Baz"), VertexInfo.forName("pkga.Foo")); + DefaultWeightedEdge fooToQux = + classGraph.addEdge(VertexInfo.forName("pkga.Foo"), VertexInfo.forName("pkgd.Qux")); Map edgeToRemoveCycleCounts = new HashMap<>(); edgeToRemoveCycleCounts.put(fooToBar, 1); @@ -317,6 +331,7 @@ void calculateRelationshipCostBenefitValues_packageCycleCountReflectsRelationshi } } + /** Verifies that only class edges matching a selected package relationship inherit its removal flag. */ @Test void calculateRelationshipCostBenefitValues_flagsClassEdgesWhosePackageRelationshipIsAlsoBeingRemoved() throws Exception { @@ -325,31 +340,34 @@ void calculateRelationshipCostBenefitValues_flagsClassEdgesWhosePackageRelations git.add().addFilepattern(".").call(); git.commit().setMessage("initial commit").call(); - SimpleDirectedWeightedGraph packageGraph = + SimpleDirectedWeightedGraph packageGraph = new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); - packageGraph.addVertex("pkga"); - packageGraph.addVertex("pkgb"); - packageGraph.addVertex("pkgc"); - DefaultWeightedEdge pkgaToPkgb = packageGraph.addEdge("pkga", "pkgb"); - packageGraph.addEdge("pkgb", "pkgc"); + packageGraph.addVertex(VertexInfo.forName("pkga")); + packageGraph.addVertex(VertexInfo.forName("pkgb")); + packageGraph.addVertex(VertexInfo.forName("pkgc")); + DefaultWeightedEdge pkgaToPkgb = packageGraph.addEdge(VertexInfo.forName("pkga"), VertexInfo.forName("pkgb")); + packageGraph.addEdge(VertexInfo.forName("pkgb"), VertexInfo.forName("pkgc")); // Only pkga -> pkgb is flagged as a package relationship that needs to be removed RankedDisharmony packageEdgeToRemove = new RankedDisharmony("pkga", pkgaToPkgb, 1, 1, false, false, 0, false); List packageRelationshipDisharmonies = List.of(packageEdgeToRemove); - SimpleDirectedWeightedGraph classGraph = + SimpleDirectedWeightedGraph classGraph = new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); - classGraph.addVertex("pkga.Foo"); - classGraph.addVertex("pkgb.Bar"); - classGraph.addVertex("pkgc.Baz"); - classGraph.addVertex("pkga.Other"); + classGraph.addVertex(VertexInfo.forName("pkga.Foo")); + classGraph.addVertex(VertexInfo.forName("pkgb.Bar")); + classGraph.addVertex(VertexInfo.forName("pkgc.Baz")); + classGraph.addVertex(VertexInfo.forName("pkga.Other")); // matches the removed package relationship pkga -> pkgb - DefaultWeightedEdge fooToBar = classGraph.addEdge("pkga.Foo", "pkgb.Bar"); + DefaultWeightedEdge fooToBar = + classGraph.addEdge(VertexInfo.forName("pkga.Foo"), VertexInfo.forName("pkgb.Bar")); // corresponds to pkgb -> pkgc, which is not in packageRelationshipDisharmonies - DefaultWeightedEdge barToBaz = classGraph.addEdge("pkgb.Bar", "pkgc.Baz"); + DefaultWeightedEdge barToBaz = + classGraph.addEdge(VertexInfo.forName("pkgb.Bar"), VertexInfo.forName("pkgc.Baz")); // same-package relationship: no package edge exists at all - DefaultWeightedEdge fooToOther = classGraph.addEdge("pkga.Foo", "pkga.Other"); + DefaultWeightedEdge fooToOther = + classGraph.addEdge(VertexInfo.forName("pkga.Foo"), VertexInfo.forName("pkga.Other")); Map edgeToRemoveCycleCounts = new HashMap<>(); edgeToRemoveCycleCounts.put(fooToBar, 1); diff --git a/cost-benefit-calculator/src/test/java/org/hjug/cbc/CycleRankerGraphGenerationTest.java b/cost-benefit-calculator/src/test/java/org/hjug/cbc/CycleRankerGraphGenerationTest.java index 02e1b6e5..ed812047 100644 --- a/cost-benefit-calculator/src/test/java/org/hjug/cbc/CycleRankerGraphGenerationTest.java +++ b/cost-benefit-calculator/src/test/java/org/hjug/cbc/CycleRankerGraphGenerationTest.java @@ -6,6 +6,7 @@ import java.nio.file.Files; import java.nio.file.Path; import org.hjug.graphbuilder.CodebaseGraphDTO; +import org.hjug.graphbuilder.VertexInfo; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -24,6 +25,7 @@ class CycleRankerGraphGenerationTest { @TempDir Path tempDir; + /** Verifies that cycle graph generation includes a class declared in the repository fixture. */ @Test @DisplayName("generateClassReferencesGraph builds the graph") void generatesClassReferencesGraph() throws IOException { @@ -42,7 +44,7 @@ public void hello() {} CodebaseGraphDTO dto = ranker.generateClassReferencesGraph(false, ""); assertNotNull(dto); - assertTrue(dto.getClassReferencesGraph().containsVertex("com.example.Plain")); + assertTrue(dto.getClassReferencesGraph().containsVertex(VertexInfo.forName("com.example.Plain"))); } @ParameterizedTest @@ -58,6 +60,7 @@ void rejectsMissingSourceDirectory(String repositoryPath) { assertNull(ranker.getCodebaseGraphDTO(), "A rejected request must not populate the graph DTO"); } + /** Verifies that graph generation honors the caller's custom test directory and exclusion setting. */ @ParameterizedTest(name = "excludeTests={0} should include custom test source={1}") @CsvSource({"true, false", "false, true"}) @DisplayName("generateClassReferencesGraph forwards test exclusion configuration") @@ -68,10 +71,10 @@ void forwardsTestExclusionConfiguration(boolean excludeTests, boolean expectTest CycleRanker ranker = new CycleRanker(tempDir.toString(), tempDir.toString()); CodebaseGraphDTO dto = ranker.generateClassReferencesGraph(excludeTests, "custom-tests"); - assertTrue(dto.getClassReferencesGraph().containsVertex("com.example.ProductionClass")); + assertTrue(dto.getClassReferencesGraph().containsVertex(VertexInfo.forName("com.example.ProductionClass"))); assertEquals( expectTestSource, - dto.getClassReferencesGraph().containsVertex("com.example.FixtureClass"), + dto.getClassReferencesGraph().containsVertex(VertexInfo.forName("com.example.FixtureClass")), "The custom test source directory must follow the caller's exclusion setting"); assertSame(dto, ranker.getCodebaseGraphDTO(), "The generated DTO must remain available to cycle ranking"); } diff --git a/cost-benefit-calculator/src/test/java/org/hjug/cbc/CycleRankerKotlinTest.java b/cost-benefit-calculator/src/test/java/org/hjug/cbc/CycleRankerKotlinTest.java index 3ca60929..78999cba 100644 --- a/cost-benefit-calculator/src/test/java/org/hjug/cbc/CycleRankerKotlinTest.java +++ b/cost-benefit-calculator/src/test/java/org/hjug/cbc/CycleRankerKotlinTest.java @@ -10,6 +10,7 @@ import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; import org.hjug.graphbuilder.CodebaseGraphDTO; +import org.hjug.graphbuilder.VertexInfo; import org.jgrapht.Graph; import org.jgrapht.graph.DefaultWeightedEdge; import org.junit.jupiter.api.AfterEach; @@ -84,6 +85,7 @@ public void tearDown() { } } + /** Verifies that Kotlin cycle ranking preserves the three cycle vertices and their Kotlin source paths. */ @DisplayName("CycleRanker detects cycles in a Kotlin-only repo") @Test void cycleRanker_detectsKotlinCycleViaCompositeGraphBuilder() throws IOException, GitAPIException { @@ -124,7 +126,7 @@ void cycleRanker_detectsKotlinCycleViaCompositeGraphBuilder() throws IOException CodebaseGraphDTO dto = cycleRanker.generateClassReferencesGraph(true, "src/test"); assertNotNull(dto, "CompositeGraphBuilder should produce a non-null CodebaseGraphDTO"); - Graph classGraph = dto.getClassReferencesGraph(); + Graph classGraph = dto.getClassReferencesGraph(); assertNotNull(classGraph); String a = "com.kotlin.cycles.KotlinCycleA"; @@ -132,21 +134,27 @@ void cycleRanker_detectsKotlinCycleViaCompositeGraphBuilder() throws IOException String c = "com.kotlin.cycles.KotlinCycleC"; assertTrue( - classGraph.containsVertex(a), + classGraph.containsVertex(VertexInfo.forName(a)), "KotlinCycleA vertex missing from class graph. Vertices: " + classGraph.vertexSet()); assertTrue( - classGraph.containsVertex(b), + classGraph.containsVertex(VertexInfo.forName(b)), "KotlinCycleB vertex missing from class graph. Vertices: " + classGraph.vertexSet()); assertTrue( - classGraph.containsVertex(c), + classGraph.containsVertex(VertexInfo.forName(c)), "KotlinCycleC vertex missing from class graph. Vertices: " + classGraph.vertexSet()); // Edges form the triangle A -> B -> C -> A (each class instantiates // the next one as a method return value, which KotlinDependencyVisitor // records as a class dependency). - assertTrue(classGraph.containsEdge(a, b), "Missing edge A -> B. Edges: " + classGraph.edgeSet()); - assertTrue(classGraph.containsEdge(b, c), "Missing edge B -> C. Edges: " + classGraph.edgeSet()); - assertTrue(classGraph.containsEdge(c, a), "Missing edge C -> A. Edges: " + classGraph.edgeSet()); + assertTrue( + classGraph.containsEdge(VertexInfo.forName(a), VertexInfo.forName(b)), + "Missing edge A -> B. Edges: " + classGraph.edgeSet()); + assertTrue( + classGraph.containsEdge(VertexInfo.forName(b), VertexInfo.forName(c)), + "Missing edge B -> C. Edges: " + classGraph.edgeSet()); + assertTrue( + classGraph.containsEdge(VertexInfo.forName(c), VertexInfo.forName(a)), + "Missing edge C -> A. Edges: " + classGraph.edgeSet()); List rankedCycles = cycleRanker.rankCycles(classGraph); assertNotNull(rankedCycles); diff --git a/graph-algorithms/src/main/java/org/hjug/dsm/EdgeRemovalCalculator.java b/graph-algorithms/src/main/java/org/hjug/dsm/EdgeRemovalCalculator.java index 4b0d2985..e6016965 100644 --- a/graph-algorithms/src/main/java/org/hjug/dsm/EdgeRemovalCalculator.java +++ b/graph-algorithms/src/main/java/org/hjug/dsm/EdgeRemovalCalculator.java @@ -7,23 +7,40 @@ import org.jgrapht.graph.DefaultWeightedEdge; import org.jgrapht.graph.SimpleDirectedWeightedGraph; -public class EdgeRemovalCalculator { +/** + * Calculates the cycle-removal impact of removing edges above the DSM diagonal. + * + * @param the graph vertex type + */ +public class EdgeRemovalCalculator { - private final Graph graph; - private DSM dsm; - private final Map> cycles; + private final Graph graph; + private DSM dsm; + private final Map> cycles; private Set edgesToRemove; - public EdgeRemovalCalculator(Graph graph, DSM dsm) { + /** + * Creates an edge-impact calculator using the supplied dependency structure matrix. + * + * @param graph the dependency graph to analyze + * @param dsm the matrix identifying candidate edges above the diagonal + */ + public EdgeRemovalCalculator(Graph graph, DSM dsm) { this.graph = graph; this.dsm = dsm; - this.cycles = new CircularReferenceChecker().getCycles(graph); + this.cycles = new CircularReferenceChecker().getCycles(graph); } - public EdgeRemovalCalculator(Graph graph, Set edgesToRemove) { + /** + * Creates an edge-impact calculator for an explicit set of candidate edges. + * + * @param graph the dependency graph to analyze + * @param edgesToRemove the edges whose individual removal impact will be evaluated + */ + public EdgeRemovalCalculator(Graph graph, Set edgesToRemove) { this.graph = graph; this.edgesToRemove = edgesToRemove; - this.cycles = new CircularReferenceChecker().getCycles(graph); + this.cycles = new CircularReferenceChecker().getCycles(graph); } /** @@ -66,9 +83,15 @@ public List getImpactOfEdges() { .collect(Collectors.toList()); } + /** + * Measures the remaining cycles after removing one edge from a copy of the graph. + * + * @param edgeToRemove the edge to evaluate + * @return the removed weight, remaining cycle count, and remaining cycles per unit of removed weight + */ public EdgeToRemoveInfo calculateEdgeToRemoveInfo(DefaultWeightedEdge edgeToRemove) { // clone graph and remove edge - Graph improvedGraph = new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); + Graph improvedGraph = new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); graph.vertexSet().forEach(improvedGraph::addVertex); for (DefaultWeightedEdge weightedEdge : graph.edgeSet()) { improvedGraph.addEdge(graph.getEdgeSource(weightedEdge), graph.getEdgeTarget(weightedEdge), weightedEdge); @@ -77,7 +100,7 @@ public EdgeToRemoveInfo calculateEdgeToRemoveInfo(DefaultWeightedEdge edgeToRemo improvedGraph.removeEdge(edgeToRemove); // Calculate new cycle count - int newCycleCount = new CircularReferenceChecker() + int newCycleCount = new CircularReferenceChecker() .getCycles(improvedGraph) .size(); diff --git a/graph-algorithms/src/main/java/org/hjug/dsm/SparseIntDWGEdgeRemovalCalculator.java b/graph-algorithms/src/main/java/org/hjug/dsm/SparseIntDWGEdgeRemovalCalculator.java index dd1bf1e8..532fa0b8 100644 --- a/graph-algorithms/src/main/java/org/hjug/dsm/SparseIntDWGEdgeRemovalCalculator.java +++ b/graph-algorithms/src/main/java/org/hjug/dsm/SparseIntDWGEdgeRemovalCalculator.java @@ -14,25 +14,29 @@ import org.jgrapht.graph.DefaultWeightedEdge; import org.jgrapht.opt.graph.sparse.SparseIntDirectedWeightedGraph; -class SparseIntDWGEdgeRemovalCalculator { - private final Graph graph; +class SparseIntDWGEdgeRemovalCalculator { + private final Graph graph; SparseIntDirectedWeightedGraph sparseGraph; List> sparseEdges; List sparseEdgesAboveDiagonal; private final double sumOfEdgeWeightsAboveDiagonal; int vertexCount; - Map vertexToInt; - Map intToVertex; + Map vertexToInt; + Map intToVertex; + /** + * Captures the sparse graph, candidate edges, and vertex-index mappings used to evaluate removals. + * Mutable edge lists and mappings are copied into concurrent collections. + */ SparseIntDWGEdgeRemovalCalculator( - Graph graph, + Graph graph, SparseIntDirectedWeightedGraph sparseGraph, List> sparseEdges, List sparseEdgesAboveDiagonal, double sumOfEdgeWeightsAboveDiagonal, int vertexCount, - Map vertexToInt, - Map intToVertex) { + Map vertexToInt, + Map intToVertex) { this.graph = graph; this.sparseGraph = sparseGraph; this.sparseEdges = new CopyOnWriteArrayList<>(sparseEdges); diff --git a/graph-algorithms/src/main/java/org/hjug/feedback/CycleRemovalComputer.java b/graph-algorithms/src/main/java/org/hjug/feedback/CycleRemovalComputer.java index 5395efff..f039fc68 100644 --- a/graph-algorithms/src/main/java/org/hjug/feedback/CycleRemovalComputer.java +++ b/graph-algorithms/src/main/java/org/hjug/feedback/CycleRemovalComputer.java @@ -17,36 +17,44 @@ @Slf4j public class CycleRemovalComputer { - public CycleRemovalResult computeCycleRemovalInformation(Graph graph) { - Map> cycles = - new CircularReferenceChecker().getCycles(graph); + /** + * Identifies cyclic subgraphs, feedback vertices, and feedback edges for a dependency graph. + * Acyclic graphs yield empty removal sets; selected edges include their cycle participation counts. + * + * @param graph the dependency graph to analyze + * @param the graph vertex type + * @return the cycles and proposed vertex and edge removals + */ + public CycleRemovalResult computeCycleRemovalInformation(Graph graph) { + Map> cycles = + new CircularReferenceChecker().getCycles(graph); Map edgeCycleCounts = new HashMap<>(); - Set vertexesToRemove = new HashSet<>(); + Set vertexesToRemove = new HashSet<>(); Set edgesToRemove = new HashSet<>(); // Skip vertex and edge removal analysis if there are no cycles if (!cycles.isEmpty()) { // Identify vertexes to remove log.info("Identifying vertexes to remove"); - EnhancedParameterComputer enhancedParameterComputer = + EnhancedParameterComputer enhancedParameterComputer = new EnhancedParameterComputer<>(new SuperTypeToken<>() {}); - EnhancedParameterComputer.EnhancedParameters parameters = + EnhancedParameterComputer.EnhancedParameters parameters = enhancedParameterComputer.computeOptimalParameters(graph, 4); - DirectedFeedbackVertexSetSolver vertexSolver = + DirectedFeedbackVertexSetSolver vertexSolver = new DirectedFeedbackVertexSetSolver<>( graph, parameters.getModulator(), null, parameters.getEta(), new SuperTypeToken<>() {}); - DirectedFeedbackVertexSetResult vertexSetResult = vertexSolver.solve(parameters.getK()); + DirectedFeedbackVertexSetResult vertexSetResult = vertexSolver.solve(parameters.getK()); vertexesToRemove.addAll(vertexSetResult.getFeedbackVertices()); // Identify edges to remove log.info("Identifying edges to remove"); - PageRankFAS pageRankFAS = new PageRankFAS<>(graph, new SuperTypeToken<>() {}); + PageRankFAS pageRankFAS = new PageRankFAS<>(graph, new SuperTypeToken<>() {}); edgesToRemove.addAll(pageRankFAS.computeFeedbackArcSet()); // capture the number of cycles each edge to remove is in for (DefaultWeightedEdge edgeToRemove : edgesToRemove) { int cycleCount = 0; - for (AsSubgraph cycle : cycles.values()) { + for (AsSubgraph cycle : cycles.values()) { if (cycle.containsEdge(edgeToRemove)) { cycleCount++; } diff --git a/graph-algorithms/src/main/java/org/hjug/feedback/CycleRemovalResult.java b/graph-algorithms/src/main/java/org/hjug/feedback/CycleRemovalResult.java index 9e240b98..79387bf8 100644 --- a/graph-algorithms/src/main/java/org/hjug/feedback/CycleRemovalResult.java +++ b/graph-algorithms/src/main/java/org/hjug/feedback/CycleRemovalResult.java @@ -7,9 +7,9 @@ import org.jgrapht.graph.DefaultWeightedEdge; @Data -public class CycleRemovalResult { - private final Map> cycles; +public class CycleRemovalResult { + private final Map> cycles; private final Set edgesToRemove; - private final Set vertexesToRemove; + private final Set vertexesToRemove; private final Map edgeCycleCounts; } diff --git a/graph-algorithms/src/main/java/org/hjug/feedback/arc/EdgeInfoCalculator.java b/graph-algorithms/src/main/java/org/hjug/feedback/arc/EdgeInfoCalculator.java index a844b8db..775c2320 100644 --- a/graph-algorithms/src/main/java/org/hjug/feedback/arc/EdgeInfoCalculator.java +++ b/graph-algorithms/src/main/java/org/hjug/feedback/arc/EdgeInfoCalculator.java @@ -8,12 +8,12 @@ import org.jgrapht.graph.DefaultWeightedEdge; @RequiredArgsConstructor -public class EdgeInfoCalculator { +public class EdgeInfoCalculator { - private final Graph graph; + private final Graph graph; private final Collection edgesToRemove; - private final Set vertexesToRemove; - private final Map> cycles; + private final Set vertexesToRemove; + private final Map> cycles; public Collection calculateEdgeInformation() { List edgeInfos = new ArrayList<>(); diff --git a/plans/vertex-info-implementation-plan.md b/plans/vertex-info-implementation-plan.md new file mode 100644 index 00000000..2f126033 --- /dev/null +++ b/plans/vertex-info-implementation-plan.md @@ -0,0 +1,123 @@ +# VertexInfo Implementation Plan + +## Goal + +Replace bare `String` FQNs/package names as graph vertices with a rich `VertexInfo` +object that carries the information needed to build a composed codebase map +rendered as part of the graph image: + +- `name` — the vertex name (class FQN or package name) +- `locationOnDisk` — location of the vertex relative to the repository root + (source file path for classes; member-file directory for packages) +- `parentNamespace` — the parent package if the vertex is a package, or the + enclosing construct (package or class) for non-package vertices that contain code + +When graph dependency information is collected, the collector now creates +`VertexInfo` instances as graph vertices instead of raw names, and all code that +references vertices fetches the name via `VertexInfo` (`getName()` / +`VertexInfo.forName(...)` probes). + +## Design decisions + +1. **`VertexInfo` lives in `codebase-graph-builder`** (`org.hjug.graphbuilder`). + It is the module that owns graph construction; every consumer module + (cost-benefit-calculator, report) already depends on it. + `graph-algorithms` must NOT depend on it — its String-bound helpers are + genericized instead (they contain no name-specific logic). + +2. **Name-based identity**: `equals`/`hashCode` consider only `name`. This keeps + JGraphT vertex dedup semantics identical to the previous `String` vertices and + lets code probe the graph with cheap `VertexInfo.forName(name)` lookups. + +3. **`toString()` returns `name`**: `DefaultWeightedEdge.toString()` + (`"(source : target)"`) and the `extractVertexes(edge)` parsing in the reports + continue to work unchanged. + +4. **`parentNamespace` is derived at creation time** (pure function of the name): + - `pkg.Outer$Inner` → `pkg.Outer` (enclosing class, `$` separator) + - `pkg.Outer$1` / `pkg.Outer$` → `pkg.Outer` (anonymous/synthetic) + - `pkg.Outer.` → `pkg.Outer`; bare `` → `null` + - `com.foo.Bar` → `com.foo` (enclosing package) + - package `com.foo` → `com` (parent package); top-level segment → `null` + +5. **`locationOnDisk` is hydrated after the visitor walk** from the canonical + repo-relative `classToSourceFilePathMapping` (with `$`-prefix and + ``-enclosing fallbacks), because canonicalization needs + `repositoryRoot`, which is only known to the builders/visitors. A new + `VertexInfoHydrator` populates class vertices (source file path) and package + vertices (directory of a member file). Package directories are derived by + dropping the file name from a member's mapped path. + +6. **Report/JSON DTOs stay name-based**: `RankedCycle`, `CycleNode`, + `RankedDisharmony`, JSON models etc. keep `String` class names; the + boundary code extracts them with `VertexInfo::getName`. Removal sets + (`classesToRemove`, `packagesToRemove`) pass through as `Set` + and are probed with `VertexInfo.forName(name)` where names are rendered. + +## Phases (each TDD: red-green-refactor) + +### Phase 1 — `VertexInfo` (codebase-graph-builder) +New test `VertexInfoTest` (Given-When-Then): construction, `forName` factory, +parent-namespace derivation rules, name-based equality, `toString() == name`. +Then implement `VertexInfo` (hand-written equals/hashCode/toString; mutable +`locationOnDisk` for hydration). + +### Phase 2 — `VertexInfoHydrator` (codebase-graph-builder) +New test `VertexInfoHydratorTest`: given class/package graphs + canonical +mapping, hydration sets `locationOnDisk` for +- mapped classes, +- `$`-nested/anonymous classes via the enclosing class entry, +- Kotlin `` via the enclosing entry, +- packages via a member file's directory, +and never overwrites a name-derived `parentNamespace` (already set). +Then implement `VertexInfoHydrator.hydrate(classGraph, packageGraph, mapping)`. + +### Phase 3 — Collectors emit `VertexInfo` vertices +Update `GraphDependencyCollectorTest` first (expects `VertexInfo` vertices with +`parentNamespace` populated at collection time), then change +`GraphDependencyCollector` and `GraphMetricsCollector` to +`Graph` with a name-keyed vertex registry. +`recordClassLocation` now stores locations captured during collection. + +### Phase 4 — DTO + builders + composite merge +`CodebaseGraphDTO` graphs become `Graph`. +`JavaSourceFileGraphBuilder.finalizeDto` (shared with Kotlin builder) hydrates +vertices before assembling the DTO. `CompositeGraphBuilder.merge` operates on +`VertexInfo` (name-based probes), re-hydrates after anonymous-mapping and +reconciliation. Fix module tests (`JavaGraphBuilderTest`, `KotlinGraphBuilderTest`, +`CompositeGraphBuilder*Test`, `Java25AnalysisIntegrationTest`, +`TypeParameterReferenceTest`, `GraphMetricsCollectorTest`, visitor tests). + +### Phase 5 — Genericize graph-algorithms +`EdgeRemovalCalculator`, `EdgeInfoCalculator`, +`SparseIntDWGEdgeRemovalCalculator`, `CycleRemovalResult`, +`CycleRemovalComputer.` — no behavior change; existing tests keep passing +via inference (`V = String`), which is the regression proof. + +### Phase 6 — cost-benefit-calculator +`CycleRanker.rankCycles(Graph)` builds name-based +`RankedCycle`/`CycleNode` via `getName()`. +`CostBenefitCalculator.calculateRelationshipCostBenefitValues` takes +`Graph`, `Set` and +`Map>` and extracts names at the +`RankedDisharmony` boundary. Fix cbc tests. + +### Phase 7 — report module +`HtmlReport`, `SimpleHtmlReport`, `JsonGenerator`: field/signature types change to +`VertexInfo` graphs; DOT/HTML rendering stays name-based via +`extractVertexes(edge)` (relies on `toString()`) with `VertexInfo.forName` +probes for `contains` checks. Fix report tests. + +### Phase 8 — Verify +`mvn spotless:apply`, then full `mvn clean install` (all modules, tests on). + +## Blast radius (main code) +- codebase-graph-builder: GraphDependencyCollector, GraphMetricsCollector, + CodebaseGraphDTO, CompositeGraphBuilder, JavaSourceFileGraphBuilder, + KotlinSourceFileGraphBuilder +- graph-algorithms: EdgeRemovalCalculator, EdgeInfoCalculator, + SparseIntDWGEdgeRemovalCalculator, CycleRemovalResult, CycleRemovalComputer +- cost-benefit-calculator: CycleRanker, CostBenefitCalculator +- report: HtmlReport, SimpleHtmlReport, JsonGenerator +- untouched: cli, maven plugin, gradle plugin, effort-ranker, + change-proneness-ranker, graph-data-generator (no graph vertex contact) diff --git a/report/src/main/java/org/hjug/refactorfirst/report/HtmlReport.java b/report/src/main/java/org/hjug/refactorfirst/report/HtmlReport.java index 6df5592b..6f6c3b34 100644 --- a/report/src/main/java/org/hjug/refactorfirst/report/HtmlReport.java +++ b/report/src/main/java/org/hjug/refactorfirst/report/HtmlReport.java @@ -9,6 +9,7 @@ import org.hjug.cbc.RankedDisharmony; import org.hjug.gdg.GraphDataGenerator; import org.hjug.graphbuilder.CodebaseGraphDTO; +import org.hjug.graphbuilder.VertexInfo; import org.jgrapht.Graph; import org.jgrapht.graph.DefaultWeightedEdge; @@ -564,7 +565,7 @@ private static String generateDotImage(String graphName) { /** Builds raw DOT source for the complete class relationship graph. */ String buildRawClassGraphDot( - Graph classGraph, String repoUrl, CodebaseGraphDTO codebaseGraphDTO) { + Graph classGraph, String repoUrl, CodebaseGraphDTO codebaseGraphDTO) { StringBuilder dot = new StringBuilder(); dot.append("strict digraph G {\n"); @@ -590,7 +591,7 @@ String buildRawClassGraphDot( /** Builds escaped class-graph DOT suitable for a JavaScript template literal. */ String buildClassGraphDot( - Graph classGraph, String repoUrl, CodebaseGraphDTO codebaseGraphDTO) { + Graph classGraph, String repoUrl, CodebaseGraphDTO codebaseGraphDTO) { return toJavaScriptTemplateLiteral(buildRawClassGraphDot(classGraph, repoUrl, codebaseGraphDTO)); } @@ -600,7 +601,7 @@ String buildClassGraphDot( * classes selected for removal. */ private void renderClassVertices( - Graph classGraph, + Graph classGraph, String repoUrl, CodebaseGraphDTO codebaseGraphDTO, Set vertexesToRender, @@ -633,7 +634,7 @@ private void renderClassVertices( dot.append(" label=\"").append(label).append("\""); } - if (classesToRemove.contains(vertex)) { + if (classesToRemove.contains(VertexInfo.forName(vertex))) { dot.append(" color=red style=filled"); } @@ -710,7 +711,7 @@ String renderSafeNodeId(String vertex, CodebaseGraphDTO codebaseGraphDTO) { * @param classGraph the class graph for collision detection * @return a deterministic DOT-legal node id for the vertex */ - String renderSafeNodeId(String vertex, Graph classGraph) { + String renderSafeNodeId(String vertex, Graph classGraph) { return renderSafeNodeId(vertex, classGraph, null); } @@ -726,7 +727,7 @@ String renderSafeNodeId(String vertex, Graph classG * @return a deterministic DOT-legal node id for the vertex */ String renderSafeNodeId( - String vertex, Graph classGraph, CodebaseGraphDTO codebaseGraphDTO) { + String vertex, Graph classGraph, CodebaseGraphDTO codebaseGraphDTO) { // Special handling for anonymous/synthetic vertices if (isAnonymousFqn(vertex)) { // If DTO provided, try to get source-aware ID @@ -762,7 +763,7 @@ String renderSafeNodeId( // Count occurrences of this simple name in the graph long count = classGraph.vertexSet().stream() - .map(this::getClassName) + .map(graphVertex -> getClassName(graphVertex.getName())) .filter(simpleName::equals) .count(); @@ -862,11 +863,12 @@ static boolean isAnonymousFqn(String vertex) { * @param vertex the candidate vertex * @return {@code true} if the vertex should be suppressed from the DOT graph */ - static boolean isSinkAnonymousOrSyntheticVertex(Graph graph, String vertex) { - if (!graph.containsVertex(vertex)) { + static boolean isSinkAnonymousOrSyntheticVertex(Graph graph, String vertex) { + VertexInfo probe = VertexInfo.forName(vertex); + if (!graph.containsVertex(probe)) { return false; } - if (graph.outDegreeOf(vertex) != 0) { + if (graph.outDegreeOf(probe) != 0) { return false; // keep nodes that reach out } if (isAnonymousFqn(vertex)) { @@ -890,8 +892,9 @@ private static String simpleNameAfterLastDollar(String vertex) { return dollar >= 0 ? afterDot.substring(dollar + 1) : afterDot; } + /** Appends a weighted class edge to DOT, highlighting removals and suppressing edges to hidden sink vertices. */ private void renderClassGraphEdge( - Graph classGraph, + Graph classGraph, DefaultWeightedEdge edge, CodebaseGraphDTO codebaseGraphDTO, StringBuilder dot) { @@ -962,7 +965,7 @@ public String renderClassCycleVisuals(RankedCycle cycle, String repoUrl, Codebas /** Builds raw DOT source for a ranked class cycle. */ String buildRawClassCycleDot( - Graph classGraph, + Graph classGraph, RankedCycle cycle, String repoUrl, CodebaseGraphDTO codebaseGraphDTO) { @@ -983,7 +986,7 @@ String buildRawClassCycleDot( /** Builds escaped class-cycle DOT suitable for a JavaScript template literal. */ String buildClassCycleDot( - Graph classGraph, + Graph classGraph, RankedCycle cycle, String repoUrl, CodebaseGraphDTO codebaseGraphDTO) { @@ -1026,7 +1029,7 @@ public String renderPackageGraphVisuals(String repoUrl, CodebaseGraphDTO codebas /** Builds raw DOT source for the package relationship graph. */ String buildRawPackageGraphDot( - Graph packageGraph, String repoUrl, CodebaseGraphDTO codebaseGraphDTO) { + Graph packageGraph, String repoUrl, CodebaseGraphDTO codebaseGraphDTO) { StringBuilder dot = new StringBuilder(); dot.append("strict digraph G {\n"); @@ -1051,12 +1054,13 @@ String buildRawPackageGraphDot( /** Builds escaped package-graph DOT suitable for a JavaScript template literal. */ String buildPackageGraphDot( - Graph packageGraph, String repoUrl, CodebaseGraphDTO codebaseGraphDTO) { + Graph packageGraph, String repoUrl, CodebaseGraphDTO codebaseGraphDTO) { return toJavaScriptTemplateLiteral(buildRawPackageGraphDot(packageGraph, repoUrl, codebaseGraphDTO)); } + /** Appends a weighted package edge to DOT, using safe node IDs and highlighting selected removals. */ private void renderPackageGraphEdge( - Graph packageGraph, DefaultWeightedEdge edge, StringBuilder dot) { + Graph packageGraph, DefaultWeightedEdge edge, StringBuilder dot) { // render edge String[] vertexes = extractVertexes(edge); @@ -1085,8 +1089,9 @@ private void renderPackageGraphEdge( dot.append(" ];\n"); } + /** Appends escaped package nodes to DOT and highlights packages selected for removal. */ private void renderPackageVertices( - Graph classGraph, + Graph classGraph, String repoUrl, CodebaseGraphDTO codebaseGraphDTO, Set vertexesToRender, @@ -1098,7 +1103,7 @@ private void renderPackageVertices( dot.append(escapeDotQuoted(packageName)); dot.append("\""); - if (packagesToRemove.contains(packageName)) { + if (packagesToRemove.contains(VertexInfo.forName(packageName))) { dot.append(" color=red style=filled"); } diff --git a/report/src/main/java/org/hjug/refactorfirst/report/JsonGenerator.java b/report/src/main/java/org/hjug/refactorfirst/report/JsonGenerator.java index 8f3b8bac..4fc81f7f 100644 --- a/report/src/main/java/org/hjug/refactorfirst/report/JsonGenerator.java +++ b/report/src/main/java/org/hjug/refactorfirst/report/JsonGenerator.java @@ -20,6 +20,7 @@ import org.hjug.feedback.CycleRemovalResult; import org.hjug.git.GitLogReader; import org.hjug.graphbuilder.CodebaseGraphDTO; +import org.hjug.graphbuilder.VertexInfo; import org.hjug.graphbuilder.metrics.DisharmonyMetric; import org.hjug.metrics.DisharmonyInstance; import org.hjug.refactorfirst.report.model.*; @@ -273,8 +274,8 @@ public RefactorFirstReportDTO generateReportData( .targetClassPath(classSourcePath(endVertex, codebaseGraphDTO)) .simpleSourceClassName(getClassName(startVertex)) .simpleTargetClassName(getClassName(endVertex)) - .sourceMarked(classesToRemove.contains(startVertex)) - .targetMarked(classesToRemove.contains(endVertex)) + .sourceMarked(classesToRemove.contains(VertexInfo.forName(startVertex))) + .targetMarked(classesToRemove.contains(VertexInfo.forName(endVertex))) .weight((int) classGraph.getEdgeWeight(edgeInfo.getEdge())) .priority(edgeInfo.getPriority()) .cycleCount(edgeInfo.getCycleCount()) @@ -383,8 +384,8 @@ public RefactorFirstReportDTO generateReportData( .targetPackage(endVertex) .sourcePackagePath(sourcePackagePath) .targetPackagePath(targetPackagePath) - .sourceMarked(packagesToRemove.contains(startVertex)) - .targetMarked(packagesToRemove.contains(endVertex)) + .sourceMarked(packagesToRemove.contains(VertexInfo.forName(startVertex))) + .targetMarked(packagesToRemove.contains(VertexInfo.forName(endVertex))) .weight((int) packageGraph.getEdgeWeight(edgeInfo.getEdge())) .priority(edgeInfo.getPriority()) .cycleCount(edgeInfo.getCycleCount()) @@ -432,7 +433,7 @@ public RefactorFirstReportDTO generateReportData( List breakdown = new ArrayList<>(); for (String vertex : largestCycle.getVertexSet()) { - boolean marked = classesToRemove.contains(vertex); + boolean marked = classesToRemove.contains(VertexInfo.forName(vertex)); StringBuilder edges = new StringBuilder(); for (DefaultWeightedEdge edge : largestCycle.getEdgeSet()) { @@ -793,8 +794,8 @@ List buildClassRelationshipsToBreakPackage( .targetClassPath(classSourcePath(endVertex, codebaseGraphDTO)) .simpleSourceClassName(getClassName(startVertex)) .simpleTargetClassName(getClassName(endVertex)) - .sourceMarked(classesToRemove.contains(startVertex)) - .targetMarked(classesToRemove.contains(endVertex)) + .sourceMarked(classesToRemove.contains(VertexInfo.forName(startVertex))) + .targetMarked(classesToRemove.contains(VertexInfo.forName(endVertex))) .weight(weight) .cycleCount(cycleCounts.get(classEdge)) .build()); @@ -818,7 +819,7 @@ private int countClassCyclesContaining(DefaultWeightedEdge classEdge) { return 0; } int cycleCount = 0; - for (AsSubgraph cycle : classCycles.values()) { + for (AsSubgraph cycle : classCycles.values()) { if (cycle.containsEdge(classEdge)) { cycleCount++; } diff --git a/report/src/main/java/org/hjug/refactorfirst/report/SimpleHtmlReport.java b/report/src/main/java/org/hjug/refactorfirst/report/SimpleHtmlReport.java index 4f09587c..603c5fee 100644 --- a/report/src/main/java/org/hjug/refactorfirst/report/SimpleHtmlReport.java +++ b/report/src/main/java/org/hjug/refactorfirst/report/SimpleHtmlReport.java @@ -20,6 +20,7 @@ import org.hjug.feedback.CycleRemovalResult; import org.hjug.git.GitLogReader; import org.hjug.graphbuilder.CodebaseGraphDTO; +import org.hjug.graphbuilder.VertexInfo; import org.hjug.graphbuilder.metrics.DisharmonyMetric; import org.hjug.graphbuilder.metrics.DisharmonyTypes; import org.hjug.metrics.DisharmonyInstance; @@ -159,12 +160,12 @@ public class SimpleHtmlReport { "Data class contains non-accessor methods with business logic, violating the data carrier principle.", "Move business logic to separate service classes. Keep data classes as pure data holders with only accessor methods.")); - Graph classGraph; - Graph packageGraph; - Map> classCycles; - Map> packageCycles; - Set classesToRemove = Set.of(); // initialize for unit tests - Set packagesToRemove = Set.of(); // initialize for unit tests + Graph classGraph; + Graph packageGraph; + Map> classCycles; + Map> packageCycles; + Set classesToRemove = Set.of(); // initialize for unit tests + Set packagesToRemove = Set.of(); // initialize for unit tests Set classRelationshipsToRemove = Set.of(); Set packageRelationshipsToRemove = Set.of(); @@ -678,7 +679,10 @@ List sortedClassEdgesInPackageRelationship(Set + classGraph.getEdgeSource(classEdge).getName()) + .thenComparing( + classEdge -> classGraph.getEdgeTarget(classEdge).getName())) .toList(); } @@ -738,7 +742,7 @@ private String renderClassEdge(DefaultWeightedEdge edge, String repoUrl, Codebas String startVertex = vertexes[0].trim(); String start; - if (classesToRemove.contains(startVertex)) { + if (classesToRemove.contains(VertexInfo.forName(startVertex))) { start = hyperlinkClass(startVertex, repoUrl, codebaseGraphDTO) + "*"; } else { start = hyperlinkClass(startVertex, repoUrl, codebaseGraphDTO); @@ -746,7 +750,7 @@ private String renderClassEdge(DefaultWeightedEdge edge, String repoUrl, Codebas String endVertex = vertexes[1].trim(); String end; - if (classesToRemove.contains(endVertex)) { + if (classesToRemove.contains(VertexInfo.forName(endVertex))) { end = hyperlinkClass(endVertex, repoUrl, codebaseGraphDTO) + "*"; } else { end = hyperlinkClass(endVertex, repoUrl, codebaseGraphDTO); @@ -765,7 +769,7 @@ private String renderPackageEdge(DefaultWeightedEdge edge, String repoUrl, Codeb String startVertex = vertexes[0].trim(); String start; - if (packagesToRemove.contains(startVertex)) { + if (packagesToRemove.contains(VertexInfo.forName(startVertex))) { start = escapeHtmlLabel(startVertex) + "*"; } else { start = escapeHtmlLabel(startVertex); @@ -773,7 +777,7 @@ private String renderPackageEdge(DefaultWeightedEdge edge, String repoUrl, Codeb String endVertex = vertexes[1].trim(); String end; - if (packagesToRemove.contains(endVertex)) { + if (packagesToRemove.contains(VertexInfo.forName(endVertex))) { end = escapeHtmlLabel(endVertex) + "*"; } else { end = escapeHtmlLabel(endVertex); @@ -877,7 +881,7 @@ private String renderSingleCycle(RankedCycle cycle, String repoUrl, CodebaseGrap for (String vertex : cycle.getVertexSet()) { stringBuilder.append(""); String className; - if (classesToRemove.contains(vertex)) { + if (classesToRemove.contains(VertexInfo.forName(vertex))) { className = hyperlinkClass(vertex, repoUrl, codebaseGraphDTO) + "*"; } else { className = hyperlinkClass(vertex, repoUrl, codebaseGraphDTO); diff --git a/report/src/test/java/org/hjug/refactorfirst/report/HtmlReportKotlinTest.java b/report/src/test/java/org/hjug/refactorfirst/report/HtmlReportKotlinTest.java index 0eed6bf2..60c130df 100644 --- a/report/src/test/java/org/hjug/refactorfirst/report/HtmlReportKotlinTest.java +++ b/report/src/test/java/org/hjug/refactorfirst/report/HtmlReportKotlinTest.java @@ -10,6 +10,7 @@ import org.hjug.cbc.CycleNode; import org.hjug.cbc.RankedCycle; import org.hjug.graphbuilder.CodebaseGraphDTO; +import org.hjug.graphbuilder.VertexInfo; import org.jgrapht.Graph; import org.jgrapht.graph.DefaultDirectedWeightedGraph; import org.jgrapht.graph.DefaultWeightedEdge; @@ -57,24 +58,30 @@ class HtmlReportKotlinTest { @DisplayName("HtmlReport.buildClassCycleDot emits .kt URLs from Kotlin source-path mapping") @Test void buildClassCycleDot_kotlinSourcePaths() { - Graph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + Graph classGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); // The renderer uses simple class names for the edge spec (matches the // existing Java case which expects A -> B, not FQN). The class-graph // vertices are still FQNs; the renderer strips the package prefix. String a = "com.kotlin.cycles.KotlinCycleA"; String b = "com.kotlin.cycles.KotlinCycleB"; String c = "com.kotlin.cycles.KotlinCycleC"; - classGraph.addVertex(a); - classGraph.addVertex(b); - classGraph.addVertex(c); - classGraph.addEdge(a, b); - classGraph.addEdge(b, c); - classGraph.addEdge(c, a); - classGraph.setEdgeWeight(a, b, 2); + classGraph.addVertex(VertexInfo.forName(a)); + classGraph.addVertex(VertexInfo.forName(b)); + classGraph.addVertex(VertexInfo.forName(c)); + classGraph.addEdge(VertexInfo.forName(a), VertexInfo.forName(b)); + classGraph.addEdge(VertexInfo.forName(b), VertexInfo.forName(c)); + classGraph.addEdge(VertexInfo.forName(c), VertexInfo.forName(a)); + classGraph.setEdgeWeight(VertexInfo.forName(a), VertexInfo.forName(b), 2); List cycleNodes = new ArrayList<>(); - RankedCycle rankedCycle = - new RankedCycle("KotlinCycle", classGraph.vertexSet(), classGraph.edgeSet(), cycleNodes); + RankedCycle rankedCycle = new RankedCycle( + "KotlinCycle", + classGraph.vertexSet().stream() + .map(VertexInfo::getName) + .collect(java.util.stream.Collectors.toCollection(java.util.LinkedHashSet::new)), + classGraph.edgeSet(), + cycleNodes); HtmlReport htmlReport = new HtmlReport(); CodebaseGraphDTO dto = mock(CodebaseGraphDTO.class); diff --git a/report/src/test/java/org/hjug/refactorfirst/report/HtmlReportTest.java b/report/src/test/java/org/hjug/refactorfirst/report/HtmlReportTest.java index 50a84296..5c10b43a 100644 --- a/report/src/test/java/org/hjug/refactorfirst/report/HtmlReportTest.java +++ b/report/src/test/java/org/hjug/refactorfirst/report/HtmlReportTest.java @@ -8,6 +8,7 @@ import org.hjug.cbc.CycleNode; import org.hjug.cbc.RankedCycle; import org.hjug.graphbuilder.CodebaseGraphDTO; +import org.hjug.graphbuilder.VertexInfo; import org.jgrapht.Graph; import org.jgrapht.graph.DefaultDirectedWeightedGraph; import org.jgrapht.graph.DefaultWeightedEdge; @@ -41,18 +42,25 @@ void getDescription() { /** Verifies cycle DOT preserves Java source links and edge weights while omitting redundant labels. */ @Test void buildClassCycleDot() { - Graph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - classGraph.addVertex("A"); - classGraph.addVertex("B"); - classGraph.addVertex("C"); - classGraph.addEdge("A", "B"); - classGraph.addEdge("B", "C"); - classGraph.addEdge("C", "A"); - classGraph.setEdgeWeight("A", "B", 2); + Graph classGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + classGraph.addVertex(VertexInfo.forName("A")); + classGraph.addVertex(VertexInfo.forName("B")); + classGraph.addVertex(VertexInfo.forName("C")); + classGraph.addEdge(VertexInfo.forName("A"), VertexInfo.forName("B")); + classGraph.addEdge(VertexInfo.forName("B"), VertexInfo.forName("C")); + classGraph.addEdge(VertexInfo.forName("C"), VertexInfo.forName("A")); + classGraph.setEdgeWeight(VertexInfo.forName("A"), VertexInfo.forName("B"), 2); String cycleName = "Test"; List cycleNodes = new ArrayList<>(); - RankedCycle rankedCycle = new RankedCycle(cycleName, classGraph.vertexSet(), classGraph.edgeSet(), cycleNodes); + RankedCycle rankedCycle = new RankedCycle( + cycleName, + classGraph.vertexSet().stream() + .map(VertexInfo::getName) + .collect(java.util.stream.Collectors.toCollection(java.util.LinkedHashSet::new)), + classGraph.edgeSet(), + cycleNodes); HtmlReport htmlReport = new HtmlReport(); CodebaseGraphDTO dto = mock(CodebaseGraphDTO.class); @@ -82,20 +90,21 @@ void buildClassCycleDot() { */ @Test void isSinkAnonymousOrSyntheticVertex_truthTable() { - Graph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - classGraph.addVertex("com.foo.Outer$1"); - classGraph.addVertex("com.foo.Outer$"); - classGraph.addVertex("com.foo.Outer$2"); - classGraph.addVertex("com.foo.Outer$Inner"); - classGraph.addVertex(""); - classGraph.addVertex(""); - classGraph.addVertex("com.foo.Named"); + Graph classGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + classGraph.addVertex(VertexInfo.forName("com.foo.Outer$1")); + classGraph.addVertex(VertexInfo.forName("com.foo.Outer$")); + classGraph.addVertex(VertexInfo.forName("com.foo.Outer$2")); + classGraph.addVertex(VertexInfo.forName("com.foo.Outer$Inner")); + classGraph.addVertex(VertexInfo.forName("")); + classGraph.addVertex(VertexInfo.forName("")); + classGraph.addVertex(VertexInfo.forName("com.foo.Named")); // give the "active" vertices an outgoing edge - classGraph.addVertex("com.foo.Other"); - classGraph.addEdge("com.foo.Outer$1", "com.foo.Other"); - classGraph.addEdge("", "com.foo.Other"); - classGraph.addEdge("com.foo.Outer$Inner", "com.foo.Other"); - classGraph.addEdge("com.foo.Named", "com.foo.Other"); + classGraph.addVertex(VertexInfo.forName("com.foo.Other")); + classGraph.addEdge(VertexInfo.forName("com.foo.Outer$1"), VertexInfo.forName("com.foo.Other")); + classGraph.addEdge(VertexInfo.forName(""), VertexInfo.forName("com.foo.Other")); + classGraph.addEdge(VertexInfo.forName("com.foo.Outer$Inner"), VertexInfo.forName("com.foo.Other")); + classGraph.addEdge(VertexInfo.forName("com.foo.Named"), VertexInfo.forName("com.foo.Other")); // sinks (outDegreeOf == 0) and anonymous/synthetic -> suppressed assertTrue(HtmlReport.isSinkAnonymousOrSyntheticVertex(classGraph, "com.foo.Outer$2"), "Outer$2 numeric sink"); @@ -129,22 +138,29 @@ void isSinkAnonymousOrSyntheticVertex_truthTable() { */ @Test void buildClassCycleDot_rendersAnonymousAndSyntheticVertices() { - Graph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - classGraph.addVertex("com.example.Outer"); - classGraph.addVertex("com.example.Outer$1"); // active (outgoing edge) - classGraph.addVertex("com.example.Outer$2"); // sink (no outgoing edge) - classGraph.addVertex("com.example.Other"); - classGraph.addVertex(""); // active (outgoing edge) - classGraph.addVertex(""); // sink + Graph classGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + classGraph.addVertex(VertexInfo.forName("com.example.Outer")); + classGraph.addVertex(VertexInfo.forName("com.example.Outer$1")); // active (outgoing edge) + classGraph.addVertex(VertexInfo.forName("com.example.Outer$2")); // sink (no outgoing edge) + classGraph.addVertex(VertexInfo.forName("com.example.Other")); + classGraph.addVertex(VertexInfo.forName("")); // active (outgoing edge) + classGraph.addVertex(VertexInfo.forName("")); // sink // wire a cycle: Outer -> Outer$1 -> Other -> Outer; anonymous -> Outer; Outer -> anonymous - classGraph.addEdge("com.example.Outer", "com.example.Outer$1"); - classGraph.addEdge("com.example.Outer$1", "com.example.Other"); - classGraph.addEdge("com.example.Other", "com.example.Outer"); - classGraph.addEdge("com.example.Outer", ""); - classGraph.addEdge("", "com.example.Outer"); + classGraph.addEdge(VertexInfo.forName("com.example.Outer"), VertexInfo.forName("com.example.Outer$1")); + classGraph.addEdge(VertexInfo.forName("com.example.Outer$1"), VertexInfo.forName("com.example.Other")); + classGraph.addEdge(VertexInfo.forName("com.example.Other"), VertexInfo.forName("com.example.Outer")); + classGraph.addEdge(VertexInfo.forName("com.example.Outer"), VertexInfo.forName("")); + classGraph.addEdge(VertexInfo.forName(""), VertexInfo.forName("com.example.Outer")); List cycleNodes = new ArrayList<>(); - RankedCycle rankedCycle = new RankedCycle("Cycle", classGraph.vertexSet(), classGraph.edgeSet(), cycleNodes); + RankedCycle rankedCycle = new RankedCycle( + "Cycle", + classGraph.vertexSet().stream() + .map(VertexInfo::getName) + .collect(java.util.stream.Collectors.toCollection(java.util.LinkedHashSet::new)), + classGraph.edgeSet(), + cycleNodes); HtmlReport htmlReport = new HtmlReport(); CodebaseGraphDTO dto = mock(CodebaseGraphDTO.class); @@ -201,20 +217,33 @@ void buildClassCycleDot_rendersAnonymousAndSyntheticVertices() { */ @Test void buildClassCycleDot_rendersKotlinAnonymousWithEnclosingSourceFileName() { - Graph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - classGraph.addVertex("dev.DeveloperWASDControl"); + Graph classGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + classGraph.addVertex(VertexInfo.forName("dev.DeveloperWASDControl")); // OpenRewrite attributes Kotlin anonymous objects with an FQN whose trailing segment is // the literal "" but whose prefix carries the enclosing class, e.g. // "dev.DeveloperWASDControl.". The source-mapping key equals this full FQN. - classGraph.addVertex("dev.DeveloperWASDControl."); - classGraph.addVertex("com.almasb.fxgl.app.GameApplication"); + classGraph.addVertex(VertexInfo.forName("dev.DeveloperWASDControl.")); + classGraph.addVertex(VertexInfo.forName("com.almasb.fxgl.app.GameApplication")); // anonymous depends on GameApplication; DeveloperWASDControl depends on anonymous -> cycle - classGraph.addEdge("dev.DeveloperWASDControl.", "com.almasb.fxgl.app.GameApplication"); - classGraph.addEdge("dev.DeveloperWASDControl", "dev.DeveloperWASDControl."); - classGraph.addEdge("com.almasb.fxgl.app.GameApplication", "dev.DeveloperWASDControl"); + classGraph.addEdge( + VertexInfo.forName("dev.DeveloperWASDControl."), + VertexInfo.forName("com.almasb.fxgl.app.GameApplication")); + classGraph.addEdge( + VertexInfo.forName("dev.DeveloperWASDControl"), + VertexInfo.forName("dev.DeveloperWASDControl.")); + classGraph.addEdge( + VertexInfo.forName("com.almasb.fxgl.app.GameApplication"), + VertexInfo.forName("dev.DeveloperWASDControl")); List cycleNodes = new ArrayList<>(); - RankedCycle rankedCycle = new RankedCycle("Cycle", classGraph.vertexSet(), classGraph.edgeSet(), cycleNodes); + RankedCycle rankedCycle = new RankedCycle( + "Cycle", + classGraph.vertexSet().stream() + .map(VertexInfo::getName) + .collect(java.util.stream.Collectors.toCollection(java.util.LinkedHashSet::new)), + classGraph.edgeSet(), + cycleNodes); HtmlReport htmlReport = new HtmlReport(); CodebaseGraphDTO dto = mock(CodebaseGraphDTO.class); @@ -305,14 +334,15 @@ void renderClassCycleVisuals_usesNonEmptyIdentifierForEmptyName() { assertFalse(result.contains("const _dot")); } + /** Verifies distinct package node IDs and escaping of script terminators and backslashes in embedded DOT. */ @Test void buildPackageGraphDot_usesDistinctIdsAndEscapesRawScriptContext() { - Graph graph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + Graph graph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); String dotted = "1.a.b"; String underscored = "1.a_b\\name"; - graph.addVertex(dotted); - graph.addVertex(underscored); - graph.addEdge(dotted, underscored); + graph.addVertex(VertexInfo.forName(dotted)); + graph.addVertex(VertexInfo.forName(underscored)); + graph.addEdge(VertexInfo.forName(dotted), VertexInfo.forName(underscored)); String dot = new HtmlReport().buildPackageGraphDot(graph, "", null); @@ -340,9 +370,10 @@ void renderSafeNodeId_withGraphContext_usesSimpleNameWhenUnique() { String pixel = "com.almasb.fxgl.texture.Pixel"; // Create a graph with only one Pixel class - Graph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - classGraph.addVertex("com.almasb.fxgl.texture.Pixel"); - classGraph.addVertex("com.almasb.fxgl.texture.Texture"); + Graph classGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + classGraph.addVertex(VertexInfo.forName("com.almasb.fxgl.texture.Pixel")); + classGraph.addVertex(VertexInfo.forName("com.almasb.fxgl.texture.Texture")); String nodeId = htmlReport.renderSafeNodeId(pixel, classGraph); @@ -361,10 +392,11 @@ void renderSafeNodeId_withGraphContext_usesFqnWhenCollision() { String pixel2 = "com.almasb.fxgl.texture.Pixel"; // Create a graph with both Pixel classes - Graph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - classGraph.addVertex("com.almasb.fxgl.app.scene.Pixel"); - classGraph.addVertex("com.almasb.fxgl.texture.Pixel"); - classGraph.addVertex("com.almasb.fxgl.texture.Texture"); + Graph classGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + classGraph.addVertex(VertexInfo.forName("com.almasb.fxgl.app.scene.Pixel")); + classGraph.addVertex(VertexInfo.forName("com.almasb.fxgl.texture.Pixel")); + classGraph.addVertex(VertexInfo.forName("com.almasb.fxgl.texture.Texture")); String nodeId1 = htmlReport.renderSafeNodeId("com.almasb.fxgl.app.scene.Pixel", classGraph); String nodeId2 = htmlReport.renderSafeNodeId("com.almasb.fxgl.texture.Pixel", classGraph); @@ -383,9 +415,10 @@ void renderSafeNodeId_withGraphContext_usesFqnWhenCollision() { void renderSafeNodeId_withGraphContext_innerClassUsesFqn() { HtmlReport htmlReport = new HtmlReport(); - Graph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - classGraph.addVertex("com.example.Outer$Inner"); - classGraph.addVertex("com.example.Outer"); + Graph classGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + classGraph.addVertex(VertexInfo.forName("com.example.Outer$Inner")); + classGraph.addVertex(VertexInfo.forName("com.example.Outer")); String nodeId = htmlReport.renderSafeNodeId("com.example.Outer$Inner", classGraph); @@ -401,8 +434,9 @@ void renderSafeNodeId_withGraphContext_innerClassUsesFqn() { void renderSafeNodeId_withGraphContext_anonymousUsesSpecialEncoding() { HtmlReport htmlReport = new HtmlReport(); - Graph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - classGraph.addVertex(""); + Graph classGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + classGraph.addVertex(VertexInfo.forName("")); String nodeId = htmlReport.renderSafeNodeId("", classGraph); @@ -417,8 +451,9 @@ void renderSafeNodeId_withGraphContext_anonymousUsesSpecialEncoding() { void renderSafeNodeId_withGraphContext_anonymousWithPrefixUsesFqn() { HtmlReport htmlReport = new HtmlReport(); - Graph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - classGraph.addVertex("dev.DeveloperWASDControl."); + Graph classGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + classGraph.addVertex(VertexInfo.forName("dev.DeveloperWASDControl.")); String nodeId = htmlReport.renderSafeNodeId("dev.DeveloperWASDControl.", classGraph); diff --git a/report/src/test/java/org/hjug/refactorfirst/report/PackageRelationshipClassEdgeCycleCountTest.java b/report/src/test/java/org/hjug/refactorfirst/report/PackageRelationshipClassEdgeCycleCountTest.java index 48ba93fd..1430e0f4 100644 --- a/report/src/test/java/org/hjug/refactorfirst/report/PackageRelationshipClassEdgeCycleCountTest.java +++ b/report/src/test/java/org/hjug/refactorfirst/report/PackageRelationshipClassEdgeCycleCountTest.java @@ -8,6 +8,7 @@ import java.util.Set; import org.hjug.dsm.CircularReferenceChecker; import org.hjug.graphbuilder.CodebaseGraphDTO; +import org.hjug.graphbuilder.VertexInfo; import org.hjug.refactorfirst.report.model.ClassRelationshipDTO; import org.jgrapht.Graph; import org.jgrapht.graph.AsSubgraph; @@ -34,17 +35,18 @@ class PackageRelationshipClassEdgeCycleCountTest { private static final String DELTA = PKG_A + ".Delta"; /** Alpha and Beta form a 2-cycle; Delta -> Beta hangs off the cycle and is in no cycle. */ - private Graph createClassGraph() { - Graph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - classGraph.addVertex(ALPHA); - classGraph.addVertex(BETA); - classGraph.addVertex(DELTA); + private Graph createClassGraph() { + Graph classGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + classGraph.addVertex(VertexInfo.forName(ALPHA)); + classGraph.addVertex(VertexInfo.forName(BETA)); + classGraph.addVertex(VertexInfo.forName(DELTA)); - DefaultWeightedEdge alphaToBeta = classGraph.addEdge(ALPHA, BETA); + DefaultWeightedEdge alphaToBeta = classGraph.addEdge(VertexInfo.forName(ALPHA), VertexInfo.forName(BETA)); classGraph.setEdgeWeight(alphaToBeta, 5); - DefaultWeightedEdge betaToAlpha = classGraph.addEdge(BETA, ALPHA); + DefaultWeightedEdge betaToAlpha = classGraph.addEdge(VertexInfo.forName(BETA), VertexInfo.forName(ALPHA)); classGraph.setEdgeWeight(betaToAlpha, 2); - DefaultWeightedEdge deltaToBeta = classGraph.addEdge(DELTA, BETA); + DefaultWeightedEdge deltaToBeta = classGraph.addEdge(VertexInfo.forName(DELTA), VertexInfo.forName(BETA)); classGraph.setEdgeWeight(deltaToBeta, 1); return classGraph; @@ -63,16 +65,16 @@ private CodebaseGraphDTO mockDto() { void buildClassRelationshipsToBreakPackage_reportsActualCycleMembership() throws Exception { // Given: a class graph whose Alpha -> Beta and Beta -> Alpha edges participate in the // Alpha/Beta class cycle, and a Delta -> Beta edge that participates in no cycle - Graph classGraph = createClassGraph(); - DefaultWeightedEdge alphaToBeta = classGraph.getEdge(ALPHA, BETA); - DefaultWeightedEdge betaToAlpha = classGraph.getEdge(BETA, ALPHA); - DefaultWeightedEdge deltaToBeta = classGraph.getEdge(DELTA, BETA); + Graph classGraph = createClassGraph(); + DefaultWeightedEdge alphaToBeta = classGraph.getEdge(VertexInfo.forName(ALPHA), VertexInfo.forName(BETA)); + DefaultWeightedEdge betaToAlpha = classGraph.getEdge(VertexInfo.forName(BETA), VertexInfo.forName(ALPHA)); + DefaultWeightedEdge deltaToBeta = classGraph.getEdge(VertexInfo.forName(DELTA), VertexInfo.forName(BETA)); JsonGenerator generator = new JsonGenerator(); generator.classGraph = classGraph; // The class cycles, built the same way CycleRemovalComputer builds them - Map> cycles = - new CircularReferenceChecker().getCycles(classGraph); + Map> cycles = + new CircularReferenceChecker().getCycles(classGraph); generator.classCycles = cycles; assertEquals(1, cycles.size(), "Test graph must contain exactly one class cycle"); assertEquals( diff --git a/report/src/test/java/org/hjug/refactorfirst/report/PackageRelationshipClassEdgeOrderTest.java b/report/src/test/java/org/hjug/refactorfirst/report/PackageRelationshipClassEdgeOrderTest.java index ba708a6e..b4b3cc4d 100644 --- a/report/src/test/java/org/hjug/refactorfirst/report/PackageRelationshipClassEdgeOrderTest.java +++ b/report/src/test/java/org/hjug/refactorfirst/report/PackageRelationshipClassEdgeOrderTest.java @@ -5,16 +5,10 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import org.hjug.cbc.RankedDisharmony; import org.hjug.graphbuilder.CodebaseGraphDTO; +import org.hjug.graphbuilder.VertexInfo; import org.hjug.refactorfirst.report.model.ClassRelationshipDTO; import org.jgrapht.Graph; import org.jgrapht.graph.DefaultDirectedWeightedGraph; @@ -45,14 +39,15 @@ class PackageRelationshipClassEdgeOrderTest { * {@link #PKG_B}, plus two edges sharing one source whose targets' natural order differs * from their insertion order ({@code TargetZ} is inserted before {@code TargetB}). */ - private Graph createClassGraph() { - Graph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + private Graph createClassGraph() { + Graph classGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); for (int i = 0; i < EDGE_COUNT; i++) { String source = PKG_A + ".Class" + (char) ('A' + i); String target = PKG_B + ".Target" + (char) ('A' + i); - classGraph.addVertex(source); - classGraph.addVertex(target); - DefaultWeightedEdge edge = classGraph.addEdge(source, target); + classGraph.addVertex(VertexInfo.forName(source)); + classGraph.addVertex(VertexInfo.forName(target)); + DefaultWeightedEdge edge = classGraph.addEdge(VertexInfo.forName(source), VertexInfo.forName(target)); classGraph.setEdgeWeight(edge, i + 1); } @@ -61,23 +56,26 @@ private Graph createClassGraph() { String multiSource = PKG_A + ".Multi"; String targetZ = PKG_B + ".TargetZ"; String targetB = PKG_B + ".TargetB"; - classGraph.addVertex(multiSource); - classGraph.addVertex(targetZ); - classGraph.addVertex(targetB); - DefaultWeightedEdge multiToTargetZ = classGraph.addEdge(multiSource, targetZ); + classGraph.addVertex(VertexInfo.forName(multiSource)); + classGraph.addVertex(VertexInfo.forName(targetZ)); + classGraph.addVertex(VertexInfo.forName(targetB)); + DefaultWeightedEdge multiToTargetZ = + classGraph.addEdge(VertexInfo.forName(multiSource), VertexInfo.forName(targetZ)); classGraph.setEdgeWeight(multiToTargetZ, EDGE_COUNT + 1); - DefaultWeightedEdge multiToTargetB = classGraph.addEdge(multiSource, targetB); + DefaultWeightedEdge multiToTargetB = + classGraph.addEdge(VertexInfo.forName(multiSource), VertexInfo.forName(targetB)); classGraph.setEdgeWeight(multiToTargetB, EDGE_COUNT + 2); return classGraph; } /** Builds a package graph with a single A -> B edge. */ - private Graph createPackageGraph() { - Graph packageGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - packageGraph.addVertex(PKG_A); - packageGraph.addVertex(PKG_B); - packageGraph.addEdge(PKG_A, PKG_B); + private Graph createPackageGraph() { + Graph packageGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + packageGraph.addVertex(VertexInfo.forName(PKG_A)); + packageGraph.addVertex(VertexInfo.forName(PKG_B)); + packageGraph.addEdge(VertexInfo.forName(PKG_A), VertexInfo.forName(PKG_B)); return packageGraph; } @@ -142,8 +140,8 @@ private List buildJsonList(JsonGenerator generator, Codeba void classRelationshipsToBreakPackage_areOrderedDeterministically() throws Exception { // Given: a class graph with edges crossing a package boundary, and DTOs whose // class-relationship sets are populated in many different insertion orders - Graph classGraph = createClassGraph(); - Graph packageGraph = createPackageGraph(); + Graph classGraph = createClassGraph(); + Graph packageGraph = createPackageGraph(); DefaultWeightedEdge packageEdge = packageGraph.edgeSet().iterator().next(); List edges = new ArrayList<>(classGraph.edgeSet()); RankedDisharmony edgeInfo = mockRankedDisharmony(packageEdge); @@ -188,22 +186,21 @@ void classRelationshipsToBreakPackage_areOrderedDeterministically() throws Excep // Then: the order is the deterministic source-class order, identical in both reports List expectedOrder = edges.stream() - .sorted((e1, e2) -> { - int bySource = classGraph.getEdgeSource(e1).compareTo(classGraph.getEdgeSource(e2)); - return bySource != 0 - ? bySource - : classGraph.getEdgeTarget(e1).compareTo(classGraph.getEdgeTarget(e2)); - }) + .sorted(Comparator.comparing((DefaultWeightedEdge e) -> + classGraph.getEdgeSource(e).getName()) + .thenComparing(e -> classGraph.getEdgeTarget(e).getName())) .toList(); assertEquals( - expectedOrder.stream().map(classGraph::getEdgeSource).toList(), + expectedOrder.stream() + .map(edge -> classGraph.getEdgeSource(edge).getName()) + .toList(), firstJsonList.stream().map(ClassRelationshipDTO::getSourceClass).toList(), "JSON class relationships must be ordered by source class FQN"); String expectedHtmlCell = String.join( "
", expectedOrder.stream() - .map(edge -> simpleName(classGraph.getEdgeSource(edge)) + " → " - + simpleName(classGraph.getEdgeTarget(edge)) + " : " + .map(edge -> simpleName(classGraph.getEdgeSource(edge).getName()) + " → " + + simpleName(classGraph.getEdgeTarget(edge).getName()) + " : " + (int) classGraph.getEdgeWeight(edge)) .toList()); assertEquals( diff --git a/report/src/test/java/org/hjug/refactorfirst/report/SimpleHtmlReportTest.java b/report/src/test/java/org/hjug/refactorfirst/report/SimpleHtmlReportTest.java index 0aa82f88..bbf44064 100644 --- a/report/src/test/java/org/hjug/refactorfirst/report/SimpleHtmlReportTest.java +++ b/report/src/test/java/org/hjug/refactorfirst/report/SimpleHtmlReportTest.java @@ -7,6 +7,7 @@ import java.util.*; import org.hjug.cbc.RankedDisharmony; import org.hjug.graphbuilder.CodebaseGraphDTO; +import org.hjug.graphbuilder.VertexInfo; import org.jgrapht.Graph; import org.jgrapht.graph.DefaultDirectedWeightedGraph; import org.jgrapht.graph.DefaultWeightedEdge; @@ -214,10 +215,12 @@ void getPackageRelationshipDisharmony_nullClassRelationships_returnsEmptyList() CodebaseGraphDTO dto = Mockito.mock(CodebaseGraphDTO.class); // Mock package graph with an edge - Graph packageGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - packageGraph.addVertex("com.pkg1"); - packageGraph.addVertex("com.pkg2"); - DefaultWeightedEdge pkgEdge = packageGraph.addEdge("com.pkg1", "com.pkg2"); + Graph packageGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + packageGraph.addVertex(VertexInfo.forName("com.pkg1")); + packageGraph.addVertex(VertexInfo.forName("com.pkg2")); + DefaultWeightedEdge pkgEdge = + packageGraph.addEdge(VertexInfo.forName("com.pkg1"), VertexInfo.forName("com.pkg2")); // Set the packageGraph field via reflection java.lang.reflect.Field packageGraphField = SimpleHtmlReport.class.getDeclaredField("packageGraph");