@lifeaitools/rdc-skills 0.34.0 → 0.35.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/.claude-plugin/plugin.json +284 -1
- package/VALIDATOR-ARCHITECTURE.md +534 -0
- package/commands/analyze-tests.md +11 -0
- package/commands/check-clean-code.md +11 -0
- package/commands/check-packages.md +10 -0
- package/commands/compare-compliance.md +14 -0
- package/commands/full-analysis.md +50 -0
- package/commands/get-refactoring-plan.md +13 -0
- package/commands/quick-check.md +13 -0
- package/commands/recover.md +149 -0
- package/commands/review-arch.md +12 -0
- package/commands/review.md +12 -113
- package/commands/suggest-patterns.md +11 -0
- package/commands/validate-solid.md +11 -0
- package/package.json +14 -2
- package/scripts/architecture-score.mjs +157 -0
- package/scripts/clean-code-score.mjs +177 -0
- package/scripts/duplication-score.mjs +66 -0
- package/scripts/lib/architecture-scoring.mjs +695 -0
- package/scripts/lib/clean-code-scoring.mjs +258 -0
- package/scripts/lib/duplication-scoring.mjs +238 -0
- package/scripts/lib/language-plugin.mjs +82 -0
- package/scripts/lib/package-metrics.mjs +439 -0
- package/scripts/lib/pattern-scoring.mjs +351 -0
- package/scripts/lib/plugins/treesitter.mjs +1182 -0
- package/scripts/lib/plugins/typescript.mjs +672 -0
- package/scripts/lib/refactoring-scoring.mjs +307 -0
- package/scripts/lib/solid-scoring.mjs +101 -0
- package/scripts/lib/test-smell-scoring.mjs +581 -0
- package/scripts/lib/vendor/codeflow-parser/.source-commit +1 -0
- package/scripts/lib/vendor/codeflow-parser/grammars.d.ts +23 -0
- package/scripts/lib/vendor/codeflow-parser/grammars.js +57 -0
- package/scripts/lib/vendor/codeflow-parser/memberFacts.d.ts +274 -0
- package/scripts/lib/vendor/codeflow-parser/memberFacts.js +1117 -0
- package/scripts/lib/vendor/codeflow-parser/nativeParser.d.ts +115 -0
- package/scripts/lib/vendor/codeflow-parser/nativeParser.js +759 -0
- package/scripts/lib/vendor/codeflow-parser/package.json +3 -0
- package/scripts/lib/vendor/codeflow-parser/xmlParser.d.ts +77 -0
- package/scripts/lib/vendor/codeflow-parser/xmlParser.js +400 -0
- package/scripts/package-metrics-cli.mjs +112 -0
- package/scripts/pattern-score.mjs +143 -0
- package/scripts/refactoring-score.mjs +253 -0
- package/scripts/solid-score.mjs +337 -0
- package/skills/architecture-reviewer/SKILL.md +287 -0
- package/skills/clean-code-analyzer/SKILL.md +147 -0
- package/skills/package-design/SKILL.md +118 -0
- package/skills/pattern-advisor/SKILL.md +237 -0
- package/skills/pattern-refactoring-guide/SKILL.md +262 -0
- package/skills/review/SKILL.md +29 -0
- package/skills/solid-validator/SKILL.md +92 -0
- package/skills/testing-strategy/SKILL.md +132 -0
- package/tests/lib/architecture-scoring.test.mjs +335 -0
- package/tests/lib/clean-code-scoring.test.mjs +241 -0
- package/tests/lib/duplication-scoring.test.mjs +144 -0
- package/tests/lib/fixtures.mjs +58 -0
- package/tests/lib/package-metrics.test.mjs +241 -0
- package/tests/lib/pattern-scoring.test.mjs +251 -0
- package/tests/lib/refactoring-scoring.test.mjs +264 -0
- package/tests/lib/solid-scoring.test.mjs +291 -0
- package/tests/lib/test-smell-scoring.test.mjs +281 -0
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript/JavaScript language plugin — the Day-1 implementation of the
|
|
3
|
+
* language-plugin contract (see ../language-plugin.mjs). This is the ONLY
|
|
4
|
+
* file in the scorer allowed to import ts-morph or reason about its AST.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Project, SyntaxKind, VariableDeclarationKind } from 'ts-morph';
|
|
8
|
+
|
|
9
|
+
const STDLIB_WHITELIST = new Set([
|
|
10
|
+
'Map', 'Set', 'WeakMap', 'WeakSet', 'Array', 'Object', 'Date', 'Error',
|
|
11
|
+
'TypeError', 'RangeError', 'RegExp', 'Promise', 'URL', 'URLSearchParams',
|
|
12
|
+
'AbortController', 'Buffer', 'Headers', 'Request', 'Response',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
function fieldsOf(m) {
|
|
16
|
+
return m.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)
|
|
17
|
+
.filter((p) => p.getExpression().getKind() === SyntaxKind.ThisKeyword)
|
|
18
|
+
.map((p) => p.getName());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function callsOf(m) {
|
|
22
|
+
return m.getDescendantsOfKind(SyntaxKind.CallExpression)
|
|
23
|
+
.map((c) => c.getExpression().getText().replace(/^this\./, ''));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function branchHitsOf(m) {
|
|
27
|
+
let n = 0;
|
|
28
|
+
n += m.getDescendantsOfKind(SyntaxKind.SwitchStatement)
|
|
29
|
+
.reduce((s, sw) => s + sw.getClauses().filter((c) => c.getKind() === SyntaxKind.CaseClause).length, 0);
|
|
30
|
+
n += m.getDescendantsOfKind(SyntaxKind.BinaryExpression).filter((b) => b.getOperatorToken().getText() === 'instanceof').length;
|
|
31
|
+
n += m.getDescendantsOfKind(SyntaxKind.TypeOfExpression).length;
|
|
32
|
+
n += m.getDescendantsOfKind(SyntaxKind.IfStatement).filter((s) => s.getElseStatement()?.getKind() === SyntaxKind.IfStatement).length;
|
|
33
|
+
return n;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── Clean Code facts (N1/N2/N4/F1/E1/G9's unreachable-code half) ──────────
|
|
37
|
+
// Real detection logic ported from architecture-toolkit's src/agents/
|
|
38
|
+
// clean-code-analyzer/tools (MIT, github.com/OnSightTeam/architecture-toolkit,
|
|
39
|
+
// explicit reuse approval from the operator) — its own checks run whole-file
|
|
40
|
+
// text regexes against `code.match(...)` with an occurrence-count threshold,
|
|
41
|
+
// which is why it needs ">3 single-letter assignments" as noise suppression
|
|
42
|
+
// instead of just flagging one. Ours walks the real AST per declared binding,
|
|
43
|
+
// so context (a for-loop counter, a const-declared magic number) is known
|
|
44
|
+
// directly and every occurrence is its own finding — no threshold needed to
|
|
45
|
+
// separate signal from regex false-positives.
|
|
46
|
+
|
|
47
|
+
const STATEMENT_KINDS = [
|
|
48
|
+
SyntaxKind.ExpressionStatement, SyntaxKind.VariableStatement, SyntaxKind.IfStatement,
|
|
49
|
+
SyntaxKind.ForStatement, SyntaxKind.ForInStatement, SyntaxKind.ForOfStatement,
|
|
50
|
+
SyntaxKind.WhileStatement, SyntaxKind.DoStatement, SyntaxKind.SwitchStatement,
|
|
51
|
+
SyntaxKind.ReturnStatement, SyntaxKind.ThrowStatement, SyntaxKind.TryStatement,
|
|
52
|
+
SyntaxKind.BreakStatement, SyntaxKind.ContinueStatement, SyntaxKind.LabeledStatement,
|
|
53
|
+
];
|
|
54
|
+
// F1 (long method, threshold 20) — architecture-toolkit's own real threshold
|
|
55
|
+
// at src/agents/clean-code-analyzer/tools/function-validator.ts:52
|
|
56
|
+
// (`if (avgLinesPerFunction > 20)`) independently lands on the same number
|
|
57
|
+
// our task spec names; corroborated, not just copied.
|
|
58
|
+
function statementCountOf(m) {
|
|
59
|
+
return STATEMENT_KINDS.reduce((n, k) => n + m.getDescendantsOfKind(k).length, 0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// N1/N2 raw material — every simple (non-destructured) local binding name:
|
|
63
|
+
// parameters plus `let`/`const`/`var` declarations, with its declaration
|
|
64
|
+
// line. Destructuring patterns are skipped (getName() on a binding pattern
|
|
65
|
+
// isn't a single identifier) rather than mis-flagged.
|
|
66
|
+
function declaredNamesOf(paramsNode, bodyNode) {
|
|
67
|
+
const names = [];
|
|
68
|
+
for (const p of paramsNode.getParameters()) {
|
|
69
|
+
if (p.getNameNode().getKind() === SyntaxKind.Identifier) names.push({ name: p.getName(), line: p.getStartLineNumber() });
|
|
70
|
+
}
|
|
71
|
+
for (const decl of bodyNode.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
|
|
72
|
+
if (decl.getNameNode().getKind() === SyntaxKind.Identifier) names.push({ name: decl.getName(), line: decl.getStartLineNumber() });
|
|
73
|
+
}
|
|
74
|
+
return names;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// N4 magic numbers — numeric literal other than 0/1/-1, excluded only when
|
|
78
|
+
// it is the DIRECT initializer of a `const` variable declaration or an enum
|
|
79
|
+
// member (the two AST-visible "this number has already been named" shapes).
|
|
80
|
+
function magicNumbersOf(m) {
|
|
81
|
+
const found = [];
|
|
82
|
+
for (const lit of m.getDescendantsOfKind(SyntaxKind.NumericLiteral)) {
|
|
83
|
+
let node = lit;
|
|
84
|
+
let value = Number(lit.getText());
|
|
85
|
+
const parent = lit.getParent();
|
|
86
|
+
if (parent?.getKind() === SyntaxKind.PrefixUnaryExpression && parent.getOperatorToken() === SyntaxKind.MinusToken) {
|
|
87
|
+
value = -value;
|
|
88
|
+
node = parent;
|
|
89
|
+
}
|
|
90
|
+
if (value === 0 || value === 1 || value === -1) continue;
|
|
91
|
+
|
|
92
|
+
const initParent = node.getParent();
|
|
93
|
+
let excluded = false;
|
|
94
|
+
if (initParent?.getKind() === SyntaxKind.VariableDeclaration && initParent.getInitializer() === node) {
|
|
95
|
+
const declList = initParent.getParent();
|
|
96
|
+
if (declList?.getKind() === SyntaxKind.VariableDeclarationList && declList.getDeclarationKind() === VariableDeclarationKind.Const) excluded = true;
|
|
97
|
+
}
|
|
98
|
+
if (initParent?.getKind() === SyntaxKind.EnumMember) excluded = true;
|
|
99
|
+
if (excluded) continue;
|
|
100
|
+
|
|
101
|
+
found.push({ value, line: lit.getStartLineNumber() });
|
|
102
|
+
}
|
|
103
|
+
return found;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// E1 empty catch blocks — real pattern is architecture-toolkit's
|
|
107
|
+
// src/agents/clean-code-analyzer/tools/code-smell-validator.ts:160
|
|
108
|
+
// (`/catch\s*\([^)]+\)\s*{\s*}/i`). AST form: zero statements in the block —
|
|
109
|
+
// strictly stronger than their regex, which a `catch(e) { /* ignored */ }`
|
|
110
|
+
// comment-only block would NOT match (comment text isn't whitespace) but is
|
|
111
|
+
// exactly as empty in intent.
|
|
112
|
+
function emptyCatchesOf(m) {
|
|
113
|
+
const found = [];
|
|
114
|
+
for (const cc of m.getDescendantsOfKind(SyntaxKind.CatchClause)) {
|
|
115
|
+
if (cc.getBlock().getStatements().length === 0) found.push({ line: cc.getStartLineNumber() });
|
|
116
|
+
}
|
|
117
|
+
return found;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// E2 unguarded risky operations — new rule, not ported from architecture-
|
|
121
|
+
// toolkit (checked: no E2/E3/E4 exist anywhere in that source or in this
|
|
122
|
+
// repo's own clean-code-analyzer/SKILL.md "not implemented" table — this is
|
|
123
|
+
// genuinely new scope, not previously deferred with a documented reason).
|
|
124
|
+
// Conservative by design: only `await` expressions and a small named list of
|
|
125
|
+
// known-throwing sync calls (JSON.parse, fs.readFileSync/writeFileSync)
|
|
126
|
+
// count as "risky" — flagging every function call as risky would swamp
|
|
127
|
+
// real findings in noise. "Guarded" means textually inside a TryStatement's
|
|
128
|
+
// TRY block specifically (not its catch/finally) — computed by walking each
|
|
129
|
+
// try block's own descendants first, so a risky op inside a catch/finally
|
|
130
|
+
// of one try but not wrapped by any try of its own still reports correctly.
|
|
131
|
+
const RISKY_SYNC_CALL_NAMES = ['JSON.parse', 'readFileSync', 'writeFileSync'];
|
|
132
|
+
function unguardedRiskyOpsOf(m) {
|
|
133
|
+
const guarded = new Set();
|
|
134
|
+
for (const tryStmt of m.getDescendantsOfKind(SyntaxKind.TryStatement)) {
|
|
135
|
+
const tryBlock = tryStmt.getTryBlock();
|
|
136
|
+
for (const node of [
|
|
137
|
+
...tryBlock.getDescendantsOfKind(SyntaxKind.AwaitExpression),
|
|
138
|
+
...tryBlock.getDescendantsOfKind(SyntaxKind.CallExpression),
|
|
139
|
+
]) {
|
|
140
|
+
guarded.add(node);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const found = [];
|
|
144
|
+
for (const awaitExpr of m.getDescendantsOfKind(SyntaxKind.AwaitExpression)) {
|
|
145
|
+
if (!guarded.has(awaitExpr)) found.push({ line: awaitExpr.getStartLineNumber(), kind: 'await' });
|
|
146
|
+
}
|
|
147
|
+
for (const call of m.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
148
|
+
if (guarded.has(call)) continue;
|
|
149
|
+
const exprText = call.getExpression().getText();
|
|
150
|
+
if (RISKY_SYNC_CALL_NAMES.some((name) => exprText === name || exprText.endsWith(`.${name}`))) {
|
|
151
|
+
found.push({ line: call.getStartLineNumber(), kind: exprText });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return found;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// G9 unreachable-code half — architecture-toolkit's actual G9 implementation
|
|
158
|
+
// (src/agents/clean-code-analyzer/tools/code-smell-validator.ts:115,
|
|
159
|
+
// `/if\s*\(\s*false\s*\)|if\s*\(\s*true\s*\)/`) is constant-conditional dead
|
|
160
|
+
// code, NOT unused-export dead code — both are legitimate readings of
|
|
161
|
+
// Clean Code's G9 "Dead Code". We ship both: this AST half plus the
|
|
162
|
+
// cross-file export-usage half in `deadExportsOf` below.
|
|
163
|
+
function deadConditionalsOf(m) {
|
|
164
|
+
const found = [];
|
|
165
|
+
for (const ifStmt of m.getDescendantsOfKind(SyntaxKind.IfStatement)) {
|
|
166
|
+
const kind = ifStmt.getExpression().getKind();
|
|
167
|
+
if (kind === SyntaxKind.TrueKeyword) found.push({ line: ifStmt.getStartLineNumber(), kind: 'if-true' });
|
|
168
|
+
else if (kind === SyntaxKind.FalseKeyword) found.push({ line: ifStmt.getStartLineNumber(), kind: 'if-false' });
|
|
169
|
+
}
|
|
170
|
+
for (const whileStmt of m.getDescendantsOfKind(SyntaxKind.WhileStatement)) {
|
|
171
|
+
if (whileStmt.getExpression().getKind() === SyntaxKind.FalseKeyword) found.push({ line: whileStmt.getStartLineNumber(), kind: 'while-false' });
|
|
172
|
+
}
|
|
173
|
+
return found;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── Refactoring facts (extract-method/class/param-object/magic-number,
|
|
177
|
+
// consolidate-duplicate-code, decompose-conditional, strategy/factory/
|
|
178
|
+
// null-object transforms) — ADDED for refactoring-scoring.mjs (2026-08-20),
|
|
179
|
+
// additive-only. Real detection logic ported from architecture-toolkit's
|
|
180
|
+
// src/agents/pattern-refactoring-guide/tools/{refactoring-analyzer,
|
|
181
|
+
// code-smell-refactoring-guide,pattern-transformation-guide}.ts (MIT,
|
|
182
|
+
// github.com/OnSightTeam/architecture-toolkit) — same discipline as the
|
|
183
|
+
// Clean Code facts above: their checks are whole-file text regexes, ours
|
|
184
|
+
// walk the real AST so context is known directly. extract-method/extract-
|
|
185
|
+
// class/introduce-parameter-object reuse statementCount/members.length/
|
|
186
|
+
// paramCount already on the contract; magic-number reuses `magicNumbers`.
|
|
187
|
+
// Only the facts below are genuinely new.
|
|
188
|
+
|
|
189
|
+
// consolidate-duplicate-code raw material — architecture-toolkit's real
|
|
190
|
+
// check (code-smell-refactoring-guide.ts:41,49) is whole-FILE line-text
|
|
191
|
+
// repetition (trimmed line length > 10 chars, repeated > 3 times, > 3 such
|
|
192
|
+
// patterns). Ours captures the same shape (normalized statement text, same
|
|
193
|
+
// length filter) per real statement NODE instead of per raw source line, so
|
|
194
|
+
// a duplicate spread across multiple physical lines by formatting still
|
|
195
|
+
// matches. Grouping/threshold logic lives in refactoring-scoring.mjs, which
|
|
196
|
+
// is the pure-function layer — this only supplies the raw per-member facts.
|
|
197
|
+
function statementTextsOf(m) {
|
|
198
|
+
const found = [];
|
|
199
|
+
for (const k of STATEMENT_KINDS) {
|
|
200
|
+
for (const node of m.getDescendantsOfKind(k)) {
|
|
201
|
+
const text = node.getText().replace(/\s+/g, ' ').trim();
|
|
202
|
+
if (text.length > 10) found.push({ text, line: node.getStartLineNumber() });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return found;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// null-object-transform raw material — architecture-toolkit's real pattern
|
|
209
|
+
// (pattern-transformation-guide.ts:158, `/if\s*\(\s*\w+\s*[!=]==\s*null/g`)
|
|
210
|
+
// is a whole-file regex counting occurrences of the text shape. AST form:
|
|
211
|
+
// an `if` whose condition contains a top-level `===`/`!==` comparison
|
|
212
|
+
// against the `null` keyword, counted once per `if` (matches the regex's
|
|
213
|
+
// per-match count for the common one-comparison-per-if case).
|
|
214
|
+
function nullChecksOf(m) {
|
|
215
|
+
const found = [];
|
|
216
|
+
for (const ifStmt of m.getDescendantsOfKind(SyntaxKind.IfStatement)) {
|
|
217
|
+
const expr = ifStmt.getExpression();
|
|
218
|
+
// Walk every BinaryExpression in the condition, not just a lone
|
|
219
|
+
// top-level one — a chained `a === null && b === null` condition's own
|
|
220
|
+
// top-level node is the `&&` BinaryExpression, so a self-or-descendants
|
|
221
|
+
// walk is required to reach the `=== null` comparisons nested inside it.
|
|
222
|
+
const bins = [expr, ...expr.getDescendantsOfKind(SyntaxKind.BinaryExpression)].filter((n) => n.getKind() === SyntaxKind.BinaryExpression);
|
|
223
|
+
for (const b of bins) {
|
|
224
|
+
const op = b.getOperatorToken().getText();
|
|
225
|
+
if (op !== '===' && op !== '!==') continue;
|
|
226
|
+
if (b.getLeft().getKind() === SyntaxKind.NullKeyword || b.getRight().getKind() === SyntaxKind.NullKeyword) {
|
|
227
|
+
found.push({ line: ifStmt.getStartLineNumber() });
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return found;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// strategy-transform / factory-transform raw material — architecture-
|
|
236
|
+
// toolkit's real patterns are whole-file regexes: switch-on-behavior
|
|
237
|
+
// (pattern-transformation-guide.ts:42, `/switch\s*\([^)]*\)\s*{[^}]*
|
|
238
|
+
// (calculate|process|validate|format)/i`) and switch-on-type-creating
|
|
239
|
+
// (`:100`, `/switch\s*\([^)]*type[^)]*\)\s*{[^}]*new\s+/i`). AST form: per
|
|
240
|
+
// real SwitchStatement node, test the same word list against the switch's
|
|
241
|
+
// own text (not the whole file), so a match is attributable to a specific
|
|
242
|
+
// switch instead of "somewhere in this file".
|
|
243
|
+
const STRATEGY_BEHAVIOR_RE = /(calculate|process|validate|format)/i;
|
|
244
|
+
function switchStatementsOf(m) {
|
|
245
|
+
const found = [];
|
|
246
|
+
for (const sw of m.getDescendantsOfKind(SyntaxKind.SwitchStatement)) {
|
|
247
|
+
const discriminantText = sw.getExpression().getText();
|
|
248
|
+
const swText = sw.getText();
|
|
249
|
+
found.push({
|
|
250
|
+
line: sw.getStartLineNumber(),
|
|
251
|
+
hasBehaviorCall: STRATEGY_BEHAVIOR_RE.test(swText),
|
|
252
|
+
hasTypeCreation: /type/i.test(discriminantText) && /\bnew\s+/.test(swText),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
return found;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// decompose-conditional raw material — architecture-toolkit's real pattern
|
|
259
|
+
// (code-smell-refactoring-guide.ts:111, `/if\s*\([^)]{50,}\)/g`) is a
|
|
260
|
+
// whole-file regex on parenthesized condition length. AST form: the real
|
|
261
|
+
// `if` condition expression's own source text length, per `if`.
|
|
262
|
+
function complexConditionalsOf(m) {
|
|
263
|
+
const found = [];
|
|
264
|
+
for (const ifStmt of m.getDescendantsOfKind(SyntaxKind.IfStatement)) {
|
|
265
|
+
const condText = ifStmt.getExpression().getText();
|
|
266
|
+
if (condText.length >= 50) found.push({ line: ifStmt.getStartLineNumber(), length: condText.length });
|
|
267
|
+
}
|
|
268
|
+
return found;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ── pattern-advisor facts (Factory Method/Builder/Singleton/Decorator/
|
|
272
|
+
// Adapter/Facade/Strategy/Observer/Command/Template Method) — ADDED for
|
|
273
|
+
// pattern-scoring.mjs (2026-08-20), additive-only. Real detection heuristics
|
|
274
|
+
// ported from architecture-toolkit's src/agents/pattern-advisor/tools/
|
|
275
|
+
// {creational,structural,behavioral}-pattern-analyzer.ts (MIT,
|
|
276
|
+
// github.com/OnSightTeam/architecture-toolkit, raw source fetched and read in
|
|
277
|
+
// full this task) — same discipline as every other fact block in this file:
|
|
278
|
+
// their checks are whole-file text regexes with no scoping to which
|
|
279
|
+
// switch/if/call the signal came from; ours walk the real AST per node.
|
|
280
|
+
//
|
|
281
|
+
// Factory Method's switch-on-type-constructs-new signal
|
|
282
|
+
// (creational-pattern-analyzer.ts:43-44, `/switch\s*\([^)]*type[^)]*\)\s*{
|
|
283
|
+
// [^}]*new\s+/i`) is the IDENTICAL regex shape already captured by
|
|
284
|
+
// `switchStatements[].hasTypeCreation` above (added for
|
|
285
|
+
// refactoring-scoring.mjs's factory-transform, itself ported from
|
|
286
|
+
// pattern-transformation-guide.ts:100 — the same regex, different toolkit
|
|
287
|
+
// tool) — pattern-scoring.mjs reuses that existing field directly rather
|
|
288
|
+
// than duplicating the switch walk. Strategy's behavior-call word list
|
|
289
|
+
// (behavioral-pattern-analyzer.ts:44) is `calculate|process|execute|
|
|
290
|
+
// validate|format` — a SUPERSET of `switchStatements[].hasBehaviorCall`'s
|
|
291
|
+
// list (`calculate|process|validate|format`, no "execute", ported from a
|
|
292
|
+
// different toolkit file, pattern-transformation-guide.ts:42) — rather than
|
|
293
|
+
// editing that shared field's regex (refactoring-scoring.mjs already
|
|
294
|
+
// consumes it), `switchBehaviorCallLine` below is a small separate fact with
|
|
295
|
+
// the exact word list this task's source citation requires.
|
|
296
|
+
const PATTERN_ADVISOR_BEHAVIOR_RE = /(calculate|process|execute|validate|format)/i;
|
|
297
|
+
function switchBehaviorCallLineOf(m) {
|
|
298
|
+
for (const sw of m.getDescendantsOfKind(SyntaxKind.SwitchStatement)) {
|
|
299
|
+
if (PATTERN_ADVISOR_BEHAVIOR_RE.test(sw.getText())) return sw.getStartLineNumber();
|
|
300
|
+
}
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Factory Method's "scattered instantiation" signal
|
|
305
|
+
// (creational-pattern-analyzer.ts:68-83): >5 total `new` calls, >3 unique
|
|
306
|
+
// constructor names. UNFILTERED (includes stdlib targets), matching the
|
|
307
|
+
// original's blind regex — deliberately NOT the same as `concreteInstantiations`
|
|
308
|
+
// (computed elsewhere in this file), which counts only project-local classes
|
|
309
|
+
// for DIP and would under-count this check.
|
|
310
|
+
function constructorNewCallTargetsOf(m) {
|
|
311
|
+
return m.getDescendantsOfKind(SyntaxKind.NewExpression).map((n) => n.getExpression().getText());
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Decorator's conditional-feature-addition signal
|
|
315
|
+
// (structural-pattern-analyzer.ts:39-43, `/if\s*\([^)]*\)\s*{[^}]*(wrap|add|
|
|
316
|
+
// extend|enhance)/i`): an if-statement whose THEN block calls a function
|
|
317
|
+
// named wrap/add/extend/enhance. AST form scopes the keyword search to the
|
|
318
|
+
// if's own consequent block, not "anywhere after an if in the file".
|
|
319
|
+
const FEATURE_CALL_RE = /(wrap|add|extend|enhance)/i;
|
|
320
|
+
function trailingCallName(callExpr) {
|
|
321
|
+
const expr = callExpr.getExpression();
|
|
322
|
+
return expr.getKind() === SyntaxKind.PropertyAccessExpression ? expr.getName() : expr.getText().replace(/^this\./, '');
|
|
323
|
+
}
|
|
324
|
+
function conditionalFeatureCallLineOf(m) {
|
|
325
|
+
for (const ifStmt of m.getDescendantsOfKind(SyntaxKind.IfStatement)) {
|
|
326
|
+
const then = ifStmt.getThenStatement();
|
|
327
|
+
if (!then) continue;
|
|
328
|
+
if (then.getDescendantsOfKind(SyntaxKind.CallExpression).some((c) => FEATURE_CALL_RE.test(trailingCallName(c)))) {
|
|
329
|
+
return ifStmt.getStartLineNumber();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Facade's complex-subsystem-interaction signal
|
|
336
|
+
// (structural-pattern-analyzer.ts:104-105, `code.match(/\w+\.\w+\.\w+\(/g)`,
|
|
337
|
+
// threshold >5): a call shaped `a.b.c(...)` — the callee is a property
|
|
338
|
+
// access whose OWN receiver is itself a property access (two dots before the
|
|
339
|
+
// paren). AST form counts real call expressions of that shape instead of a
|
|
340
|
+
// regex that also matches inside strings/comments.
|
|
341
|
+
function deepChainCallCountOf(m) {
|
|
342
|
+
let n = 0;
|
|
343
|
+
for (const c of m.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
344
|
+
const expr = c.getExpression();
|
|
345
|
+
if (expr.getKind() === SyntaxKind.PropertyAccessExpression && expr.getExpression().getKind() === SyntaxKind.PropertyAccessExpression) n++;
|
|
346
|
+
}
|
|
347
|
+
return n;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// General-purpose bare trailing call name for every call in this member —
|
|
351
|
+
// used by pattern-scoring.mjs for Adapter (convert/transform/adapt),
|
|
352
|
+
// Observer (notify/update/inform/broadcast), Command (undo/redo/history/
|
|
353
|
+
// queue/execute), and Template Method (initialize/process/cleanup) keyword
|
|
354
|
+
// scans. Distinct from the existing `calls` fact (which keeps the
|
|
355
|
+
// `this.`-stripped but otherwise full receiver-qualified text, e.g.
|
|
356
|
+
// "obj.notify") — these callers need just the trailing method/function name
|
|
357
|
+
// regardless of receiver, matching the original regexes'
|
|
358
|
+
// `\.(notify|update|...)\w*\(` shape (any receiver, bare trailing name).
|
|
359
|
+
function calleeNamesOf(m) {
|
|
360
|
+
return m.getDescendantsOfKind(SyntaxKind.CallExpression).map((c) => trailingCallName(c));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* `cls.getMethods()` alone misses constructors, getters/setters, and
|
|
365
|
+
* arrow-function property members ("class ArrowGod { greet = () => {} }") —
|
|
366
|
+
* a mainstream TS/JS style. A class scored on methods alone with none
|
|
367
|
+
* present reads as `members.length === 0`, which scores 100 at 'high'
|
|
368
|
+
* confidence on SRP/ISP/DIP: a silent perfect score on a class the scorer
|
|
369
|
+
* never actually looked inside. Reviewer-confirmed live: an ArrowGod fixture
|
|
370
|
+
* with 3 constructor-injected deps and 3 arrow methods scored 100/100.
|
|
371
|
+
*
|
|
372
|
+
* @returns {{name: string, paramsNode: object, bodyNode: object, isPublicNode: object|null}[]}
|
|
373
|
+
*/
|
|
374
|
+
function memberEntries(cls) {
|
|
375
|
+
const entries = [];
|
|
376
|
+
for (const c of cls.getConstructors()) entries.push({ name: 'constructor', paramsNode: c, bodyNode: c, isPublicNode: null });
|
|
377
|
+
for (const m of cls.getMethods()) entries.push({ name: m.getName(), paramsNode: m, bodyNode: m, isPublicNode: m });
|
|
378
|
+
for (const g of cls.getGetAccessors()) entries.push({ name: g.getName(), paramsNode: g, bodyNode: g, isPublicNode: g });
|
|
379
|
+
for (const s of cls.getSetAccessors()) entries.push({ name: s.getName(), paramsNode: s, bodyNode: s, isPublicNode: s });
|
|
380
|
+
for (const p of cls.getProperties()) {
|
|
381
|
+
const init = p.getInitializer();
|
|
382
|
+
if (init && (init.getKind() === SyntaxKind.ArrowFunction || init.getKind() === SyntaxKind.FunctionExpression)) {
|
|
383
|
+
entries.push({ name: p.getName(), paramsNode: init, bodyNode: init, isPublicNode: p });
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return entries;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function normalizedMember({ name, paramsNode, bodyNode, isPublicNode }, { baseMethods = null } = {}) {
|
|
390
|
+
const isPublic = isPublicNode
|
|
391
|
+
? !(isPublicNode.hasModifier?.(SyntaxKind.PrivateKeyword) || isPublicNode.hasModifier?.(SyntaxKind.ProtectedKeyword))
|
|
392
|
+
&& !(name ?? '').startsWith('#')
|
|
393
|
+
: false; // constructor: not counted toward ISP's public behavioral surface
|
|
394
|
+
|
|
395
|
+
const base = name && baseMethods ? baseMethods.get(name) : null;
|
|
396
|
+
const override = base ? {
|
|
397
|
+
baseParamCount: base.getParameters().length,
|
|
398
|
+
// `super(...)` is a CallExpression whose own expression IS the super
|
|
399
|
+
// keyword; `super.method()` is a CallExpression whose expression is a
|
|
400
|
+
// PropertyAccessExpression on the super keyword. The original check
|
|
401
|
+
// matched only the first form, so `super.method(...)` — the ONLY form
|
|
402
|
+
// that appears in a real method override — never matched, penalizing
|
|
403
|
+
// every correctly-written override by one third of its LSP score.
|
|
404
|
+
callsSuper: bodyNode.getDescendantsOfKind(SyntaxKind.CallExpression).some((c) => {
|
|
405
|
+
const expr = c.getExpression();
|
|
406
|
+
if (expr.getKind() === SyntaxKind.SuperKeyword) return true;
|
|
407
|
+
if (expr.getKind() === SyntaxKind.PropertyAccessExpression) {
|
|
408
|
+
return expr.getExpression().getKind() === SyntaxKind.SuperKeyword;
|
|
409
|
+
}
|
|
410
|
+
return false;
|
|
411
|
+
}),
|
|
412
|
+
returnType: bodyNode.getReturnTypeNode?.()?.getText() ?? null,
|
|
413
|
+
baseReturnType: base.getReturnTypeNode?.()?.getText() ?? null,
|
|
414
|
+
} : null;
|
|
415
|
+
|
|
416
|
+
return {
|
|
417
|
+
name: name ?? '(anonymous)',
|
|
418
|
+
paramCount: paramsNode.getParameters().length,
|
|
419
|
+
fieldAccess: fieldsOf(bodyNode),
|
|
420
|
+
calls: callsOf(bodyNode),
|
|
421
|
+
branchHits: branchHitsOf(bodyNode),
|
|
422
|
+
isPublic,
|
|
423
|
+
override,
|
|
424
|
+
statementCount: statementCountOf(bodyNode),
|
|
425
|
+
declaredNames: declaredNamesOf(paramsNode, bodyNode),
|
|
426
|
+
magicNumbers: magicNumbersOf(bodyNode),
|
|
427
|
+
emptyCatches: emptyCatchesOf(bodyNode),
|
|
428
|
+
deadConditionals: deadConditionalsOf(bodyNode),
|
|
429
|
+
unguardedRiskyOps: unguardedRiskyOpsOf(bodyNode),
|
|
430
|
+
// ADDED for refactoring-scoring.mjs (2026-08-20), additive-only:
|
|
431
|
+
statementTexts: statementTextsOf(bodyNode),
|
|
432
|
+
nullChecks: nullChecksOf(bodyNode),
|
|
433
|
+
switchStatements: switchStatementsOf(bodyNode),
|
|
434
|
+
complexConditionals: complexConditionalsOf(bodyNode),
|
|
435
|
+
// ADDED for pattern-scoring.mjs (2026-08-20), additive-only:
|
|
436
|
+
switchBehaviorCallLine: switchBehaviorCallLineOf(bodyNode),
|
|
437
|
+
constructorNewCallTargets: constructorNewCallTargetsOf(bodyNode),
|
|
438
|
+
conditionalFeatureCallLine: conditionalFeatureCallLineOf(bodyNode),
|
|
439
|
+
deepChainCallCount: deepChainCallCountOf(bodyNode),
|
|
440
|
+
calleeNames: calleeNamesOf(bodyNode),
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* @param {object|null} constructorNode the class's constructor, if it has one —
|
|
446
|
+
* a constructor-injected dependency is the actual subject of DIP and was
|
|
447
|
+
* never counted before because constructors were never visited at all.
|
|
448
|
+
*/
|
|
449
|
+
function concreteDependencyCounts(scopeNode, sourceFile, localClassNames, constructorNode = null) {
|
|
450
|
+
const newExprs = scopeNode.getDescendantsOfKind(SyntaxKind.NewExpression);
|
|
451
|
+
let concrete = 0;
|
|
452
|
+
for (const n of newExprs) {
|
|
453
|
+
const name = n.getExpression().getText();
|
|
454
|
+
if (STDLIB_WHITELIST.has(name)) continue;
|
|
455
|
+
if (localClassNames.has(name)) concrete++;
|
|
456
|
+
}
|
|
457
|
+
const imports = sourceFile.getImportDeclarations().flatMap((d) => d.getNamedImports().map((i) => i.getName()));
|
|
458
|
+
const injected = constructorNode
|
|
459
|
+
? constructorNode.getParameters().filter((p) => {
|
|
460
|
+
const t = p.getTypeNode()?.getText();
|
|
461
|
+
return t && !['string', 'number', 'boolean', 'any', 'unknown'].includes(t);
|
|
462
|
+
}).length
|
|
463
|
+
: 0;
|
|
464
|
+
return { concreteInstantiations: concrete, totalDependencies: concrete + imports.length + injected };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function unitsFromSourceFile(sourceFile) {
|
|
468
|
+
const localClassNames = new Set(
|
|
469
|
+
sourceFile.getProject().getSourceFiles().flatMap((sf) => sf.getClasses()).map((c) => c.getName()).filter(Boolean),
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
const classes = sourceFile.getClasses();
|
|
473
|
+
if (classes.length) {
|
|
474
|
+
return classes.map((cls) => {
|
|
475
|
+
const heritage = cls.getExtends();
|
|
476
|
+
const baseName = heritage?.getExpression().getText();
|
|
477
|
+
const baseDecl = baseName
|
|
478
|
+
? sourceFile.getProject().getSourceFiles().flatMap((sf) => sf.getClasses()).find((c) => c.getName() === baseName)
|
|
479
|
+
: null;
|
|
480
|
+
const baseMethods = baseDecl ? new Map(baseDecl.getMethods().map((m) => [m.getName(), m])) : null;
|
|
481
|
+
|
|
482
|
+
const members = memberEntries(cls).map((e) => normalizedMember(e, { baseMethods }));
|
|
483
|
+
const [ctor] = cls.getConstructors();
|
|
484
|
+
const dep = concreteDependencyCounts(cls, sourceFile, localClassNames, ctor ?? null);
|
|
485
|
+
// Singleton signal (creational-pattern-analyzer.ts:124-146,
|
|
486
|
+
// `/private\s+static\s+instance|getInstance\s*\(\)/i`) — ADDED for
|
|
487
|
+
// pattern-scoring.mjs (2026-08-20), additive-only. A real static
|
|
488
|
+
// property (not a text match) plus a real method named 'getInstance'.
|
|
489
|
+
const staticPropertyNames = cls.getProperties().filter((p) => p.isStatic()).map((p) => p.getName());
|
|
490
|
+
return {
|
|
491
|
+
name: cls.getName() ?? '(anonymous)', kind: 'class', members,
|
|
492
|
+
hasBaseClass: Boolean(heritage), ...dep,
|
|
493
|
+
staticPropertyNames,
|
|
494
|
+
hasGetInstanceMethod: members.some((mm) => mm.name === 'getInstance'),
|
|
495
|
+
};
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// ALL top-level functions, not just exported ones. A library module's
|
|
500
|
+
// unexported helpers are implementation detail another module can't see —
|
|
501
|
+
// filtering to exports made sense there. A SCRIPT (tools/*.mjs, a CLI) has
|
|
502
|
+
// no consumers importing it at all; its real logic routinely lives in
|
|
503
|
+
// unexported helpers plus an unexported main(). The old filter scored these
|
|
504
|
+
// as `[]` — not a low score, no measurement whatsoever — for every script
|
|
505
|
+
// in a codebase. Confirmed live: 5 of 6 files in rdc-harness's tools/
|
|
506
|
+
// scored zero units this way; only the one file with an exported function
|
|
507
|
+
// was measured at all.
|
|
508
|
+
const fns = sourceFile.getFunctions();
|
|
509
|
+
|
|
510
|
+
// Top-level `const x = (...) => {}` / `const x = function() {}` — a
|
|
511
|
+
// FunctionDeclaration query alone never sees these; same blind spot as
|
|
512
|
+
// class arrow-property methods, one level up. `tools/ladder.mjs`'s entire
|
|
513
|
+
// helper surface (`arg`, `flagPresent`, …) is written this way and scored
|
|
514
|
+
// zero units without this.
|
|
515
|
+
const arrowFns = [];
|
|
516
|
+
for (const stmt of sourceFile.getVariableStatements()) {
|
|
517
|
+
for (const decl of stmt.getDeclarations()) {
|
|
518
|
+
const init = decl.getInitializer();
|
|
519
|
+
if (init && (init.getKind() === SyntaxKind.ArrowFunction || init.getKind() === SyntaxKind.FunctionExpression)) {
|
|
520
|
+
arrowFns.push({ name: decl.getName(), node: init, exported: stmt.isExported() });
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (!fns.length && !arrowFns.length) return [];
|
|
526
|
+
const members = [
|
|
527
|
+
...fns.map((f) => normalizedMember({
|
|
528
|
+
name: f.getName() ?? '(anonymous)', paramsNode: f, bodyNode: f,
|
|
529
|
+
// ISP still means "public surface" — an exported function is genuinely
|
|
530
|
+
// public API; an unexported script helper is not, even though it's
|
|
531
|
+
// scored for SRP/OCP/LSP/DIP the same as everything else.
|
|
532
|
+
isPublicNode: f.isExported() ? f : null,
|
|
533
|
+
})),
|
|
534
|
+
...arrowFns.map(({ name, node, exported }) => normalizedMember({
|
|
535
|
+
name, paramsNode: node, bodyNode: node, isPublicNode: exported ? node : null,
|
|
536
|
+
})),
|
|
537
|
+
];
|
|
538
|
+
const dep = concreteDependencyCounts(sourceFile, sourceFile, localClassNames);
|
|
539
|
+
// ADDED for pattern-scoring.mjs (2026-08-20), additive-only — a module has
|
|
540
|
+
// no static properties; hasGetInstanceMethod still checked for a top-level
|
|
541
|
+
// `getInstance` function, the module-shaped analog of the class case above.
|
|
542
|
+
return [{
|
|
543
|
+
name: sourceFile.getBaseName(), kind: 'module', members, hasBaseClass: false, ...dep,
|
|
544
|
+
staticPropertyNames: [],
|
|
545
|
+
hasGetInstanceMethod: members.some((mm) => mm.name === 'getInstance'),
|
|
546
|
+
}];
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const projectCache = new Map();
|
|
550
|
+
// One shared project per process keeps cross-file base-class resolution (and,
|
|
551
|
+
// below, cross-file dead-export reference resolution) working without
|
|
552
|
+
// re-parsing the whole tree per file.
|
|
553
|
+
function sharedProject() {
|
|
554
|
+
if (!projectCache.has('shared')) {
|
|
555
|
+
// `allowJs: true` is REQUIRED for the type-checker to bind exports on a
|
|
556
|
+
// .mjs/.js file at all — without it `sourceFile.getExportedDeclarations()`
|
|
557
|
+
// / `.getExportSymbols()` silently return an empty result for every plain
|
|
558
|
+
// JS file (confirmed live: every export in this very package's own .mjs
|
|
559
|
+
// files scanned as zero until this option was added). Every OTHER
|
|
560
|
+
// extraction path in this file (extractUnits, importsOf) is purely
|
|
561
|
+
// syntactic — `f.isExported()`, `getFunctions()`, `getImportDeclarations()`
|
|
562
|
+
// — and never needed the checker, which is why this bug shipped invisibly
|
|
563
|
+
// until deadExportsOf (the first checker-backed feature) was added.
|
|
564
|
+
projectCache.set('shared', new Project({ skipAddingFilesFromTsConfig: true, compilerOptions: { allowJs: true } }));
|
|
565
|
+
}
|
|
566
|
+
return projectCache.get('shared');
|
|
567
|
+
}
|
|
568
|
+
function projectFor(filePath) {
|
|
569
|
+
const project = sharedProject();
|
|
570
|
+
const existing = project.getSourceFile(filePath);
|
|
571
|
+
return existing ? { project, sourceFile: existing } : { project, sourceFile: project.addSourceFileAtPath(filePath) };
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* G9 export-usage half. A REAL reference-graph walk via ts-morph's own
|
|
576
|
+
* `findReferencesAsNodes()` (language-service-backed, resolves imports/
|
|
577
|
+
* re-exports/aliases) — not a text grep, which would count a same-named
|
|
578
|
+
* local variable in an unrelated file as a "use" and miss a renamed import.
|
|
579
|
+
*
|
|
580
|
+
* `projectFilePaths` MUST include every file a real usage could live in
|
|
581
|
+
* (tests included — a symbol only called from a test is still used). A
|
|
582
|
+
* name matching zero files in that set is reported at `referenceCount: 0`,
|
|
583
|
+
* same as a name with no callers at all — the caller is responsible for
|
|
584
|
+
* having actually scanned the whole project, per the positive-control rule
|
|
585
|
+
* in clean-code-scoring.mjs's dead-code check.
|
|
586
|
+
*/
|
|
587
|
+
export function deadExportsOf(filePath, projectFilePaths = []) {
|
|
588
|
+
const project = sharedProject();
|
|
589
|
+
for (const p of new Set([filePath, ...projectFilePaths])) {
|
|
590
|
+
if (!project.getSourceFile(p)) {
|
|
591
|
+
try { project.addSourceFileAtPath(p); } catch { /* unreadable/binary — not a TS/JS file, skip */ }
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
const sourceFile = project.getSourceFile(filePath) ?? project.addSourceFileAtPath(filePath);
|
|
595
|
+
const results = [];
|
|
596
|
+
for (const [name, decls] of sourceFile.getExportedDeclarations()) {
|
|
597
|
+
const decl = decls[0];
|
|
598
|
+
if (!decl) continue;
|
|
599
|
+
const referable = typeof decl.findReferencesAsNodes === 'function' ? decl : (decl.getNameNode?.() ?? null);
|
|
600
|
+
if (!referable || typeof referable.findReferencesAsNodes !== 'function') {
|
|
601
|
+
results.push({ name, line: decl.getStartLineNumber(), referenceCount: -1, kind: decl.getKindName() });
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
let refs = [];
|
|
605
|
+
try { refs = referable.findReferencesAsNodes(); } catch { refs = []; }
|
|
606
|
+
// `findReferencesAsNodes()` includes the declaration's own name
|
|
607
|
+
// occurrence — exclude exactly that node (same file, same start
|
|
608
|
+
// position) so a symbol with genuinely zero callers reads as 0, not 1.
|
|
609
|
+
const usageRefs = refs.filter((r) => !(r.getSourceFile() === sourceFile && r.getStart() === referable.getStart()));
|
|
610
|
+
results.push({ name, line: decl.getStartLineNumber(), referenceCount: usageRefs.length, kind: decl.getKindName() });
|
|
611
|
+
}
|
|
612
|
+
return results;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* refactoring effort estimation's call-site-count / package-boundary
|
|
617
|
+
* criteria — ADDED for refactoring-scoring.mjs (2026-08-20), additive-only.
|
|
618
|
+
* Same `findReferencesAsNodes()` mechanism as `deadExportsOf` above (same
|
|
619
|
+
* shared project, same declaration-node-vs-name-node resolution, same
|
|
620
|
+
* "exclude the declaration's own occurrence" dedup) — deliberately NOT a
|
|
621
|
+
* second implementation, just targeted at one named export and returning
|
|
622
|
+
* file paths alongside the count so a caller can test package-boundary
|
|
623
|
+
* crossing without a second AST walk.
|
|
624
|
+
*/
|
|
625
|
+
export function referenceSitesOf(filePath, exportName, projectFilePaths = []) {
|
|
626
|
+
const project = sharedProject();
|
|
627
|
+
for (const p of new Set([filePath, ...projectFilePaths])) {
|
|
628
|
+
if (!project.getSourceFile(p)) {
|
|
629
|
+
try { project.addSourceFileAtPath(p); } catch { /* unreadable/binary — not a TS/JS file, skip */ }
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
const sourceFile = project.getSourceFile(filePath) ?? project.addSourceFileAtPath(filePath);
|
|
633
|
+
const decls = sourceFile.getExportedDeclarations().get(exportName);
|
|
634
|
+
const decl = decls?.[0];
|
|
635
|
+
if (!decl) return { referenceCount: -1, files: [], kind: null };
|
|
636
|
+
const referable = typeof decl.findReferencesAsNodes === 'function' ? decl : (decl.getNameNode?.() ?? null);
|
|
637
|
+
if (!referable || typeof referable.findReferencesAsNodes !== 'function') {
|
|
638
|
+
return { referenceCount: -1, files: [], kind: decl.getKindName() };
|
|
639
|
+
}
|
|
640
|
+
let refs = [];
|
|
641
|
+
try { refs = referable.findReferencesAsNodes(); } catch { refs = []; }
|
|
642
|
+
const usageRefs = refs.filter((r) => !(r.getSourceFile() === sourceFile && r.getStart() === referable.getStart()));
|
|
643
|
+
const files = [...new Set(usageRefs.map((r) => r.getSourceFile().getFilePath()))];
|
|
644
|
+
return { referenceCount: usageRefs.length, files, kind: decl.getKindName() };
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
export const typescriptPlugin = {
|
|
648
|
+
id: 'typescript',
|
|
649
|
+
canHandle: (filePath) => /\.(mjs|ts|tsx|js|cjs)$/.test(filePath),
|
|
650
|
+
extractUnits(filePath, sourceText) {
|
|
651
|
+
if (sourceText !== undefined) {
|
|
652
|
+
// Ephemeral parse (e.g. a file's content at a git ref) — its own
|
|
653
|
+
// one-shot project, never mixed into the shared cross-file project.
|
|
654
|
+
const project = new Project({ skipAddingFilesFromTsConfig: true, useInMemoryFileSystem: true });
|
|
655
|
+
const sourceFile = project.createSourceFile(filePath.replace(/^[A-Za-z]:/, ''), sourceText);
|
|
656
|
+
return unitsFromSourceFile(sourceFile);
|
|
657
|
+
}
|
|
658
|
+
const { sourceFile } = projectFor(filePath);
|
|
659
|
+
return unitsFromSourceFile(sourceFile);
|
|
660
|
+
},
|
|
661
|
+
importsOf(filePath, sourceText) {
|
|
662
|
+
const { sourceFile } = sourceText !== undefined
|
|
663
|
+
? { sourceFile: (() => {
|
|
664
|
+
const project = new Project({ skipAddingFilesFromTsConfig: true, useInMemoryFileSystem: true });
|
|
665
|
+
return project.createSourceFile(filePath.replace(/^[A-Za-z]:/, ''), sourceText);
|
|
666
|
+
})() }
|
|
667
|
+
: projectFor(filePath);
|
|
668
|
+
return sourceFile.getImportDeclarations().map((d) => d.getModuleSpecifierValue());
|
|
669
|
+
},
|
|
670
|
+
deadExportsOf,
|
|
671
|
+
referenceSitesOf, // ADDED for refactoring-scoring.mjs (2026-08-20), additive-only
|
|
672
|
+
};
|