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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
import org.jgrapht.Graph;
import org.jgrapht.graph.DefaultWeightedEdge;

/**
* Data transfer object containing the complete codebase graph analysis results.
* Includes class and package reference graphs, class-to-package relationships,
* source file mappings, and detected disharmonies (antipatterns).
*/
@Getter
@EqualsAndHashCode
@ToString
Expand All @@ -26,6 +31,16 @@ public class CodebaseGraphDTO {
private final List<ClassDisharmony> classDisharmonies;
private final List<MethodDisharmony> methodDisharmonies;

/**
* Creates a new CodebaseGraphDTO.
*
* @param classReferencesGraph the class reference graph
* @param packageReferencesGraph the package reference graph
* @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<String, DefaultWeightedEdge> classReferencesGraph,
Graph<String, DefaultWeightedEdge> packageReferencesGraph,
Expand All @@ -41,12 +56,24 @@ public CodebaseGraphDTO(
this.methodDisharmonies = methodDisharmonies;
}

/**
* Returns class disharmonies filtered by type.
*
* @param disharmonyType the disharmony type to filter by
* @return list of matching class disharmonies
*/
public List<ClassDisharmony> getClassDisharmoniesOfType(String disharmonyType) {
return classDisharmonies.stream()
.filter(d -> disharmonyType.equals(d.getDisharmonyType()))
.collect(Collectors.toList());
}

/**
* Returns method disharmonies filtered by type.
*
* @param disharmonyType the disharmony type to filter by
* @return list of matching method disharmonies
*/
public List<MethodDisharmony> getMethodDisharmoniesOfType(String disharmonyType) {
return methodDisharmonies.stream()
.filter(d -> disharmonyType.equals(d.getDisharmonyType()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@
public class CompositeGraphBuilder {

/**
* Build a unified {@link CodebaseGraphDTO} from a directory that may contain
* both Java and Kotlin source files.
* Builds a unified graph for Java and Kotlin source files in a repository.
*
* @param repositoryPath path to the source directory
* @param excludeTests whether to exclude test files
* @param repositoryPath path to the source directory
* @param excludeTests whether to exclude test files
* @param testSourceDirectory test source directory pattern
* @return a merged CodebaseGraphDTO
* @throws IOException if parsing fails
* @return the combined Java and Kotlin codebase graph
* @throws IllegalArgumentException if {@code repositoryPath} is null or empty
* @throws IOException if source analysis fails
*/
public CodebaseGraphDTO getCodebaseGraphDTO(String repositoryPath, boolean excludeTests, String testSourceDirectory)
throws IOException {
Expand All @@ -49,6 +49,15 @@ public CodebaseGraphDTO getCodebaseGraphDTO(String repositoryPath, boolean exclu
return getCodebaseGraphDTO(repositoryPath, config);
}

/**
* Build a unified {@link CodebaseGraphDTO} from a directory that may contain
* both Java and Kotlin source files.
*
* @param repositoryPath path to the source directory
* @param config graph-builder configuration
* @return a merged CodebaseGraphDTO
* @throws IOException if parsing fails
*/
public CodebaseGraphDTO getCodebaseGraphDTO(String repositoryPath, GraphBuilderConfig config) throws IOException {
String repositoryRoot = config.getRepositoryRoot() != null ? config.getRepositoryRoot() : "";
return getCodebaseGraphDTO(repositoryPath, repositoryRoot, config);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

import org.jgrapht.graph.DefaultWeightedEdge;

/**
* Interface for collecting dependencies between classes and packages during
* source file analysis. Implementations build the class and package reference
* graphs used by the graph builder.
*/
// TODO: Revisit - I don't think this is really needed
public interface DependencyCollector {

Expand All @@ -18,6 +23,7 @@ public interface DependencyCollector {
*
* @param fromPackageName The package that depends on another
* @param toPackageName The package being depended upon
* @return the package dependency edge, or {@code null} when no package edge is created
*/
DefaultWeightedEdge addPackageDependency(String fromPackageName, String toPackageName);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

/**
* Kotlin-language source-file graph builder. Invoked unconditionally by
* {@link CompositeGraphBuilder} (Kotlin analysis is always on). This
* {@link org.hjug.graphbuilder.CompositeGraphBuilder} (Kotlin analysis is always on). This
* module declares a compile dependency on
* {@code org.openrewrite:rewrite-kotlin}, so the Kotlin parser is always
* on the classpath of any consumer of this builder.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -369,8 +369,10 @@ public void addMethod(MethodMetrics methodMetrics) {
/**
* Aggregated class-level count of Kotlin/Java callable references
* ({@code Klass::method}) across all declared methods. Returns the sum
* of {@link MethodMetrics#getNumberOfCallableReferences()} across every
* of {@code MethodMetrics#getNumberOfCallableReferences()} across every
* method on this class.
*
* @return the total number of callable references
*/
public int getNumberOfCallableReferences() {
return methods.values().stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,31 @@
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.tree.J;

/**
* Calculates cyclomatic complexity and maximum nesting depth for a method
* by visiting its AST nodes. Extends {@link JavaIsoVisitor} to traverse
* the OpenRewrite Java AST.
*/
public class ComplexityCalculator extends JavaIsoVisitor<ExecutionContext> {

private int cyclomaticComplexity = 1;
private int nestingLevel = 0;
private int maxNestingDepth = 0;

/**
* Provides the calculated cyclomatic complexity.
*
* @return the cyclomatic complexity
*/
public int getCyclomaticComplexity() {
return cyclomaticComplexity;
}

/**
* Returns the calculated maximum nesting depth.
*
* @return the maximum nesting depth
*/
public int getMaxNestingDepth() {
return maxNestingDepth;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@
import lombok.Data;
import org.hjug.graphbuilder.metrics.DisharmonyMetric.Direction;

/**
* Detects disharmonies (code smells / antipatterns) in the collected class and
* method metrics. Based on the HIDOOP / DECOR / Lanza-Marinescu catalog.
*
* <p>This class is a pure function of the {@link ClassMetrics} and {@link
* MethodMetrics} populations produced by {@link GraphMetricsCollector}. It has
* no side effects and no external dependencies.
*
* <p>Threshold constants (FEW, HALF, ONE_THIRD, etc.) are defined at the top
* of the class and tuned to the literature values cited in the Lanza-Marinescu
* book. They can be adjusted for specific codebase characteristics if needed.
*/
public class DisharmonyDetector {

// Linguistic quantifiers (Lanza & Marinescu, Table 2.4)
Expand Down Expand Up @@ -371,8 +383,10 @@ public boolean isBrainMethod(MethodMetrics metrics) {
}

/**
* Feature Envy (Fig. 5.4): method accesses more foreign data than local data.
* ATFD &gt; FEW AND LAA &lt; ONE_THIRD AND FDP &lt;= FEW
* Identifies methods that access substantially more foreign data than local data.
*
* @param metrics the method metrics to evaluate
* @return {@code true} if the method has feature envy, {@code false} otherwise
*/
public boolean hasFeatureEnvy(MethodMetrics metrics) {
return metrics.getAccessToForeignData() > FEW
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,8 @@ public void recordMethodMetric(String className, String methodSignature, String

/**
* Returns {@code true} iff at least one collected {@link ClassMetrics}
* carries a Kotlin-specific signal — i.e. {@link ClassMetrics#isDataClass()},
* {@link ClassMetrics#isSealed()}, {@link ClassMetrics#getNumberOfExtensionFunctions()}
* carries a Kotlin-specific signal — i.e. {@code ClassMetrics#isDataClass()},
* {@code ClassMetrics#isSealed()}, {@code ClassMetrics#getNumberOfExtensionFunctions()}
* > 0, a non-empty {@link ClassMetrics#getExtensionReceiverTypes()}, or a
* non-empty {@link ClassMetrics#getSealedHierarchyAncestors()}.
*
Expand All @@ -174,7 +174,7 @@ public void recordMethodMetric(String className, String methodSignature, String
* only after {@link #finalizeMetrics()} has run (detection always runs
* post-finalization); before finalization the cached value is still
* computed on demand but may not reflect {@link #computeKotlinDerivedMetrics()}
* derived flags such as {@link ClassMetrics#isHasExplicitLogic()}.
* derived flags such as {@code ClassMetrics#isHasExplicitLogic()}.
*
* @return {@code true} if any class carries a Kotlin-specific metric signal,
* {@code false} for an empty or Java-only collector
Expand All @@ -199,11 +199,9 @@ public boolean hasKotlinMetrics() {
}

/**
* Read-only lookup of a class's metrics. Returns {@code null} when the
* class has never been registered with this collector. Prefer
* {@link #getOrCreateClassMetrics(String)} from visitor logic that
* intends to mutate the returned instance and have the mutation
* reflected by {@link #getAllClassMetrics()}.
* Looks up the metrics recorded for a class.
*
* @return the class metrics, or {@code null} if no metrics are recorded
*/
public ClassMetrics getClassMetrics(String className) {
return classMetrics.get(className);
Expand Down Expand Up @@ -315,20 +313,10 @@ private int computeSealedDepth(ClassMetrics metrics) {
}

/**
* Canonical get-or-create entry point used by {@link MetricsVisitorLogic}
* and the metrics-collecting visitors. Returns the existing
* {@link ClassMetrics} for {@code className} if present, otherwise
* creates one, stores it in {@link #getAllClassMetrics()}, and returns
* it.
* <p>The returned instance is the <em>same object</em> later returned by
* {@link #getAllClassMetrics()}. This is the invariant the historical
* {@code instanceof GraphMetricsCollector} branch in
* {@link MetricsVisitorLogic#enterClass} emulated: the
* {@link ClassMetrics} the visitor mutates during the walk is the
* instance the downstream disharmony detectors read from
* {@link #getAllClassMetrics()}. Any get-or-create path that builds an
* instance without storing it would silently discard every class's
* metrics.
* Retrieves or creates metrics for a class.
*
* @param className the class name
* @return the existing or newly created class metrics
*/
public ClassMetrics getOrCreateClassMetrics(String className) {
return classMetrics.computeIfAbsent(className, ClassMetrics::new);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ public abstract class AbstractDependencyVisitor<P> extends JavaIsoVisitor<P> {
@Getter
private final DependencyVisitorState state;

/**
* Creates a new AbstractDependencyVisitor.
*
* @param repositoryPath path to the source directory
* @param repositoryRoot path to the repository root
* @param dependencyCollector the dependency collector to use
*/
protected AbstractDependencyVisitor(
String repositoryPath, String repositoryRoot, DependencyCollector dependencyCollector) {
BaseTypeProcessor typeProcessor = new BaseTypeProcessor() {
Expand Down Expand Up @@ -61,6 +68,8 @@ public Javadoc visitDocComment(Javadoc.DocComment docComment, P p) {
* Source-file extension used when synthetic source paths are produced for junit-based
* tests (where the parser's URI is not usable as a repo path). Java returns {@code ".java"},
* Kotlin returns {@code ".kt"}.
*
* @return the source file extension
*/
protected String sourceFileExtension() {
return ".java";
Expand Down Expand Up @@ -189,8 +198,9 @@ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, P
}

/**
* Returns the class-to-source-file-path mapping collected during the visit.
* Delegates to the internal state.
* Provides the source file path associated with each visited class.
*
* @return the class-to-source-file-path mapping
*/
public Map<String, String> getClassToSourceFilePathMapping() {
return state.getClassToSourceFilePathMapping();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,29 @@
import org.openrewrite.java.tree.JavaType;
import org.openrewrite.java.tree.TypeTree;

/**
* Base type processor that provides common type-processing logic for
* dependency extraction. Subclasses implement {@link #getDependencyCollector()}
* to provide the dependency collector instance.
*/
@Slf4j
public abstract class BaseTypeProcessor {

private final TypeDependencyExtractor typeDependencyExtractor = new TypeDependencyExtractor();

/**
* Returns the dependency collector to use for recording class dependencies.
*
* @return the dependency collector
*/
protected abstract DependencyCollector getDependencyCollector();

/**
* Processes a Java type and extracts class dependencies.
*
* @param ownerFqn the fully qualified name of the type owner
* @param javaType the Java type to process
*/
protected void processType(String ownerFqn, JavaType javaType) {
if (javaType == null || javaType instanceof JavaType.Unknown) {
return;
Expand All @@ -26,6 +42,13 @@ protected void processType(String ownerFqn, JavaType javaType) {
}
}

/**
* Processes an annotation and records its class and argument type dependencies.
*
* @param ownerFqn the fully qualified name of the owning type
* @param annotation the annotation to process
* @param cursor the cursor providing processing context
*/
protected void processAnnotation(String ownerFqn, J.Annotation annotation, Cursor cursor) {
if (annotation.getType() instanceof JavaType.Unknown) {
return;
Expand All @@ -45,6 +68,14 @@ protected void processAnnotation(String ownerFqn, J.Annotation annotation, Curso
}
}

/**
* Processes a type parameter and extracts class dependencies from its bounds
* and annotations.
*
* @param ownerFqn the fully qualified name of the type owner
* @param typeParameter the type parameter to process
* @param cursor the cursor for context
*/
protected void processTypeParameter(String ownerFqn, J.TypeParameter typeParameter, Cursor cursor) {
if (null != typeParameter.getBounds()) {
for (TypeTree bound : typeParameter.getBounds()) {
Expand All @@ -59,6 +90,12 @@ protected void processTypeParameter(String ownerFqn, J.TypeParameter typeParamet
}
}

/**
* Processes all annotations at the given cursor position.
*
* @param ownerFqn the fully qualified name of the type owner
* @param cursor the cursor for context
*/
protected void processAnnotations(String ownerFqn, Cursor cursor) {
AnnotationService annotationService = new AnnotationService();
for (J.Annotation annotation : annotationService.getAllAnnotations(cursor)) {
Expand Down
Loading
Loading