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
JavaScript
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
;