@eduardbar/drift 1.1.0 → 1.3.0
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/.github/workflows/publish-vscode.yml +3 -3
- package/.github/workflows/publish.yml +3 -3
- package/.github/workflows/review-pr.yml +153 -0
- package/AGENTS.md +6 -0
- package/README.md +192 -4
- package/ROADMAP.md +6 -5
- package/dist/analyzer.d.ts +2 -2
- package/dist/analyzer.js +420 -159
- package/dist/benchmark.d.ts +2 -0
- package/dist/benchmark.js +185 -0
- package/dist/cli.js +509 -23
- package/dist/diff.js +74 -10
- package/dist/git.js +12 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -0
- package/dist/map.d.ts +3 -2
- package/dist/map.js +98 -10
- package/dist/plugins.d.ts +2 -1
- package/dist/plugins.js +177 -28
- package/dist/printer.js +4 -0
- package/dist/review.js +2 -2
- package/dist/rules/comments.js +2 -2
- package/dist/rules/complexity.js +2 -7
- package/dist/rules/nesting.js +3 -13
- package/dist/rules/phase0-basic.js +10 -10
- package/dist/rules/shared.d.ts +2 -0
- package/dist/rules/shared.js +27 -3
- package/dist/saas.d.ts +219 -0
- package/dist/saas.js +762 -0
- package/dist/trust-kpi.d.ts +9 -0
- package/dist/trust-kpi.js +445 -0
- package/dist/trust.d.ts +65 -0
- package/dist/trust.js +571 -0
- package/dist/types.d.ts +160 -0
- package/docs/PRD.md +199 -172
- package/docs/plugin-contract.md +61 -0
- package/docs/trust-core-release-checklist.md +55 -0
- package/package.json +5 -3
- package/packages/vscode-drift/src/code-actions.ts +53 -0
- package/packages/vscode-drift/src/extension.ts +11 -0
- package/src/analyzer.ts +484 -155
- package/src/benchmark.ts +244 -0
- package/src/cli.ts +628 -36
- package/src/diff.ts +75 -10
- package/src/git.ts +16 -0
- package/src/index.ts +63 -0
- package/src/map.ts +112 -10
- package/src/plugins.ts +354 -26
- package/src/printer.ts +4 -0
- package/src/review.ts +2 -2
- package/src/rules/comments.ts +2 -2
- package/src/rules/complexity.ts +2 -7
- package/src/rules/nesting.ts +3 -13
- package/src/rules/phase0-basic.ts +11 -12
- package/src/rules/shared.ts +31 -3
- package/src/saas.ts +1031 -0
- package/src/trust-kpi.ts +518 -0
- package/src/trust.ts +774 -0
- package/src/types.ts +177 -0
- package/tests/diff.test.ts +124 -0
- package/tests/new-features.test.ts +98 -0
- package/tests/plugins.test.ts +219 -0
- package/tests/rules.test.ts +23 -1
- package/tests/saas-foundation.test.ts +464 -0
- package/tests/trust-kpi.test.ts +120 -0
- package/tests/trust.test.ts +584 -0
package/dist/diff.js
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
|
+
function normalizePath(filePath) {
|
|
2
|
+
return filePath.replace(/\\/g, '/');
|
|
3
|
+
}
|
|
4
|
+
function normalizeIssueText(value) {
|
|
5
|
+
return value
|
|
6
|
+
.replace(/\r\n/g, '\n')
|
|
7
|
+
.replace(/\r/g, '\n')
|
|
8
|
+
.replace(/\s+/g, ' ')
|
|
9
|
+
.trim();
|
|
10
|
+
}
|
|
1
11
|
/**
|
|
2
12
|
* Compute the diff between two DriftReports.
|
|
3
13
|
*
|
|
4
|
-
* Issues are matched
|
|
14
|
+
* Issues are matched in two passes:
|
|
15
|
+
* 1) strict location key (rule + line + column)
|
|
16
|
+
* 2) normalized content key (rule + severity + line + message + snippet)
|
|
17
|
+
*
|
|
18
|
+
* This keeps deterministic matching while preventing false churn caused by
|
|
19
|
+
* cross-platform line ending changes and small column offset noise.
|
|
5
20
|
* A "new" issue exists in `current` but not in `base`.
|
|
6
21
|
* A "resolved" issue exists in `base` but not in `current`.
|
|
7
22
|
*/
|
|
@@ -11,11 +26,60 @@ function computeFileDiff(filePath, baseFile, currentFile) {
|
|
|
11
26
|
const scoreDelta = scoreAfter - scoreBefore;
|
|
12
27
|
const baseIssues = baseFile?.issues ?? [];
|
|
13
28
|
const currentIssues = currentFile?.issues ?? [];
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
29
|
+
const strictIssueKey = (i) => `${i.rule}:${i.line}:${i.column}`;
|
|
30
|
+
const normalizedIssueKey = (i) => {
|
|
31
|
+
const normalizedMessage = normalizeIssueText(i.message);
|
|
32
|
+
const normalizedSnippetPrefix = normalizeIssueText(i.snippet).slice(0, 80);
|
|
33
|
+
return `${i.rule}:${i.severity}:${i.line}:${normalizedMessage}:${normalizedSnippetPrefix}`;
|
|
34
|
+
};
|
|
35
|
+
const matchedBaseIndexes = new Set();
|
|
36
|
+
const matchedCurrentIndexes = new Set();
|
|
37
|
+
const baseStrictIndex = new Map();
|
|
38
|
+
for (const [index, issue] of baseIssues.entries()) {
|
|
39
|
+
const key = strictIssueKey(issue);
|
|
40
|
+
const bucket = baseStrictIndex.get(key);
|
|
41
|
+
if (bucket)
|
|
42
|
+
bucket.push(index);
|
|
43
|
+
else
|
|
44
|
+
baseStrictIndex.set(key, [index]);
|
|
45
|
+
}
|
|
46
|
+
for (const [currentIndex, issue] of currentIssues.entries()) {
|
|
47
|
+
const key = strictIssueKey(issue);
|
|
48
|
+
const bucket = baseStrictIndex.get(key);
|
|
49
|
+
if (!bucket || bucket.length === 0)
|
|
50
|
+
continue;
|
|
51
|
+
const matchedIndex = bucket.shift();
|
|
52
|
+
if (matchedIndex === undefined)
|
|
53
|
+
continue;
|
|
54
|
+
matchedBaseIndexes.add(matchedIndex);
|
|
55
|
+
matchedCurrentIndexes.add(currentIndex);
|
|
56
|
+
}
|
|
57
|
+
const baseNormalizedIndex = new Map();
|
|
58
|
+
for (const [index, issue] of baseIssues.entries()) {
|
|
59
|
+
if (matchedBaseIndexes.has(index))
|
|
60
|
+
continue;
|
|
61
|
+
const key = normalizedIssueKey(issue);
|
|
62
|
+
const bucket = baseNormalizedIndex.get(key);
|
|
63
|
+
if (bucket)
|
|
64
|
+
bucket.push(index);
|
|
65
|
+
else
|
|
66
|
+
baseNormalizedIndex.set(key, [index]);
|
|
67
|
+
}
|
|
68
|
+
for (const [currentIndex, issue] of currentIssues.entries()) {
|
|
69
|
+
if (matchedCurrentIndexes.has(currentIndex))
|
|
70
|
+
continue;
|
|
71
|
+
const key = normalizedIssueKey(issue);
|
|
72
|
+
const bucket = baseNormalizedIndex.get(key);
|
|
73
|
+
if (!bucket || bucket.length === 0)
|
|
74
|
+
continue;
|
|
75
|
+
const matchedIndex = bucket.shift();
|
|
76
|
+
if (matchedIndex === undefined)
|
|
77
|
+
continue;
|
|
78
|
+
matchedBaseIndexes.add(matchedIndex);
|
|
79
|
+
matchedCurrentIndexes.add(currentIndex);
|
|
80
|
+
}
|
|
81
|
+
const newIssues = currentIssues.filter((_, index) => !matchedCurrentIndexes.has(index));
|
|
82
|
+
const resolvedIssues = baseIssues.filter((_, index) => !matchedBaseIndexes.has(index));
|
|
19
83
|
if (scoreDelta !== 0 || newIssues.length > 0 || resolvedIssues.length > 0) {
|
|
20
84
|
return {
|
|
21
85
|
path: filePath,
|
|
@@ -30,11 +94,11 @@ function computeFileDiff(filePath, baseFile, currentFile) {
|
|
|
30
94
|
}
|
|
31
95
|
export function computeDiff(base, current, baseRef) {
|
|
32
96
|
const fileDiffs = [];
|
|
33
|
-
const baseByPath = new Map(base.files.map(f => [f.path, f]));
|
|
34
|
-
const currentByPath = new Map(current.files.map(f => [f.path, f]));
|
|
97
|
+
const baseByPath = new Map(base.files.map(f => [normalizePath(f.path), f]));
|
|
98
|
+
const currentByPath = new Map(current.files.map(f => [normalizePath(f.path), f]));
|
|
35
99
|
const allPaths = new Set([
|
|
36
|
-
...base.files.map(f => f.path),
|
|
37
|
-
...current.files.map(f => f.path),
|
|
100
|
+
...base.files.map(f => normalizePath(f.path)),
|
|
101
|
+
...current.files.map(f => normalizePath(f.path)),
|
|
38
102
|
]);
|
|
39
103
|
for (const filePath of allPaths) {
|
|
40
104
|
const baseFile = baseByPath.get(filePath);
|
package/dist/git.js
CHANGED
|
@@ -54,6 +54,15 @@ function extractFile(projectPath, ref, filePath, tempDir) {
|
|
|
54
54
|
mkdirSync(destDir, { recursive: true });
|
|
55
55
|
writeFileSync(destPath, content, 'utf-8');
|
|
56
56
|
}
|
|
57
|
+
function extractArchiveAtRef(projectPath, ref, tempDir) {
|
|
58
|
+
try {
|
|
59
|
+
execSync(`git archive --format=tar ${ref} | tar -x -C "${tempDir}"`, { cwd: projectPath, stdio: 'pipe' });
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
57
66
|
export function extractFilesAtRef(projectPath, ref) {
|
|
58
67
|
verifyGitRepo(projectPath);
|
|
59
68
|
verifyRefExists(projectPath, ref);
|
|
@@ -63,6 +72,9 @@ export function extractFilesAtRef(projectPath, ref) {
|
|
|
63
72
|
}
|
|
64
73
|
const tempDir = join(tmpdir(), `drift-diff-${randomUUID()}`);
|
|
65
74
|
mkdirSync(tempDir, { recursive: true });
|
|
75
|
+
if (extractArchiveAtRef(projectPath, ref, tempDir)) {
|
|
76
|
+
return tempDir;
|
|
77
|
+
}
|
|
66
78
|
for (const filePath of tsFiles) {
|
|
67
79
|
extractFile(projectPath, ref, filePath, tempDir);
|
|
68
80
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,8 +2,12 @@ export { analyzeProject, analyzeFile, RULE_WEIGHTS } from './analyzer.js';
|
|
|
2
2
|
export { buildReport, formatMarkdown } from './reporter.js';
|
|
3
3
|
export { computeDiff } from './diff.js';
|
|
4
4
|
export { generateReview, formatReviewMarkdown } from './review.js';
|
|
5
|
+
export { buildTrustReport, formatTrustConsole, formatTrustMarkdown, formatTrustJson, shouldFailByMaxRisk, shouldFailTrustGate, normalizeMergeRiskLevel, MERGE_RISK_ORDER, } from './trust.js';
|
|
6
|
+
export { computeTrustKpis, computeTrustKpisFromReports, formatTrustKpiConsole, formatTrustKpiJson, } from './trust-kpi.js';
|
|
5
7
|
export { generateArchitectureMap, generateArchitectureSvg } from './map.js';
|
|
6
|
-
export type { DriftReport, FileReport, DriftIssue, DriftDiff, FileDiff, DriftConfig, RepoQualityScore, MaintenanceRiskMetrics, DriftPlugin, DriftPluginRule, } from './types.js';
|
|
8
|
+
export type { DriftReport, FileReport, DriftIssue, DriftDiff, FileDiff, DriftConfig, RepoQualityScore, MaintenanceRiskMetrics, DriftTrustReport, TrustReason, TrustFixPriority, TrustDiffContext, TrustKpiReport, TrustKpiDiagnostic, TrustDiffTrendSummary, TrustScoreStats, MergeRiskLevel, DriftPlugin, DriftPluginRule, } from './types.js';
|
|
7
9
|
export { loadHistory, saveSnapshot } from './snapshot.js';
|
|
8
10
|
export type { SnapshotEntry, SnapshotHistory } from './snapshot.js';
|
|
11
|
+
export { DEFAULT_SAAS_POLICY, defaultSaasStorePath, resolveSaasPolicy, SaasActorRequiredError, SaasPermissionError, getRequiredRoleForOperation, assertSaasPermission, getSaasEffectiveLimits, getOrganizationEffectiveLimits, changeOrganizationPlan, listOrganizationPlanChanges, getOrganizationUsageSnapshot, ingestSnapshotFromReport, listSaasSnapshots, getSaasSummary, generateSaasDashboardHtml, } from './saas.js';
|
|
12
|
+
export type { SaasRole, SaasPlan, SaasPolicy, SaasPolicyOverrides, SaasStore, SaasSummary, SaasSnapshot, SaasQueryOptions, IngestOptions, SaasPlanChange, SaasOperation, SaasPermissionContext, SaasPermissionResult, SaasEffectiveLimits, SaasOrganizationUsageSnapshot, ChangeOrganizationPlanOptions, SaasUsageQueryOptions, SaasPlanChangeQueryOptions, } from './saas.js';
|
|
9
13
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,9 @@ export { analyzeProject, analyzeFile, RULE_WEIGHTS } from './analyzer.js';
|
|
|
2
2
|
export { buildReport, formatMarkdown } from './reporter.js';
|
|
3
3
|
export { computeDiff } from './diff.js';
|
|
4
4
|
export { generateReview, formatReviewMarkdown } from './review.js';
|
|
5
|
+
export { buildTrustReport, formatTrustConsole, formatTrustMarkdown, formatTrustJson, shouldFailByMaxRisk, shouldFailTrustGate, normalizeMergeRiskLevel, MERGE_RISK_ORDER, } from './trust.js';
|
|
6
|
+
export { computeTrustKpis, computeTrustKpisFromReports, formatTrustKpiConsole, formatTrustKpiJson, } from './trust-kpi.js';
|
|
5
7
|
export { generateArchitectureMap, generateArchitectureSvg } from './map.js';
|
|
6
8
|
export { loadHistory, saveSnapshot } from './snapshot.js';
|
|
9
|
+
export { DEFAULT_SAAS_POLICY, defaultSaasStorePath, resolveSaasPolicy, SaasActorRequiredError, SaasPermissionError, getRequiredRoleForOperation, assertSaasPermission, getSaasEffectiveLimits, getOrganizationEffectiveLimits, changeOrganizationPlan, listOrganizationPlanChanges, getOrganizationUsageSnapshot, ingestSnapshotFromReport, listSaasSnapshots, getSaasSummary, generateSaasDashboardHtml, } from './saas.js';
|
|
7
10
|
//# sourceMappingURL=index.js.map
|
package/dist/map.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function
|
|
1
|
+
import type { DriftConfig } from './types.js';
|
|
2
|
+
export declare function generateArchitectureSvg(targetPath: string, config?: DriftConfig): string;
|
|
3
|
+
export declare function generateArchitectureMap(targetPath: string, outputFile?: string, config?: DriftConfig): string;
|
|
3
4
|
//# sourceMappingURL=map.d.ts.map
|
package/dist/map.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { writeFileSync } from 'node:fs';
|
|
2
2
|
import { resolve, relative } from 'node:path';
|
|
3
3
|
import { Project } from 'ts-morph';
|
|
4
|
+
import { detectLayerViolations } from './rules/phase3-arch.js';
|
|
5
|
+
import { RULE_WEIGHTS } from './analyzer.js';
|
|
4
6
|
function detectLayer(relPath) {
|
|
5
7
|
const normalized = relPath.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
6
8
|
const first = normalized.split('/')[0] || 'root';
|
|
@@ -9,7 +11,7 @@ function detectLayer(relPath) {
|
|
|
9
11
|
function esc(value) {
|
|
10
12
|
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
11
13
|
}
|
|
12
|
-
export function generateArchitectureSvg(targetPath) {
|
|
14
|
+
export function generateArchitectureSvg(targetPath, config) {
|
|
13
15
|
const project = new Project({
|
|
14
16
|
skipAddingFilesFromTsConfig: true,
|
|
15
17
|
compilerOptions: { allowJs: true, jsx: 1 },
|
|
@@ -26,24 +28,67 @@ export function generateArchitectureSvg(targetPath) {
|
|
|
26
28
|
]);
|
|
27
29
|
const layers = new Map();
|
|
28
30
|
const edges = new Map();
|
|
31
|
+
const layerAdjacency = new Map();
|
|
32
|
+
const fileImportGraph = new Map();
|
|
29
33
|
for (const file of project.getSourceFiles()) {
|
|
30
|
-
const
|
|
34
|
+
const filePath = file.getFilePath();
|
|
35
|
+
const rel = relative(targetPath, filePath).replace(/\\/g, '/');
|
|
31
36
|
const layerName = detectLayer(rel);
|
|
32
37
|
if (!layers.has(layerName))
|
|
33
38
|
layers.set(layerName, { name: layerName, files: new Set() });
|
|
34
39
|
layers.get(layerName).files.add(rel);
|
|
40
|
+
if (!fileImportGraph.has(filePath))
|
|
41
|
+
fileImportGraph.set(filePath, new Set());
|
|
35
42
|
for (const decl of file.getImportDeclarations()) {
|
|
36
43
|
const imported = decl.getModuleSpecifierSourceFile();
|
|
37
44
|
if (!imported)
|
|
38
45
|
continue;
|
|
46
|
+
fileImportGraph.get(filePath).add(imported.getFilePath());
|
|
39
47
|
const importedRel = relative(targetPath, imported.getFilePath()).replace(/\\/g, '/');
|
|
40
48
|
const importedLayer = detectLayer(importedRel);
|
|
41
49
|
if (importedLayer === layerName)
|
|
42
50
|
continue;
|
|
43
51
|
const key = `${layerName}->${importedLayer}`;
|
|
44
52
|
edges.set(key, (edges.get(key) ?? 0) + 1);
|
|
53
|
+
if (!layerAdjacency.has(layerName))
|
|
54
|
+
layerAdjacency.set(layerName, new Set());
|
|
55
|
+
layerAdjacency.get(layerName).add(importedLayer);
|
|
45
56
|
}
|
|
46
57
|
}
|
|
58
|
+
const cycleEdges = detectCycleEdges(layerAdjacency);
|
|
59
|
+
const violationEdges = new Set();
|
|
60
|
+
if (config?.layers && config.layers.length > 0) {
|
|
61
|
+
const violations = detectLayerViolations(fileImportGraph, config.layers, targetPath, RULE_WEIGHTS);
|
|
62
|
+
for (const issues of violations.values()) {
|
|
63
|
+
for (const issue of issues) {
|
|
64
|
+
const match = issue.message.match(/Layer '([^']+)' must not import from layer '([^']+)'/);
|
|
65
|
+
if (!match)
|
|
66
|
+
continue;
|
|
67
|
+
const from = match[1];
|
|
68
|
+
const to = match[2];
|
|
69
|
+
violationEdges.add(`${from}->${to}`);
|
|
70
|
+
if (!layers.has(from))
|
|
71
|
+
layers.set(from, { name: from, files: new Set() });
|
|
72
|
+
if (!layers.has(to))
|
|
73
|
+
layers.set(to, { name: to, files: new Set() });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const edgeList = [...edges.entries()].map(([key, count]) => {
|
|
78
|
+
const [from, to] = key.split('->');
|
|
79
|
+
const kind = violationEdges.has(key)
|
|
80
|
+
? 'violation'
|
|
81
|
+
: cycleEdges.has(key)
|
|
82
|
+
? 'cycle'
|
|
83
|
+
: 'normal';
|
|
84
|
+
return { key, from, to, count, kind };
|
|
85
|
+
});
|
|
86
|
+
for (const key of violationEdges) {
|
|
87
|
+
if (edges.has(key))
|
|
88
|
+
continue;
|
|
89
|
+
const [from, to] = key.split('->');
|
|
90
|
+
edgeList.push({ key, from, to, count: 1, kind: 'violation' });
|
|
91
|
+
}
|
|
47
92
|
const layerList = [...layers.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
48
93
|
const width = 960;
|
|
49
94
|
const rowHeight = 90;
|
|
@@ -60,19 +105,24 @@ export function generateArchitectureSvg(targetPath) {
|
|
|
60
105
|
};
|
|
61
106
|
});
|
|
62
107
|
const boxByName = new Map(boxes.map((box) => [box.name, box]));
|
|
63
|
-
const lines =
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const b = boxByName.get(to);
|
|
108
|
+
const lines = edgeList.map((edge) => {
|
|
109
|
+
const a = boxByName.get(edge.from);
|
|
110
|
+
const b = boxByName.get(edge.to);
|
|
67
111
|
if (!a || !b)
|
|
68
112
|
return '';
|
|
69
113
|
const startX = a.x + boxWidth;
|
|
70
114
|
const startY = a.y + boxHeight / 2;
|
|
71
115
|
const endX = b.x;
|
|
72
116
|
const endY = b.y + boxHeight / 2;
|
|
117
|
+
const stroke = edge.kind === 'violation'
|
|
118
|
+
? '#ef4444'
|
|
119
|
+
: edge.kind === 'cycle'
|
|
120
|
+
? '#f59e0b'
|
|
121
|
+
: '#64748b';
|
|
122
|
+
const widthPx = edge.kind === 'normal' ? 2 : 3;
|
|
73
123
|
return `
|
|
74
|
-
<line x1="${startX}" y1="${startY}" x2="${endX}" y2="${endY}" stroke="
|
|
75
|
-
<text x="${(startX + endX) / 2}" y="${(startY + endY) / 2 - 4}" fill="#94a3b8" font-size="11" text-anchor="middle">${count}</text>`;
|
|
124
|
+
<line x1="${startX}" y1="${startY}" x2="${endX}" y2="${endY}" stroke="${stroke}" stroke-width="${widthPx}" marker-end="url(#arrow)" data-edge="${esc(edge.key)}" data-kind="${edge.kind}" />
|
|
125
|
+
<text x="${(startX + endX) / 2}" y="${(startY + endY) / 2 - 4}" fill="#94a3b8" font-size="11" text-anchor="middle">${edge.count}</text>`;
|
|
76
126
|
}).join('');
|
|
77
127
|
const nodes = boxes.map((box) => `
|
|
78
128
|
<g>
|
|
@@ -80,6 +130,8 @@ export function generateArchitectureSvg(targetPath) {
|
|
|
80
130
|
<text x="${box.x + 12}" y="${box.y + 22}" fill="#e2e8f0" font-size="13" font-family="monospace">${esc(box.name)}</text>
|
|
81
131
|
<text x="${box.x + 12}" y="${box.y + 38}" fill="#94a3b8" font-size="11" font-family="monospace">${box.files.size} file(s)</text>
|
|
82
132
|
</g>`).join('');
|
|
133
|
+
const cycleCount = edgeList.filter((edge) => edge.kind === 'cycle').length;
|
|
134
|
+
const violationCount = edgeList.filter((edge) => edge.kind === 'violation').length;
|
|
83
135
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
|
|
84
136
|
<defs>
|
|
85
137
|
<marker id="arrow" markerWidth="10" markerHeight="10" refX="6" refY="3" orient="auto">
|
|
@@ -89,13 +141,49 @@ export function generateArchitectureSvg(targetPath) {
|
|
|
89
141
|
<rect x="0" y="0" width="${width}" height="${height}" fill="#020617" />
|
|
90
142
|
<text x="28" y="34" fill="#f8fafc" font-size="16" font-family="monospace">drift architecture map</text>
|
|
91
143
|
<text x="28" y="54" fill="#94a3b8" font-size="11" font-family="monospace">Layers inferred from top-level directories</text>
|
|
144
|
+
<text x="28" y="72" fill="#94a3b8" font-size="11" font-family="monospace">Cycle edges: ${cycleCount} | Layer violations: ${violationCount}</text>
|
|
145
|
+
<line x1="520" y1="66" x2="560" y2="66" stroke="#f59e0b" stroke-width="3" /><text x="567" y="69" fill="#94a3b8" font-size="11" font-family="monospace">cycle</text>
|
|
146
|
+
<line x1="630" y1="66" x2="670" y2="66" stroke="#ef4444" stroke-width="3" /><text x="677" y="69" fill="#94a3b8" font-size="11" font-family="monospace">violation</text>
|
|
92
147
|
${lines}
|
|
93
148
|
${nodes}
|
|
94
149
|
</svg>`;
|
|
95
150
|
}
|
|
96
|
-
|
|
151
|
+
function detectCycleEdges(adjacency) {
|
|
152
|
+
const visited = new Set();
|
|
153
|
+
const inStack = new Set();
|
|
154
|
+
const stack = [];
|
|
155
|
+
const cycleEdges = new Set();
|
|
156
|
+
function dfs(node) {
|
|
157
|
+
visited.add(node);
|
|
158
|
+
inStack.add(node);
|
|
159
|
+
stack.push(node);
|
|
160
|
+
for (const neighbor of adjacency.get(node) ?? []) {
|
|
161
|
+
if (!visited.has(neighbor)) {
|
|
162
|
+
dfs(neighbor);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (!inStack.has(neighbor))
|
|
166
|
+
continue;
|
|
167
|
+
const startIndex = stack.indexOf(neighbor);
|
|
168
|
+
if (startIndex >= 0) {
|
|
169
|
+
for (let i = startIndex; i < stack.length - 1; i++) {
|
|
170
|
+
cycleEdges.add(`${stack[i]}->${stack[i + 1]}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
cycleEdges.add(`${node}->${neighbor}`);
|
|
174
|
+
}
|
|
175
|
+
stack.pop();
|
|
176
|
+
inStack.delete(node);
|
|
177
|
+
}
|
|
178
|
+
for (const node of adjacency.keys()) {
|
|
179
|
+
if (!visited.has(node))
|
|
180
|
+
dfs(node);
|
|
181
|
+
}
|
|
182
|
+
return cycleEdges;
|
|
183
|
+
}
|
|
184
|
+
export function generateArchitectureMap(targetPath, outputFile = 'architecture.svg', config) {
|
|
97
185
|
const resolvedTarget = resolve(targetPath);
|
|
98
|
-
const svg = generateArchitectureSvg(resolvedTarget);
|
|
186
|
+
const svg = generateArchitectureSvg(resolvedTarget, config);
|
|
99
187
|
const outPath = resolve(outputFile);
|
|
100
188
|
writeFileSync(outPath, svg, 'utf8');
|
|
101
189
|
return outPath;
|
package/dist/plugins.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { PluginLoadError, LoadedPlugin } from './types.js';
|
|
1
|
+
import type { PluginLoadError, PluginLoadWarning, LoadedPlugin } from './types.js';
|
|
2
2
|
export declare function loadPlugins(projectRoot: string, pluginIds: string[] | undefined): {
|
|
3
3
|
plugins: LoadedPlugin[];
|
|
4
4
|
errors: PluginLoadError[];
|
|
5
|
+
warnings: PluginLoadWarning[];
|
|
5
6
|
};
|
|
6
7
|
//# sourceMappingURL=plugins.d.ts.map
|
package/dist/plugins.js
CHANGED
|
@@ -2,28 +2,176 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import { isAbsolute, resolve } from 'node:path';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
const require = createRequire(import.meta.url);
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const candidate = value;
|
|
9
|
-
if (typeof candidate.name !== 'string')
|
|
10
|
-
return false;
|
|
11
|
-
if (!Array.isArray(candidate.rules))
|
|
12
|
-
return false;
|
|
13
|
-
return candidate.rules.every((rule) => rule &&
|
|
14
|
-
typeof rule === 'object' &&
|
|
15
|
-
typeof rule.name === 'string' &&
|
|
16
|
-
typeof rule.detect === 'function');
|
|
17
|
-
}
|
|
5
|
+
const VALID_SEVERITIES = ['error', 'warning', 'info'];
|
|
6
|
+
const SUPPORTED_PLUGIN_API_VERSION = 1;
|
|
7
|
+
const RULE_ID_REQUIRED = /^[a-z][a-z0-9]*(?:[-_/][a-z0-9]+)*$/;
|
|
18
8
|
function normalizePluginExport(mod) {
|
|
19
|
-
if (isPluginShape(mod))
|
|
20
|
-
return mod;
|
|
21
9
|
if (mod && typeof mod === 'object' && 'default' in mod) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
10
|
+
return mod.default ?? mod;
|
|
11
|
+
}
|
|
12
|
+
return mod;
|
|
13
|
+
}
|
|
14
|
+
function pushError(errors, pluginId, message, options) {
|
|
15
|
+
errors.push({
|
|
16
|
+
pluginId,
|
|
17
|
+
pluginName: options?.pluginName,
|
|
18
|
+
ruleId: options?.ruleId,
|
|
19
|
+
code: options?.code,
|
|
20
|
+
message,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function pushWarning(warnings, pluginId, message, options) {
|
|
24
|
+
warnings.push({
|
|
25
|
+
pluginId,
|
|
26
|
+
pluginName: options?.pluginName,
|
|
27
|
+
ruleId: options?.ruleId,
|
|
28
|
+
code: options?.code,
|
|
29
|
+
message,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function normalizeRule(pluginId, pluginName, rawRule, ruleIndex, options, errors, warnings) {
|
|
33
|
+
const rawRuleId = typeof rawRule.id === 'string'
|
|
34
|
+
? rawRule.id.trim()
|
|
35
|
+
: typeof rawRule.name === 'string'
|
|
36
|
+
? rawRule.name.trim()
|
|
37
|
+
: '';
|
|
38
|
+
const ruleLabel = rawRuleId || `rule#${ruleIndex + 1}`;
|
|
39
|
+
if (!rawRuleId) {
|
|
40
|
+
pushError(errors, pluginId, `Invalid rule at index ${ruleIndex}. Expected 'id' or 'name' as a non-empty string.`, { pluginName, code: 'plugin-rule-id-missing' });
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
if (typeof rawRule.detect !== 'function') {
|
|
44
|
+
pushError(errors, pluginId, `Rule '${rawRuleId}' is invalid. Expected 'detect(file, context)' function.`, { pluginName, ruleId: rawRuleId, code: 'plugin-rule-detect-invalid' });
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
if (rawRule.detect.length > 2) {
|
|
48
|
+
pushWarning(warnings, pluginId, `Rule '${rawRuleId}' detect() declares ${rawRule.detect.length} parameters. Expected 1-2 parameters (file, context).`, { pluginName, ruleId: rawRuleId, code: 'plugin-rule-detect-arity' });
|
|
49
|
+
}
|
|
50
|
+
if (!RULE_ID_REQUIRED.test(rawRuleId)) {
|
|
51
|
+
if (options.strictRuleId) {
|
|
52
|
+
pushError(errors, pluginId, `Rule id '${ruleLabel}' is invalid. Use lowercase letters, numbers, and separators (-, _, /), starting with a letter.`, { pluginName, ruleId: rawRuleId, code: 'plugin-rule-id-invalid' });
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
pushWarning(warnings, pluginId, `Rule id '${ruleLabel}' uses a legacy format. For forward compatibility, migrate to lowercase kebab-case and set apiVersion: ${SUPPORTED_PLUGIN_API_VERSION}.`, { pluginName, ruleId: rawRuleId, code: 'plugin-rule-id-format-legacy' });
|
|
56
|
+
}
|
|
57
|
+
let severity;
|
|
58
|
+
if (rawRule.severity !== undefined) {
|
|
59
|
+
if (typeof rawRule.severity === 'string' && VALID_SEVERITIES.includes(rawRule.severity)) {
|
|
60
|
+
severity = rawRule.severity;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
pushError(errors, pluginId, `Rule '${rawRuleId}' has invalid severity '${String(rawRule.severity)}'. Allowed: error, warning, info.`, { pluginName, ruleId: rawRuleId, code: 'plugin-rule-severity-invalid' });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
let weight;
|
|
67
|
+
if (rawRule.weight !== undefined) {
|
|
68
|
+
if (typeof rawRule.weight === 'number' && Number.isFinite(rawRule.weight) && rawRule.weight >= 0 && rawRule.weight <= 100) {
|
|
69
|
+
weight = rawRule.weight;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
pushError(errors, pluginId, `Rule '${rawRuleId}' has invalid weight '${String(rawRule.weight)}'. Expected a finite number between 0 and 100.`, { pluginName, ruleId: rawRuleId, code: 'plugin-rule-weight-invalid' });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
let fix;
|
|
76
|
+
if (rawRule.fix !== undefined) {
|
|
77
|
+
if (typeof rawRule.fix === 'function') {
|
|
78
|
+
fix = rawRule.fix;
|
|
79
|
+
if (rawRule.fix.length > 3) {
|
|
80
|
+
pushWarning(warnings, pluginId, `Rule '${rawRuleId}' fix() declares ${rawRule.fix.length} parameters. Expected up to 3 (issue, file, context).`, { pluginName, ruleId: rawRuleId, code: 'plugin-rule-fix-arity' });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
pushError(errors, pluginId, `Rule '${rawRuleId}' has invalid fix. Expected a function when provided.`, { pluginName, ruleId: rawRuleId, code: 'plugin-rule-fix-invalid' });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
id: rawRuleId,
|
|
89
|
+
name: rawRuleId,
|
|
90
|
+
detect: rawRule.detect,
|
|
91
|
+
severity,
|
|
92
|
+
weight,
|
|
93
|
+
fix,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function validatePluginContract(pluginId, candidate) {
|
|
97
|
+
const errors = [];
|
|
98
|
+
const warnings = [];
|
|
99
|
+
if (!candidate || typeof candidate !== 'object') {
|
|
100
|
+
pushError(errors, pluginId, `Invalid plugin contract in '${pluginId}'. Expected an object export with shape { name, rules[] }`, { code: 'plugin-shape-invalid' });
|
|
101
|
+
return { errors, warnings };
|
|
102
|
+
}
|
|
103
|
+
const plugin = candidate;
|
|
104
|
+
const pluginName = typeof plugin.name === 'string' ? plugin.name.trim() : '';
|
|
105
|
+
if (!pluginName) {
|
|
106
|
+
pushError(errors, pluginId, `Invalid plugin contract in '${pluginId}'. Expected 'name' as a non-empty string.`, { code: 'plugin-name-missing' });
|
|
107
|
+
return { errors, warnings };
|
|
108
|
+
}
|
|
109
|
+
const hasExplicitApiVersion = plugin.apiVersion !== undefined;
|
|
110
|
+
const isLegacyPlugin = !hasExplicitApiVersion;
|
|
111
|
+
if (isLegacyPlugin) {
|
|
112
|
+
pushWarning(warnings, pluginId, `Plugin '${pluginName}' does not declare 'apiVersion'. Assuming ${SUPPORTED_PLUGIN_API_VERSION} for backward compatibility; please add apiVersion: ${SUPPORTED_PLUGIN_API_VERSION}.`, { pluginName, code: 'plugin-api-version-implicit' });
|
|
113
|
+
}
|
|
114
|
+
else if (typeof plugin.apiVersion !== 'number' || !Number.isInteger(plugin.apiVersion) || plugin.apiVersion <= 0) {
|
|
115
|
+
pushError(errors, pluginId, `Plugin '${pluginName}' has invalid apiVersion '${String(plugin.apiVersion)}'. Expected a positive integer (for example: ${SUPPORTED_PLUGIN_API_VERSION}).`, { pluginName, code: 'plugin-api-version-invalid' });
|
|
116
|
+
return { errors, warnings };
|
|
117
|
+
}
|
|
118
|
+
else if (plugin.apiVersion !== SUPPORTED_PLUGIN_API_VERSION) {
|
|
119
|
+
pushError(errors, pluginId, `Plugin '${pluginName}' targets apiVersion ${plugin.apiVersion}, but this drift build supports apiVersion ${SUPPORTED_PLUGIN_API_VERSION}.`, { pluginName, code: 'plugin-api-version-unsupported' });
|
|
120
|
+
return { errors, warnings };
|
|
121
|
+
}
|
|
122
|
+
let capabilities;
|
|
123
|
+
if (plugin.capabilities !== undefined) {
|
|
124
|
+
if (!plugin.capabilities || typeof plugin.capabilities !== 'object' || Array.isArray(plugin.capabilities)) {
|
|
125
|
+
pushError(errors, pluginId, `Plugin '${pluginName}' has invalid capabilities metadata. Expected an object map like { "fixes": true } when provided.`, { pluginName, code: 'plugin-capabilities-invalid' });
|
|
126
|
+
return { errors, warnings };
|
|
127
|
+
}
|
|
128
|
+
const entries = Object.entries(plugin.capabilities);
|
|
129
|
+
for (const [capabilityKey, capabilityValue] of entries) {
|
|
130
|
+
const capabilityType = typeof capabilityValue;
|
|
131
|
+
if (capabilityType !== 'string' && capabilityType !== 'number' && capabilityType !== 'boolean') {
|
|
132
|
+
pushError(errors, pluginId, `Plugin '${pluginName}' capability '${capabilityKey}' has invalid value type '${capabilityType}'. Allowed: string | number | boolean.`, { pluginName, code: 'plugin-capabilities-value-invalid' });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (errors.length > 0) {
|
|
136
|
+
return { errors, warnings };
|
|
137
|
+
}
|
|
138
|
+
capabilities = plugin.capabilities;
|
|
139
|
+
}
|
|
140
|
+
if (!Array.isArray(plugin.rules)) {
|
|
141
|
+
pushError(errors, pluginId, `Invalid plugin '${pluginName}'. Expected 'rules' to be an array.`, { pluginName, code: 'plugin-rules-not-array' });
|
|
142
|
+
return { errors, warnings };
|
|
143
|
+
}
|
|
144
|
+
const normalizedRules = [];
|
|
145
|
+
const seenRuleIds = new Set();
|
|
146
|
+
for (const [ruleIndex, rawRule] of plugin.rules.entries()) {
|
|
147
|
+
if (!rawRule || typeof rawRule !== 'object') {
|
|
148
|
+
pushError(errors, pluginId, `Invalid rule at index ${ruleIndex} in plugin '${pluginName}'. Expected an object.`, { pluginName, code: 'plugin-rule-shape-invalid' });
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const normalized = normalizeRule(pluginId, pluginName, rawRule, ruleIndex, { strictRuleId: !isLegacyPlugin }, errors, warnings);
|
|
152
|
+
if (normalized) {
|
|
153
|
+
if (seenRuleIds.has(normalized.id ?? normalized.name)) {
|
|
154
|
+
pushError(errors, pluginId, `Plugin '${pluginName}' defines duplicate rule id '${normalized.id ?? normalized.name}'. Rule ids must be unique within a plugin.`, { pluginName, ruleId: normalized.id ?? normalized.name, code: 'plugin-rule-id-duplicate' });
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
seenRuleIds.add(normalized.id ?? normalized.name);
|
|
158
|
+
normalizedRules.push(normalized);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (normalizedRules.length === 0) {
|
|
162
|
+
pushError(errors, pluginId, `Plugin '${pluginName}' has no valid rules after validation.`, { pluginName, code: 'plugin-rules-empty' });
|
|
163
|
+
return { errors, warnings };
|
|
25
164
|
}
|
|
26
|
-
return
|
|
165
|
+
return {
|
|
166
|
+
plugin: {
|
|
167
|
+
name: pluginName,
|
|
168
|
+
apiVersion: hasExplicitApiVersion ? plugin.apiVersion : SUPPORTED_PLUGIN_API_VERSION,
|
|
169
|
+
capabilities,
|
|
170
|
+
rules: normalizedRules,
|
|
171
|
+
},
|
|
172
|
+
errors,
|
|
173
|
+
warnings,
|
|
174
|
+
};
|
|
27
175
|
}
|
|
28
176
|
function resolvePluginSpecifier(projectRoot, pluginId) {
|
|
29
177
|
if (pluginId.startsWith('.') || pluginId.startsWith('/')) {
|
|
@@ -44,31 +192,32 @@ function resolvePluginSpecifier(projectRoot, pluginId) {
|
|
|
44
192
|
}
|
|
45
193
|
export function loadPlugins(projectRoot, pluginIds) {
|
|
46
194
|
if (!pluginIds || pluginIds.length === 0) {
|
|
47
|
-
return { plugins: [], errors: [] };
|
|
195
|
+
return { plugins: [], errors: [], warnings: [] };
|
|
48
196
|
}
|
|
49
197
|
const loaded = [];
|
|
50
198
|
const errors = [];
|
|
199
|
+
const warnings = [];
|
|
51
200
|
for (const pluginId of pluginIds) {
|
|
52
201
|
const resolved = resolvePluginSpecifier(projectRoot, pluginId);
|
|
53
202
|
try {
|
|
54
203
|
const mod = require(resolved);
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
});
|
|
204
|
+
const normalized = normalizePluginExport(mod);
|
|
205
|
+
const validation = validatePluginContract(pluginId, normalized);
|
|
206
|
+
errors.push(...validation.errors);
|
|
207
|
+
warnings.push(...validation.warnings);
|
|
208
|
+
if (!validation.plugin) {
|
|
61
209
|
continue;
|
|
62
210
|
}
|
|
63
|
-
loaded.push({ id: pluginId, plugin });
|
|
211
|
+
loaded.push({ id: pluginId, plugin: validation.plugin });
|
|
64
212
|
}
|
|
65
213
|
catch (error) {
|
|
66
214
|
errors.push({
|
|
67
215
|
pluginId,
|
|
216
|
+
code: 'plugin-load-failed',
|
|
68
217
|
message: error instanceof Error ? error.message : String(error),
|
|
69
218
|
});
|
|
70
219
|
}
|
|
71
220
|
}
|
|
72
|
-
return { plugins: loaded, errors };
|
|
221
|
+
return { plugins: loaded, errors, warnings };
|
|
73
222
|
}
|
|
74
223
|
//# sourceMappingURL=plugins.js.map
|
package/dist/printer.js
CHANGED
|
@@ -131,6 +131,10 @@ function formatFixSuggestion(issue) {
|
|
|
131
131
|
'Fix or remove the failing plugin in drift.config.*',
|
|
132
132
|
'Validate plugin contract: export { name, rules[] } and detector functions',
|
|
133
133
|
],
|
|
134
|
+
'plugin-warning': [
|
|
135
|
+
'Review plugin validation warnings and align with the recommended contract',
|
|
136
|
+
'Use explicit rule ids and valid severity/weight values',
|
|
137
|
+
],
|
|
134
138
|
};
|
|
135
139
|
return suggestions[issue.rule] ?? ['Review and fix manually'];
|
|
136
140
|
}
|
package/dist/review.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolve } from 'node:path';
|
|
1
|
+
import { relative, resolve } from 'node:path';
|
|
2
2
|
import { analyzeProject } from './analyzer.js';
|
|
3
3
|
import { loadConfig } from './config.js';
|
|
4
4
|
import { buildReport } from './reporter.js';
|
|
@@ -48,7 +48,7 @@ export async function generateReview(projectPath, baseRef) {
|
|
|
48
48
|
...baseReport,
|
|
49
49
|
files: baseReport.files.map((file) => ({
|
|
50
50
|
...file,
|
|
51
|
-
path:
|
|
51
|
+
path: resolve(resolvedPath, relative(tempDir, file.path)),
|
|
52
52
|
})),
|
|
53
53
|
};
|
|
54
54
|
const diff = computeDiff(remappedBase, currentReport, baseRef);
|
package/dist/rules/comments.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { hasIgnoreComment } from './shared.js';
|
|
1
|
+
import { hasIgnoreComment, getFileLines } from './shared.js';
|
|
2
2
|
const TRIVIAL_COMMENT_PATTERNS = [
|
|
3
3
|
{ comment: /\/\/\s*return\b/i, code: /^\s*return\b/ },
|
|
4
4
|
{ comment: /\/\/\s*(increment|increase|add\s+1|plus\s+1)\b/i, code: /\+\+|(\+= ?1)\b/ },
|
|
@@ -31,7 +31,7 @@ function checkLineForContradiction(commentLine, nextLine, lineNumber, file) {
|
|
|
31
31
|
}
|
|
32
32
|
export function detectCommentContradiction(file) {
|
|
33
33
|
const issues = [];
|
|
34
|
-
const lines = file
|
|
34
|
+
const lines = getFileLines(file);
|
|
35
35
|
for (let i = 0; i < lines.length - 1; i++) {
|
|
36
36
|
const commentLine = lines[i].trim();
|
|
37
37
|
const nextLine = lines[i + 1];
|
package/dist/rules/complexity.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SyntaxKind } from 'ts-morph';
|
|
2
|
-
import { hasIgnoreComment, getSnippet } from './shared.js';
|
|
2
|
+
import { hasIgnoreComment, getSnippet, collectFunctionLikes } from './shared.js';
|
|
3
3
|
const COMPLEXITY_THRESHOLD = 10;
|
|
4
4
|
const INCREMENT_KINDS = [
|
|
5
5
|
SyntaxKind.IfStatement,
|
|
@@ -24,12 +24,7 @@ function getCyclomaticComplexity(fn) {
|
|
|
24
24
|
}
|
|
25
25
|
export function detectHighComplexity(file) {
|
|
26
26
|
const issues = [];
|
|
27
|
-
const fns =
|
|
28
|
-
...file.getFunctions(),
|
|
29
|
-
...file.getDescendantsOfKind(SyntaxKind.ArrowFunction),
|
|
30
|
-
...file.getDescendantsOfKind(SyntaxKind.FunctionExpression),
|
|
31
|
-
...file.getClasses().flatMap((c) => c.getMethods()),
|
|
32
|
-
];
|
|
27
|
+
const fns = collectFunctionLikes(file);
|
|
33
28
|
for (const fn of fns) {
|
|
34
29
|
const complexity = getCyclomaticComplexity(fn);
|
|
35
30
|
if (complexity > COMPLEXITY_THRESHOLD) {
|