@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,1182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree-sitter language plugin — the multi-language-capable, in-process AST
|
|
3
|
+
* backend for the LanguagePlugin contract (see ../language-plugin.mjs).
|
|
4
|
+
*
|
|
5
|
+
* Direct operator instruction (Dave, 2026-08-20): stop depending on a
|
|
6
|
+
* third-party TS-only parser (ts-morph, scripts/lib/plugins/typescript.mjs)
|
|
7
|
+
* when the fleet already owns a standalone, in-process, multi-language
|
|
8
|
+
* tree-sitter parser — CodeFlow's own `packages/codeflow-parser/src/
|
|
9
|
+
* nativeParser.ts` in the separate `regen-root` monorepo.
|
|
10
|
+
*
|
|
11
|
+
* UPDATE (2026-08-20/21, same night): `nativeParser.ts` shipped a real
|
|
12
|
+
* `members[]`/`units[]` surface (via the new `src/memberFacts.ts`) that
|
|
13
|
+
* walks EVERY callable body — not just top-level exported functions — and
|
|
14
|
+
* computes almost exactly the per-member/per-unit facts this file used to
|
|
15
|
+
* duplicate from scratch. This plugin now VENDORS the compiled
|
|
16
|
+
* `memberFacts.js` (plus `nativeParser.js`/`grammars.js`/`xmlParser.js`, for
|
|
17
|
+
* fidelity and any future use) into `../vendor/codeflow-parser/` and calls
|
|
18
|
+
* its exported `extractMembers(rootNode, language)` DIRECTLY against this
|
|
19
|
+
* plugin's OWN tree-sitter parse of the file — not through
|
|
20
|
+
* `createNativeParser().parse()`'s async service wrapper. That wrapper's
|
|
21
|
+
* `parse()` is `async` (it exists to front an XML branch and a batch
|
|
22
|
+
* override/reference pass this plugin doesn't use) and therefore cannot be
|
|
23
|
+
* called from `extractUnits()`, which the LanguagePlugin contract
|
|
24
|
+
* (../language-plugin.mjs) requires to stay SYNCHRONOUS. `extractMembers`
|
|
25
|
+
* itself has no `await` anywhere in it — it is a plain, pure, synchronous
|
|
26
|
+
* function of `(rootNode, language)` — so calling it directly is both the
|
|
27
|
+
* correct fix for the sync/async mismatch and a smaller surface to vendor.
|
|
28
|
+
*
|
|
29
|
+
* WHAT THIS PLUGIN NOW GETS FROM THE VENDORED EXTRACTOR (native-sourced,
|
|
30
|
+
* per member, when a correlated entry is found — see `nativeMemberKey`
|
|
31
|
+
* below): `paramCount`, `fieldAccess`, `calleeNames`, `deepChainCallCount`,
|
|
32
|
+
* `constructorNewCallTargets`, `branchHits`, `statementCount`,
|
|
33
|
+
* `declaredNames` (destructured-pattern entries filtered — see
|
|
34
|
+
* `filterDeclaredNames`), `magicNumbers` (value coerced string→number). Per
|
|
35
|
+
* unit (class only — the vendored extractor has no "module" unit concept,
|
|
36
|
+
* see below): `concreteInstantiations`, `totalDependencies`,
|
|
37
|
+
* `staticPropertyNames`, `hasGetInstanceMethod`, `hasBaseClass`.
|
|
38
|
+
*
|
|
39
|
+
* WHAT STAYS LOCALLY COMPUTED, AND WHY — this is a real, disclosed residual,
|
|
40
|
+
* not a "small gap": the vendored `ParsedMember` shape reports several
|
|
41
|
+
* clean-code/refactoring facts as either a bare COUNT (`nullChecks`,
|
|
42
|
+
* `emptyCatches`, `deadConditionals` are `number`, not an array) or as
|
|
43
|
+
* LINE-LESS text (`statementTexts`, `complexConditionals` are `string[]`,
|
|
44
|
+
* not `{text/length, line}[]`). This repo's clean-code-scoring.mjs and
|
|
45
|
+
* refactoring-scoring.mjs read `.line` (and, for `deadConditionals`,
|
|
46
|
+
* `.kind`; for `complexConditionals`, `.length`) on every one of those to
|
|
47
|
+
* build a findable location — so all five stay a local per-item CST walk on
|
|
48
|
+
* the member's own `bodyNode`, same functions as before this change.
|
|
49
|
+
* `switchStatements[].hasBehaviorCall/hasTypeCreation`,
|
|
50
|
+
* `switchBehaviorCallLine`, and `conditionalFeatureCallLine` also stay
|
|
51
|
+
* local: the vendored `SwitchFact.behaviorDispatch`/`.typeConstruction` use
|
|
52
|
+
* a DIFFERENT, broader test (any call-or-return in a case; any `new` in a
|
|
53
|
+
* type-discriminant-named switch) than this repo's specific
|
|
54
|
+
* architecture-toolkit-derived word lists
|
|
55
|
+
* (calculate/process/validate/format, and separately
|
|
56
|
+
* calculate/process/execute/validate/format for the pattern-advisor's
|
|
57
|
+
* Strategy signal, and wrap/add/extend/enhance for Decorator) — reusing the
|
|
58
|
+
* vendored flags would silently change which findings fire. `calls` also
|
|
59
|
+
* stays local: this repo's contract keeps a receiver-qualified-except-`this.`
|
|
60
|
+
* form (`obj.method()` → `"obj.method"`) for solid-scoring.mjs's SRP
|
|
61
|
+
* same-component test; the vendored `calleeNames` strips EVERY receiver,
|
|
62
|
+
* which is the right fact for pattern-scoring.mjs's keyword scans but the
|
|
63
|
+
* wrong one for SRP's sibling-method-call detection. `isPublic` stays
|
|
64
|
+
* local: the vendored `ParsedMember.exported` is the OWNING CLASS's export
|
|
65
|
+
* status propagated to every member, not an accessibility check — it has no
|
|
66
|
+
* `private`/`protected`/`#`-prefix signal at all, so this plugin still reads
|
|
67
|
+
* `accessibility_modifier` off the located CST node itself. `override`
|
|
68
|
+
* (LSP base-method drift) stays local for a different reason: the vendored
|
|
69
|
+
* resolver (`resolveOverrideShapes`) is BATCH-scoped over one `parse()` call
|
|
70
|
+
* across every file handed to it at once, but this plugin's `extractUnits`
|
|
71
|
+
* is called incrementally, one file at a time — re-running a whole-project
|
|
72
|
+
* batch parse on every single-file call would be a real performance
|
|
73
|
+
* regression, so cross-file override resolution keeps using this plugin's
|
|
74
|
+
* own existing `fileCache`-backed base-class lookup (unchanged).
|
|
75
|
+
* `deadExportsOf`/`referenceSitesOf` (cross-file dead-export / reference-site
|
|
76
|
+
* scanning) are UNCHANGED and fully local for the same reason, plus SOLID
|
|
77
|
+
* itself never calls either.
|
|
78
|
+
*
|
|
79
|
+
* CORRELATION: `extractMembers` returns a FLAT `members[]` array that
|
|
80
|
+
* includes every nested closure as its own entry with `owner: null` — not
|
|
81
|
+
* just top-level/class members — because a callback passed to `.map()` is
|
|
82
|
+
* genuinely its own member with its own facts (see memberFacts.js's own
|
|
83
|
+
* header comment). This plugin still needs to know WHICH callable node is a
|
|
84
|
+
* "member" per its own NormalizedUnit contract (a class's own methods, or a
|
|
85
|
+
* file's own top-level declarations — never an inner closure folded into
|
|
86
|
+
* one of those). So this plugin keeps its OWN existing identity walk
|
|
87
|
+
* (`memberEntriesOf` for a class body, the top-level declaration scan for a
|
|
88
|
+
* module) to decide what counts as a member and to locate each one's
|
|
89
|
+
* `bodyNode` for the local residual facts above, and uses that identity
|
|
90
|
+
* walk's own `(owner, name, start_line)` to look up the matching entry in
|
|
91
|
+
* the vendored extractor's flat list (`nativeMemberKey`/
|
|
92
|
+
* `buildNativeMemberIndex`) for the native-sourced fields. A correlation
|
|
93
|
+
* miss (should not happen for a real class method or top-level declaration;
|
|
94
|
+
* kept as a safety net for a grammar edge case this plugin's own detection
|
|
95
|
+
* and memberFacts.js's disagree on) falls back to this plugin's original,
|
|
96
|
+
* fully-local computation for that one member's mappable fields, unchanged
|
|
97
|
+
* from before this pass — a miss degrades to old-but-correct, never to a
|
|
98
|
+
* silently dropped or wrong fact.
|
|
99
|
+
*
|
|
100
|
+
* nativeParser.ts's top-level CST-walking functions (`extractTsJsSymbols`,
|
|
101
|
+
* `extractCallsFromBody`, `extractTsJsImports`) were the ORIGINAL cited
|
|
102
|
+
* foundation for this file's now-superseded from-scratch member/unit
|
|
103
|
+
* assembly and remain the model for `importsOf`/`deadExportsOf`/
|
|
104
|
+
* `referenceSitesOf` below, which this plugin still computes itself.
|
|
105
|
+
*
|
|
106
|
+
* Every node-type name and field name used below (e.g. `public_field_
|
|
107
|
+
* definition`, `method_definition`'s `parameters`/`body`/`return_type`
|
|
108
|
+
* fields, `if_statement`'s `condition`/`consequence`/`alternative` fields,
|
|
109
|
+
* `else_clause` wrapping an `else if` as a nested `if_statement`, `super()`
|
|
110
|
+
* as function-field type `super` vs `super.method()` as a `member_expression`
|
|
111
|
+
* whose `object` field is `super`, single-param parenless arrow functions
|
|
112
|
+
* exposing a bare `parameter` field instead of `formal_parameters`, TS
|
|
113
|
+
* `enum_assignment` nodes) was VERIFIED empirically this session by parsing
|
|
114
|
+
* representative TypeScript source with the installed
|
|
115
|
+
* `tree-sitter-wasms@0.1.13` grammar and inspecting the resulting CST —
|
|
116
|
+
* not guessed from memory of the grammar.
|
|
117
|
+
*
|
|
118
|
+
* WASM grammar resolution mirrors `packages/codeflow-parser/src/
|
|
119
|
+
* grammars.ts`'s approach for THIS plugin's own tree-sitter Parser/Language
|
|
120
|
+
* setup below (kept separate from the vendored `grammars.js`, which backs
|
|
121
|
+
* only the unused async `createNativeParser()` wrapper): `require.resolve(
|
|
122
|
+
* 'tree-sitter-wasms/out/tree-sitter-<lang>.wasm')`, confirmed against the
|
|
123
|
+
* real installed package layout (`node_modules/tree-sitter-wasms/out/*.wasm`).
|
|
124
|
+
*
|
|
125
|
+
* Scope this pass: TypeScript, TSX, and JavaScript — parity with
|
|
126
|
+
* typescript.mjs, the only two source extensions that plugin ever handled
|
|
127
|
+
* (`.tsx` is new; ts-morph's single plugin already accepted `.tsx` files but
|
|
128
|
+
* parsed them with the same TS-family checker, so this plugin routes `.tsx`
|
|
129
|
+
* to the dedicated `tree-sitter-tsx` grammar for correctness rather than
|
|
130
|
+
* matching that shortcut). Python is explicitly NOT implemented here.
|
|
131
|
+
* nativeParser.ts has real, working Python extraction logic
|
|
132
|
+
* (`extractPythonSymbols`) that could be ported in a follow-up pass, but no
|
|
133
|
+
* scoring CLI in this repo has ever targeted Python — shipping an unproven,
|
|
134
|
+
* un-dogfooded third language in the same pass as the TS/JS swap is scope
|
|
135
|
+
* creep this task explicitly called out as optional and skippable. C/C++/C#
|
|
136
|
+
* are out of scope entirely for the same reason.
|
|
137
|
+
*/
|
|
138
|
+
|
|
139
|
+
import { extractMembers } from '../vendor/codeflow-parser/memberFacts.js';
|
|
140
|
+
|
|
141
|
+
import { createRequire } from 'node:module';
|
|
142
|
+
import { readFileSync } from 'node:fs';
|
|
143
|
+
import { basename } from 'node:path';
|
|
144
|
+
|
|
145
|
+
const require = createRequire(import.meta.url);
|
|
146
|
+
|
|
147
|
+
// ── Runtime init ────────────────────────────────────────────────────────
|
|
148
|
+
// web-tree-sitter's Parser.init() and Language.load() are inherently async
|
|
149
|
+
// (WASM instantiation). The LanguagePlugin contract's extractUnits/importsOf
|
|
150
|
+
// are synchronous (see the JSDoc in ../language-plugin.mjs). Top-level await
|
|
151
|
+
// closes that gap: ESM blocks the whole module graph on this module's own
|
|
152
|
+
// top-level await before ANY importer's code can run — so by the time
|
|
153
|
+
// solid-score.mjs's `main()` calls `plugin.extractUnits(...)`, both the
|
|
154
|
+
// runtime and both grammars below are already loaded, and every exported
|
|
155
|
+
// method is genuinely synchronous. No call site anywhere else changes.
|
|
156
|
+
|
|
157
|
+
const mod = await import('web-tree-sitter');
|
|
158
|
+
const Parser = mod.default ?? mod.Parser;
|
|
159
|
+
await Parser.init();
|
|
160
|
+
const Language = mod.Language ?? Parser.Language;
|
|
161
|
+
|
|
162
|
+
const GRAMMAR_FILES = {
|
|
163
|
+
typescript: 'tree-sitter-typescript.wasm',
|
|
164
|
+
tsx: 'tree-sitter-tsx.wasm',
|
|
165
|
+
javascript: 'tree-sitter-javascript.wasm',
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
async function loadGrammar(name) {
|
|
169
|
+
const wasmPath = require.resolve(`tree-sitter-wasms/out/${GRAMMAR_FILES[name]}`);
|
|
170
|
+
return Language.load(wasmPath);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const TS_LANG = await loadGrammar('typescript');
|
|
174
|
+
const TSX_LANG = await loadGrammar('tsx');
|
|
175
|
+
const JS_LANG = await loadGrammar('javascript');
|
|
176
|
+
|
|
177
|
+
function grammarFor(filePath) {
|
|
178
|
+
if (/\.tsx$/.test(filePath)) return TSX_LANG;
|
|
179
|
+
if (/\.(ts|mjs|cjs)$/.test(filePath)) return TS_LANG; // TS grammar syntactically supersets plain JS — same "one plugin, both languages" shape as typescript.mjs's ts-morph `allowJs`
|
|
180
|
+
return JS_LANG; // plain .js
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* memberFacts.js's `LANGUAGE_PROFILES` table (vendored) has no separate
|
|
185
|
+
* 'tsx' entry — only 'typescript'/'javascript'/'python'/'csharp'/'c'/'cpp'.
|
|
186
|
+
* A `.tsx` file's node-type vocabulary for class/method/call/etc. is the
|
|
187
|
+
* SAME TS-family grammar as `.ts` (only JSX-specific node types like
|
|
188
|
+
* `jsx_element` differ, none of which this plugin's facts touch), so
|
|
189
|
+
* passing 'tsx' through would silently fall back to the C-like default
|
|
190
|
+
* profile (whose `klass`/`method` node types are wrong for TS) rather than
|
|
191
|
+
* erroring — 'typescript' is the correct language string for both.
|
|
192
|
+
*/
|
|
193
|
+
function nativeLanguageFor(filePath) {
|
|
194
|
+
if (/\.(ts|tsx|mjs|cjs)$/.test(filePath)) return 'typescript';
|
|
195
|
+
return 'javascript';
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Correlation key between this plugin's own member-identity walk
|
|
200
|
+
* (`memberEntriesOf` / the module-level top-declaration scan) and
|
|
201
|
+
* `extractMembers`'s flat `members[]` output. `owner` is the class name (or
|
|
202
|
+
* `null` at module scope); `startLine` is the callable node's OWN start
|
|
203
|
+
* line — for a `method_definition` that's the method node itself, for an
|
|
204
|
+
* arrow-valued class field or a `const foo = () => {}` it's the
|
|
205
|
+
* `arrow_function`/`function_expression` node's start line, not the
|
|
206
|
+
* enclosing field-definition/declarator's — matching exactly what
|
|
207
|
+
* `extractMembers`'s own `visit()` uses as `node.startPosition.row + 1`
|
|
208
|
+
* (see memberFacts.js's `buildMember`). This plugin's own entries already
|
|
209
|
+
* carry that exact node as `entry.typeSourceNode` for every entry shape
|
|
210
|
+
* (method, arrow field, top-level function, top-level arrow const) — see
|
|
211
|
+
* `memberEntriesOf` and `unitsFromModuleLevel` below.
|
|
212
|
+
*/
|
|
213
|
+
function nativeMemberKey(owner, name, startLine) {
|
|
214
|
+
return `${owner ?? ''}::${name}::${startLine}`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* A get/set pair declared on the exact same source line (same owner, same
|
|
219
|
+
* name, same start line) would collide in this index — the second one wins
|
|
220
|
+
* and the first falls back to local computation via the correlation-miss
|
|
221
|
+
* path in `normalizedMember`. Real code essentially never writes get/set
|
|
222
|
+
* accessors on one line, so this is an accepted, disclosed edge case rather
|
|
223
|
+
* than a bug worth a more expensive per-kind key.
|
|
224
|
+
*/
|
|
225
|
+
function buildNativeMemberIndex(nativeMembers) {
|
|
226
|
+
const map = new Map();
|
|
227
|
+
for (const nm of nativeMembers) {
|
|
228
|
+
map.set(nativeMemberKey(nm.owner, nm.name, nm.start_line), nm);
|
|
229
|
+
}
|
|
230
|
+
return map;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const parserCache = new Map(); // grammar object -> reusable Parser instance
|
|
234
|
+
function parserFor(grammar) {
|
|
235
|
+
let p = parserCache.get(grammar);
|
|
236
|
+
if (!p) {
|
|
237
|
+
p = new Parser();
|
|
238
|
+
p.setLanguage(grammar);
|
|
239
|
+
parserCache.set(grammar, p);
|
|
240
|
+
}
|
|
241
|
+
return p;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function parseText(filePath, sourceText) {
|
|
245
|
+
return parserFor(grammarFor(filePath)).parse(sourceText);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ── File cache (mirrors typescript.mjs's `sharedProject()`) ───────────────
|
|
249
|
+
// One shared cache per process. A file is added to it the first time
|
|
250
|
+
// extractUnits/importsOf/deadExportsOf/referenceSitesOf touches it, exactly
|
|
251
|
+
// mirroring ts-morph's incremental Project: solid-score.mjs's own directory
|
|
252
|
+
// walk calls extractUnits() on every file before the boundary/dead-export
|
|
253
|
+
// checks run, so by the time cross-file resolution (base-class lookup,
|
|
254
|
+
// local-class-name set, reference counting) is needed, every file already
|
|
255
|
+
// scanned this run is present.
|
|
256
|
+
|
|
257
|
+
const fileCache = new Map(); // absolute path -> { rootNode, sourceText }
|
|
258
|
+
|
|
259
|
+
function cacheEntryFor(filePath, sourceText) {
|
|
260
|
+
if (sourceText !== undefined) {
|
|
261
|
+
// Ephemeral parse (e.g. a file's content at a git ref via `--diff`) —
|
|
262
|
+
// its own one-shot parse, never mixed into the shared cross-file cache,
|
|
263
|
+
// matching typescript.mjs's ephemeral in-memory Project branch.
|
|
264
|
+
const tree = parseText(filePath, sourceText);
|
|
265
|
+
return { rootNode: tree.rootNode, sourceText };
|
|
266
|
+
}
|
|
267
|
+
const cached = fileCache.get(filePath);
|
|
268
|
+
if (cached) return cached;
|
|
269
|
+
const text = readFileSync(filePath, 'utf8');
|
|
270
|
+
const tree = parseText(filePath, text);
|
|
271
|
+
const entry = { rootNode: tree.rootNode, sourceText: text };
|
|
272
|
+
fileCache.set(filePath, entry);
|
|
273
|
+
return entry;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function ensureCached(paths) {
|
|
277
|
+
for (const p of paths) {
|
|
278
|
+
if (fileCache.has(p)) continue;
|
|
279
|
+
try {
|
|
280
|
+
const text = readFileSync(p, 'utf8');
|
|
281
|
+
const tree = parseText(p, text);
|
|
282
|
+
fileCache.set(p, { rootNode: tree.rootNode, sourceText: text });
|
|
283
|
+
} catch {
|
|
284
|
+
// unreadable/binary/non-source file — not a TS/JS file, skip (matches
|
|
285
|
+
// typescript.mjs's identical try/catch around addSourceFileAtPath)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ── Generic CST walk helpers ────────────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
function lineOf(node) {
|
|
293
|
+
return node.startPosition.row + 1;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Depth-first, self-INCLUSIVE, ALL nesting levels (no boundary stop at
|
|
298
|
+
* nested function/class) — matches ts-morph's getDescendantsOfKind
|
|
299
|
+
* semantics for the "all nesting depths" part (the NormalizedMember JSDoc
|
|
300
|
+
* calls this out explicitly), but is deliberately SELF-inclusive where
|
|
301
|
+
* ts-morph's own getDescendantsOfKind is not.
|
|
302
|
+
*
|
|
303
|
+
* Real bug found and fixed this session: a concise-body arrow class
|
|
304
|
+
* property — `model = () => new PhaseModel({...})` (verified against
|
|
305
|
+
* `packages/phases/test/phase-model.test.mjs:21` in rdc-harness, found via
|
|
306
|
+
* a `constructorNewCallTargets` parity diff against ts-morph) — has its
|
|
307
|
+
* ENTIRE body AS the `new_expression`/`call_expression`/`member_expression`
|
|
308
|
+
* node itself (arrow_function's `body` field for a concise body is the
|
|
309
|
+
* expression directly, not a `statement_block` wrapping it; verified by
|
|
310
|
+
* parsing `x => x * 2` and inspecting the CST). A self-exclusive walk
|
|
311
|
+
* starting at that body node only visits ITS CHILDREN (the `new` call's
|
|
312
|
+
* arguments), never testing the body node's own type — so `new PhaseModel`
|
|
313
|
+
* itself was invisible to constructorNewCallTargetsOf/callsOf/fieldsOf/
|
|
314
|
+
* branchHitsOf/calleeNamesOf/deepChainCallCountOf for every concise-body
|
|
315
|
+
* arrow member. ts-morph's typescript.mjs does not hit this because
|
|
316
|
+
* ts-morph's own body/expression node distinction differs internally; the
|
|
317
|
+
* gap here is a tree-sitter-CST-specific consequence of `body` sometimes
|
|
318
|
+
* being a bare expression rather than a block. Self-inclusion is safe
|
|
319
|
+
* everywhere ELSE in this file: every other target type this file searches
|
|
320
|
+
* for (if_statement, switch_statement, catch_clause, variable_declarator,
|
|
321
|
+
* class_declaration, identifier, …) can never structurally BE the root node
|
|
322
|
+
* passed in (a `statement_block`, `class_declaration`, or `program`), so
|
|
323
|
+
* testing the root is a harmless no-op there and a real fix here.
|
|
324
|
+
*/
|
|
325
|
+
function walkSelfAndDescendants(node, visit) {
|
|
326
|
+
visit(node);
|
|
327
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
328
|
+
const c = node.namedChild(i);
|
|
329
|
+
if (c) walkSelfAndDescendants(c, visit);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function descendantsOfType(root, types) {
|
|
334
|
+
const set = types instanceof Set ? types : new Set(types);
|
|
335
|
+
const out = [];
|
|
336
|
+
walkSelfAndDescendants(root, (n) => { if (set.has(n.type)) out.push(n); });
|
|
337
|
+
return out;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function hasAnonChild(node, text) {
|
|
341
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
342
|
+
const c = node.child(i);
|
|
343
|
+
if (c && !c.isNamed && c.text === text) return true;
|
|
344
|
+
}
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function isStaticNode(node) {
|
|
349
|
+
return hasAnonChild(node, 'static');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function accessibilityOf(node) {
|
|
353
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
354
|
+
const c = node.namedChild(i);
|
|
355
|
+
if (c && c.type === 'accessibility_modifier') return c.text; // 'private' | 'protected' | 'public'
|
|
356
|
+
}
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function unwrapParen(node) {
|
|
361
|
+
return node && node.type === 'parenthesized_expression' ? node.namedChild(0) : node;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function normalizeText(t) {
|
|
365
|
+
return t.replace(/\s+/g, ' ').trim();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** `required_parameter`/`optional_parameter` wrap the binding in a `pattern` field; a parenless single-param arrow's own parameter node IS the binding. */
|
|
369
|
+
function patternOf(paramNode) {
|
|
370
|
+
if (paramNode.type === 'required_parameter' || paramNode.type === 'optional_parameter') {
|
|
371
|
+
return paramNode.childForFieldName('pattern');
|
|
372
|
+
}
|
|
373
|
+
return paramNode;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** `formal_parameters` holds 0+ params as named children; a parenless single-param arrow's `parameter` field IS the one param, not a list. */
|
|
377
|
+
function paramListOf(paramsNode) {
|
|
378
|
+
if (!paramsNode) return [];
|
|
379
|
+
if (paramsNode.type === 'formal_parameters') {
|
|
380
|
+
const out = [];
|
|
381
|
+
for (let i = 0; i < paramsNode.namedChildCount; i++) out.push(paramsNode.namedChild(i));
|
|
382
|
+
return out;
|
|
383
|
+
}
|
|
384
|
+
return [paramsNode];
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ── Clean Code / Refactoring / Pattern-advisor facts ───────────────────
|
|
388
|
+
// Same detection intent as typescript.mjs's block of the same name (real
|
|
389
|
+
// logic ported from architecture-toolkit, MIT, explicit reuse approval —
|
|
390
|
+
// see typescript.mjs's own citations for the original source); only the
|
|
391
|
+
// AST-walk mechanics differ (tree-sitter CST vs ts-morph wrapper API).
|
|
392
|
+
|
|
393
|
+
const STATEMENT_TYPES = new Set([
|
|
394
|
+
'expression_statement', 'lexical_declaration', 'variable_declaration',
|
|
395
|
+
'if_statement', 'for_statement', 'for_in_statement', 'while_statement',
|
|
396
|
+
'do_statement', 'switch_statement', 'return_statement', 'throw_statement',
|
|
397
|
+
'try_statement', 'break_statement', 'continue_statement', 'labeled_statement',
|
|
398
|
+
]);
|
|
399
|
+
// Tree-sitter's grammar unifies `for...in` and `for...of` into ONE node type
|
|
400
|
+
// (`for_in_statement`, disambiguated only by an anonymous `in`/`of` child
|
|
401
|
+
// token) — verified by parsing both forms and inspecting the CST. ts-morph
|
|
402
|
+
// keeps ForInStatement/ForOfStatement as two SyntaxKinds; STATEMENT_TYPES
|
|
403
|
+
// above counts `for_in_statement` once, which already covers both source
|
|
404
|
+
// forms — no separate entry needed or possible.
|
|
405
|
+
|
|
406
|
+
function statementCountOf(bodyNode) {
|
|
407
|
+
return descendantsOfType(bodyNode, STATEMENT_TYPES).length;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function fieldsOf(bodyNode) {
|
|
411
|
+
const out = [];
|
|
412
|
+
walkSelfAndDescendants(bodyNode, (n) => {
|
|
413
|
+
if (n.type !== 'member_expression') return;
|
|
414
|
+
const obj = n.childForFieldName('object');
|
|
415
|
+
if (obj && obj.type === 'this') {
|
|
416
|
+
const prop = n.childForFieldName('property');
|
|
417
|
+
if (prop) out.push(prop.text);
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
return out;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function callsOf(bodyNode) {
|
|
424
|
+
const out = [];
|
|
425
|
+
walkSelfAndDescendants(bodyNode, (n) => {
|
|
426
|
+
if (n.type !== 'call_expression') return;
|
|
427
|
+
const fn = n.childForFieldName('function');
|
|
428
|
+
if (fn) out.push(fn.text.replace(/^this\./, ''));
|
|
429
|
+
});
|
|
430
|
+
return out;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** `else if` is `else_clause` wrapping a nested `if_statement` as its sole child — verified by parsing an `if/else if/else` chain and inspecting the CST (NOT assumed from the plain-JS/TS spec, which some grammars implement differently). */
|
|
434
|
+
function isElseIfChain(ifStmt) {
|
|
435
|
+
const alt = ifStmt.childForFieldName('alternative');
|
|
436
|
+
if (!alt) return false;
|
|
437
|
+
if (alt.type === 'if_statement') return true;
|
|
438
|
+
if (alt.type === 'else_clause') return alt.namedChild(0)?.type === 'if_statement';
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function branchHitsOf(bodyNode) {
|
|
443
|
+
let n = 0;
|
|
444
|
+
walkSelfAndDescendants(bodyNode, (node) => {
|
|
445
|
+
if (node.type === 'switch_statement') {
|
|
446
|
+
const body = node.childForFieldName('body');
|
|
447
|
+
if (body) {
|
|
448
|
+
for (let i = 0; i < body.namedChildCount; i++) {
|
|
449
|
+
if (body.namedChild(i).type === 'switch_case') n++;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
} else if (node.type === 'binary_expression') {
|
|
453
|
+
if (node.childForFieldName('operator')?.text === 'instanceof') n++;
|
|
454
|
+
} else if (node.type === 'unary_expression') {
|
|
455
|
+
if (node.childForFieldName('operator')?.text === 'typeof') n++;
|
|
456
|
+
} else if (node.type === 'if_statement' && isElseIfChain(node)) {
|
|
457
|
+
n++;
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
return n;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function declaredNamesOf(paramsNode, bodyNode) {
|
|
464
|
+
const names = [];
|
|
465
|
+
for (const p of paramListOf(paramsNode)) {
|
|
466
|
+
const pat = patternOf(p);
|
|
467
|
+
if (pat && pat.type === 'identifier') names.push({ name: pat.text, line: lineOf(p) });
|
|
468
|
+
}
|
|
469
|
+
for (const decl of descendantsOfType(bodyNode, ['variable_declarator'])) {
|
|
470
|
+
const nameNode = decl.childForFieldName('name');
|
|
471
|
+
if (nameNode && nameNode.type === 'identifier') names.push({ name: nameNode.text, line: lineOf(decl) });
|
|
472
|
+
}
|
|
473
|
+
return names;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function magicNumbersOf(bodyNode) {
|
|
477
|
+
const found = [];
|
|
478
|
+
for (const lit of descendantsOfType(bodyNode, ['number'])) {
|
|
479
|
+
let node = lit;
|
|
480
|
+
let value = Number(lit.text);
|
|
481
|
+
const parent = lit.parent;
|
|
482
|
+
if (parent?.type === 'unary_expression' && parent.childForFieldName('operator')?.text === '-') {
|
|
483
|
+
value = -value;
|
|
484
|
+
node = parent;
|
|
485
|
+
}
|
|
486
|
+
if (value === 0 || value === 1 || value === -1) continue;
|
|
487
|
+
|
|
488
|
+
const initParent = node.parent;
|
|
489
|
+
let excluded = false;
|
|
490
|
+
if (initParent?.type === 'variable_declarator' && initParent.childForFieldName('value') === node) {
|
|
491
|
+
const declList = initParent.parent;
|
|
492
|
+
if (declList?.type === 'lexical_declaration' && declList.child(0)?.type === 'const') excluded = true;
|
|
493
|
+
}
|
|
494
|
+
// TS enum members (`enum_assignment`, e.g. `Red = 1`) — verified node
|
|
495
|
+
// type by parsing a real `enum` declaration and inspecting the CST.
|
|
496
|
+
if (initParent?.type === 'enum_assignment') excluded = true;
|
|
497
|
+
if (excluded) continue;
|
|
498
|
+
|
|
499
|
+
found.push({ value, line: lineOf(lit) });
|
|
500
|
+
}
|
|
501
|
+
return found;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function emptyCatchesOf(bodyNode) {
|
|
505
|
+
const found = [];
|
|
506
|
+
for (const cc of descendantsOfType(bodyNode, ['catch_clause'])) {
|
|
507
|
+
const block = cc.childForFieldName('body');
|
|
508
|
+
if (block && block.namedChildCount === 0) found.push({ line: lineOf(cc) });
|
|
509
|
+
}
|
|
510
|
+
return found;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function deadConditionalsOf(bodyNode) {
|
|
514
|
+
const found = [];
|
|
515
|
+
for (const ifStmt of descendantsOfType(bodyNode, ['if_statement'])) {
|
|
516
|
+
const cond = unwrapParen(ifStmt.childForFieldName('condition'));
|
|
517
|
+
if (cond?.type === 'true') found.push({ line: lineOf(ifStmt), kind: 'if-true' });
|
|
518
|
+
else if (cond?.type === 'false') found.push({ line: lineOf(ifStmt), kind: 'if-false' });
|
|
519
|
+
}
|
|
520
|
+
for (const whileStmt of descendantsOfType(bodyNode, ['while_statement'])) {
|
|
521
|
+
const cond = unwrapParen(whileStmt.childForFieldName('condition'));
|
|
522
|
+
if (cond?.type === 'false') found.push({ line: lineOf(whileStmt), kind: 'while-false' });
|
|
523
|
+
}
|
|
524
|
+
return found;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function statementTextsOf(bodyNode) {
|
|
528
|
+
const found = [];
|
|
529
|
+
for (const node of descendantsOfType(bodyNode, STATEMENT_TYPES)) {
|
|
530
|
+
const text = normalizeText(node.text);
|
|
531
|
+
if (text.length > 10) found.push({ text, line: lineOf(node) });
|
|
532
|
+
}
|
|
533
|
+
return found;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function nullChecksOf(bodyNode) {
|
|
537
|
+
const found = [];
|
|
538
|
+
for (const ifStmt of descendantsOfType(bodyNode, ['if_statement'])) {
|
|
539
|
+
const cond = ifStmt.childForFieldName('condition');
|
|
540
|
+
if (!cond) continue;
|
|
541
|
+
for (const b of descendantsOfType(cond, ['binary_expression'])) {
|
|
542
|
+
const op = b.childForFieldName('operator')?.text;
|
|
543
|
+
if (op !== '===' && op !== '!==') continue;
|
|
544
|
+
const left = b.childForFieldName('left');
|
|
545
|
+
const right = b.childForFieldName('right');
|
|
546
|
+
if (left?.type === 'null' || right?.type === 'null') { found.push({ line: lineOf(ifStmt) }); break; }
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return found;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const STRATEGY_BEHAVIOR_RE = /(calculate|process|validate|format)/i;
|
|
553
|
+
function switchStatementsOf(bodyNode) {
|
|
554
|
+
const found = [];
|
|
555
|
+
for (const sw of descendantsOfType(bodyNode, ['switch_statement'])) {
|
|
556
|
+
const discriminant = sw.childForFieldName('value');
|
|
557
|
+
const discriminantText = discriminant ? discriminant.text : '';
|
|
558
|
+
const swText = sw.text;
|
|
559
|
+
found.push({
|
|
560
|
+
line: lineOf(sw),
|
|
561
|
+
hasBehaviorCall: STRATEGY_BEHAVIOR_RE.test(swText),
|
|
562
|
+
hasTypeCreation: /type/i.test(discriminantText) && /\bnew\s+/.test(swText),
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
return found;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function complexConditionalsOf(bodyNode) {
|
|
569
|
+
const found = [];
|
|
570
|
+
for (const ifStmt of descendantsOfType(bodyNode, ['if_statement'])) {
|
|
571
|
+
const inner = unwrapParen(ifStmt.childForFieldName('condition'));
|
|
572
|
+
const condText = inner ? inner.text : '';
|
|
573
|
+
if (condText.length >= 50) found.push({ line: lineOf(ifStmt), length: condText.length });
|
|
574
|
+
}
|
|
575
|
+
return found;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const PATTERN_ADVISOR_BEHAVIOR_RE = /(calculate|process|execute|validate|format)/i;
|
|
579
|
+
function switchBehaviorCallLineOf(bodyNode) {
|
|
580
|
+
for (const sw of descendantsOfType(bodyNode, ['switch_statement'])) {
|
|
581
|
+
if (PATTERN_ADVISOR_BEHAVIOR_RE.test(sw.text)) return lineOf(sw);
|
|
582
|
+
}
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function constructorNewCallTargetsOf(bodyNode) {
|
|
587
|
+
const out = [];
|
|
588
|
+
for (const n of descendantsOfType(bodyNode, ['new_expression'])) {
|
|
589
|
+
const ctor = n.childForFieldName('constructor');
|
|
590
|
+
if (ctor) out.push(ctor.text);
|
|
591
|
+
}
|
|
592
|
+
return out;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const FEATURE_CALL_RE = /(wrap|add|extend|enhance)/i;
|
|
596
|
+
function trailingCallName(callExpr) {
|
|
597
|
+
const fn = callExpr.childForFieldName('function');
|
|
598
|
+
if (!fn) return '';
|
|
599
|
+
if (fn.type === 'member_expression') {
|
|
600
|
+
const prop = fn.childForFieldName('property');
|
|
601
|
+
return prop ? prop.text : fn.text;
|
|
602
|
+
}
|
|
603
|
+
return fn.text.replace(/^this\./, '');
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function conditionalFeatureCallLineOf(bodyNode) {
|
|
607
|
+
for (const ifStmt of descendantsOfType(bodyNode, ['if_statement'])) {
|
|
608
|
+
const then = ifStmt.childForFieldName('consequence');
|
|
609
|
+
if (!then) continue;
|
|
610
|
+
const calls = descendantsOfType(then, ['call_expression']);
|
|
611
|
+
if (calls.some((c) => FEATURE_CALL_RE.test(trailingCallName(c)))) return lineOf(ifStmt);
|
|
612
|
+
}
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function deepChainCallCountOf(bodyNode) {
|
|
617
|
+
let n = 0;
|
|
618
|
+
for (const c of descendantsOfType(bodyNode, ['call_expression'])) {
|
|
619
|
+
const fn = c.childForFieldName('function');
|
|
620
|
+
if (fn?.type === 'member_expression' && fn.childForFieldName('object')?.type === 'member_expression') n++;
|
|
621
|
+
}
|
|
622
|
+
return n;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function calleeNamesOf(bodyNode) {
|
|
626
|
+
return descendantsOfType(bodyNode, ['call_expression']).map(trailingCallName);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// ── Member/unit assembly ────────────────────────────────────────────────
|
|
630
|
+
|
|
631
|
+
const STDLIB_WHITELIST = new Set([
|
|
632
|
+
'Map', 'Set', 'WeakMap', 'WeakSet', 'Array', 'Object', 'Date', 'Error',
|
|
633
|
+
'TypeError', 'RangeError', 'RegExp', 'Promise', 'URL', 'URLSearchParams',
|
|
634
|
+
'AbortController', 'Buffer', 'Headers', 'Request', 'Response',
|
|
635
|
+
]);
|
|
636
|
+
const PRIMITIVE_TYPES = new Set(['string', 'number', 'boolean', 'any', 'unknown']);
|
|
637
|
+
|
|
638
|
+
function isPublicOf(isPublicNode, name) {
|
|
639
|
+
if (!isPublicNode) return false;
|
|
640
|
+
const access = accessibilityOf(isPublicNode);
|
|
641
|
+
if (access === 'private' || access === 'protected') return false;
|
|
642
|
+
if ((name ?? '').startsWith('#')) return false;
|
|
643
|
+
return true;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* `cls.getMethods()`-equivalent alone would miss constructors, get/set
|
|
648
|
+
* accessors, and arrow-function class properties ("class ArrowGod { greet =
|
|
649
|
+
* () => {} }") — a mainstream TS/JS style. Mirrors typescript.mjs's
|
|
650
|
+
* `memberEntries()` for the same reason: a class scored on methods alone
|
|
651
|
+
* with none present reads as `members.length === 0`, a silent perfect score
|
|
652
|
+
* on a class the scorer never actually looked inside.
|
|
653
|
+
*/
|
|
654
|
+
function memberEntriesOf(classBody) {
|
|
655
|
+
const entries = [];
|
|
656
|
+
for (let i = 0; i < classBody.namedChildCount; i++) {
|
|
657
|
+
const node = classBody.namedChild(i);
|
|
658
|
+
if (!node) continue;
|
|
659
|
+
if (node.type === 'method_definition') {
|
|
660
|
+
const nameNode = node.childForFieldName('name');
|
|
661
|
+
const name = nameNode ? nameNode.text : '(anonymous)';
|
|
662
|
+
const bodyNode = node.childForFieldName('body');
|
|
663
|
+
if (!bodyNode) continue; // abstract/overload signature — no body to analyze
|
|
664
|
+
const isCtor = name === 'constructor';
|
|
665
|
+
entries.push({
|
|
666
|
+
name,
|
|
667
|
+
paramsNode: node.childForFieldName('parameters'),
|
|
668
|
+
bodyNode,
|
|
669
|
+
isPublicNode: isCtor ? null : node, // constructor: not counted toward ISP's public behavioral surface, matching typescript.mjs
|
|
670
|
+
isStatic: isStaticNode(node),
|
|
671
|
+
typeSourceNode: node,
|
|
672
|
+
defNode: node,
|
|
673
|
+
});
|
|
674
|
+
} else if (node.type === 'public_field_definition') {
|
|
675
|
+
const valueNode = node.childForFieldName('value');
|
|
676
|
+
if (valueNode && (valueNode.type === 'arrow_function' || valueNode.type === 'function_expression' || valueNode.type === 'generator_function')) {
|
|
677
|
+
const nameNode = node.childForFieldName('name');
|
|
678
|
+
const name = nameNode ? nameNode.text : '(anonymous)';
|
|
679
|
+
entries.push({
|
|
680
|
+
name,
|
|
681
|
+
paramsNode: valueNode.childForFieldName('parameters') ?? valueNode.childForFieldName('parameter'),
|
|
682
|
+
bodyNode: valueNode.childForFieldName('body'),
|
|
683
|
+
isPublicNode: node,
|
|
684
|
+
isStatic: isStaticNode(node),
|
|
685
|
+
typeSourceNode: valueNode,
|
|
686
|
+
defNode: node,
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return entries;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function findConstructorNode(bodyNode) {
|
|
695
|
+
if (!bodyNode) return null;
|
|
696
|
+
for (let i = 0; i < bodyNode.namedChildCount; i++) {
|
|
697
|
+
const c = bodyNode.namedChild(i);
|
|
698
|
+
if (c && c.type === 'method_definition' && c.childForFieldName('name')?.text === 'constructor') return c;
|
|
699
|
+
}
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function callsSuperIn(bodyNode) {
|
|
704
|
+
return descendantsOfType(bodyNode, ['call_expression']).some((c) => {
|
|
705
|
+
const fn = c.childForFieldName('function');
|
|
706
|
+
if (!fn) return false;
|
|
707
|
+
// `super(...)` — function field IS a `super` node (verified: distinct
|
|
708
|
+
// from `super.method(...)`, whose function field is a `member_expression`
|
|
709
|
+
// with `object` field of type `super`).
|
|
710
|
+
if (fn.type === 'super') return true;
|
|
711
|
+
if (fn.type === 'member_expression') return fn.childForFieldName('object')?.type === 'super';
|
|
712
|
+
return false;
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function returnTypeTextOf(typeSourceNode) {
|
|
717
|
+
const n = typeSourceNode?.childForFieldName?.('return_type');
|
|
718
|
+
return n ? n.text.replace(/^:\s*/, '') : null;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* `declaredNames` entries whose name contains a destructuring bracket are a
|
|
723
|
+
* known wrinkle in the vendored extractor: a destructured parameter
|
|
724
|
+
* (`{ handle, target, snapshot }`) or local (`const { a, b } = x`) has no
|
|
725
|
+
* single bound identifier at the grammar's `name`/`pattern` field, so
|
|
726
|
+
* `memberFacts.js`'s `parameterName()`/declarator handling falls back to
|
|
727
|
+
* the whole pattern's normalized source text as ONE combined "name" — verified
|
|
728
|
+
* empirically this session (`{ handle, target, snapshot }` as a single
|
|
729
|
+
* `declaredNames` entry). This repo's N1/N2 naming rules (clean-code-
|
|
730
|
+
* scoring.mjs) want simple, non-destructured bindings only, so any entry
|
|
731
|
+
* shaped like a pattern is dropped rather than reported as a single
|
|
732
|
+
* cryptic/noise-word "name".
|
|
733
|
+
*/
|
|
734
|
+
function filterDeclaredNames(declaredNames) {
|
|
735
|
+
const out = [];
|
|
736
|
+
for (const d of declaredNames) {
|
|
737
|
+
if (/[{}[\]]/.test(d.name)) continue;
|
|
738
|
+
out.push({ name: d.name, line: d.line });
|
|
739
|
+
}
|
|
740
|
+
return out;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function normalizedMember(entry, baseMethodsMap, nativeMember) {
|
|
744
|
+
const { name, paramsNode, bodyNode, isPublicNode, typeSourceNode } = entry;
|
|
745
|
+
const isPublic = isPublicOf(isPublicNode, name);
|
|
746
|
+
const base = name && baseMethodsMap ? baseMethodsMap.get(name) : null;
|
|
747
|
+
let override = null;
|
|
748
|
+
if (base) {
|
|
749
|
+
const baseParamCount = paramListOf(base.childForFieldName('parameters')).length;
|
|
750
|
+
override = {
|
|
751
|
+
baseParamCount,
|
|
752
|
+
callsSuper: callsSuperIn(bodyNode),
|
|
753
|
+
returnType: returnTypeTextOf(typeSourceNode),
|
|
754
|
+
baseReturnType: returnTypeTextOf(base),
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Facts that stay local regardless of correlation — see the file header
|
|
759
|
+
// ("WHAT STAYS LOCALLY COMPUTED, AND WHY") for why each of these cannot
|
|
760
|
+
// be sourced from the vendored extractor's ParsedMember shape.
|
|
761
|
+
const localFacts = {
|
|
762
|
+
calls: callsOf(bodyNode),
|
|
763
|
+
emptyCatches: emptyCatchesOf(bodyNode),
|
|
764
|
+
deadConditionals: deadConditionalsOf(bodyNode),
|
|
765
|
+
statementTexts: statementTextsOf(bodyNode),
|
|
766
|
+
nullChecks: nullChecksOf(bodyNode),
|
|
767
|
+
switchStatements: switchStatementsOf(bodyNode),
|
|
768
|
+
complexConditionals: complexConditionalsOf(bodyNode),
|
|
769
|
+
switchBehaviorCallLine: switchBehaviorCallLineOf(bodyNode),
|
|
770
|
+
conditionalFeatureCallLine: conditionalFeatureCallLineOf(bodyNode),
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
if (nativeMember) {
|
|
774
|
+
return {
|
|
775
|
+
name: name ?? '(anonymous)',
|
|
776
|
+
paramCount: nativeMember.paramCount,
|
|
777
|
+
fieldAccess: nativeMember.fieldAccess,
|
|
778
|
+
branchHits: nativeMember.branchHits,
|
|
779
|
+
isPublic,
|
|
780
|
+
override,
|
|
781
|
+
statementCount: nativeMember.statementCount,
|
|
782
|
+
declaredNames: filterDeclaredNames(nativeMember.declaredNames),
|
|
783
|
+
magicNumbers: nativeMember.magicNumbers.map((n) => ({ value: Number(n.value), line: n.line })),
|
|
784
|
+
constructorNewCallTargets: nativeMember.constructorNewCallTargets,
|
|
785
|
+
deepChainCallCount: nativeMember.deepChainCallCount,
|
|
786
|
+
calleeNames: nativeMember.calleeNames,
|
|
787
|
+
...localFacts,
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Correlation miss — see the file header's CORRELATION section. Compute
|
|
792
|
+
// every field locally, exactly as this plugin did before the native
|
|
793
|
+
// extractor existed, so a miss degrades to old-but-correct rather than a
|
|
794
|
+
// dropped or wrong fact.
|
|
795
|
+
return {
|
|
796
|
+
name: name ?? '(anonymous)',
|
|
797
|
+
paramCount: paramListOf(paramsNode).length,
|
|
798
|
+
fieldAccess: fieldsOf(bodyNode),
|
|
799
|
+
branchHits: branchHitsOf(bodyNode),
|
|
800
|
+
isPublic,
|
|
801
|
+
override,
|
|
802
|
+
statementCount: statementCountOf(bodyNode),
|
|
803
|
+
declaredNames: declaredNamesOf(paramsNode, bodyNode),
|
|
804
|
+
magicNumbers: magicNumbersOf(bodyNode),
|
|
805
|
+
constructorNewCallTargets: constructorNewCallTargetsOf(bodyNode),
|
|
806
|
+
deepChainCallCount: deepChainCallCountOf(bodyNode),
|
|
807
|
+
calleeNames: calleeNamesOf(bodyNode),
|
|
808
|
+
...localFacts,
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function namedImportSpecifiersOf(rootNode) {
|
|
813
|
+
const out = [];
|
|
814
|
+
for (let i = 0; i < rootNode.namedChildCount; i++) {
|
|
815
|
+
const node = rootNode.namedChild(i);
|
|
816
|
+
if (!node || node.type !== 'import_statement') continue;
|
|
817
|
+
for (let j = 0; j < node.namedChildCount; j++) {
|
|
818
|
+
const clause = node.namedChild(j);
|
|
819
|
+
if (!clause || clause.type !== 'import_clause') continue;
|
|
820
|
+
for (let k = 0; k < clause.namedChildCount; k++) {
|
|
821
|
+
const cc = clause.namedChild(k);
|
|
822
|
+
if (cc && cc.type === 'named_imports') {
|
|
823
|
+
for (let m = 0; m < cc.namedChildCount; m++) {
|
|
824
|
+
const spec = cc.namedChild(m);
|
|
825
|
+
if (spec && spec.type === 'import_specifier') {
|
|
826
|
+
const nameNode = spec.childForFieldName('name');
|
|
827
|
+
if (nameNode) out.push(nameNode.text);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return out;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function concreteDependencyCounts(scopeNode, rootNode, localClassNames, constructorNode) {
|
|
838
|
+
let concrete = 0;
|
|
839
|
+
for (const n of descendantsOfType(scopeNode, ['new_expression'])) {
|
|
840
|
+
const ctor = n.childForFieldName('constructor');
|
|
841
|
+
const name = ctor ? ctor.text : '';
|
|
842
|
+
if (STDLIB_WHITELIST.has(name)) continue;
|
|
843
|
+
if (localClassNames.has(name)) concrete++;
|
|
844
|
+
}
|
|
845
|
+
const importNames = namedImportSpecifiersOf(rootNode);
|
|
846
|
+
let injected = 0;
|
|
847
|
+
if (constructorNode) {
|
|
848
|
+
for (const p of paramListOf(constructorNode.childForFieldName('parameters'))) {
|
|
849
|
+
if (p.type !== 'required_parameter' && p.type !== 'optional_parameter') continue;
|
|
850
|
+
const typeNode = p.childForFieldName('type');
|
|
851
|
+
const t = typeNode ? typeNode.text.replace(/^:\s*/, '').trim() : null;
|
|
852
|
+
if (t && !PRIMITIVE_TYPES.has(t)) injected++;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
return { concreteInstantiations: concrete, totalDependencies: concrete + importNames.length + injected };
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function getDeclarationNode(topNode) {
|
|
859
|
+
if (topNode.type === 'export_statement') {
|
|
860
|
+
for (let i = 0; i < topNode.namedChildCount; i++) {
|
|
861
|
+
const child = topNode.namedChild(i);
|
|
862
|
+
if (child && child.type !== 'comment') return child;
|
|
863
|
+
}
|
|
864
|
+
return null;
|
|
865
|
+
}
|
|
866
|
+
return topNode;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function classHeritageBaseName(cls) {
|
|
870
|
+
for (let i = 0; i < cls.namedChildCount; i++) {
|
|
871
|
+
const c = cls.namedChild(i);
|
|
872
|
+
if (c && c.type === 'class_heritage') {
|
|
873
|
+
for (let j = 0; j < c.namedChildCount; j++) {
|
|
874
|
+
const cc = c.namedChild(j);
|
|
875
|
+
if (cc && cc.type === 'extends_clause') {
|
|
876
|
+
const target = cc.namedChild(0);
|
|
877
|
+
return target ? target.text : null;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return null;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function allClassDeclarationsAcrossCache(extraRoot) {
|
|
886
|
+
const out = [];
|
|
887
|
+
const roots = new Set([...[...fileCache.values()].map((e) => e.rootNode), extraRoot]);
|
|
888
|
+
for (const root of roots) {
|
|
889
|
+
for (const cls of descendantsOfType(root, ['class_declaration'])) out.push(cls);
|
|
890
|
+
}
|
|
891
|
+
return out;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function findClassDeclByName(name, extraRoot) {
|
|
895
|
+
for (const cls of allClassDeclarationsAcrossCache(extraRoot)) {
|
|
896
|
+
if (cls.childForFieldName('name')?.text === name) return cls;
|
|
897
|
+
}
|
|
898
|
+
return null;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function unitFromClass(cls, rootNode, localClassNames, nativeUnitsByName, nativeIndex) {
|
|
902
|
+
const nameNode = cls.childForFieldName('name');
|
|
903
|
+
const bodyNode = cls.childForFieldName('body');
|
|
904
|
+
const baseName = classHeritageBaseName(cls);
|
|
905
|
+
const baseDecl = baseName ? findClassDeclByName(baseName, rootNode) : null;
|
|
906
|
+
const className = nameNode ? nameNode.text : '(anonymous)';
|
|
907
|
+
|
|
908
|
+
let baseMethodsMap = null;
|
|
909
|
+
if (baseDecl) {
|
|
910
|
+
const baseBody = baseDecl.childForFieldName('body');
|
|
911
|
+
baseMethodsMap = new Map();
|
|
912
|
+
if (baseBody) {
|
|
913
|
+
for (let i = 0; i < baseBody.namedChildCount; i++) {
|
|
914
|
+
const m = baseBody.namedChild(i);
|
|
915
|
+
if (m && m.type === 'method_definition') {
|
|
916
|
+
const mn = m.childForFieldName('name');
|
|
917
|
+
if (mn) baseMethodsMap.set(mn.text, m);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const entries = bodyNode ? memberEntriesOf(bodyNode) : [];
|
|
924
|
+
const members = entries.map((e) => {
|
|
925
|
+
const nativeStartLine = e.typeSourceNode.startPosition.row + 1;
|
|
926
|
+
const nm = nativeIndex.get(nativeMemberKey(className, e.name, nativeStartLine));
|
|
927
|
+
return normalizedMember(e, baseMethodsMap, nm);
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
const nativeUnit = nativeUnitsByName.get(className);
|
|
931
|
+
|
|
932
|
+
let dep;
|
|
933
|
+
let staticPropertyNames;
|
|
934
|
+
let hasBaseClassFlag;
|
|
935
|
+
if (nativeUnit) {
|
|
936
|
+
dep = { concreteInstantiations: nativeUnit.concreteInstantiations, totalDependencies: nativeUnit.totalDependencies };
|
|
937
|
+
staticPropertyNames = nativeUnit.staticPropertyNames;
|
|
938
|
+
hasBaseClassFlag = nativeUnit.hasBaseClass;
|
|
939
|
+
} else {
|
|
940
|
+
// Correlation miss (e.g. two classes sharing a name in one file) — fall
|
|
941
|
+
// back to this plugin's original local computation, unchanged.
|
|
942
|
+
const ctorNode = findConstructorNode(bodyNode);
|
|
943
|
+
dep = concreteDependencyCounts(cls, rootNode, localClassNames, ctorNode);
|
|
944
|
+
staticPropertyNames = [];
|
|
945
|
+
if (bodyNode) {
|
|
946
|
+
for (let i = 0; i < bodyNode.namedChildCount; i++) {
|
|
947
|
+
const c = bodyNode.namedChild(i);
|
|
948
|
+
if (c && c.type === 'public_field_definition' && isStaticNode(c)) {
|
|
949
|
+
const n = c.childForFieldName('name');
|
|
950
|
+
if (n) staticPropertyNames.push(n.text);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
hasBaseClassFlag = Boolean(baseName);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
return {
|
|
958
|
+
name: className,
|
|
959
|
+
kind: 'class',
|
|
960
|
+
members,
|
|
961
|
+
hasBaseClass: hasBaseClassFlag,
|
|
962
|
+
...dep,
|
|
963
|
+
staticPropertyNames,
|
|
964
|
+
hasGetInstanceMethod: nativeUnit ? nativeUnit.hasGetInstanceMethod : members.some((m) => m.name === 'getInstance'),
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function unitsFromModuleLevel(rootNode, localClassNames, filePath, nativeIndex) {
|
|
969
|
+
const fnDecls = [];
|
|
970
|
+
const arrowFns = [];
|
|
971
|
+
for (let i = 0; i < rootNode.namedChildCount; i++) {
|
|
972
|
+
const top = rootNode.namedChild(i);
|
|
973
|
+
const exported = top.type === 'export_statement';
|
|
974
|
+
const decl = getDeclarationNode(top);
|
|
975
|
+
if (!decl) continue;
|
|
976
|
+
if (decl.type === 'function_declaration') {
|
|
977
|
+
fnDecls.push({ decl, exported });
|
|
978
|
+
} else if (decl.type === 'lexical_declaration' || decl.type === 'variable_declaration') {
|
|
979
|
+
for (let j = 0; j < decl.namedChildCount; j++) {
|
|
980
|
+
const declarator = decl.namedChild(j);
|
|
981
|
+
if (!declarator || declarator.type !== 'variable_declarator') continue;
|
|
982
|
+
const nameNode = declarator.childForFieldName('name');
|
|
983
|
+
const valueNode = declarator.childForFieldName('value');
|
|
984
|
+
if (!nameNode || nameNode.type !== 'identifier') continue;
|
|
985
|
+
if (valueNode && (valueNode.type === 'arrow_function' || valueNode.type === 'function_expression')) {
|
|
986
|
+
arrowFns.push({ name: nameNode.text, node: valueNode, exported });
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
if (!fnDecls.length && !arrowFns.length) return [];
|
|
993
|
+
|
|
994
|
+
const members = [
|
|
995
|
+
...fnDecls.map(({ decl, exported }) => {
|
|
996
|
+
const entry = {
|
|
997
|
+
name: decl.childForFieldName('name')?.text ?? '(anonymous)',
|
|
998
|
+
paramsNode: decl.childForFieldName('parameters'),
|
|
999
|
+
bodyNode: decl.childForFieldName('body'),
|
|
1000
|
+
isPublicNode: exported ? decl : null,
|
|
1001
|
+
typeSourceNode: decl,
|
|
1002
|
+
};
|
|
1003
|
+
const nativeStartLine = decl.startPosition.row + 1;
|
|
1004
|
+
const nm = nativeIndex.get(nativeMemberKey(null, entry.name, nativeStartLine));
|
|
1005
|
+
return normalizedMember(entry, null, nm);
|
|
1006
|
+
}),
|
|
1007
|
+
...arrowFns.map(({ name, node, exported }) => {
|
|
1008
|
+
const entry = {
|
|
1009
|
+
name,
|
|
1010
|
+
paramsNode: node.childForFieldName('parameters') ?? node.childForFieldName('parameter'),
|
|
1011
|
+
bodyNode: node.childForFieldName('body'),
|
|
1012
|
+
isPublicNode: exported ? node : null,
|
|
1013
|
+
typeSourceNode: node,
|
|
1014
|
+
};
|
|
1015
|
+
const nativeStartLine = node.startPosition.row + 1;
|
|
1016
|
+
const nm = nativeIndex.get(nativeMemberKey(null, name, nativeStartLine));
|
|
1017
|
+
return normalizedMember(entry, null, nm);
|
|
1018
|
+
}),
|
|
1019
|
+
];
|
|
1020
|
+
|
|
1021
|
+
const dep = concreteDependencyCounts(rootNode, rootNode, localClassNames, null);
|
|
1022
|
+
return [{
|
|
1023
|
+
name: basename(filePath),
|
|
1024
|
+
kind: 'module',
|
|
1025
|
+
members,
|
|
1026
|
+
hasBaseClass: false,
|
|
1027
|
+
...dep,
|
|
1028
|
+
staticPropertyNames: [],
|
|
1029
|
+
hasGetInstanceMethod: members.some((m) => m.name === 'getInstance'),
|
|
1030
|
+
}];
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function unitsFromParsedFile(rootNode, filePath) {
|
|
1034
|
+
const localClassNames = new Set();
|
|
1035
|
+
for (const cls of allClassDeclarationsAcrossCache(rootNode)) {
|
|
1036
|
+
const n = cls.childForFieldName('name');
|
|
1037
|
+
if (n) localClassNames.add(n.text);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
const classDecls = [];
|
|
1041
|
+
for (let i = 0; i < rootNode.namedChildCount; i++) {
|
|
1042
|
+
const top = rootNode.namedChild(i);
|
|
1043
|
+
const decl = getDeclarationNode(top);
|
|
1044
|
+
if (decl && decl.type === 'class_declaration') classDecls.push(decl);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// One synchronous call to the vendored extractor per file. `extractMembers`
|
|
1048
|
+
// is a pure function of (rootNode, language) — see the file header — so
|
|
1049
|
+
// this is safe to call from this plugin's synchronous extractUnits(). A
|
|
1050
|
+
// throw here (grammar/extractor mismatch this plugin has not seen) must
|
|
1051
|
+
// never take down the whole scorer: every member/unit falls back to fully
|
|
1052
|
+
// local computation via an empty index, same as a correlation miss.
|
|
1053
|
+
let native;
|
|
1054
|
+
try {
|
|
1055
|
+
native = extractMembers(rootNode, nativeLanguageFor(filePath));
|
|
1056
|
+
} catch {
|
|
1057
|
+
native = { members: [], units: [] };
|
|
1058
|
+
}
|
|
1059
|
+
const nativeIndex = buildNativeMemberIndex(native.members);
|
|
1060
|
+
const nativeUnitsByName = new Map(native.units.map((u) => [u.name, u]));
|
|
1061
|
+
|
|
1062
|
+
if (classDecls.length) {
|
|
1063
|
+
return classDecls.map((cls) => unitFromClass(cls, rootNode, localClassNames, nativeUnitsByName, nativeIndex));
|
|
1064
|
+
}
|
|
1065
|
+
return unitsFromModuleLevel(rootNode, localClassNames, filePath, nativeIndex);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// ── Cross-file dead-export / reference-site scanning ──────────────────
|
|
1069
|
+
// tree-sitter has no language-service `findReferencesAsNodes()` — this is a
|
|
1070
|
+
// real identifier-text walk over every cached parsed file (declaration's
|
|
1071
|
+
// own name-node occurrence excluded), not a semantic resolver. It will
|
|
1072
|
+
// count a same-named unrelated identifier in another file as a "reference"
|
|
1073
|
+
// where ts-morph's type-aware resolver would not — a real, disclosed
|
|
1074
|
+
// precision tradeoff of the syntactic approach, not a bug. See
|
|
1075
|
+
// VALIDATOR-ARCHITECTURE.md's parity write-up for the practical impact
|
|
1076
|
+
// (SOLID does not call either of these two functions, so it does not affect
|
|
1077
|
+
// the parity numbers reported there).
|
|
1078
|
+
|
|
1079
|
+
function exportedDeclarationsOf(rootNode) {
|
|
1080
|
+
const out = [];
|
|
1081
|
+
for (let i = 0; i < rootNode.namedChildCount; i++) {
|
|
1082
|
+
const top = rootNode.namedChild(i);
|
|
1083
|
+
if (top.type !== 'export_statement') continue;
|
|
1084
|
+
const decl = getDeclarationNode(top);
|
|
1085
|
+
if (decl) {
|
|
1086
|
+
if (decl.type === 'function_declaration' || decl.type === 'class_declaration'
|
|
1087
|
+
|| decl.type === 'interface_declaration' || decl.type === 'type_alias_declaration') {
|
|
1088
|
+
const n = decl.childForFieldName('name');
|
|
1089
|
+
if (n) out.push({ name: n.text, line: lineOf(decl), kind: decl.type, nameNode: n });
|
|
1090
|
+
} else if (decl.type === 'lexical_declaration' || decl.type === 'variable_declaration') {
|
|
1091
|
+
for (const d of descendantsOfType(decl, ['variable_declarator'])) {
|
|
1092
|
+
const n = d.childForFieldName('name');
|
|
1093
|
+
if (n && n.type === 'identifier') out.push({ name: n.text, line: lineOf(d), kind: 'variable_declarator', nameNode: n });
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
// `export { a, b }` named-export-list form
|
|
1098
|
+
for (let j = 0; j < top.namedChildCount; j++) {
|
|
1099
|
+
const c = top.namedChild(j);
|
|
1100
|
+
if (c.type === 'export_clause') {
|
|
1101
|
+
for (let k = 0; k < c.namedChildCount; k++) {
|
|
1102
|
+
const spec = c.namedChild(k);
|
|
1103
|
+
if (spec.type === 'export_specifier') {
|
|
1104
|
+
const n = spec.childForFieldName('name');
|
|
1105
|
+
if (n) out.push({ name: n.text, line: lineOf(spec), kind: 'export_specifier', nameNode: n });
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
return out;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function countReferencesAcrossFiles(name, declNode, declFilePath) {
|
|
1115
|
+
let count = 0;
|
|
1116
|
+
const files = [];
|
|
1117
|
+
for (const [path, entry] of fileCache.entries()) {
|
|
1118
|
+
walkSelfAndDescendants(entry.rootNode, (n) => {
|
|
1119
|
+
if (n.type !== 'identifier' && n.type !== 'type_identifier') return;
|
|
1120
|
+
if (n.text !== name) return;
|
|
1121
|
+
if (path === declFilePath && n.startIndex === declNode.startIndex) return; // exclude the declaration's own name occurrence
|
|
1122
|
+
count++;
|
|
1123
|
+
if (!files.includes(path)) files.push(path);
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
return { count, files };
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
/**
|
|
1130
|
+
* G9 export-usage half — cross-file identifier scan (see comment block
|
|
1131
|
+
* above). `projectFilePaths` MUST include every file a real usage could
|
|
1132
|
+
* live in; a name matching zero files in that set reports `referenceCount:
|
|
1133
|
+
* 0`, same as a name with no callers at all.
|
|
1134
|
+
*/
|
|
1135
|
+
export function deadExportsOf(filePath, projectFilePaths = []) {
|
|
1136
|
+
ensureCached(new Set([filePath, ...projectFilePaths]));
|
|
1137
|
+
const entry = fileCache.get(filePath);
|
|
1138
|
+
if (!entry) return [];
|
|
1139
|
+
return exportedDeclarationsOf(entry.rootNode).map(({ name, line, kind, nameNode }) => {
|
|
1140
|
+
const { count } = countReferencesAcrossFiles(name, nameNode, filePath);
|
|
1141
|
+
return { name, line, referenceCount: count, kind };
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* refactoring effort estimation's call-site-count / package-boundary
|
|
1147
|
+
* criteria — same cross-file identifier scan as deadExportsOf above,
|
|
1148
|
+
* targeted at ONE named export, also returning the deduplicated file list.
|
|
1149
|
+
*/
|
|
1150
|
+
export function referenceSitesOf(filePath, exportName, projectFilePaths = []) {
|
|
1151
|
+
ensureCached(new Set([filePath, ...projectFilePaths]));
|
|
1152
|
+
const entry = fileCache.get(filePath);
|
|
1153
|
+
if (!entry) return { referenceCount: -1, files: [], kind: null };
|
|
1154
|
+
const exported = exportedDeclarationsOf(entry.rootNode).find((e) => e.name === exportName);
|
|
1155
|
+
if (!exported) return { referenceCount: -1, files: [], kind: null };
|
|
1156
|
+
const { count, files } = countReferencesAcrossFiles(exportName, exported.nameNode, filePath);
|
|
1157
|
+
return { referenceCount: count, files, kind: exported.kind };
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// ── Plugin surface ──────────────────────────────────────────────────────
|
|
1161
|
+
|
|
1162
|
+
export const treesitterPlugin = {
|
|
1163
|
+
id: 'tree-sitter',
|
|
1164
|
+
canHandle: (filePath) => /\.(mjs|ts|tsx|js|cjs)$/.test(filePath),
|
|
1165
|
+
extractUnits(filePath, sourceText) {
|
|
1166
|
+
const entry = cacheEntryFor(filePath, sourceText);
|
|
1167
|
+
return unitsFromParsedFile(entry.rootNode, filePath);
|
|
1168
|
+
},
|
|
1169
|
+
importsOf(filePath, sourceText) {
|
|
1170
|
+
const entry = cacheEntryFor(filePath, sourceText);
|
|
1171
|
+
const out = [];
|
|
1172
|
+
for (let i = 0; i < entry.rootNode.namedChildCount; i++) {
|
|
1173
|
+
const node = entry.rootNode.namedChild(i);
|
|
1174
|
+
if (node.type !== 'import_statement') continue;
|
|
1175
|
+
const source = node.childForFieldName('source');
|
|
1176
|
+
if (source) out.push(source.text.replace(/^['"]|['"]$/g, ''));
|
|
1177
|
+
}
|
|
1178
|
+
return out;
|
|
1179
|
+
},
|
|
1180
|
+
deadExportsOf,
|
|
1181
|
+
referenceSitesOf,
|
|
1182
|
+
};
|