@boyingliu01/xp-gate 0.19.2 → 0.20.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/adapters/gate-4.sh +83 -18
- package/adapters/gate-8.sh +49 -5
- package/adapters/iac.sh +1 -1
- package/adapters/typescript.sh +15 -60
- package/bin/xp-gate.js +15 -16
- package/gate-10.sh +35 -2
- package/gate-4.sh +110 -21
- package/gate-8.sh +49 -5
- package/hooks/gate-10.sh +35 -2
- package/hooks/gate-4.sh +110 -21
- package/hooks/gate-8.sh +49 -5
- package/hooks/lib/jscpd-run.sh +63 -0
- package/hooks/lib/sprint-gate-report.sh +58 -0
- package/hooks/lib/test-failure.sh +152 -0
- package/hooks/lib/typecheck.sh +141 -0
- package/hooks/lib/validate-code-walkthrough.cjs +43 -5
- package/hooks/post-merge +100 -0
- package/hooks/pre-commit +401 -155
- package/hooks/pre-push +75 -19
- package/lib/__tests__/bootstrap.test.js +23 -41
- package/lib/__tests__/check-version.test.js +8 -1
- package/lib/__tests__/detect-deps.test.js +16 -0
- package/lib/__tests__/doctor-drift-round2.test.js +212 -0
- package/lib/__tests__/doctor-executed-surface.test.js +209 -0
- package/lib/__tests__/doctor-hook-drift.test.js +329 -0
- package/lib/__tests__/doctor-machine-mutation.test.js +116 -0
- package/lib/__tests__/doctor-module-drift.test.js +206 -0
- package/lib/__tests__/doctor-module-resolution-parity.test.js +314 -0
- package/lib/__tests__/doctor-path-equivalence.test.js +59 -0
- package/lib/__tests__/doctor-repair-boundary.test.js +52 -0
- package/lib/__tests__/doctor-tui-skill-freshness.test.js +105 -0
- package/lib/__tests__/doctor.test.js +155 -0
- package/lib/__tests__/gate-runner.test.js +19 -3
- package/lib/__tests__/helpers/lib-sandbox.cjs +55 -0
- package/lib/__tests__/init.test.js +44 -17
- package/lib/__tests__/install-skill-full-dir.test.js +151 -0
- package/lib/__tests__/install-skill.test.js +51 -132
- package/lib/__tests__/language-tools-detect.test.js +158 -0
- package/lib/__tests__/phase-transition.test.js +15 -1
- package/lib/__tests__/qoder-delphi-agents.test.js +285 -0
- package/lib/__tests__/shared-paths.test.js +20 -0
- package/lib/__tests__/shared-utils.test.js +69 -0
- package/lib/__tests__/skill-flags.test.js +48 -0
- package/lib/__tests__/ui-detector.test.ts +6 -2
- package/lib/__tests__/uninstall.test.js +20 -0
- package/lib/__tests__/update-hooks.test.js +12 -0
- package/lib/__tests__/update-skill.test.js +7 -4
- package/lib/detect-deps.js +3 -20
- package/lib/doctor-tui.js +22 -5
- package/lib/doctor.js +1178 -31
- package/lib/init.js +184 -33
- package/lib/install-cmd.js +7 -4
- package/lib/install-skill.js +38 -95
- package/lib/language-tools.js +83 -4
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/shared-paths.js +5 -2
- package/lib/shared-utils.js +47 -3
- package/lib/uninstall.js +12 -0
- package/lib/update-hooks.js +4 -4
- package/lib/update-skill.js +6 -7
- package/mock-policy/AGENTS.md +4 -4
- package/mutation/AGENTS.md +4 -4
- package/mutation/__tests__/gate-m.test.ts +66 -14
- package/mutation/__tests__/mutmut-runner.test.ts +91 -42
- package/mutation/__tests__/test-filter-parity.test.ts +37 -0
- package/mutation/gate-m.ts +21 -8
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +2 -2
- package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/claude-code/skills/delphi-review/SKILL.md +98 -14
- package/plugins/claude-code/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/plugins/claude-code/skills/delphi-review/references/requirements.md +6 -6
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/dsh/README.md +169 -4
- package/plugins/dsh/dsh-overlay.patch.yml +67 -0
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/scripts/prepack.cjs +24 -19
- package/plugins/dsh/setup-dsh.ps1 +160 -0
- package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/dsh/skills/delphi-review/SKILL.md +98 -14
- package/plugins/dsh/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/plugins/dsh/skills/delphi-review/references/requirements.md +6 -6
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/dsh/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/dsh/src/command.test.ts +66 -2
- package/plugins/dsh/src/command.ts +36 -14
- package/plugins/dsh/src/contract.test.ts +31 -1
- package/plugins/dsh/src/delphi-config.test.ts +190 -0
- package/plugins/dsh/src/delphi-config.ts +223 -0
- package/plugins/dsh/src/delphi-run.test.ts +181 -0
- package/plugins/dsh/src/delphi-run.ts +129 -0
- package/plugins/dsh/src/delphi.test.ts +222 -0
- package/plugins/dsh/src/delphi.ts +247 -0
- package/plugins/dsh/src/dialect.test.ts +59 -0
- package/plugins/dsh/src/dialect.ts +40 -0
- package/plugins/dsh/src/index.ts +70 -4
- package/plugins/dsh/src/skills.test.ts +26 -19
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/opencode/skills/delphi-review/SKILL.md +98 -14
- package/plugins/opencode/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/plugins/opencode/skills/delphi-review/references/requirements.md +6 -6
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/qoder/agents/delphi-architecture.md +1 -1
- package/plugins/qoder/agents/delphi-feasibility.md +1 -1
- package/plugins/qoder/agents/delphi-technical.md +1 -1
- package/plugins/qoder/plugin.json +2 -2
- package/plugins/qoder/skills/admin-template-guidelines/SKILL.md +6 -797
- package/plugins/qoder/skills/admin-template-guidelines/references/rule-1.md +91 -0
- package/plugins/qoder/skills/admin-template-guidelines/references/rule-2.md +155 -0
- package/plugins/qoder/skills/admin-template-guidelines/references/rule-3.md +70 -0
- package/plugins/qoder/skills/admin-template-guidelines/references/rule-4.md +154 -0
- package/plugins/qoder/skills/admin-template-guidelines/references/rule-5.md +126 -0
- package/plugins/qoder/skills/admin-template-guidelines/references/rule-6.md +180 -0
- package/plugins/qoder/skills/batch-grill-me/SKILL.md +15 -0
- package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/plugins/qoder/skills/clipboard-vision/describe-clipboard.ps1 +115 -0
- package/plugins/qoder/skills/clipboard-vision/describe-clipboard.sh +161 -0
- package/plugins/qoder/skills/delphi-review/.delphi-config.json.example +61 -28
- package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/qoder/skills/delphi-review/INSTALL.md +97 -98
- package/plugins/qoder/skills/delphi-review/SKILL.md +170 -63
- package/plugins/qoder/skills/delphi-review/hooks.md +19 -0
- package/plugins/qoder/skills/delphi-review/opencode.json.delphi.example +6 -10
- package/plugins/qoder/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/plugins/qoder/skills/delphi-review/references/orchestrator-dispatch.md +64 -0
- package/plugins/qoder/skills/delphi-review/references/requirements.md +402 -0
- package/plugins/qoder/skills/delphi-review/references/round-templates.md +134 -0
- package/plugins/qoder/skills/delphi-review/scripts/verify-consensus.sh +43 -0
- package/plugins/qoder/skills/delphi-review/tools.md +29 -0
- package/plugins/qoder/skills/domain-modeling/ADR-FORMAT.md +64 -0
- package/plugins/qoder/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/plugins/qoder/skills/domain-modeling/SKILL.md +74 -0
- package/plugins/qoder/skills/grill-with-docs/SKILL.md +7 -0
- package/plugins/qoder/skills/grilling/SKILL.md +12 -0
- package/plugins/qoder/skills/ralph-loop/SKILL.md +87 -428
- package/plugins/qoder/skills/ralph-loop/references/components.md +113 -0
- package/plugins/qoder/skills/ralph-loop/references/workflow-details.md +99 -0
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +5 -5
- package/plugins/qoder/skills/sprint-flow/SKILL.md +255 -454
- package/plugins/qoder/skills/sprint-flow/__tests__/sprint-flow.test.cjs +457 -0
- package/plugins/qoder/skills/sprint-flow/hooks.md +18 -0
- package/plugins/qoder/skills/sprint-flow/references/components/memory.md +1 -1
- package/plugins/qoder/skills/sprint-flow/references/components/middleware.md +1 -1
- package/plugins/qoder/skills/sprint-flow/references/components/skill-invocations.md +21 -20
- package/plugins/qoder/skills/sprint-flow/references/components/tool-descriptions.md +22 -23
- package/plugins/qoder/skills/sprint-flow/references/force-levels.md +0 -1
- package/plugins/qoder/skills/sprint-flow/references/orchestration-rules.md +34 -27
- package/plugins/qoder/skills/sprint-flow/references/phase-1-prep.md +1 -1
- package/plugins/qoder/skills/sprint-flow/references/phase-2-design.md +160 -123
- package/plugins/qoder/skills/sprint-flow/references/phase-3-build.md +203 -2
- package/plugins/qoder/skills/sprint-flow/references/phase-4-verify.md +107 -34
- package/plugins/qoder/skills/sprint-flow/references/phase-5-ship.md +194 -12
- package/plugins/qoder/skills/sprint-flow/references/phase-6-close.md +139 -7
- package/plugins/qoder/skills/sprint-flow/references/phase-overview.md +486 -0
- package/plugins/qoder/skills/sprint-flow/scripts/verify-sprint-state.sh +38 -0
- package/plugins/qoder/skills/sprint-flow/tools.md +29 -0
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/qoder/skills/test-specification-alignment/SKILL.md +104 -601
- package/plugins/qoder/skills/to-issues/SKILL.md +1 -1
- package/principles/AGENTS.md +9 -8
- package/principles/__tests__/adapter-metrics.test.ts +292 -0
- package/principles/__tests__/analyzer.test.ts +29 -6
- package/principles/__tests__/baseline-prune-timeout.test.ts +71 -0
- package/principles/__tests__/baseline-prune.test.ts +338 -0
- package/principles/__tests__/boy-scout-init-baseline.test.ts +167 -0
- package/principles/__tests__/boy-scout-integration.test.ts +97 -6
- package/principles/__tests__/boy-scout.test.ts +253 -7
- package/principles/__tests__/config-enforcement.test.ts +469 -0
- package/principles/__tests__/config-git-timeout.test.ts +66 -0
- package/principles/__tests__/direct-execution.test.ts +187 -0
- package/principles/__tests__/index.test.ts +16 -3
- package/principles/__tests__/warnings-baseline-budgets.test.ts +74 -0
- package/principles/adapters/typescript.ts +613 -13
- package/principles/analyzer.ts +22 -0
- package/principles/baseline-prune.ts +217 -0
- package/principles/boy-scout.ts +481 -51
- package/principles/config.ts +362 -54
- package/principles/direct-execution.ts +60 -0
- package/principles/index.ts +40 -17
- package/principles/rules/__tests__/clean-code/many-exports.test.ts +16 -10
- package/principles/rules/clean-code/code-duplication.ts +8 -8
- package/principles/rules/clean-code/deep-nesting.ts +8 -8
- package/principles/rules/clean-code/god-class.ts +8 -8
- package/principles/rules/clean-code/large-file.ts +15 -11
- package/principles/rules/clean-code/long-function.ts +8 -8
- package/principles/rules/clean-code/magic-numbers.ts +10 -7
- package/principles/rules/clean-code/many-exports.ts +7 -7
- package/principles/rules/clean-code/missing-error-handling.ts +5 -5
- package/principles/rules/clean-code/too-many-params.ts +8 -8
- package/principles/rules/clean-code/unused-imports.ts +5 -5
- package/principles/rules/solid/dip.ts +18 -8
- package/principles/rules/solid/isp.ts +9 -8
- package/principles/rules/solid/lsp.ts +6 -6
- package/principles/rules/solid/ocp.ts +6 -5
- package/principles/rules/solid/srp.ts +7 -8
- package/principles/types.ts +15 -0
- package/scripts/__tests__/sync-package-content.test.js +28 -0
- package/scripts/delphi-external-review.cjs +193 -10
- package/scripts/sync-package-content.js +7 -4
- package/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/skills/delphi-review/AGENTS.md +4 -4
- package/skills/delphi-review/SKILL.md +98 -14
- package/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/skills/delphi-review/references/requirements.md +6 -6
- package/skills/sprint-flow/AGENTS.md +4 -4
- package/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +4 -4
- package/lib/__tests__/download-skill.test.js +0 -281
- package/lib/download-skill.js +0 -96
- package/plugins/qoder/skills/ralph-loop/references/components/memory.md +0 -55
- package/plugins/qoder/skills/ralph-loop/references/components/middleware.md +0 -54
- package/plugins/qoder/skills/ralph-loop/references/components/skill-invocations.md +0 -39
- package/plugins/qoder/skills/ralph-loop/references/components/system-prompt.md +0 -24
- package/plugins/qoder/skills/ralph-loop/references/components/tool-descriptions.md +0 -32
|
@@ -1,6 +1,312 @@
|
|
|
1
1
|
import { BaseAdapter } from './base';
|
|
2
2
|
import { Adapter } from '../types';
|
|
3
3
|
|
|
4
|
+
/** IO-ish callees that the missing-error-handling rule cares about. */
|
|
5
|
+
const IO_CALLEES = [
|
|
6
|
+
'readFile', 'readFileSync', 'writeFile', 'writeFileSync', 'appendFile', 'appendFileSync',
|
|
7
|
+
'open', 'openSync', 'unlink', 'unlinkSync', 'mkdir', 'mkdirSync', 'readdir', 'readdirSync',
|
|
8
|
+
'stat', 'statSync', 'access', 'accessSync', 'fetch', 'axios', 'createReadStream', 'createWriteStream',
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Child-process / network entry points. `exec` and friends are matched only as
|
|
13
|
+
* bare calls (no `obj.` receiver): `regex.exec(str)` is not IO, so requiring the
|
|
14
|
+
* call to start a statement avoids flagging `RegExp.prototype.exec` (#446).
|
|
15
|
+
*/
|
|
16
|
+
const IO_GLOBAL_CALLEES = ['exec', 'execSync', 'spawn', 'spawnSync', 'request'];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Builders, not shared literals: a module-level regex with the `g` flag carries
|
|
20
|
+
* mutable `lastIndex` across calls, so any caller that forgets to reset it
|
|
21
|
+
* silently drops matches. Fresh instances keep every call independent.
|
|
22
|
+
*/
|
|
23
|
+
function ioCalleeRe(): RegExp {
|
|
24
|
+
return new RegExp(`\\b(?:${IO_CALLEES.join('|')})\\s*\\(`, 'g');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function ioGlobalRe(): RegExp {
|
|
28
|
+
return new RegExp(`(?:^|[^.\\w$])(?:${IO_GLOBAL_CALLEES.join('|')})\\s*\\(`, 'gm');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Keywords that look like `name(` but are control flow, not callables. */
|
|
32
|
+
const CONTROL_KEYWORDS = new Set([
|
|
33
|
+
'if', 'for', 'while', 'switch', 'catch', 'return', 'typeof', 'new', 'delete',
|
|
34
|
+
'void', 'in', 'instanceof', 'do', 'else', 'try', 'finally', 'throw', 'await',
|
|
35
|
+
'yield', 'function', 'class', 'super', 'this', 'import', 'export', 'default',
|
|
36
|
+
'case', 'break', 'continue', 'const', 'let', 'var', 'as', 'satisfies', 'keyof',
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Find the index of the `{` that opens a declaration body for the parameter
|
|
41
|
+
* list starting at `parenIndex`.
|
|
42
|
+
*
|
|
43
|
+
* Everything between the closing `)` and the body is scanned with a small depth
|
|
44
|
+
* tracker so that a `{` belonging to a type literal in the return annotation
|
|
45
|
+
* (`extract(): Array<{value: number}> {`) is not mistaken for the body. The
|
|
46
|
+
* body brace is the first `{` seen at depth 0.
|
|
47
|
+
*/
|
|
48
|
+
function findBodyBrace(masked: string, parenIndex: number): number {
|
|
49
|
+
if (masked[parenIndex] !== '(') return -1;
|
|
50
|
+
const { end } = readParenGroup(masked, parenIndex);
|
|
51
|
+
let depth = 0;
|
|
52
|
+
for (let i = end; i < masked.length; i++) {
|
|
53
|
+
const c = masked[i];
|
|
54
|
+
if (c === '(') { const g = readParenGroup(masked, i); i = g.end - 1; continue; }
|
|
55
|
+
if (c === '<' || c === '[') { depth++; continue; }
|
|
56
|
+
if (c === '>' || c === ']') { if (depth > 0) depth--; continue; }
|
|
57
|
+
if (c === '{') {
|
|
58
|
+
if (depth === 0) return i;
|
|
59
|
+
depth++;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (c === '}') { if (depth > 0) depth--; continue; }
|
|
63
|
+
if (c === ';' || c === '\n') {
|
|
64
|
+
// A line break before any `{` means this header has no braced body
|
|
65
|
+
// (e.g. an interface member or an overload signature).
|
|
66
|
+
if (depth === 0) return -1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return -1;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Index just past the matching `}` for the block opened at `open`. */
|
|
73
|
+
function findBlockEnd(masked: string, open: number): number {
|
|
74
|
+
let depth = 0;
|
|
75
|
+
for (let i = open; i < masked.length; i++) {
|
|
76
|
+
const c = masked[i];
|
|
77
|
+
if (c === '{') depth++;
|
|
78
|
+
else if (c === '}') {
|
|
79
|
+
depth--;
|
|
80
|
+
if (depth === 0) return i + 1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return masked.length;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Body brace for declarations that have no parameter list (`class`, `interface`).
|
|
88
|
+
* Scans forward from the end of the declaration header; the first `{` at generic
|
|
89
|
+
* depth 0 is the body. A `;` before any brace means there is no body.
|
|
90
|
+
*/
|
|
91
|
+
function findDeclarationBrace(masked: string, from: number): number {
|
|
92
|
+
let depth = 0;
|
|
93
|
+
for (let i = from; i < masked.length; i++) {
|
|
94
|
+
const c = masked[i];
|
|
95
|
+
if (c === '<' || c === '[' || c === '(') {
|
|
96
|
+
if (c === '(') { const g = readParenGroup(masked, i); i = g.end - 1; continue; }
|
|
97
|
+
depth++;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (c === '>' || c === ']') { if (depth > 0) depth--; continue; }
|
|
101
|
+
if (c === '{') {
|
|
102
|
+
if (depth === 0) return i;
|
|
103
|
+
depth++;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (c === '}') { if (depth > 0) depth--; continue; }
|
|
107
|
+
if (c === ';' && depth === 0) return -1;
|
|
108
|
+
// Stop at a blank line / a new top-level keyword so we do not run into the
|
|
109
|
+
// next declaration when this one has no body.
|
|
110
|
+
if (c === '\n') {
|
|
111
|
+
const rest = masked.substring(i + 1).replace(/^[ \t]*/, '');
|
|
112
|
+
if (/^(?:export|import|interface|class|const|let|var|function|type|declare|@)/.test(rest) && depth === 0) {
|
|
113
|
+
return -1;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return -1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function lineOf(src: string, index: number): number {
|
|
121
|
+
return src.substring(0, index).split('\n').length;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Count top-level, comma-separated parameters in a raw parameter list. */
|
|
125
|
+
function countParams(rawList: string): number {
|
|
126
|
+
const trimmed = rawList.trim();
|
|
127
|
+
if (trimmed === '') return 0;
|
|
128
|
+
let depth = 0;
|
|
129
|
+
let count = 1;
|
|
130
|
+
let inSingle = false;
|
|
131
|
+
let inDouble = false;
|
|
132
|
+
let inTemplate = false;
|
|
133
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
134
|
+
const c = trimmed[i];
|
|
135
|
+
if (inSingle) { if (c === '\\') i++; else if (c === "'") inSingle = false; continue; }
|
|
136
|
+
if (inDouble) { if (c === '\\') i++; else if (c === '"') inDouble = false; continue; }
|
|
137
|
+
if (inTemplate) { if (c === '\\') i++; else if (c === '`') inTemplate = false; continue; }
|
|
138
|
+
if (c === "'") { inSingle = true; continue; }
|
|
139
|
+
if (c === '"') { inDouble = true; continue; }
|
|
140
|
+
if (c === '`') { inTemplate = true; continue; }
|
|
141
|
+
if (c === '(' || c === '[' || c === '{' || c === '<') depth++;
|
|
142
|
+
else if (c === ')' || c === ']' || c === '}' || c === '>') depth--;
|
|
143
|
+
else if (c === ',' && depth === 0) count++;
|
|
144
|
+
}
|
|
145
|
+
return count;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Extract the balanced `( ... )` group that starts at `open`.
|
|
150
|
+
* Returns the inner text and the index just past the closing paren.
|
|
151
|
+
*/
|
|
152
|
+
function readParenGroup(src: string, open: number): { inner: string; end: number } {
|
|
153
|
+
let depth = 0;
|
|
154
|
+
let inSingle = false;
|
|
155
|
+
let inDouble = false;
|
|
156
|
+
let inTemplate = false;
|
|
157
|
+
for (let i = open; i < src.length; i++) {
|
|
158
|
+
const c = src[i];
|
|
159
|
+
if (inSingle) { if (c === '\\') i++; else if (c === "'") inSingle = false; continue; }
|
|
160
|
+
if (inDouble) { if (c === '\\') i++; else if (c === '"') inDouble = false; continue; }
|
|
161
|
+
if (inTemplate) { if (c === '\\') i++; else if (c === '`') inTemplate = false; continue; }
|
|
162
|
+
if (c === "'") { inSingle = true; continue; }
|
|
163
|
+
if (c === '"') { inDouble = true; continue; }
|
|
164
|
+
if (c === '`') { inTemplate = true; continue; }
|
|
165
|
+
if (c === '(') depth++;
|
|
166
|
+
else if (c === ')') {
|
|
167
|
+
depth--;
|
|
168
|
+
if (depth === 0) return { inner: src.substring(open + 1, i), end: i + 1 };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return { inner: src.substring(open + 1), end: src.length };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Max brace nesting depth inside a function body (1 = body itself). */
|
|
175
|
+
function maxNesting(body: string): number {
|
|
176
|
+
let depth = 0;
|
|
177
|
+
let max = 0;
|
|
178
|
+
let inSingle = false;
|
|
179
|
+
let inDouble = false;
|
|
180
|
+
let inTemplate = false;
|
|
181
|
+
let inLineComment = false;
|
|
182
|
+
let inBlockComment = false;
|
|
183
|
+
for (let i = 0; i < body.length; i++) {
|
|
184
|
+
const c = body[i];
|
|
185
|
+
const next = body[i + 1];
|
|
186
|
+
if (inLineComment) { if (c === '\n') inLineComment = false; continue; }
|
|
187
|
+
if (inBlockComment) { if (c === '*' && next === '/') { inBlockComment = false; i++; } continue; }
|
|
188
|
+
if (inSingle) { if (c === '\\') i++; else if (c === "'") inSingle = false; continue; }
|
|
189
|
+
if (inDouble) { if (c === '\\') i++; else if (c === '"') inDouble = false; continue; }
|
|
190
|
+
if (inTemplate) { if (c === '\\') i++; else if (c === '`') inTemplate = false; continue; }
|
|
191
|
+
if (c === '/' && next === '/') { inLineComment = true; i++; continue; }
|
|
192
|
+
if (c === '/' && next === '*') { inBlockComment = true; i++; continue; }
|
|
193
|
+
if (c === "'") { inSingle = true; continue; }
|
|
194
|
+
if (c === '"') { inDouble = true; continue; }
|
|
195
|
+
if (c === '`') { inTemplate = true; continue; }
|
|
196
|
+
if (c === '{') { depth++; if (depth > max) max = depth; }
|
|
197
|
+
else if (c === '}') depth--;
|
|
198
|
+
}
|
|
199
|
+
return max;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Which IO callees appear in this body. */
|
|
203
|
+
function ioOperationsIn(body: string): string[] {
|
|
204
|
+
const found = new Set<string>();
|
|
205
|
+
// Fresh regex instances per call: no shared lastIndex to reset or leak.
|
|
206
|
+
for (const m of body.matchAll(ioCalleeRe())) {
|
|
207
|
+
found.add(m[0].replace(/\s*\($/, ''));
|
|
208
|
+
}
|
|
209
|
+
for (const m of body.matchAll(ioGlobalRe())) {
|
|
210
|
+
const name = /(\w+)\s*\($/.exec(m[0].trim());
|
|
211
|
+
if (name) found.add(name[1]);
|
|
212
|
+
}
|
|
213
|
+
return [...found];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Blank out comments and string/template literals, preserving offsets.
|
|
218
|
+
* A single left-to-right scan is used rather than chained `.replace()` calls:
|
|
219
|
+
* sequential regex replacement re-scans text that earlier passes produced and
|
|
220
|
+
* can pair a quote inside a regex literal with a quote in a later comment,
|
|
221
|
+
* swallowing hundreds of lines (observed as a 302-line phantom function).
|
|
222
|
+
*/
|
|
223
|
+
function maskLiterals(src: string): string {
|
|
224
|
+
const out = src.split('');
|
|
225
|
+
let i = 0;
|
|
226
|
+
while (i < src.length) {
|
|
227
|
+
const c = src[i];
|
|
228
|
+
const next = src[i + 1];
|
|
229
|
+
|
|
230
|
+
if (c === '/' && next === '/') {
|
|
231
|
+
while (i < src.length && src[i] !== '\n') { out[i] = ' '; i++; }
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (c === '/' && next === '*') {
|
|
235
|
+
out[i] = ' '; out[i + 1] = ' '; i += 2;
|
|
236
|
+
while (i < src.length && !(src[i] === '*' && src[i + 1] === '/')) { out[i] = ' '; i++; }
|
|
237
|
+
if (i < src.length) { out[i] = ' '; out[i + 1] = ' '; i += 2; }
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Regex literals: `/.../flags`. Only treat `/` as a regex start when the
|
|
242
|
+
// previous significant character cannot end an expression.
|
|
243
|
+
if (c === '/') {
|
|
244
|
+
const before = src.substring(0, i).replace(/\s+$/, '');
|
|
245
|
+
const prev = before[before.length - 1];
|
|
246
|
+
const prevWord = /(?:^|[^\w$])(return|typeof|case|in|of|new|delete|void|instanceof)$/.test(before);
|
|
247
|
+
if (prev === undefined || /[=(,:[!&|?{};+\-*%^~<>]/.test(prev) || prevWord) {
|
|
248
|
+
let j = i + 1;
|
|
249
|
+
let inClass = false;
|
|
250
|
+
let closed = false;
|
|
251
|
+
while (j < src.length) {
|
|
252
|
+
const d = src[j];
|
|
253
|
+
if (d === '\\') { j += 2; continue; }
|
|
254
|
+
if (d === '\n') break;
|
|
255
|
+
if (d === '[') inClass = true;
|
|
256
|
+
else if (d === ']') inClass = false;
|
|
257
|
+
else if (d === '/' && !inClass) { closed = true; break; }
|
|
258
|
+
j++;
|
|
259
|
+
}
|
|
260
|
+
if (closed) {
|
|
261
|
+
for (let k = i; k <= j; k++) out[k] = ' ';
|
|
262
|
+
i = j + 1;
|
|
263
|
+
while (i < src.length && /[a-z]/i.test(src[i])) { out[i] = ' '; i++; }
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (c === "'" || c === '"' || c === '`') {
|
|
270
|
+
const quote = c;
|
|
271
|
+
out[i] = ' ';
|
|
272
|
+
i++;
|
|
273
|
+
while (i < src.length) {
|
|
274
|
+
const d = src[i];
|
|
275
|
+
if (d === '\\') { out[i] = ' '; if (i + 1 < src.length) out[i + 1] = ' '; i += 2; continue; }
|
|
276
|
+
if (d === quote) { out[i] = ' '; i++; break; }
|
|
277
|
+
if (d === '\n' && quote !== '`') break;
|
|
278
|
+
out[i] = ' ';
|
|
279
|
+
i++;
|
|
280
|
+
}
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
i++;
|
|
285
|
+
}
|
|
286
|
+
return out.join('');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* The metrics the rule set actually reads (see src/principles/rules/**).
|
|
291
|
+
* Previously only {name,type,line,code} was returned, so every rule that reads
|
|
292
|
+
* `length` / `paramCount` / `nestingDepth` / `hasTryCatch` / `methodCount`
|
|
293
|
+
* silently never fired on TypeScript (#446).
|
|
294
|
+
*/
|
|
295
|
+
interface FunctionMetrics {
|
|
296
|
+
name: string;
|
|
297
|
+
type: 'function' | 'method' | 'arrow';
|
|
298
|
+
line: number;
|
|
299
|
+
startLine: number;
|
|
300
|
+
endLine: number;
|
|
301
|
+
/** Body line span, matching the `length` field long-function reads. */
|
|
302
|
+
length: number;
|
|
303
|
+
paramCount: number;
|
|
304
|
+
nestingDepth: number;
|
|
305
|
+
hasTryCatch: boolean;
|
|
306
|
+
ioOperations: string[];
|
|
307
|
+
code: string;
|
|
308
|
+
}
|
|
309
|
+
|
|
4
310
|
export class TypeScriptAdapter extends BaseAdapter implements Adapter {
|
|
5
311
|
detectLanguage(): string {
|
|
6
312
|
const ext = this.filePath.toLowerCase();
|
|
@@ -14,28 +320,322 @@ export class TypeScriptAdapter extends BaseAdapter implements Adapter {
|
|
|
14
320
|
return this.createParseResult('typescript');
|
|
15
321
|
}
|
|
16
322
|
|
|
323
|
+
private maskedCache?: string;
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* The file with comments and string/template/regex literals blanked out,
|
|
327
|
+
* computed ONCE per adapter instance. Every extraction pass used to call
|
|
328
|
+
* maskLiterals(this.fileContent) itself, and buildFunction() called it again
|
|
329
|
+
* for each function -- O(functions x fileLength) per file.
|
|
330
|
+
*/
|
|
331
|
+
private get maskedSource(): string {
|
|
332
|
+
if (this.maskedCache === undefined) {
|
|
333
|
+
this.maskedCache = maskLiterals(this.fileContent);
|
|
334
|
+
}
|
|
335
|
+
return this.maskedCache;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Numeric literals for the magic-numbers rule, which reads
|
|
341
|
+
* `adapter.extract()` -> `[{value, line}]`. Numbers inside comments and
|
|
342
|
+
* string/template literals are skipped via maskLiterals().
|
|
343
|
+
*
|
|
344
|
+
* A number that IS the initializer of a named constant is skipped: the rule's
|
|
345
|
+
* own advice is "consider using a named constant instead", so reporting the
|
|
346
|
+
* constant it just asked for made the violation unfixable -- extracting the
|
|
347
|
+
* literal could never clear it.
|
|
348
|
+
*/
|
|
349
|
+
extract(): Array<{ value: number; line: number }> {
|
|
350
|
+
const src = this.fileContent;
|
|
351
|
+
const results: Array<{ value: number; line: number }> = [];
|
|
352
|
+
const masked = this.maskedSource;
|
|
353
|
+
|
|
354
|
+
// `const NAME = 3` / `const NAME: T = 3` -- capture the value's offset so the
|
|
355
|
+
// literal can be matched positionally rather than by value.
|
|
356
|
+
const namedConstRe = /\b(?:const|readonly)\s+[A-Za-z_$][\w$]*\s*(?::[^=;]+)?=\s*(-?\d+(?:\.\d+)?)\s*;/g;
|
|
357
|
+
const namedConstOffsets = new Set<number>();
|
|
358
|
+
let c: RegExpExecArray | null;
|
|
359
|
+
while ((c = namedConstRe.exec(masked)) !== null) {
|
|
360
|
+
namedConstOffsets.add(c.index + c[0].lastIndexOf(c[1]));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const numRe = /(?<![\w$.])(-?\d+(?:\.\d+)?)(?![\w$])/g;
|
|
364
|
+
let m: RegExpExecArray | null;
|
|
365
|
+
while ((m = numRe.exec(masked)) !== null) {
|
|
366
|
+
if (namedConstOffsets.has(m.index)) continue;
|
|
367
|
+
results.push({ value: Number(m[1]), line: lineOf(src, m.index) });
|
|
368
|
+
}
|
|
369
|
+
return results;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Import specifiers for the unused-imports rule. The rule reads
|
|
374
|
+
* `adapter.imports` as `Array<{name, line, used, type?}>` -- returning bare
|
|
375
|
+
* strings made every entry look unused and rendered `undefined` as the name.
|
|
376
|
+
* `used` is computed by counting identifier occurrences outside the import
|
|
377
|
+
* statements themselves.
|
|
378
|
+
*/
|
|
379
|
+
get imports(): Array<{ name: string; line: number; used: boolean; type?: string }> {
|
|
380
|
+
const src = this.fileContent;
|
|
381
|
+
const entries: Array<{ name: string; line: number; used: boolean; type?: string }> = [];
|
|
382
|
+
const importRe = /^[ \t]*import\s+(?:type\s+)?([\s\S]*?)\s+from\s+['"][^'"]+['"]/gm;
|
|
383
|
+
const importSpans: Array<[number, number]> = [];
|
|
384
|
+
let m: RegExpExecArray | null;
|
|
385
|
+
const idRe = /^[A-Za-z_$][\w$]*$/;
|
|
386
|
+
|
|
387
|
+
while ((m = importRe.exec(src)) !== null) {
|
|
388
|
+
const clause = m[1].trim();
|
|
389
|
+
const line = lineOf(src, m.index);
|
|
390
|
+
const isTypeOnly = /^import\s+type\b/.test(m[0]);
|
|
391
|
+
const push = (name: string, type?: string) => {
|
|
392
|
+
if (idRe.test(name)) entries.push({ name, line, used: false, ...(type ? { type } : {}) });
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
// default import
|
|
396
|
+
const def = /^(\w+)/.exec(clause);
|
|
397
|
+
if (def) push(def[1], isTypeOnly ? 'type' : undefined);
|
|
398
|
+
|
|
399
|
+
// named imports { a, b as c }
|
|
400
|
+
const braces = /\{([\s\S]*?)\}/.exec(clause);
|
|
401
|
+
if (braces) {
|
|
402
|
+
for (const raw of braces[1].split(',')) {
|
|
403
|
+
const part = raw.trim();
|
|
404
|
+
if (!part) continue;
|
|
405
|
+
const asMatch = /\bas\s+(\w+)\s*$/.exec(part);
|
|
406
|
+
const local = asMatch ? asMatch[1] : part.replace(/^type\s+/, '').trim();
|
|
407
|
+
const memberTypeOnly = /^type\s+/.test(part);
|
|
408
|
+
push(local, isTypeOnly || memberTypeOnly ? 'type' : undefined);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// namespace import * as ns
|
|
413
|
+
const ns = /\*\s+as\s+(\w+)/.exec(clause);
|
|
414
|
+
if (ns) push(ns[1], isTypeOnly ? 'type' : undefined);
|
|
415
|
+
|
|
416
|
+
importSpans.push([m.index, m.index + m[0].length]);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Count identifier usage outside import statements.
|
|
420
|
+
let body = src;
|
|
421
|
+
for (const [start, end] of [...importSpans].reverse()) {
|
|
422
|
+
body = body.substring(0, start) + ' '.repeat(end - start) + body.substring(end);
|
|
423
|
+
}
|
|
424
|
+
for (const entry of entries) {
|
|
425
|
+
const useRe = new RegExp(`(?<![\\w$.])${entry.name.replace(/\$/g, '\\$')}(?![\\w$])`, 'g');
|
|
426
|
+
entry.used = useRe.test(body);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return entries;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Interfaces for the ISP rule, which reads `adapter.extractInterfaces()`.
|
|
434
|
+
*/
|
|
435
|
+
extractInterfaces(): Array<{ name: string; line: number; methods: string[]; methodCount: number; code: string }> {
|
|
436
|
+
const src = this.fileContent;
|
|
437
|
+
const masked = this.maskedSource;
|
|
438
|
+
const results: Array<{ name: string; line: number; methods: string[]; methodCount: number; code: string }> = [];
|
|
439
|
+
const ifaceRe = /(?:export\s+)?(?:declare\s+)?interface\s+(\w+)/g;
|
|
440
|
+
let m: RegExpExecArray | null;
|
|
441
|
+
while ((m = ifaceRe.exec(masked)) !== null) {
|
|
442
|
+
const bodyOpen = findDeclarationBrace(masked, ifaceRe.lastIndex);
|
|
443
|
+
if (bodyOpen === -1) continue;
|
|
444
|
+
const bodyEnd = findBlockEnd(masked, bodyOpen);
|
|
445
|
+
const body = src.substring(bodyOpen, bodyEnd);
|
|
446
|
+
const methods = [...body.matchAll(/(?<![\w$.])(\w+)\s*\(/g)]
|
|
447
|
+
.map(x => x[1])
|
|
448
|
+
.filter(n => !CONTROL_KEYWORDS.has(n));
|
|
449
|
+
results.push({
|
|
450
|
+
name: m[1],
|
|
451
|
+
line: lineOf(src, m.index),
|
|
452
|
+
methods,
|
|
453
|
+
methodCount: methods.length,
|
|
454
|
+
code: body,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
return results;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
private buildFunction(
|
|
461
|
+
name: string,
|
|
462
|
+
type: 'function' | 'method' | 'arrow',
|
|
463
|
+
nameIndex: number,
|
|
464
|
+
parenIndex: number,
|
|
465
|
+
bodyOpen: number,
|
|
466
|
+
): FunctionMetrics {
|
|
467
|
+
const src = this.fileContent;
|
|
468
|
+
// Bodies/positions come from the real source; brace matching uses the masked
|
|
469
|
+
// text so braces inside regex/string literals cannot desync the block scan.
|
|
470
|
+
const masked = this.maskedSource;
|
|
471
|
+
const { inner } = readParenGroup(masked, parenIndex);
|
|
472
|
+
const bodyEnd = findBlockEnd(masked, bodyOpen);
|
|
473
|
+
const body = src.substring(bodyOpen, bodyEnd);
|
|
474
|
+
const startLine = lineOf(src, nameIndex);
|
|
475
|
+
const endLine = lineOf(src, bodyEnd);
|
|
476
|
+
|
|
477
|
+
// Arrow functions may be `=> expr` with no braces; then the "body" is a
|
|
478
|
+
// single expression and `findBlockEnd` will not have found a `{`.
|
|
479
|
+
const hasBraces = masked[bodyOpen] === '{';
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
name,
|
|
483
|
+
type,
|
|
484
|
+
line: startLine,
|
|
485
|
+
startLine,
|
|
486
|
+
endLine,
|
|
487
|
+
length: hasBraces ? Math.max(1, endLine - startLine + 1) : 1,
|
|
488
|
+
paramCount: countParams(inner),
|
|
489
|
+
nestingDepth: hasBraces ? maxNesting(maskLiterals(body)) : 0,
|
|
490
|
+
hasTryCatch: /\btry\s*\{/.test(masked.substring(bodyOpen, bodyEnd)),
|
|
491
|
+
ioOperations: ioOperationsIn(masked.substring(bodyOpen, bodyEnd)),
|
|
492
|
+
code: body,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
17
496
|
extractFunctions(): unknown[] {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
497
|
+
const src = this.fileContent;
|
|
498
|
+
// Match declarations against the masked source so identifiers that appear
|
|
499
|
+
// only in comments or strings are never mistaken for functions, while
|
|
500
|
+
// bodies/positions still come from the original text (same offsets).
|
|
501
|
+
const masked = this.maskedSource;
|
|
502
|
+
const found: FunctionMetrics[] = [];
|
|
503
|
+
const seen = new Set<string>();
|
|
504
|
+
|
|
505
|
+
// 1. `function name(...)` declarations (incl. export / async / generators).
|
|
506
|
+
const declRe = /(?:^|[^\w$.])(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*\*?\s*(\w+)\s*/g;
|
|
507
|
+
let m: RegExpExecArray | null;
|
|
508
|
+
while ((m = declRe.exec(masked)) !== null) {
|
|
509
|
+
const nameIndex = m.index + m[0].length - m[1].length;
|
|
510
|
+
const parenIndex = masked.indexOf('(', declRe.lastIndex - 1);
|
|
511
|
+
if (parenIndex === -1) continue;
|
|
512
|
+
const bodyOpen = findBodyBrace(masked, parenIndex);
|
|
513
|
+
if (bodyOpen === -1) continue;
|
|
514
|
+
const key = `${m[1]}@${nameIndex}`;
|
|
515
|
+
if (seen.has(key)) continue;
|
|
516
|
+
seen.add(key);
|
|
517
|
+
found.push(this.buildFunction(m[1], 'function', nameIndex, parenIndex, bodyOpen));
|
|
518
|
+
}
|
|
21
519
|
|
|
22
|
-
|
|
23
|
-
|
|
520
|
+
// 2. Class methods: `name(...) {`. Not anchored to line start so minified
|
|
521
|
+
// single-line bodies count too. Control-flow keywords are excluded -- a
|
|
522
|
+
// bare `if (a > 0) {` matches the shape but is not a method.
|
|
523
|
+
const memberRe = /(?<![\w$.])(?:public\s+|private\s+|protected\s+|static\s+|async\s+|readonly\s+|get\s+|set\s+|\*\s*)*(\w+)\s*\(/g;
|
|
524
|
+
while ((m = memberRe.exec(masked)) !== null) {
|
|
525
|
+
if (CONTROL_KEYWORDS.has(m[1])) continue;
|
|
526
|
+
// The pattern ends with `\(`, so the match's last character IS the opening
|
|
527
|
+
// paren. Re-searching with indexOf() could drift to a later `(` inside the
|
|
528
|
+
// parameter list or a type annotation, which corrupted the reported span.
|
|
529
|
+
const parenIndex = m.index + m[0].length - 1;
|
|
530
|
+
const nameIndex = parenIndex - m[1].length;
|
|
531
|
+
// Only treat as a method when a body brace follows the parameter list.
|
|
532
|
+
const bodyOpen = findBodyBrace(masked, parenIndex);
|
|
533
|
+
if (bodyOpen === -1) continue;
|
|
534
|
+
const key = `${m[1]}@${nameIndex}`;
|
|
535
|
+
if (seen.has(key)) continue;
|
|
536
|
+
seen.add(key);
|
|
537
|
+
found.push(this.buildFunction(m[1], 'method', nameIndex, parenIndex, bodyOpen));
|
|
24
538
|
}
|
|
25
539
|
|
|
26
|
-
|
|
540
|
+
// 3. Arrow consts / assignments: `const name = (...) =>` / `name = x =>`.
|
|
541
|
+
const arrowRe = /(?:const|let|var)\s+(\w+)\s*(?::[^=]+)?=\s*(?:async\s*)?/g;
|
|
542
|
+
while ((m = arrowRe.exec(masked)) !== null) {
|
|
543
|
+
const rest = masked.substring(arrowRe.lastIndex);
|
|
544
|
+
const parenOffset = /^\s*\(/.test(rest) ? rest.indexOf('(') : -1;
|
|
545
|
+
let parenIndex: number;
|
|
546
|
+
if (parenOffset !== -1) {
|
|
547
|
+
parenIndex = arrowRe.lastIndex + parenOffset;
|
|
548
|
+
} else {
|
|
549
|
+
// Single-identifier param: `x => ...`
|
|
550
|
+
const simple = /^\s*(\w+)\s*=>/.exec(rest);
|
|
551
|
+
if (!simple) continue;
|
|
552
|
+
parenIndex = -1;
|
|
553
|
+
}
|
|
554
|
+
const afterParams = parenIndex === -1
|
|
555
|
+
? arrowRe.lastIndex + (/^\s*(\w+)/.exec(rest) as RegExpExecArray)[0].length
|
|
556
|
+
: readParenGroup(src, parenIndex).end;
|
|
557
|
+
if (!/^\s*(?::[^=]+)?=>/.test(masked.substring(afterParams))) continue;
|
|
558
|
+
|
|
559
|
+
const nameIndex = m.index + m[0].length - m[1].length;
|
|
560
|
+
const braceAfter = masked.indexOf('{', afterParams);
|
|
561
|
+
const arrowBodyStart = braceAfter !== -1 && /^\s*(?::[^=]+)?=>\s*\{/.test(masked.substring(afterParams, braceAfter + 1))
|
|
562
|
+
? braceAfter
|
|
563
|
+
: -1;
|
|
564
|
+
|
|
565
|
+
let length = 1;
|
|
566
|
+
let nestingDepth = 0;
|
|
567
|
+
let hasTryCatch = false;
|
|
568
|
+
let ioOperations: string[] = [];
|
|
569
|
+
let code = '';
|
|
570
|
+
if (arrowBodyStart !== -1) {
|
|
571
|
+
const bodyEnd = findBlockEnd(masked, arrowBodyStart);
|
|
572
|
+
code = src.substring(arrowBodyStart, bodyEnd);
|
|
573
|
+
const startLine = lineOf(src, nameIndex);
|
|
574
|
+
const endLine = lineOf(src, bodyEnd);
|
|
575
|
+
length = Math.max(1, endLine - startLine + 1);
|
|
576
|
+
nestingDepth = maxNesting(code);
|
|
577
|
+
hasTryCatch = /\btry\s*\{/.test(code);
|
|
578
|
+
ioOperations = ioOperationsIn(code);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const startLine = lineOf(src, nameIndex);
|
|
582
|
+
const key = `${m[1]}@${nameIndex}`;
|
|
583
|
+
if (seen.has(key)) continue;
|
|
584
|
+
seen.add(key);
|
|
585
|
+
found.push({
|
|
586
|
+
name: m[1],
|
|
587
|
+
type: 'arrow',
|
|
588
|
+
line: startLine,
|
|
589
|
+
startLine,
|
|
590
|
+
endLine: startLine,
|
|
591
|
+
length,
|
|
592
|
+
paramCount: parenIndex === -1 ? 1 : countParams(readParenGroup(src, parenIndex).inner),
|
|
593
|
+
nestingDepth,
|
|
594
|
+
hasTryCatch,
|
|
595
|
+
ioOperations,
|
|
596
|
+
code,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return found;
|
|
27
601
|
}
|
|
28
602
|
|
|
29
603
|
extractClasses(): unknown[] {
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
604
|
+
const src = this.fileContent;
|
|
605
|
+
const classRe = /(export\s+)?(?:default\s+)?(?:abstract\s+)?class\s+(\w+)/g;
|
|
606
|
+
const results: Array<Record<string, unknown>> = [];
|
|
607
|
+
const masked = this.maskedSource;
|
|
608
|
+
let m: RegExpExecArray | null;
|
|
609
|
+
|
|
610
|
+
while ((m = classRe.exec(masked)) !== null) {
|
|
611
|
+
const name = m[2];
|
|
612
|
+
const nameIndex = m.index + m[0].length - name.length;
|
|
613
|
+
const bodyOpen = findDeclarationBrace(masked, classRe.lastIndex);
|
|
614
|
+
if (bodyOpen === -1) continue;
|
|
615
|
+
const bodyEnd = findBlockEnd(masked, bodyOpen);
|
|
616
|
+
const body = src.substring(bodyOpen, bodyEnd);
|
|
33
617
|
|
|
34
|
-
|
|
35
|
-
|
|
618
|
+
// Count method-ish members. Not anchored to line start: minified /
|
|
619
|
+
// single-line class bodies (`m1() {} m2() {}`) are valid TS and must count.
|
|
620
|
+
// `(?<![\w$.])` avoids matching call sites like `foo.bar(`.
|
|
621
|
+
const memberRe = /(?<![\w$.])(?:public\s+|private\s+|protected\s+|static\s+|async\s+|readonly\s+|get\s+|set\s+|\*\s*)*(\w+)\s*\(/g;
|
|
622
|
+
const methods = [...body.matchAll(memberRe)]
|
|
623
|
+
.map(x => x[1])
|
|
624
|
+
.filter(n => !['if', 'for', 'while', 'switch', 'catch', 'return', 'typeof', 'new'].includes(n));
|
|
625
|
+
|
|
626
|
+
results.push({
|
|
627
|
+
name,
|
|
628
|
+
type: 'class',
|
|
629
|
+
line: lineOf(src, nameIndex),
|
|
630
|
+
startLine: lineOf(src, nameIndex),
|
|
631
|
+
endLine: lineOf(src, bodyEnd),
|
|
632
|
+
methodCount: methods.length,
|
|
633
|
+
methods,
|
|
634
|
+
code: body,
|
|
635
|
+
});
|
|
36
636
|
}
|
|
37
637
|
|
|
38
|
-
return
|
|
638
|
+
return results;
|
|
39
639
|
}
|
|
40
640
|
|
|
41
641
|
extractExports(): unknown[] {
|
|
@@ -63,4 +663,4 @@ export class TypeScriptAdapter extends BaseAdapter implements Adapter {
|
|
|
63
663
|
|
|
64
664
|
return exportMatches;
|
|
65
665
|
}
|
|
66
|
-
}
|
|
666
|
+
}
|
package/principles/analyzer.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Rule, Violation, Adapter } from './types';
|
|
2
|
+
import { getActiveConfig } from './config';
|
|
2
3
|
import { TypeScriptAdapter } from './adapters/typescript';
|
|
3
4
|
import { PythonAdapter } from './adapters/python';
|
|
4
5
|
import { GoAdapter } from './adapters/go';
|
|
@@ -113,6 +114,12 @@ function computeSummary(
|
|
|
113
114
|
|
|
114
115
|
function runRuleOnFile(ctx: RunRuleContext): void {
|
|
115
116
|
const { file, rule, adapter, violations, fileResult, ruleResult, errors } = ctx;
|
|
117
|
+
// Honour `enabled: false` from `.principlesrc`. Nothing read this flag before
|
|
118
|
+
// #457, so a project could not turn a rule off.
|
|
119
|
+
//
|
|
120
|
+
// Checked BEFORE `filesChecked++`: a rule that never ran must not report that it
|
|
121
|
+
// examined a file, or the per-rule stats claim coverage the rule did not perform.
|
|
122
|
+
if (!isRuleEnabled(rule.id)) return;
|
|
116
123
|
ruleResult.filesChecked++;
|
|
117
124
|
try {
|
|
118
125
|
const ruleViolations = rule.check(file, adapter);
|
|
@@ -128,6 +135,21 @@ function runRuleOnFile(ctx: RunRuleContext): void {
|
|
|
128
135
|
}
|
|
129
136
|
}
|
|
130
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Whether `ruleId` (e.g. `clean-code.large-file`) is enabled in the active
|
|
140
|
+
* config. Unknown or malformed entries default to enabled: a config typo must
|
|
141
|
+
* not silently disable a gate.
|
|
142
|
+
*/
|
|
143
|
+
function isRuleEnabled(ruleId: string): boolean {
|
|
144
|
+
const separator = ruleId.indexOf('.');
|
|
145
|
+
if (separator <= 0) return true;
|
|
146
|
+
const group = ruleId.slice(0, separator);
|
|
147
|
+
const name = ruleId.slice(separator + 1);
|
|
148
|
+
const rules = getActiveConfig().rules as Record<string, Record<string, { enabled?: boolean }>>;
|
|
149
|
+
const settings = rules[group]?.[name];
|
|
150
|
+
return settings?.enabled !== false;
|
|
151
|
+
}
|
|
152
|
+
|
|
131
153
|
interface RunRuleContext {
|
|
132
154
|
file: string;
|
|
133
155
|
rule: Rule;
|