tech-debt-score 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/input/FileSystemReader.d.ts.map +1 -1
- package/dist/adapters/input/FileSystemReader.js +4 -5
- package/dist/adapters/input/FileSystemReader.js.map +1 -1
- package/dist/adapters/output/TerminalReporter.d.ts.map +1 -1
- package/dist/adapters/output/TerminalReporter.js +8 -0
- package/dist/adapters/output/TerminalReporter.js.map +1 -1
- package/dist/application/config/AnalysisConfig.d.ts.map +1 -1
- package/dist/application/config/AnalysisConfig.js +10 -2
- package/dist/application/config/AnalysisConfig.js.map +1 -1
- package/dist/application/services/AnalysisService.d.ts.map +1 -1
- package/dist/application/services/AnalysisService.js +29 -11
- package/dist/application/services/AnalysisService.js.map +1 -1
- package/dist/cli/commands/analyze.d.ts.map +1 -1
- package/dist/cli/commands/analyze.js +8 -7
- package/dist/cli/commands/analyze.js.map +1 -1
- package/package.json +8 -2
- package/DEVELOPMENT.md +0 -147
- package/SETUP_COMPLETE.md +0 -188
- package/TECHNICAL_DESIGN.md +0 -563
- package/src/adapters/input/FileSystemReader.ts +0 -47
- package/src/adapters/input/TypeScriptParser.ts +0 -367
- package/src/adapters/output/JsonExporter.ts +0 -48
- package/src/adapters/output/TerminalReporter.ts +0 -94
- package/src/application/config/AnalysisConfig.ts +0 -58
- package/src/application/ports/IFileReader.ts +0 -36
- package/src/application/ports/IParser.ts +0 -40
- package/src/application/ports/IReporter.ts +0 -26
- package/src/application/services/AnalysisService.ts +0 -218
- package/src/application/services/DependencyAnalyzer.ts +0 -158
- package/src/application/services/DuplicationDetector.ts +0 -207
- package/src/cli/commands/analyze.ts +0 -77
- package/src/cli/index.ts +0 -81
- package/src/domain/entities/Finding.ts +0 -79
- package/src/domain/entities/Metric.ts +0 -70
- package/src/domain/entities/Rule.ts +0 -49
- package/src/domain/entities/Score.ts +0 -94
- package/src/domain/index.ts +0 -15
- package/src/domain/rules/CircularDependencyRule.ts +0 -65
- package/src/domain/rules/ComplexityRule.ts +0 -88
- package/src/domain/rules/DuplicationRule.ts +0 -70
- package/src/domain/rules/SizeRule.ts +0 -98
- package/src/domain/rules/TypeSafetyRule.ts +0 -63
- package/src/index.ts +0 -0
- package/src/shared/types.ts +0 -18
- package/tests/application/index.test.ts +0 -12
- package/tests/domain/index.test.ts +0 -14
- package/tests/e2e/index.test.ts +0 -13
- package/tsconfig.json +0 -31
package/src/cli/index.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* CLI Entry Point
|
|
4
|
-
* Command-line interface for tech-debt-score
|
|
5
|
-
*
|
|
6
|
-
* NO BUSINESS LOGIC HERE - just argument parsing and delegation
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { analyzeCommand } from './commands/analyze.js';
|
|
10
|
-
|
|
11
|
-
async function main() {
|
|
12
|
-
const args = process.argv.slice(2);
|
|
13
|
-
|
|
14
|
-
if (args.includes('--help') || args.includes('-h')) {
|
|
15
|
-
printHelp();
|
|
16
|
-
process.exit(0);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Parse arguments
|
|
20
|
-
let rootPath = process.cwd();
|
|
21
|
-
let jsonOutputPath: string | undefined;
|
|
22
|
-
|
|
23
|
-
for (let i = 0; i < args.length; i++) {
|
|
24
|
-
const arg = args[i];
|
|
25
|
-
if (!arg) continue;
|
|
26
|
-
|
|
27
|
-
if (arg === '--json' || arg === '-j') {
|
|
28
|
-
const nextArg = args[i + 1];
|
|
29
|
-
if (nextArg && !nextArg.startsWith('-')) {
|
|
30
|
-
jsonOutputPath = nextArg;
|
|
31
|
-
i++; // Skip next arg
|
|
32
|
-
}
|
|
33
|
-
} else if (!arg.startsWith('-')) {
|
|
34
|
-
rootPath = arg;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
await analyzeCommand(rootPath, jsonOutputPath);
|
|
40
|
-
process.exit(0);
|
|
41
|
-
} catch (error) {
|
|
42
|
-
console.error('❌ Analysis failed:', error instanceof Error ? error.message : error);
|
|
43
|
-
process.exit(1);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function printHelp() {
|
|
48
|
-
console.log(`
|
|
49
|
-
┌─────────────────────────────────────────────┐
|
|
50
|
-
│ tech-debt-score - Quantify Technical Debt │
|
|
51
|
-
│ Built by developers, for developers │
|
|
52
|
-
└─────────────────────────────────────────────┘
|
|
53
|
-
|
|
54
|
-
Usage:
|
|
55
|
-
tech-debt-score [path]
|
|
56
|
-
|
|
57
|
-
Arguments:
|
|
58
|
-
path Path to analyze (default: current directory)
|
|
59
|
-
|
|
60
|
-
Options:
|
|
61
|
-
-h, --help Show this help message
|
|
62
|
-
|
|
63
|
-
Examples:
|
|
64
|
-
tech-debt-score # Analyze current directory
|
|
65
|
-
tech-debt-score ./my-project # Analyze specific directory
|
|
66
|
-
tech-debt-score /path/to/code # Analyze absolute path
|
|
67
|
-
|
|
68
|
-
Options:
|
|
69
|
-
-h, --help Show this help message
|
|
70
|
-
-j, --json <file> Output JSON report to specific file
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
For more information, visit:
|
|
74
|
-
https://github.com/panduken/tech-debt-score
|
|
75
|
-
`);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Run if executed directly
|
|
79
|
-
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
80
|
-
main();
|
|
81
|
-
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Domain Entity: Finding
|
|
3
|
-
* Represents an identified issue or code smell in the codebase
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { SourceLocation, Severity } from '../../shared/types.js';
|
|
7
|
-
|
|
8
|
-
export interface Finding {
|
|
9
|
-
/**
|
|
10
|
-
* ID of the rule that generated this finding
|
|
11
|
-
*/
|
|
12
|
-
ruleId: string;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Severity level of the issue
|
|
16
|
-
*/
|
|
17
|
-
severity: Severity;
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Human-readable message describing the issue
|
|
21
|
-
*/
|
|
22
|
-
message: string;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* File path where the issue was found
|
|
26
|
-
*/
|
|
27
|
-
filePath: string;
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Optional exact location in the source code
|
|
31
|
-
*/
|
|
32
|
-
location?: SourceLocation;
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Optional suggestion for fixing the issue
|
|
36
|
-
*/
|
|
37
|
-
suggestion?: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export class FindingBuilder {
|
|
41
|
-
private finding: Partial<Finding> = {};
|
|
42
|
-
|
|
43
|
-
withRuleId(ruleId: string): this {
|
|
44
|
-
this.finding.ruleId = ruleId;
|
|
45
|
-
return this;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
withSeverity(severity: Severity): this {
|
|
49
|
-
this.finding.severity = severity;
|
|
50
|
-
return this;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
withMessage(message: string): this {
|
|
54
|
-
this.finding.message = message;
|
|
55
|
-
return this;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
withFilePath(filePath: string): this {
|
|
59
|
-
this.finding.filePath = filePath;
|
|
60
|
-
return this;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
withLocation(location: SourceLocation): this {
|
|
64
|
-
this.finding.location = location;
|
|
65
|
-
return this;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
withSuggestion(suggestion: string): this {
|
|
69
|
-
this.finding.suggestion = suggestion;
|
|
70
|
-
return this;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
build(): Finding {
|
|
74
|
-
if (!this.finding.ruleId || !this.finding.severity || !this.finding.message || !this.finding.filePath) {
|
|
75
|
-
throw new Error('Finding requires ruleId, severity, message, and filePath');
|
|
76
|
-
}
|
|
77
|
-
return this.finding as Finding;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Domain Entity: Metric
|
|
3
|
-
* Represents a single measurement extracted from code analysis
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { SourceLocation } from '../../shared/types.js';
|
|
7
|
-
|
|
8
|
-
export interface Metric {
|
|
9
|
-
/**
|
|
10
|
-
* Unique identifier for the metric type
|
|
11
|
-
* e.g., 'cyclomatic-complexity', 'function-length', 'any-usage'
|
|
12
|
-
*/
|
|
13
|
-
name: string;
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* The measured value
|
|
17
|
-
*/
|
|
18
|
-
value: number;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* File path where this metric was measured
|
|
22
|
-
*/
|
|
23
|
-
filePath: string;
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Optional location in the source code
|
|
27
|
-
*/
|
|
28
|
-
location?: SourceLocation;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Optional context (e.g., function name, class name)
|
|
32
|
-
*/
|
|
33
|
-
context?: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export class MetricBuilder {
|
|
37
|
-
private metric: Partial<Metric> = {};
|
|
38
|
-
|
|
39
|
-
withName(name: string): this {
|
|
40
|
-
this.metric.name = name;
|
|
41
|
-
return this;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
withValue(value: number): this {
|
|
45
|
-
this.metric.value = value;
|
|
46
|
-
return this;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
withFilePath(filePath: string): this {
|
|
50
|
-
this.metric.filePath = filePath;
|
|
51
|
-
return this;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
withLocation(location: SourceLocation): this {
|
|
55
|
-
this.metric.location = location;
|
|
56
|
-
return this;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
withContext(context: string): this {
|
|
60
|
-
this.metric.context = context;
|
|
61
|
-
return this;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
build(): Metric {
|
|
65
|
-
if (!this.metric.name || this.metric.value === undefined || !this.metric.filePath) {
|
|
66
|
-
throw new Error('Metric requires name, value, and filePath');
|
|
67
|
-
}
|
|
68
|
-
return this.metric as Metric;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Domain Entity: Rule
|
|
3
|
-
* Represents an evaluation rule that processes metrics and generates findings
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { Metric } from './Metric.js';
|
|
7
|
-
import type { Finding } from './Finding.js';
|
|
8
|
-
|
|
9
|
-
export interface Rule {
|
|
10
|
-
/**
|
|
11
|
-
* Unique identifier for this rule
|
|
12
|
-
*/
|
|
13
|
-
id: string;
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Human-readable name
|
|
17
|
-
*/
|
|
18
|
-
name: string;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Description of what this rule evaluates
|
|
22
|
-
*/
|
|
23
|
-
description: string;
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Evaluate metrics and produce findings
|
|
27
|
-
*/
|
|
28
|
-
evaluate(metrics: Metric[]): Finding[];
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Calculate a score (0-100) based on findings
|
|
32
|
-
* where 100 = no issues, 0 = maximum issues
|
|
33
|
-
*/
|
|
34
|
-
calculateScore(findings: Finding[]): number;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Abstract base class for rules
|
|
39
|
-
*/
|
|
40
|
-
export abstract class BaseRule implements Rule {
|
|
41
|
-
constructor(
|
|
42
|
-
public readonly id: string,
|
|
43
|
-
public readonly name: string,
|
|
44
|
-
public readonly description: string
|
|
45
|
-
) {}
|
|
46
|
-
|
|
47
|
-
abstract evaluate(metrics: Metric[]): Finding[];
|
|
48
|
-
abstract calculateScore(findings: Finding[]): number;
|
|
49
|
-
}
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Domain Entity: Score
|
|
3
|
-
* Represents the technical debt score and category breakdowns
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export interface CategoryScore {
|
|
7
|
-
/**
|
|
8
|
-
* Category name (e.g., 'Complexity', 'Size', 'Type Safety')
|
|
9
|
-
*/
|
|
10
|
-
name: string;
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Score for this category (0-100, where 100 = no debt)
|
|
14
|
-
*/
|
|
15
|
-
score: number;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Weight of this category in the overall score (0-1)
|
|
19
|
-
*/
|
|
20
|
-
weight: number;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Optional description of what this category measures
|
|
24
|
-
*/
|
|
25
|
-
description?: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface Score {
|
|
29
|
-
/**
|
|
30
|
-
* Overall technical debt score (0-100, where 100 = no debt)
|
|
31
|
-
*/
|
|
32
|
-
overall: number;
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Breakdown by category
|
|
36
|
-
*/
|
|
37
|
-
categories: CategoryScore[];
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* When this score was calculated
|
|
41
|
-
*/
|
|
42
|
-
timestamp: Date;
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Optional metadata about the analysis
|
|
46
|
-
*/
|
|
47
|
-
metadata?: {
|
|
48
|
-
filesAnalyzed: number;
|
|
49
|
-
totalMetrics: number;
|
|
50
|
-
totalFindings: number;
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export class ScoreCalculator {
|
|
55
|
-
/**
|
|
56
|
-
* Calculate overall score from category scores
|
|
57
|
-
*
|
|
58
|
-
* Formula: Σ(category_score × category_weight)
|
|
59
|
-
*/
|
|
60
|
-
static calculateOverall(categories: CategoryScore[]): number {
|
|
61
|
-
// Validate weights sum to 1.0 (or close to it)
|
|
62
|
-
const totalWeight = categories.reduce((sum, cat) => sum + cat.weight, 0);
|
|
63
|
-
if (Math.abs(totalWeight - 1.0) > 0.01) {
|
|
64
|
-
throw new Error(`Category weights must sum to 1.0, got ${totalWeight}`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Calculate weighted average
|
|
68
|
-
const weightedSum = categories.reduce(
|
|
69
|
-
(sum, cat) => sum + (cat.score * cat.weight),
|
|
70
|
-
0
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
// Round to 2 decimal places
|
|
74
|
-
return Math.round(weightedSum * 100) / 100;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Normalize a raw value to 0-100 scale
|
|
79
|
-
*
|
|
80
|
-
* @param value - The raw value to normalize
|
|
81
|
-
* @param min - Minimum value (maps to 100)
|
|
82
|
-
* @param max - Maximum value (maps to 0)
|
|
83
|
-
* @param invert - If true, higher values = better score
|
|
84
|
-
*/
|
|
85
|
-
static normalize(value: number, min: number, max: number, invert = false): number {
|
|
86
|
-
if (value <= min) return invert ? 0 : 100;
|
|
87
|
-
if (value >= max) return invert ? 100 : 0;
|
|
88
|
-
|
|
89
|
-
const normalized = ((value - min) / (max - min)) * 100;
|
|
90
|
-
const score = invert ? normalized : 100 - normalized;
|
|
91
|
-
|
|
92
|
-
return Math.round(score * 100) / 100;
|
|
93
|
-
}
|
|
94
|
-
}
|
package/src/domain/index.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Domain Layer - Public API
|
|
3
|
-
* Exports all domain entities and services
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
// Entities
|
|
7
|
-
export * from './entities/Metric.js';
|
|
8
|
-
export * from './entities/Finding.js';
|
|
9
|
-
export * from './entities/Score.js';
|
|
10
|
-
export * from './entities/Rule.js';
|
|
11
|
-
|
|
12
|
-
// Rules
|
|
13
|
-
export * from './rules/ComplexityRule.js';
|
|
14
|
-
export * from './rules/SizeRule.js';
|
|
15
|
-
export * from './rules/TypeSafetyRule.js';
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Domain Rule: Circular Dependency Rule
|
|
3
|
-
* Detects circular dependencies between modules
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { BaseRule } from '../entities/Rule.js';
|
|
7
|
-
import type { Metric } from '../entities/Metric.js';
|
|
8
|
-
import type { Finding } from '../entities/Finding.js';
|
|
9
|
-
import { FindingBuilder } from '../entities/Finding.js';
|
|
10
|
-
|
|
11
|
-
export class CircularDependencyRule extends BaseRule {
|
|
12
|
-
constructor() {
|
|
13
|
-
super(
|
|
14
|
-
'circular-dependency',
|
|
15
|
-
'Circular Dependency Rule',
|
|
16
|
-
'Detects circular dependencies between modules that can cause maintenance issues'
|
|
17
|
-
);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
evaluate(metrics: Metric[]): Finding[] {
|
|
21
|
-
const findings: Finding[] = [];
|
|
22
|
-
|
|
23
|
-
// Find circular dependency metrics
|
|
24
|
-
const circularDepMetrics = metrics.filter(m => m.name === 'circular-dependency');
|
|
25
|
-
|
|
26
|
-
for (const metric of circularDepMetrics) {
|
|
27
|
-
if (metric.value > 0) {
|
|
28
|
-
// Extract cycle information from context
|
|
29
|
-
const cycleLength = metric.value;
|
|
30
|
-
const severity = cycleLength >= 4 ? 'high' : cycleLength >= 3 ? 'medium' : 'low';
|
|
31
|
-
|
|
32
|
-
findings.push(
|
|
33
|
-
new FindingBuilder()
|
|
34
|
-
.withRuleId(this.id)
|
|
35
|
-
.withSeverity(severity)
|
|
36
|
-
.withMessage(
|
|
37
|
-
`Circular dependency detected: ${metric.context || 'module cycle'} (${cycleLength} files in cycle)`
|
|
38
|
-
)
|
|
39
|
-
.withFilePath(metric.filePath)
|
|
40
|
-
.withSuggestion('Refactor to break the circular dependency by introducing interfaces or dependency injection')
|
|
41
|
-
.build()
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return findings;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
calculateScore(findings: Finding[]): number {
|
|
50
|
-
if (findings.length === 0) return 100;
|
|
51
|
-
|
|
52
|
-
// Circular dependencies are serious - heavy penalty
|
|
53
|
-
const penalty = findings.reduce((sum, finding) => {
|
|
54
|
-
switch (finding.severity) {
|
|
55
|
-
case 'high': return sum + 20; // Very bad
|
|
56
|
-
case 'medium': return sum + 12;
|
|
57
|
-
case 'low': return sum + 6;
|
|
58
|
-
default: return sum;
|
|
59
|
-
}
|
|
60
|
-
}, 0);
|
|
61
|
-
|
|
62
|
-
const score = Math.max(0, 100 - penalty);
|
|
63
|
-
return Math.round(score * 100) / 100;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Domain Rule: Complexity Rule
|
|
3
|
-
* Evaluates cyclomatic complexity and nesting depth
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { BaseRule } from '../entities/Rule.js';
|
|
7
|
-
import type { Metric } from '../entities/Metric.js';
|
|
8
|
-
import type { Finding } from '../entities/Finding.js';
|
|
9
|
-
import { FindingBuilder } from '../entities/Finding.js';
|
|
10
|
-
|
|
11
|
-
export class ComplexityRule extends BaseRule {
|
|
12
|
-
// Thresholds (configurable in future versions)
|
|
13
|
-
private readonly COMPLEXITY_THRESHOLD = 10;
|
|
14
|
-
private readonly NESTING_THRESHOLD = 4;
|
|
15
|
-
|
|
16
|
-
constructor() {
|
|
17
|
-
super(
|
|
18
|
-
'complexity',
|
|
19
|
-
'Complexity Rule',
|
|
20
|
-
'Evaluates cyclomatic complexity and nesting depth to identify overly complex code'
|
|
21
|
-
);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
evaluate(metrics: Metric[]): Finding[] {
|
|
25
|
-
const findings: Finding[] = [];
|
|
26
|
-
|
|
27
|
-
// Filter for complexity-related metrics
|
|
28
|
-
const complexityMetrics = metrics.filter(
|
|
29
|
-
m => m.name === 'cyclomatic-complexity' || m.name === 'nesting-depth'
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
for (const metric of complexityMetrics) {
|
|
33
|
-
if (metric.name === 'cyclomatic-complexity' && metric.value > this.COMPLEXITY_THRESHOLD) {
|
|
34
|
-
const builder = new FindingBuilder()
|
|
35
|
-
.withRuleId(this.id)
|
|
36
|
-
.withSeverity(metric.value > 20 ? 'high' : 'medium')
|
|
37
|
-
.withMessage(
|
|
38
|
-
`High cyclomatic complexity (${metric.value}) in ${metric.context || 'function'}`
|
|
39
|
-
)
|
|
40
|
-
.withFilePath(metric.filePath)
|
|
41
|
-
.withSuggestion('Consider breaking this function into smaller, more focused functions');
|
|
42
|
-
|
|
43
|
-
if (metric.location) {
|
|
44
|
-
builder.withLocation(metric.location);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
findings.push(builder.build());
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (metric.name === 'nesting-depth' && metric.value > this.NESTING_THRESHOLD) {
|
|
51
|
-
const builder = new FindingBuilder()
|
|
52
|
-
.withRuleId(this.id)
|
|
53
|
-
.withSeverity(metric.value > 6 ? 'high' : 'medium')
|
|
54
|
-
.withMessage(
|
|
55
|
-
`Deep nesting (${metric.value} levels) in ${metric.context || 'function'}`
|
|
56
|
-
)
|
|
57
|
-
.withFilePath(metric.filePath)
|
|
58
|
-
.withSuggestion('Reduce nesting by using early returns or extracting functions');
|
|
59
|
-
|
|
60
|
-
if (metric.location) {
|
|
61
|
-
builder.withLocation(metric.location);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
findings.push(builder.build());
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return findings;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
calculateScore(findings: Finding[]): number {
|
|
72
|
-
if (findings.length === 0) return 100;
|
|
73
|
-
|
|
74
|
-
// Penalty based on severity
|
|
75
|
-
const penalty = findings.reduce((sum, finding) => {
|
|
76
|
-
switch (finding.severity) {
|
|
77
|
-
case 'high': return sum + 10;
|
|
78
|
-
case 'medium': return sum + 5;
|
|
79
|
-
case 'low': return sum + 2;
|
|
80
|
-
default: return sum;
|
|
81
|
-
}
|
|
82
|
-
}, 0);
|
|
83
|
-
|
|
84
|
-
// Cap the penalty and invert to get score
|
|
85
|
-
const score = Math.max(0, 100 - penalty);
|
|
86
|
-
return Math.round(score * 100) / 100;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Domain Rule: Code Duplication Rule
|
|
3
|
-
* Detects duplicate code blocks using token-based similarity
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { BaseRule } from '../entities/Rule.js';
|
|
7
|
-
import type { Metric } from '../entities/Metric.js';
|
|
8
|
-
import type { Finding } from '../entities/Finding.js';
|
|
9
|
-
import { FindingBuilder } from '../entities/Finding.js';
|
|
10
|
-
|
|
11
|
-
export class DuplicationRule extends BaseRule {
|
|
12
|
-
// Thresholds
|
|
13
|
-
private readonly MIN_DUPLICATE_TOKENS = 50; // Minimum tokens to consider as duplication
|
|
14
|
-
private readonly SIMILARITY_THRESHOLD = 0.95; // 95% similarity
|
|
15
|
-
|
|
16
|
-
constructor() {
|
|
17
|
-
super(
|
|
18
|
-
'duplication',
|
|
19
|
-
'Code Duplication Rule',
|
|
20
|
-
'Detects duplicate or highly similar code blocks across the codebase'
|
|
21
|
-
);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
evaluate(metrics: Metric[]): Finding[] {
|
|
25
|
-
const findings: Finding[] = [];
|
|
26
|
-
|
|
27
|
-
// Group metrics by type
|
|
28
|
-
const duplicationMetrics = metrics.filter(m => m.name === 'code-duplication');
|
|
29
|
-
|
|
30
|
-
for (const metric of duplicationMetrics) {
|
|
31
|
-
if (metric.value > 0) {
|
|
32
|
-
const severity = metric.value > 5 ? 'high' : metric.value > 2 ? 'medium' : 'low';
|
|
33
|
-
|
|
34
|
-
const builder = new FindingBuilder()
|
|
35
|
-
.withRuleId(this.id)
|
|
36
|
-
.withSeverity(severity)
|
|
37
|
-
.withMessage(
|
|
38
|
-
`Found ${metric.value} duplicate code block(s) in ${metric.context || 'file'}`
|
|
39
|
-
)
|
|
40
|
-
.withFilePath(metric.filePath)
|
|
41
|
-
.withSuggestion('Extract duplicate code into reusable functions or modules');
|
|
42
|
-
|
|
43
|
-
if (metric.location) {
|
|
44
|
-
builder.withLocation(metric.location);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
findings.push(builder.build());
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return findings;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
calculateScore(findings: Finding[]): number {
|
|
55
|
-
if (findings.length === 0) return 100;
|
|
56
|
-
|
|
57
|
-
// Each duplication finding reduces score
|
|
58
|
-
const penalty = findings.reduce((sum, finding) => {
|
|
59
|
-
switch (finding.severity) {
|
|
60
|
-
case 'high': return sum + 15;
|
|
61
|
-
case 'medium': return sum + 8;
|
|
62
|
-
case 'low': return sum + 4;
|
|
63
|
-
default: return sum;
|
|
64
|
-
}
|
|
65
|
-
}, 0);
|
|
66
|
-
|
|
67
|
-
const score = Math.max(0, 100 - penalty);
|
|
68
|
-
return Math.round(score * 100) / 100;
|
|
69
|
-
}
|
|
70
|
-
}
|