universal-ast-mapper 1.27.0 → 2.0.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.
Files changed (55) hide show
  1. package/BLUEPRINT.md +230 -230
  2. package/CHANGELOG.md +466 -321
  3. package/README.md +878 -877
  4. package/package.json +48 -47
  5. package/scripts/install-skill.mjs +187 -187
  6. package/dist/analysis.js +0 -134
  7. package/dist/callgraph.js +0 -467
  8. package/dist/check.js +0 -112
  9. package/dist/cli.js +0 -1275
  10. package/dist/complexity.js +0 -98
  11. package/dist/config.js +0 -53
  12. package/dist/contextpack.js +0 -79
  13. package/dist/coupling.js +0 -35
  14. package/dist/crosslang.js +0 -425
  15. package/dist/diskcache.js +0 -97
  16. package/dist/explorer.js +0 -123
  17. package/dist/extractors/c.js +0 -204
  18. package/dist/extractors/common.js +0 -56
  19. package/dist/extractors/cpp.js +0 -272
  20. package/dist/extractors/csharp.js +0 -209
  21. package/dist/extractors/go.js +0 -212
  22. package/dist/extractors/java.js +0 -152
  23. package/dist/extractors/kotlin.js +0 -159
  24. package/dist/extractors/php.js +0 -208
  25. package/dist/extractors/python.js +0 -153
  26. package/dist/extractors/ruby.js +0 -146
  27. package/dist/extractors/rust.js +0 -249
  28. package/dist/extractors/swift.js +0 -192
  29. package/dist/extractors/typescript.js +0 -577
  30. package/dist/gitdiff.js +0 -178
  31. package/dist/graph-analysis.js +0 -279
  32. package/dist/graph.js +0 -165
  33. package/dist/html.js +0 -326
  34. package/dist/index.js +0 -1407
  35. package/dist/layers.js +0 -36
  36. package/dist/modulecoupling.js +0 -0
  37. package/dist/parser.js +0 -84
  38. package/dist/pool.js +0 -114
  39. package/dist/prompts.js +0 -67
  40. package/dist/registry.js +0 -87
  41. package/dist/report.js +0 -187
  42. package/dist/resolver.js +0 -222
  43. package/dist/roots.js +0 -47
  44. package/dist/search.js +0 -68
  45. package/dist/semantic.js +0 -365
  46. package/dist/sfc.js +0 -27
  47. package/dist/skeleton.js +0 -132
  48. package/dist/sourcemap.js +0 -60
  49. package/dist/testmap.js +0 -167
  50. package/dist/tsconfig.js +0 -212
  51. package/dist/typeflow.js +0 -124
  52. package/dist/types.js +0 -5
  53. package/dist/unused-params.js +0 -127
  54. package/dist/worker.js +0 -27
  55. package/dist/workspace.js +0 -330
