UNPKG

archunit

Version:

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

257 lines (254 loc) 13.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ViolationFactory = void 0; const assertion_1 = require("../../common/assertion"); const assertion_2 = require("../../files/assertion"); const assertion_3 = require("../../slices/assertion"); const assertion_4 = require("../../metrics/assertion"); const count_metrics_1 = require("../../metrics/fluentapi/metrics/count-metrics"); const color_utils_1 = require("./color-utils"); const regex_factory_1 = require("../../common/regex-factory"); const path_1 = __importDefault(require("path")); class UnknownTestViolation { constructor(details = Object()) { this.details = Object(); this.message = 'Unknown Violation found'; this.details = details; } } class ViolationFactory { // Convert relative path to absolute path static preparePath(relativePath) { const makePathAbsolute = false; if (!makePathAbsolute) { return path_1.default.normalize(relativePath); } // If the path is already absolute (contains : or starts with /), return it if (relativePath.includes(':') || relativePath.startsWith('/')) { return path_1.default.normalize(relativePath); } // For relative paths, use Node.js path handling // This will resolve against the current working directory try { const path = require('path'); const cwd = process.cwd(); // Convert to forward slashes for better IDE clickability return path.normalize(path.resolve(cwd, relativePath)); } catch (error) { // Fallback in case we're not in Node environment return path_1.default.normalize(relativePath); } } static from(violation) { if (violation instanceof assertion_2.ViolatingNode) { return this.fromViolatingFile(violation); } if (violation instanceof assertion_1.EmptyTestViolation) { return this.fromEmptyTestViolation(violation); } if (violation instanceof assertion_3.ViolatingEdge) { return this.fromViolatingEdge(violation); } if (violation instanceof assertion_2.ViolatingCycle) { return this.fromViolatingCycle(violation); } if (violation instanceof assertion_2.ViolatingFileDependency) { return this.fromViolatingFileDependency(violation); } if (violation instanceof assertion_4.MetricViolation) { return this.fromMetricViolation(violation); } if (violation instanceof count_metrics_1.FileCountViolation) { return this.fromFileCountViolation(violation); } if (violation instanceof assertion_2.CustomFileViolation) { return this.fromCustomFileViolation(violation); } return new UnknownTestViolation(violation); } static fromMetricViolation(metric) { const comparisonText = this.getComparisonDescription(metric.comparison); const message = `${color_utils_1.ColorUtils.formatViolationType('Metric violation')} in class '${color_utils_1.ColorUtils.cyan(metric.className)}': File: ${color_utils_1.ColorUtils.formatFilePath(`${this.preparePath(metric.filePath)}:1:1`)} Metric: ${color_utils_1.ColorUtils.formatMetricValue(metric.metricName)} Actual value: ${color_utils_1.ColorUtils.formatMetricValue(metric.metricValue.toString())} Expected: ${color_utils_1.ColorUtils.formatRule(`${comparisonText} ${metric.threshold}`)}`; return { message, details: metric, }; } static fromFileCountViolation(violation) { const comparisonText = this.getComparisonDescription(violation.comparison); const message = `${color_utils_1.ColorUtils.formatViolationType('File count violation')}: File: ${color_utils_1.ColorUtils.formatFilePath(`${this.preparePath(violation.filePath)}:1:1`)} Metric: ${color_utils_1.ColorUtils.formatMetricValue(violation.metricName)} Actual value: ${color_utils_1.ColorUtils.formatMetricValue(violation.metricValue.toString())} Expected: ${color_utils_1.ColorUtils.formatRule(`${comparisonText} ${violation.threshold}`)}`; return { message, details: violation, }; } static fromCustomFileViolation(violation) { const message = `${color_utils_1.ColorUtils.formatViolationType('Custom file condition violation')}: File: ${color_utils_1.ColorUtils.formatFilePath(`${this.preparePath(violation.fileInfo.path)}:1:1`)} Rule: ${color_utils_1.ColorUtils.formatRule(violation.message)} ${color_utils_1.ColorUtils.dim('File details:')} ${color_utils_1.ColorUtils.dim(`• Name: ${violation.fileInfo.name}`)} ${color_utils_1.ColorUtils.dim(`• Extension: ${violation.fileInfo.extension}`)} ${color_utils_1.ColorUtils.dim(`• Directory: ${this.preparePath(violation.fileInfo.directory)}`)} ${color_utils_1.ColorUtils.dim(`• Lines of code: ${violation.fileInfo.linesOfCode}`)}`; return { message, details: violation, }; } static fromViolatingFile(file) { const action = file.isNegated ? 'should not match' : 'should match'; const message = `${color_utils_1.ColorUtils.formatViolationType('File pattern violation')}: File: ${color_utils_1.ColorUtils.formatFilePath(`${this.preparePath(file.projectedNode.label)}:1:1`)} File: (${this.preparePath(file.projectedNode.label)}:1:1) Rule: ${color_utils_1.ColorUtils.formatRule(`${action} pattern '${file.checkPattern}'`)}`; return { message, details: file, }; } static fromEmptyTestViolation(emptyTest) { let patternString = ''; const filters = emptyTest.filters; if (filters.length > 0) { if (typeof filters[0] === 'string') { patternString = filters.join(','); } else { patternString = filters .map((filter) => (0, regex_factory_1.getPatternString)(filter.regExp)) .join(', '); } } const message = `${color_utils_1.ColorUtils.formatViolationType('Empty test violation')}: ${color_utils_1.ColorUtils.formatRule('No files found matching the specified pattern(s)')} Patterns: ${color_utils_1.ColorUtils.formatMetricValue(patternString)} ${color_utils_1.ColorUtils.yellow('This usually indicates:')} ${color_utils_1.ColorUtils.dim('• Pattern is too restrictive or incorrect')} ${color_utils_1.ColorUtils.dim('• Files might not exist in the expected location')} ${color_utils_1.ColorUtils.dim('• Test is not actually testing anything')} ${color_utils_1.ColorUtils.cyan('To fix:')} ${color_utils_1.ColorUtils.dim('• Verify the patterns match existing files')} ${color_utils_1.ColorUtils.dim('• Use .check({ allowEmptyTests: true }) to disable this check')}`; return { message, details: emptyTest, }; } static fromViolatingEdge(edge) { // Extract actual file paths from cumulatedEdges for more detailed reporting const actualFiles = edge.projectedEdge.cumulatedEdges.map((e) => ({ source: e.source, target: e.target, importKinds: e.importKinds, })); // Create a comprehensive violation message let message = `${color_utils_1.ColorUtils.formatViolationType('Slice dependency violation')}: From slice: ${color_utils_1.ColorUtils.formatFilePath(`${this.preparePath(edge.projectedEdge.sourceLabel)}:1:1`)} To slice: ${color_utils_1.ColorUtils.formatFilePath(`${this.preparePath(edge.projectedEdge.targetLabel)}:1:1`)} Rule: ${color_utils_1.ColorUtils.formatRule('This dependency is not allowed')}`; // Add detailed file-level information if available if (actualFiles.length > 0) { message += `\n\n ${color_utils_1.ColorUtils.formatViolationType('Violating dependencies:')}`; actualFiles.forEach((file, index) => { const importInfo = file.importKinds && file.importKinds.length > 0 ? ` (${file.importKinds.join(', ')})` : ''; message += `\n ${index + 1}. ${color_utils_1.ColorUtils.formatFilePath(`${this.preparePath(file.source)}:1:1`)}${color_utils_1.ColorUtils.formatFilePath(`${this.preparePath(file.target)}:1:1`)}${importInfo}`; }); } return { message, details: edge, }; } static fromViolatingFileDependency(edge) { const ruleDescription = edge.isNegated ? 'This dependency should not exist' : 'This dependency violates the architecture rule'; // Extract actual file paths from cumulatedEdges for more detailed reporting const actualFiles = edge.dependency.cumulatedEdges.map((e) => ({ source: e.source, target: e.target, importKinds: e.importKinds, })); let message = `${color_utils_1.ColorUtils.formatViolationType('File dependency violation')}: From pattern: ${color_utils_1.ColorUtils.formatFilePath(`${this.preparePath(edge.dependency.sourceLabel)}:1:1`)} To pattern: ${color_utils_1.ColorUtils.formatFilePath(`${this.preparePath(edge.dependency.targetLabel)}:1:1`)} Rule: ${color_utils_1.ColorUtils.formatRule(ruleDescription)}`; // Add detailed file-level information if available if (actualFiles.length > 0) { message += `\n\n ${color_utils_1.ColorUtils.formatViolationType('Violating dependencies:')}`; actualFiles.forEach((file, index) => { const importInfo = file.importKinds && file.importKinds.length > 0 ? ` (${file.importKinds.join(', ')})` : ''; message += `\n ${index + 1}. ${color_utils_1.ColorUtils.formatFilePath(`${this.preparePath(file.source)}:1:1`)}${color_utils_1.ColorUtils.formatFilePath(`${this.preparePath(file.target)}:1:1`)}${importInfo}`; }); } return { message, details: edge, }; } static fromViolatingCycle(cycle) { // Make each file in the cycle clickable with colors const coloredCycle = cycle.cycle .map((edge) => color_utils_1.ColorUtils.formatFilePath(`${this.preparePath(edge.sourceLabel)}:1:1`)) .concat(color_utils_1.ColorUtils.formatFilePath(`${this.preparePath(cycle.cycle[cycle.cycle.length - 1].targetLabel)}:1:1`)) .join(` ${color_utils_1.ColorUtils.gray('→')} `); let message = `${color_utils_1.ColorUtils.formatViolationType('Circular dependency detected')}: Cycle: ${coloredCycle} Rule: ${color_utils_1.ColorUtils.formatRule('Circular dependencies are not allowed')}`; // Add detailed file-level information for each edge in the cycle const hasDetailedFiles = cycle.cycle.some((edge) => edge.cumulatedEdges && edge.cumulatedEdges.length > 0); if (hasDetailedFiles) { message += `\n\n ${color_utils_1.ColorUtils.formatViolationType('Detailed cycle dependencies:')}`; cycle.cycle.forEach((edge, index) => { if (edge.cumulatedEdges && edge.cumulatedEdges.length > 0) { message += `\n ${index + 1}. ${color_utils_1.ColorUtils.formatFilePath(`${this.preparePath(edge.sourceLabel)}:1:1`)}${color_utils_1.ColorUtils.formatFilePath(`${this.preparePath(edge.targetLabel)}:1:1`)}`; edge.cumulatedEdges.forEach((file, fileIndex) => { const importInfo = file.importKinds && file.importKinds.length > 0 ? ` (${file.importKinds.join(', ')})` : ''; message += `\n ${String.fromCharCode(97 + fileIndex)}. ${color_utils_1.ColorUtils.formatFilePath(`${this.preparePath(file.source)}:1:1`)}${color_utils_1.ColorUtils.formatFilePath(`${this.preparePath(file.target)}:1:1`)}${importInfo}`; }); } }); } return { message, details: cycle, }; } static getComparisonDescription(comparison) { switch (comparison) { case 'below': return 'should be below'; case 'below-equal': return 'should be below or equal to'; case 'above': return 'should be above'; case 'above-equal': return 'should be above or equal to'; case 'equal': return 'should be equal to'; default: return 'should be'; } } } exports.ViolationFactory = ViolationFactory; //# sourceMappingURL=violation-factory.js.map