UNPKG

archunit

Version:

ArchUnit TypeScript is an architecture testing library, to specify and assert architecture rules in your TypeScript app

266 lines 11.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.calculateDistanceMetricsForProject = exports.calculateFileDistanceMetrics = exports.NormalizedDistance = exports.CouplingFactor = exports.DistanceFromMainSequence = exports.Instability = exports.Abstractness = void 0; const extraction_1 = require("../extraction"); const util_1 = require("../../common/util"); /** * Abstractness metric (A) * * Measures the ratio of abstract declarations to total declarations in a file. * Formula: A = Na / N * Where: * - Na = number of abstract elements (interfaces, abstract classes, abstract methods) * - N = total number of declarations */ class Abstractness { constructor() { this.name = 'Abstractness'; this.description = 'Measures the ratio of abstract elements to total declarations (0 = concrete, 1 = abstract)'; } calculateForFile(analysisResult) { // Use sourceFile property if available for more accurate file-wise analysis if (analysisResult.sourceFile) { // Using countDeclarations for more accurate analysis const declarationCounts = (0, util_1.countDeclarations)(analysisResult.sourceFile); if (declarationCounts.total === 0) { return 0; // No declarations to analyze } // Na = Abstract elements (interfaces + abstract classes + abstract methods) const abstractElements = declarationCounts.interfaces + declarationCounts.abstractClasses + declarationCounts.abstractMethods; // N = Total declarations const totalDeclarations = declarationCounts.total; // A = Na / N return abstractElements / totalDeclarations; } else { // Fall back to the previous implementation if sourceFile isn't available // This maintains backward compatibility if (analysisResult.totalTypes === 0) { return 0; // No declarations to analyze } // Na = Abstract elements (interfaces + abstract classes) // Note: Without sourceFile, we can't count abstract methods accurately const abstractElements = analysisResult.interfaces + analysisResult.abstractClasses; // N = Total types const totalDeclarations = analysisResult.totalTypes; // A = Na / N return abstractElements / totalDeclarations; } } } exports.Abstractness = Abstractness; /** * Instability metric (I) * * Measures the ratio of efferent coupling (outgoing dependencies) to total coupling. * Formula: I = Ce / (Ca + Ce) * Where: * - Ce = efferent coupling (outgoing dependencies) * - Ca = afferent coupling (incoming dependencies) */ class Instability { constructor() { this.name = 'Instability'; this.description = 'Measures the instability of a file based on its dependencies (0 = stable, 1 = unstable)'; } calculateForFile(analysisResult) { // Get dependency info from file analysis const dependencies = analysisResult.dependencies; // Calculate efferent coupling (Ce) - outgoing dependencies const efferentCoupling = dependencies.efferentCoupling; // Calculate afferent coupling (Ca) - incoming dependencies const afferentCoupling = dependencies.afferentCoupling; // Calculate total coupling const totalCoupling = efferentCoupling + afferentCoupling; if (totalCoupling === 0) { return 0; // No coupling means stable by default } // I = Ce / (Ca + Ce) return efferentCoupling / totalCoupling; } } exports.Instability = Instability; /** * Distance from Main Sequence metric (D) * * Robert Martin's metric that measures how far a component is from the ideal * balance between abstractness and instability. * * Formula: D = |A + I - 1| * Where: * - A = Abstractness (0 to 1) * - I = Instability (0 to 1) * - The main sequence is the line A + I = 1 * * Interpretation: * - D = 0: On the main sequence (ideal) * - D closer to 0: Better design * - D closer to 1: Further from ideal balance * * When applied to files (file-wise analysis): * - Files that are abstract and stable (A high, I low) are on the main sequence * - Files that are concrete and unstable (A low, I high) are on the main sequence * - Files that are abstract and unstable are in the Zone of Uselessness * - Files that are concrete and stable are in the Zone of Pain */ class DistanceFromMainSequence { constructor() { this.name = 'DistanceFromMainSequence'; this.description = 'Distance from the Main Sequence (Robert Martin) - measures balance between abstractness and instability'; this.abstractness = new Abstractness(); this.instability = new Instability(); } calculateForFile(analysisResult) { // Calculate file-wise abstractness metric (A) const A = this.abstractness.calculateForFile(analysisResult); // Calculate file-wise instability metric (I) const I = this.instability.calculateForFile(analysisResult); // D = |A + I - 1| return Math.abs(A + I - 1); } } exports.DistanceFromMainSequence = DistanceFromMainSequence; /** * Coupling Factor metric (CF) * * Measures how tightly coupled a file is based on both incoming and outgoing dependencies. * Higher values indicate more coupling, which generally should be minimized. * Formula: CF = (Ca + Ce) / Cmax * Where: * - Ca = afferent coupling (incoming dependencies) * - Ce = efferent coupling (outgoing dependencies) * - Cmax = maximum possible coupling (a normalization factor, typically file count - 1) */ class CouplingFactor { constructor() { this.name = 'CouplingFactor'; this.description = 'Measures how tightly coupled a file is based on dependencies (0 = no coupling, 1 = maximum coupling)'; } calculateForFile(analysisResult, totalFiles) { // Get dependency info from file analysis const dependencies = analysisResult.dependencies; // Calculate efferent coupling (Ce) - outgoing dependencies const efferentCoupling = dependencies.efferentCoupling; // Calculate afferent coupling (Ca) - incoming dependencies const afferentCoupling = dependencies.afferentCoupling; // Total coupling is the sum of incoming and outgoing dependencies const totalCoupling = efferentCoupling + afferentCoupling; // If we know total files in project, normalize by that, otherwise use a default normalization const maxPossibleCoupling = totalFiles ? totalFiles - 1 : 10; if (maxPossibleCoupling <= 0) { return 0; // Avoid division by zero } // Normalize to [0,1] return Math.min(1, totalCoupling / maxPossibleCoupling); } } exports.CouplingFactor = CouplingFactor; /** * Normalized Distance metric (ND) * * A modified version of Distance from Main Sequence that accounts for file size. * Files with more code are expected to be more complex and potentially have * more responsibilities, so they should be closer to the main sequence. * Formula: ND = D * (1 - S/Smax) * Where: * - D = Distance from Main Sequence * - S = Size of file (measured in LOC or declarations) * - Smax = Maximum size for normalization */ class NormalizedDistance { constructor() { this.name = 'NormalizedDistance'; this.description = 'A size-adjusted distance metric that accounts for file complexity (0 = ideal balance, 1 = furthest from ideal)'; // Use the standard distance metric as a base this.distance = new DistanceFromMainSequence(); } calculateForFile(analysisResult) { // Calculate the standard distance from main sequence const baseDistance = this.distance.calculateForFile(analysisResult); // Use declaration count as a measure of file size let fileSize = 0; if (analysisResult.sourceFile) { const declarationCounts = (0, util_1.countDeclarations)(analysisResult.sourceFile); fileSize = declarationCounts.total; } else { fileSize = analysisResult.totalTypes; } // Maximum file size for normalization - this can be adjusted based on project standards const maxFileSize = 100; // Normalize file size to a factor between 0 and 1 const sizeFactor = Math.min(1, fileSize / maxFileSize); // Reduce the distance penalty for larger files (more complex files are expected to have // more responsibilities, so they get more leeway in terms of distance from main sequence) return baseDistance * (1 - sizeFactor * 0.5); // Only reduce by up to 50% } } exports.NormalizedDistance = NormalizedDistance; /** * Calculate distance metrics for a single file * @param analysisResult The file analysis result * @param totalFiles Optional total number of files for coupling factor calculation * @returns Distance metrics for the file */ function calculateFileDistanceMetrics(analysisResult, totalFiles) { const abstractness = new Abstractness(); const instability = new Instability(); const distance = new DistanceFromMainSequence(); const coupling = new CouplingFactor(); const normalizedDistance = new NormalizedDistance(); return { filePath: analysisResult.filePath, abstractness: abstractness.calculateForFile(analysisResult), instability: instability.calculateForFile(analysisResult), distanceFromMainSequence: distance.calculateForFile(analysisResult), couplingFactor: coupling.calculateForFile(analysisResult, totalFiles), normalizedDistance: normalizedDistance.calculateForFile(analysisResult), analysisResult, }; } exports.calculateFileDistanceMetrics = calculateFileDistanceMetrics; /** * Utility function to calculate distance metrics for an entire project * using file-wise analysis based on TypeScript AST and dependency graphs */ async function calculateDistanceMetricsForProject(tsConfigPath, projectPath, options) { const analysisResults = await (0, extraction_1.extractEnhancedClassInfo)(tsConfigPath, projectPath, options); const fileCount = analysisResults.length; // Use the helper function to calculate metrics for each file const fileResults = analysisResults.map((result) => calculateFileDistanceMetrics(result, fileCount)); // Calculate project-level summary statistics const totalFiles = fileResults.length; const averageAbstractness = totalFiles > 0 ? fileResults.reduce((sum, f) => sum + f.abstractness, 0) / totalFiles : 0; const averageInstability = totalFiles > 0 ? fileResults.reduce((sum, f) => sum + f.instability, 0) / totalFiles : 0; const averageDistance = totalFiles > 0 ? fileResults.reduce((sum, f) => sum + f.distanceFromMainSequence, 0) / totalFiles : 0; const averageCouplingFactor = totalFiles > 0 ? fileResults.reduce((sum, f) => sum + f.couplingFactor, 0) / totalFiles : 0; const averageNormalizedDistance = totalFiles > 0 ? fileResults.reduce((sum, f) => sum + f.normalizedDistance, 0) / totalFiles : 0; const filesOnMainSequence = fileResults.filter((f) => f.distanceFromMainSequence < 0.1).length; return { fileResults, projectSummary: { totalFiles, averageAbstractness, averageInstability, averageDistance, averageCouplingFactor, averageNormalizedDistance, filesOnMainSequence, }, }; } exports.calculateDistanceMetricsForProject = calculateDistanceMetricsForProject; //# sourceMappingURL=distance.js.map