package/dist/testmap.js DELETED
@@ -1,167 +0,0 @@
1
- /**
2
- * Test-coverage mapping — pair test files with the source files they exercise,
3
- * and surface source files no test touches.
4
- *
5
- * This is *structural* coverage (which files have tests at all), not line
6
- * coverage — no instrumentation, no test runner, works on a cold checkout.
7
- *
8
- * Two matching signals, strongest first:
9
- * 1. import — a test file imports the source file (graph edge; definitive).
10
- * 2. name — naming convention pairs them ("auth.test.ts" → "auth.ts",
11
- * "test_utils.py" → "utils.py", "FooTest.java" → "Foo.java"),
12
- * resolved to the candidate sharing the longest path prefix.
13
- */
14
- // ─── Test-file detection ───────────────────────────────────────────────────────
15
- const TEST_DIRS = new Set(["test", "tests", "__tests__", "spec", "specs", "testing", "e2e", "integration-tests"]);
16
- /** Support material, not tests and not production source: excluded from both sides. */
17
- const FIXTURE_DIRS = new Set(["fixtures", "fixture", "__fixtures__", "__mocks__", "mocks", "testdata", "snapshots", "__snapshots__"]);
18
- /** True when a rel path lives under a fixtures/mocks/testdata directory. */
19
- export function isFixtureFile(rel) {
20
- return rel.split("/").slice(0, -1).some((d) => FIXTURE_DIRS.has(d.toLowerCase()));
21
- }
22
- const TEST_BASENAME_PATTERNS = [
23
- /\.(test|spec)\.[^.]+$/i, // auth.test.ts, auth.spec.js
24
- /[_-](test|tests|spec)\.[^.]+$/i, // auth_test.go, auth-test.js, auth_spec.rb
25
- /^(test|spec)[_-]/i, // test_auth.py, spec_auth.rb
26
- /Tests?\.(java|cs|kt|kts|swift|scala)$/, // AuthTest.java, AuthTests.cs
27
- /Spec\.(java|cs|kt|kts|swift|scala)$/, // AuthSpec.kt
28
- ];
29
- /** True when a rel path (forward-slashed) looks like a test file. */
30
- export function isTestFile(rel) {
31
- const parts = rel.split("/");
32
- const base = parts[parts.length - 1];
33
- if (parts.slice(0, -1).some((d) => TEST_DIRS.has(d.toLowerCase())))
34
- return true;
35
- return TEST_BASENAME_PATTERNS.some((re) => re.test(base));
36
- }
37
- /**
38
- * Derive the source basename a test file's name points at, or null when the
39
- * name carries no convention ("smoke.mjs" in a test dir → null).
40
- * "auth.test.ts" → "auth" · "test_utils.py" → "utils" · "AuthTest.java" → "Auth"
41
- */
42
- export function testNameTarget(rel) {
43
- const base = rel.split("/").pop();
44
- const noExt = base.replace(/\.[^.]+$/, "");
45
- let t = noExt
46
- .replace(/\.(test|spec)$/i, "")
47
- .replace(/[_-](test|tests|spec|smoke)$/i, "")
48
- .replace(/\.(smoke)$/i, "")
49
- .replace(/^(test|spec)[_-]/i, "")
50
- .replace(/(Test|Tests|Spec)$/, "");
51
- if (t === noExt || t.length === 0)
52
- return null;
53
- return t;
54
- }
55
- // ─── Mapping ───────────────────────────────────────────────────────────────────
56
- function commonPrefixLen(a, b) {
57
- const pa = a.split("/");
58
- const pb = b.split("/");
59
- let i = 0;
60
- while (i < pa.length - 1 && i < pb.length - 1 && pa[i] === pb[i])
61
- i++;
62
- return i;
63
- }
64
- function baseNoExt(rel) {
65
- return rel.split("/").pop().replace(/\.[^.]+$/, "");
66
- }
67
- /**
68
- * Build the test↔source coverage map from a symbol graph
69
- * (use `buildSymbolGraph` over a directory that includes the test files).
70
- */
71
- export function mapTestCoverage(graph) {
72
- const nodeMap = new Map(graph.nodes.map((n) => [n.id, n]));
73
- const allFiles = graph.nodes.filter((n) => n.nodeType === "file");
74
- const fixtureCount = allFiles.filter((f) => isFixtureFile(f.id)).length;
75
- const files = allFiles.filter((f) => !isFixtureFile(f.id));
76
- const testFiles = files.filter((f) => isTestFile(f.id));
77
- const sourceFiles = files.filter((f) => !isTestFile(f.id));
78
- const isTest = new Set(testFiles.map((f) => f.id));
79
- const sourceByBase = new Map();
80
- for (const s of sourceFiles) {
81
- const b = baseNoExt(s.id).toLowerCase();
82
- (sourceByBase.get(b) ?? sourceByBase.set(b, []).get(b)).push(s.id);
83
- }
84
- // Signal 1: import edges test → source. Also count source-side fan-in (Ca).
85
- const links = [];
86
- const seen = new Set();
87
- const afferent = new Map(); // source ← importers (non-test)
88
- for (const e of graph.edges) {
89
- if (e.edgeType !== "imports")
90
- continue;
91
- const to = nodeMap.get(e.to);
92
- const toFile = to ? (to.nodeType === "file" ? to.id : to.file) : null;
93
- if (!toFile || e.from === toFile)
94
- continue;
95
- if (isTest.has(e.from) && !isTest.has(toFile)) {
96
- const key = e.from + "|" + toFile;
97
- if (!seen.has(key)) {
98
- seen.add(key);
99
- links.push({ test: e.from, source: toFile, via: "import" });
100
- }
101
- }
102
- else if (!isTest.has(e.from) && !isTest.has(toFile)) {
103
- (afferent.get(toFile) ?? afferent.set(toFile, new Set()).get(toFile)).add(e.from);
104
- }
105
- }
106
- // Signal 2: naming convention, for test files with no import link yet.
107
- const linkedTests = new Set(links.map((l) => l.test));
108
- for (const t of testFiles) {
109
- if (linkedTests.has(t.id))
110
- continue;
111
- // Explicit marker in the name, else (for files inside test dirs, where the
112
- // dir itself is the marker) the plain basename: test/analysis.mjs → "analysis".
113
- const target = testNameTarget(t.id) ?? baseNoExt(t.id);
114
- const candidates = sourceByBase.get(target.toLowerCase());
115
- if (!candidates || candidates.length === 0)
116
- continue;
117
- // Prefer the candidate sharing the longest directory prefix with the test.
118
- let best = [];
119
- let bestScore = -1;
120
- for (const c of candidates) {
121
- const s = commonPrefixLen(t.id, c);
122
- if (s > bestScore) {
123
- bestScore = s;
124
- best = [c];
125
- }
126
- else if (s === bestScore)
127
- best.push(c);
128
- }
129
- for (const c of best) {
130
- const key = t.id + "|" + c;
131
- if (!seen.has(key)) {
132
- seen.add(key);
133
- links.push({ test: t.id, source: c, via: "name" });
134
- }
135
- }
136
- }
137
- // Aggregate.
138
- const testsBySource = new Map();
139
- for (const l of links) {
140
- (testsBySource.get(l.source) ?? testsBySource.set(l.source, []).get(l.source)).push(l.test);
141
- }
142
- const tested = [];
143
- const untested = [];
144
- for (const s of sourceFiles) {
145
- const tests = testsBySource.get(s.id);
146
- if (tests)
147
- tested.push({ file: s.id, tests: [...new Set(tests)].sort() });
148
- else
149
- untested.push({ file: s.id, symbols: s.symbolCount, afferent: afferent.get(s.id)?.size ?? 0 });
150
- }
151
- tested.sort((a, b) => a.file.localeCompare(b.file));
152
- untested.sort((a, b) => b.afferent - a.afferent || b.symbols - a.symbols || a.file.localeCompare(b.file));
153
- const covered = new Set(links.map((l) => l.test));
154
- const orphanTests = testFiles.map((f) => f.id).filter((id) => !covered.has(id)).sort();
155
- return {
156
- sourceFiles: sourceFiles.length,
157
- testFiles: testFiles.length,
158
- fixtureFiles: fixtureCount,
159
- testedSources: tested.length,
160
- untestedSources: untested.length,
161
- coverageRatio: sourceFiles.length === 0 ? 0 : Math.round((tested.length / sourceFiles.length) * 100) / 100,
162
- links,
163
- tested,
164
- untested,
165
- orphanTests,
166
- };
167
- }
package/dist/tsconfig.js DELETED
@@ -1,212 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- const CONFIG_NAMES = ["tsconfig.json", "jsconfig.json"];
4
- /**
5
- * Tolerant JSONC parse. String-aware: comments and trailing commas are removed
6
- * with a character walk, never with regex — naive stripping corrupts configs
7
- * whose strings contain comment-like text (e.g. Next.js `"include": ["**\/*.ts"]`
8
- * pairs the `/*` inside `"@/*"` with the `*\/` inside the glob).
9
- */
10
- function parseJsonc(raw) {
11
- let out = "";
12
- let i = 0;
13
- let inStr = false;
14
- while (i < raw.length) {
15
- const c = raw[i];
16
- if (inStr) {
17
- out += c;
18
- if (c === "\\") {
19
- out += raw[i + 1] ?? "";
20
- i += 2;
21
- continue;
22
- }
23
- if (c === '"')
24
- inStr = false;
25
- i++;
26
- }
27
- else if (c === '"') {
28
- inStr = true;
29
- out += c;
30
- i++;
31
- }
32
- else if (c === "/" && raw[i + 1] === "/") {
33
- while (i < raw.length && raw[i] !== "\n")
34
- i++;
35
- }
36
- else if (c === "/" && raw[i + 1] === "*") {
37
- i += 2;
38
- while (i < raw.length && !(raw[i] === "*" && raw[i + 1] === "/"))
39
- i++;
40
- i += 2;
41
- }
42
- else if (c === ",") {
43
- // trailing comma: skip when the next non-whitespace char closes a scope
44
- let j = i + 1;
45
- while (j < raw.length && /\s/.test(raw[j]))
46
- j++;
47
- if (raw[j] === "}" || raw[j] === "]")
48
- i++; // drop the comma
49
- else {
50
- out += c;
51
- i++;
52
- }
53
- }
54
- else {
55
- out += c;
56
- i++;
57
- }
58
- }
59
- try {
60
- return JSON.parse(out);
61
- }
62
- catch {
63
- return null;
64
- }
65
- }
66
- /** Read a config file, following relative `extends` (child overrides parent). */
67
- function readConfigChain(configPath, depth = 0) {
68
- if (depth > 5)
69
- return null;
70
- let raw;
71
- try {
72
- raw = fs.readFileSync(configPath, "utf8");
73
- }
74
- catch {
75
- return null;
76
- }
77
- const json = parseJsonc(raw);
78
- if (!json || typeof json !== "object")
79
- return null;
80
- const dir = path.dirname(configPath);
81
- let baseUrl;
82
- let paths;
83
- let baseDir = dir;
84
- const ext = json.extends;
85
- if (typeof ext === "string" && ext.startsWith(".")) {
86
- let parentPath = path.resolve(dir, ext);
87
- if (!parentPath.endsWith(".json"))
88
- parentPath += ".json";
89
- const parent = readConfigChain(parentPath, depth + 1);
90
- if (parent) {
91
- baseUrl = parent.baseUrl;
92
- paths = parent.paths;
93
- baseDir = parent.dir; // paths in a parent resolve against the parent's dir
94
- }
95
- }
96
- const co = json.compilerOptions;
97
- if (co && typeof co === "object") {
98
- if (typeof co.baseUrl === "string") {
99
- baseUrl = co.baseUrl;
100
- baseDir = dir;
101
- }
102
- if (co.paths && typeof co.paths === "object") {
103
- paths = co.paths;
104
- baseDir = dir;
105
- }
106
- }
107
- return { baseUrl, paths, dir: baseDir };
108
- }
109
- function buildAliasConfig(configPath) {
110
- const merged = readConfigChain(configPath);
111
- if (!merged || !merged.paths)
112
- return null;
113
- const base = path.resolve(merged.dir, merged.baseUrl ?? ".");
114
- const patterns = [];
115
- for (const [key, targets] of Object.entries(merged.paths)) {
116
- if (!Array.isArray(targets) || targets.length === 0)
117
- continue;
118
- const star = key.indexOf("*");
119
- const abs = targets
120
- .filter((t) => typeof t === "string")
121
- .map((t) => path.resolve(base, t));
122
- if (abs.length === 0)
123
- continue;
124
- if (star === -1) {
125
- patterns.push({ prefix: key, suffix: "", exact: true, targets: abs });
126
- }
127
- else {
128
- patterns.push({ prefix: key.slice(0, star), suffix: key.slice(star + 1), exact: false, targets: abs });
129
- }
130
- }
131
- // Longest prefix wins (TypeScript's matching rule).
132
- patterns.sort((a, b) => b.prefix.length - a.prefix.length);
133
- return patterns.length > 0 ? { patterns } : null;
134
- }
135
- // dir → config path (or null when none found up the tree)
136
- const configPathCache = new Map();
137
- // config path → parsed alias config (or null when it has no paths)
138
- const aliasCache = new Map();
139
- function findNearestConfig(fromDir) {
140
- const cached = configPathCache.get(fromDir);
141
- if (cached !== undefined)
142
- return cached;
143
- let dir = fromDir;
144
- let result = null;
145
- const visited = [];
146
- for (;;) {
147
- const hit = configPathCache.get(dir);
148
- if (hit !== undefined) {
149
- result = hit;
150
- break;
151
- }
152
- visited.push(dir);
153
- let found = null;
154
- for (const name of CONFIG_NAMES) {
155
- const p = path.join(dir, name);
156
- if (fs.existsSync(p)) {
157
- found = p;
158
- break;
159
- }
160
- }
161
- if (found) {
162
- result = found;
163
- break;
164
- }
165
- const parent = path.dirname(dir);
166
- if (parent === dir || dir.includes("node_modules")) {
167
- result = null;
168
- break;
169
- }
170
- dir = parent;
171
- }
172
- for (const d of visited)
173
- configPathCache.set(d, result);
174
- return result;
175
- }
176
- /** Test-only: clear the per-process caches. */
177
- export function clearAliasCaches() {
178
- configPathCache.clear();
179
- aliasCache.clear();
180
- }
181
- /**
182
- * Map an aliased bare import to absolute candidate base paths (no extension
183
- * probing). Empty array = not an alias / no config / no pattern match.
184
- */
185
- export function aliasCandidates(importFrom, fromAbs) {
186
- if (importFrom.startsWith(".") || path.isAbsolute(importFrom))
187
- return [];
188
- const configPath = findNearestConfig(path.dirname(fromAbs));
189
- if (!configPath)
190
- return [];
191
- let cfg = aliasCache.get(configPath);
192
- if (cfg === undefined) {
193
- cfg = buildAliasConfig(configPath);
194
- aliasCache.set(configPath, cfg);
195
- }
196
- if (!cfg)
197
- return [];
198
- for (const p of cfg.patterns) {
199
- if (p.exact) {
200
- if (importFrom === p.prefix)
201
- return p.targets;
202
- continue;
203
- }
204
- if (importFrom.length >= p.prefix.length + p.suffix.length &&
205
- importFrom.startsWith(p.prefix) &&
206
- importFrom.endsWith(p.suffix)) {
207
- const star = importFrom.slice(p.prefix.length, importFrom.length - p.suffix.length);
208
- return p.targets.map((t) => t.replace("*", star));
209
- }
210
- }
211
- return [];
212
- }
package/dist/typeflow.js DELETED
@@ -1,124 +0,0 @@
1
- import fs from "node:fs";
2
- import { parseSource } from "./parser.js";
3
- import { detectLanguage } from "./registry.js";
4
- const FN_TYPES = new Set([
5
- "function_declaration", "generator_function_declaration", "function_definition",
6
- "async_function_definition", "method_definition", "method_declaration",
7
- "constructor_declaration", "function_item",
8
- ]);
9
- const ID_TYPES = new Set(["identifier", "simple_identifier"]);
10
- const TYPE_ID_TYPES = new Set(["type_identifier", "identifier"]);
11
- const PARAM_CONTAINERS = new Set([
12
- "formal_parameters", "parameters", "parameter_list", "function_value_parameters",
13
- ]);
14
- function fnName(node) {
15
- const nm = node.childForFieldName("name");
16
- if (nm)
17
- return nm.text;
18
- for (let i = 0; i < node.namedChildCount; i++) {
19
- const c = node.namedChild(i);
20
- if (c && c.type === "simple_identifier")
21
- return c.text;
22
- }
23
- return "(anonymous)";
24
- }
25
- /** Does this type-annotation subtree reference the bare type name `name`? */
26
- function typeRefsName(node, name) {
27
- if (!node)
28
- return false;
29
- let hit = false;
30
- const walk = (n) => {
31
- if (hit)
32
- return;
33
- if (TYPE_ID_TYPES.has(n.type) && n.text === name) {
34
- hit = true;
35
- return;
36
- }
37
- for (let i = 0; i < n.namedChildCount; i++) {
38
- const c = n.namedChild(i);
39
- if (c)
40
- walk(c);
41
- }
42
- };
43
- walk(node);
44
- return hit;
45
- }
46
- function paramName(p) {
47
- if (ID_TYPES.has(p.type))
48
- return p.text;
49
- const pat = p.childForFieldName("pattern");
50
- if (pat && ID_TYPES.has(pat.type))
51
- return pat.text;
52
- const nm = p.childForFieldName("name");
53
- if (nm && ID_TYPES.has(nm.type))
54
- return nm.text;
55
- return undefined;
56
- }
57
- function paramsNode(fn) {
58
- const p = fn.childForFieldName("parameters");
59
- if (p)
60
- return p;
61
- for (let i = 0; i < fn.namedChildCount; i++) {
62
- const c = fn.namedChild(i);
63
- if (c && PARAM_CONTAINERS.has(c.type))
64
- return c;
65
- }
66
- return null;
67
- }
68
- export async function traceTypeInFile(absPath, relPath, typeName) {
69
- const lang = detectLanguage(absPath);
70
- if (!lang)
71
- return [];
72
- const source = fs.readFileSync(absPath, "utf8");
73
- const root = await parseSource(lang.grammar, source);
74
- const refs = [];
75
- const walk = (node) => {
76
- if (FN_TYPES.has(node.type)) {
77
- const name = fnName(node);
78
- // return type
79
- const rt = node.childForFieldName("return_type");
80
- if (typeRefsName(rt, typeName)) {
81
- refs.push({ file: relPath, symbol: name, role: "return", line: node.startPosition.row + 1 });
82
- }
83
- // params
84
- const pnode = paramsNode(node);
85
- if (pnode) {
86
- for (let i = 0; i < pnode.namedChildCount; i++) {
87
- const p = pnode.namedChild(i);
88
- if (!p)
89
- continue;
90
- const ty = p.childForFieldName("type");
91
- if (typeRefsName(ty, typeName)) {
92
- const pn = paramName(p);
93
- refs.push({
94
- file: relPath, symbol: name, role: "param",
95
- ...(pn ? { detail: pn } : {}),
96
- line: p.startPosition.row + 1,
97
- });
98
- }
99
- }
100
- }
101
- }
102
- else if (node.type === "variable_declarator") {
103
- const ty = node.childForFieldName("type");
104
- const nm = node.childForFieldName("name");
105
- if (ty && nm && typeRefsName(ty, typeName)) {
106
- refs.push({ file: relPath, symbol: nm.text, role: "variable", line: node.startPosition.row + 1 });
107
- }
108
- }
109
- else if (node.type === "public_field_definition" || node.type === "field_definition") {
110
- const ty = node.childForFieldName("type");
111
- const nm = node.childForFieldName("name");
112
- if (ty && nm && typeRefsName(ty, typeName)) {
113
- refs.push({ file: relPath, symbol: nm.text, role: "field", line: node.startPosition.row + 1 });
114
- }
115
- }
116
- for (let i = 0; i < node.namedChildCount; i++) {
117
- const c = node.namedChild(i);
118
- if (c)
119
- walk(c);
120
- }
121
- };
122
- walk(root);
123
- return refs;
124
- }
package/dist/types.js DELETED
@@ -1,5 +0,0 @@
1
- /**
2
- * Standard Skeleton JSON schema — the shared "interlingua" that every
3
- * language extractor must produce, regardless of the source language.
4
- */
5
- export {};
@@ -1,127 +0,0 @@
1
- import fs from "node:fs";
2
- import { parseSource } from "./parser.js";
3
- import { detectLanguage } from "./registry.js";
4
- // Named function-like nodes across the supported languages. Anonymous arrows /
5
- // lambdas are intentionally skipped: they're usually callbacks where an unused
6
- // parameter is required by the caller's signature (event handlers, map indices).
7
- const FN_TYPES = new Set([
8
- "function_declaration",
9
- "generator_function_declaration",
10
- "function_definition", // Python / C / C++
11
- "async_function_definition", // Python
12
- "method_definition", // TS/JS class member
13
- "method_declaration", // Go / Java / C#
14
- "constructor_declaration", // Java / C#
15
- "function_item", // Rust
16
- ]);
17
- const PARAM_CONTAINERS = new Set([
18
- "formal_parameters", "parameters", "parameter_list", "function_value_parameters",
19
- ]);
20
- const ID_TYPES = new Set(["identifier", "simple_identifier"]);
21
- // Identifier-like nodes that count as a *usage* of a name. Includes object
22
- // shorthand (`{ foo }` references `foo`), which is ubiquitous in JS/TS.
23
- const USE_TYPES = new Set([
24
- "identifier", "simple_identifier",
25
- "shorthand_property_identifier", "shorthand_property_identifier_pattern",
26
- ]);
27
- // Binding shapes we do NOT try to resolve to a single name (avoid false positives).
28
- const SKIP_PARAM = /splat|rest|spread|object_pattern|array_pattern|tuple_pattern|object_type/;
29
- function fnName(node) {
30
- const nm = node.childForFieldName("name");
31
- if (nm)
32
- return nm.text;
33
- // Kotlin/Swift function_declaration: name is the first simple_identifier child.
34
- for (let i = 0; i < node.namedChildCount; i++) {
35
- const c = node.namedChild(i);
36
- if (c && c.type === "simple_identifier")
37
- return c.text;
38
- }
39
- return "(anonymous)";
40
- }
41
- function paramsNode(fn) {
42
- const p = fn.childForFieldName("parameters");
43
- if (p)
44
- return p;
45
- for (let i = 0; i < fn.namedChildCount; i++) {
46
- const c = fn.namedChild(i);
47
- if (c && PARAM_CONTAINERS.has(c.type))
48
- return c;
49
- }
50
- return null;
51
- }
52
- /** Best-effort binding names for one parameter node (may be empty when unsure). */
53
- function paramNames(p) {
54
- if (ID_TYPES.has(p.type))
55
- return [p.text];
56
- if (SKIP_PARAM.test(p.type))
57
- return [];
58
- const pat = p.childForFieldName("pattern");
59
- if (pat)
60
- return ID_TYPES.has(pat.type) ? [pat.text] : [];
61
- const nm = p.childForFieldName("name");
62
- if (nm && ID_TYPES.has(nm.type))
63
- return [nm.text];
64
- // Go: `a, b int` → several identifier children before the type.
65
- const ids = [];
66
- for (let i = 0; i < p.namedChildCount; i++) {
67
- const c = p.namedChild(i);
68
- if (c && c.type === "identifier")
69
- ids.push(c.text);
70
- }
71
- return ids;
72
- }
73
- /** Collect every bare identifier reference in the subtree (not member/field names). */
74
- function collectIdentifierUses(node, out) {
75
- if (USE_TYPES.has(node.type))
76
- out.add(node.text);
77
- for (let i = 0; i < node.namedChildCount; i++) {
78
- const c = node.namedChild(i);
79
- if (c)
80
- collectIdentifierUses(c, out);
81
- }
82
- }
83
- function unusedInFunction(fn) {
84
- const pnode = paramsNode(fn);
85
- const body = fn.childForFieldName("body");
86
- if (!pnode || !body)
87
- return [];
88
- const names = [];
89
- for (let i = 0; i < pnode.namedChildCount; i++) {
90
- const p = pnode.namedChild(i);
91
- if (p)
92
- names.push(...paramNames(p));
93
- }
94
- if (names.length === 0)
95
- return [];
96
- const used = new Set();
97
- collectIdentifierUses(body, used);
98
- // Skip `_`-prefixed (conventionally intentional) and `this`/`self`.
99
- return names.filter((n) => n !== "_" && !n.startsWith("_") && n !== "this" && n !== "self" && !used.has(n));
100
- }
101
- export async function findUnusedParams(absPath, relPath) {
102
- const lang = detectLanguage(absPath);
103
- if (!lang)
104
- return null;
105
- const source = fs.readFileSync(absPath, "utf8");
106
- const root = await parseSource(lang.grammar, source);
107
- const functions = [];
108
- const walk = (node) => {
109
- if (FN_TYPES.has(node.type)) {
110
- const unused = unusedInFunction(node);
111
- if (unused.length > 0) {
112
- functions.push({
113
- function: fnName(node),
114
- line: node.startPosition.row + 1,
115
- unused,
116
- });
117
- }
118
- }
119
- for (let i = 0; i < node.namedChildCount; i++) {
120
- const c = node.namedChild(i);
121
- if (c)
122
- walk(c);
123
- }
124
- };
125
- walk(root);
126
- return { file: relPath, functions };
127
- }
package/dist/worker.js DELETED
@@ -1,27 +0,0 @@
1
- // Worker-thread entry: builds skeletons (and optionally per-file complexity)
2
- // off the main thread. Spawned by pool.ts with { cacheDir } as workerData.
3
- import { parentPort, workerData } from "node:worker_threads";
4
- import { buildSkeleton } from "./skeleton.js";
5
- import { computeFileComplexity } from "./complexity.js";
6
- import { initDiskCache } from "./diskcache.js";
7
- const data = (workerData ?? {});
8
- if (data.cacheDir)
9
- initDiskCache(data.cacheDir);
10
- parentPort.on("message", (msg) => {
11
- void (async () => {
12
- try {
13
- const skel = await buildSkeleton(msg.abs, msg.rel, msg.opts);
14
- const complexity = msg.withComplexity
15
- ? await computeFileComplexity(msg.abs, msg.rel)
16
- : undefined;
17
- parentPort.postMessage({ id: msg.id, ok: true, skel, complexity });
18
- }
19
- catch (e) {
20
- parentPort.postMessage({
21
- id: msg.id,
22
- ok: false,
23
- error: e instanceof Error ? e.message : String(e),
24
- });
25
- }
26
- })();
27
- });