universal-ast-mapper 2.0.0 → 2.0.1
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/CHANGELOG.md +9 -0
- package/README.md +261 -12
- package/dist/ai-refactor.js +185 -0
- package/dist/ai-testgen.js +105 -0
- package/dist/analysis.js +134 -0
- package/dist/arch-rules.js +82 -0
- package/dist/callgraph.js +467 -0
- package/dist/check.js +112 -0
- package/dist/cli.js +2284 -0
- package/dist/complexity.js +98 -0
- package/dist/config.js +53 -0
- package/dist/contextpack.js +79 -0
- package/dist/coupling.js +35 -0
- package/dist/covmerge.js +176 -0
- package/dist/crosslang.js +425 -0
- package/dist/dashboard.js +259 -0
- package/dist/diagram.js +264 -0
- package/dist/diskcache.js +97 -0
- package/dist/docgen.js +156 -0
- package/dist/embeddings.js +136 -0
- package/dist/explain.js +123 -0
- package/dist/explorer.js +123 -0
- package/dist/extractors/c.js +204 -0
- package/dist/extractors/common.js +56 -0
- package/dist/extractors/cpp.js +272 -0
- package/dist/extractors/csharp.js +209 -0
- package/dist/extractors/go.js +212 -0
- package/dist/extractors/java.js +152 -0
- package/dist/extractors/kotlin.js +159 -0
- package/dist/extractors/php.js +208 -0
- package/dist/extractors/python.js +153 -0
- package/dist/extractors/ruby.js +146 -0
- package/dist/extractors/rust.js +249 -0
- package/dist/extractors/swift.js +192 -0
- package/dist/extractors/typescript.js +577 -0
- package/dist/fix.js +92 -0
- package/dist/gitdiff.js +178 -0
- package/dist/graph-analysis.js +279 -0
- package/dist/graph.js +165 -0
- package/dist/history.js +36 -0
- package/dist/html.js +658 -0
- package/dist/incremental.js +122 -0
- package/dist/index.js +1945 -0
- package/dist/indexstore.js +105 -0
- package/dist/layers.js +36 -0
- package/dist/lsp.js +238 -0
- package/dist/modulecoupling.js +0 -0
- package/dist/parser.js +84 -0
- package/dist/patch.js +199 -0
- package/dist/plugins.js +88 -0
- package/dist/pool.js +114 -0
- package/dist/prompts.js +67 -0
- package/dist/registry.js +87 -0
- package/dist/report.js +441 -0
- package/dist/resolver.js +222 -0
- package/dist/roots.js +47 -0
- package/dist/search.js +68 -0
- package/dist/security.js +178 -0
- package/dist/semantic.js +365 -0
- package/dist/serve.js +185 -0
- package/dist/sfc.js +27 -0
- package/dist/similar.js +98 -0
- package/dist/skeleton.js +132 -0
- package/dist/smells.js +285 -0
- package/dist/sourcemap.js +60 -0
- package/dist/testgen.js +280 -0
- package/dist/testmap.js +167 -0
- package/dist/tsconfig.js +212 -0
- package/dist/typeflow.js +124 -0
- package/dist/types.js +5 -0
- package/dist/unused-params.js +127 -0
- package/dist/webapp.js +341 -0
- package/dist/worker.js +27 -0
- package/dist/workspace.js +330 -0
- package/package.json +2 -1
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
import { namedChildren, nameOf, headerSignature, leadingComment } from "../parser.js";
|
|
2
|
+
import { makeSymbol } from "./common.js";
|
|
3
|
+
/**
|
|
4
|
+
* Extract "use client" / "use server" directives from the top of a TS/TSX/JS file.
|
|
5
|
+
* Directives are string-literal expression statements that appear before any other code.
|
|
6
|
+
*/
|
|
7
|
+
export function extractDirectivesTS(root, _source) {
|
|
8
|
+
const directives = [];
|
|
9
|
+
for (const child of namedChildren(root)) {
|
|
10
|
+
if (child.type !== "expression_statement")
|
|
11
|
+
break;
|
|
12
|
+
const expr = child.namedChild(0);
|
|
13
|
+
if (!expr || expr.type !== "string")
|
|
14
|
+
break;
|
|
15
|
+
const val = expr.text.replace(/^['"`]|['"`]$/g, "");
|
|
16
|
+
if (val === "use client" || val === "use server") {
|
|
17
|
+
directives.push(val);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return directives;
|
|
24
|
+
}
|
|
25
|
+
export function extractTypeScript(root, _source) {
|
|
26
|
+
const typeIndex = buildTypeIndex(root);
|
|
27
|
+
return collect(namedChildren(root), false, typeIndex);
|
|
28
|
+
}
|
|
29
|
+
function collect(nodes, exported, typeIndex) {
|
|
30
|
+
const out = [];
|
|
31
|
+
for (const n of nodes) {
|
|
32
|
+
const res = handle(n, exported, typeIndex);
|
|
33
|
+
if (Array.isArray(res))
|
|
34
|
+
out.push(...res);
|
|
35
|
+
else if (res)
|
|
36
|
+
out.push(res);
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
function handle(node, exported, typeIndex) {
|
|
41
|
+
switch (node.type) {
|
|
42
|
+
case "export_statement":
|
|
43
|
+
// `export <decl>` / `export default <decl>` — mark the inner declarations exported.
|
|
44
|
+
return collect(namedChildren(node), true, typeIndex);
|
|
45
|
+
case "class_declaration":
|
|
46
|
+
case "abstract_class_declaration": {
|
|
47
|
+
const name = nameOf(node) ?? "(anonymous class)";
|
|
48
|
+
const body = node.childForFieldName("body");
|
|
49
|
+
const children = body ? collect(namedChildren(body), false, typeIndex) : [];
|
|
50
|
+
const clsSym = makeSymbol({
|
|
51
|
+
name,
|
|
52
|
+
kind: "class",
|
|
53
|
+
node,
|
|
54
|
+
rawKind: node.type,
|
|
55
|
+
exported,
|
|
56
|
+
doc: leadingComment(node),
|
|
57
|
+
children,
|
|
58
|
+
});
|
|
59
|
+
attachDecorators(clsSym, node);
|
|
60
|
+
return clsSym;
|
|
61
|
+
}
|
|
62
|
+
case "interface_declaration": {
|
|
63
|
+
const name = nameOf(node) ?? "(anonymous interface)";
|
|
64
|
+
const body = node.childForFieldName("body");
|
|
65
|
+
const children = body ? collect(namedChildren(body), false, typeIndex) : [];
|
|
66
|
+
return makeSymbol({
|
|
67
|
+
name,
|
|
68
|
+
kind: "interface",
|
|
69
|
+
node,
|
|
70
|
+
rawKind: node.type,
|
|
71
|
+
exported,
|
|
72
|
+
doc: leadingComment(node),
|
|
73
|
+
children,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
case "function_declaration":
|
|
77
|
+
case "generator_function_declaration": {
|
|
78
|
+
const name = nameOf(node) ?? "(anonymous function)";
|
|
79
|
+
const body = node.childForFieldName("body");
|
|
80
|
+
const fnSym = makeSymbol({
|
|
81
|
+
name,
|
|
82
|
+
kind: "function",
|
|
83
|
+
node,
|
|
84
|
+
rawKind: node.type,
|
|
85
|
+
signature: headerSignature(node, body),
|
|
86
|
+
exported,
|
|
87
|
+
doc: leadingComment(node),
|
|
88
|
+
});
|
|
89
|
+
attachComponentInfo(fnSym, node, null, name, typeIndex);
|
|
90
|
+
return fnSym;
|
|
91
|
+
}
|
|
92
|
+
case "type_alias_declaration":
|
|
93
|
+
return makeSymbol({
|
|
94
|
+
name: nameOf(node) ?? "(type)",
|
|
95
|
+
kind: "type",
|
|
96
|
+
node,
|
|
97
|
+
rawKind: node.type,
|
|
98
|
+
signature: headerSignature(node, null),
|
|
99
|
+
exported,
|
|
100
|
+
doc: leadingComment(node),
|
|
101
|
+
});
|
|
102
|
+
case "enum_declaration":
|
|
103
|
+
return makeSymbol({
|
|
104
|
+
name: nameOf(node) ?? "(enum)",
|
|
105
|
+
kind: "enum",
|
|
106
|
+
node,
|
|
107
|
+
rawKind: node.type,
|
|
108
|
+
exported,
|
|
109
|
+
doc: leadingComment(node),
|
|
110
|
+
});
|
|
111
|
+
case "lexical_declaration":
|
|
112
|
+
case "variable_declaration":
|
|
113
|
+
return fromVariableDeclaration(node, exported, typeIndex);
|
|
114
|
+
case "method_definition":
|
|
115
|
+
case "method_signature":
|
|
116
|
+
case "abstract_method_signature": {
|
|
117
|
+
const name = nameOf(node) ?? "(method)";
|
|
118
|
+
const body = node.childForFieldName("body");
|
|
119
|
+
const mSym = makeSymbol({
|
|
120
|
+
name,
|
|
121
|
+
kind: "method",
|
|
122
|
+
node,
|
|
123
|
+
rawKind: node.type,
|
|
124
|
+
signature: headerSignature(node, body),
|
|
125
|
+
visibility: memberVisibility(node),
|
|
126
|
+
doc: leadingComment(node),
|
|
127
|
+
});
|
|
128
|
+
attachDecorators(mSym, node);
|
|
129
|
+
return mSym;
|
|
130
|
+
}
|
|
131
|
+
case "public_field_definition":
|
|
132
|
+
case "field_definition": {
|
|
133
|
+
// Only surface fields that hold an arrow/function (i.e. behave like methods).
|
|
134
|
+
const value = node.childForFieldName("value");
|
|
135
|
+
if (value && (value.type === "arrow_function" || value.type === "function" || value.type === "function_expression")) {
|
|
136
|
+
const name = nameOf(node) ?? "(method)";
|
|
137
|
+
const body = value.childForFieldName("body");
|
|
138
|
+
return makeSymbol({
|
|
139
|
+
name,
|
|
140
|
+
kind: "method",
|
|
141
|
+
node,
|
|
142
|
+
rawKind: node.type,
|
|
143
|
+
signature: headerSignature(node, body),
|
|
144
|
+
visibility: memberVisibility(node),
|
|
145
|
+
doc: leadingComment(node),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
case "ambient_declaration":
|
|
151
|
+
// `.d.ts` / `declare ...` — surface the declared API as exported.
|
|
152
|
+
return collect(namedChildren(node), true, typeIndex);
|
|
153
|
+
case "function_signature": {
|
|
154
|
+
const name = nameOf(node) ?? "(function)";
|
|
155
|
+
return makeSymbol({
|
|
156
|
+
name,
|
|
157
|
+
kind: "function",
|
|
158
|
+
node,
|
|
159
|
+
rawKind: node.type,
|
|
160
|
+
signature: node.text.replace(/\s+/g, " ").replace(/;\s*$/, "").trim(),
|
|
161
|
+
exported,
|
|
162
|
+
doc: leadingComment(node),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
case "module": // declare module "name" { ... }
|
|
166
|
+
case "internal_module": { // namespace Name { ... }
|
|
167
|
+
const nameNode = node.childForFieldName("name");
|
|
168
|
+
const rawName = nameNode ? nameNode.text : "(namespace)";
|
|
169
|
+
const name = rawName.replace(/^['"`]|['"`]$/g, "");
|
|
170
|
+
const body = node.childForFieldName("body");
|
|
171
|
+
const children = body ? collect(namedChildren(body), false, typeIndex) : [];
|
|
172
|
+
return makeSymbol({
|
|
173
|
+
name,
|
|
174
|
+
kind: "namespace",
|
|
175
|
+
node,
|
|
176
|
+
rawKind: node.type,
|
|
177
|
+
exported,
|
|
178
|
+
doc: leadingComment(node),
|
|
179
|
+
children,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
default:
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function fromVariableDeclaration(node, exported, typeIndex) {
|
|
187
|
+
const out = [];
|
|
188
|
+
for (const decl of namedChildren(node)) {
|
|
189
|
+
if (decl.type !== "variable_declarator")
|
|
190
|
+
continue;
|
|
191
|
+
const value = decl.childForFieldName("value");
|
|
192
|
+
const name = nameOf(decl);
|
|
193
|
+
if (!name)
|
|
194
|
+
continue;
|
|
195
|
+
if (value && (value.type === "arrow_function" || value.type === "function" || value.type === "function_expression")) {
|
|
196
|
+
const body = value.childForFieldName("body");
|
|
197
|
+
const arrowSym = makeSymbol({
|
|
198
|
+
name,
|
|
199
|
+
kind: "function",
|
|
200
|
+
node: decl,
|
|
201
|
+
rawKind: `${node.type}>arrow`,
|
|
202
|
+
signature: headerSignature(value, body),
|
|
203
|
+
exported,
|
|
204
|
+
doc: leadingComment(node),
|
|
205
|
+
});
|
|
206
|
+
attachComponentInfo(arrowSym, value, decl, name, typeIndex);
|
|
207
|
+
out.push(arrowSym);
|
|
208
|
+
}
|
|
209
|
+
else if (value && (value.type === "class_expression" || value.type === "class")) {
|
|
210
|
+
// const MyClass = class { ... }
|
|
211
|
+
const body = value.childForFieldName("body");
|
|
212
|
+
const children = body ? collect(namedChildren(body), false, typeIndex) : [];
|
|
213
|
+
out.push(makeSymbol({
|
|
214
|
+
name,
|
|
215
|
+
kind: "class",
|
|
216
|
+
node: decl,
|
|
217
|
+
rawKind: `${node.type}>class`,
|
|
218
|
+
exported,
|
|
219
|
+
doc: leadingComment(node),
|
|
220
|
+
children,
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
else if (exported && value) {
|
|
224
|
+
// export const FOO = <any non-function value> — track for dead code detection
|
|
225
|
+
out.push(makeSymbol({
|
|
226
|
+
name,
|
|
227
|
+
kind: "const",
|
|
228
|
+
node: decl,
|
|
229
|
+
rawKind: `${node.type}>const`,
|
|
230
|
+
signature: decl.text.replace(/\s+/g, " ").trim().slice(0, 120),
|
|
231
|
+
exported: true,
|
|
232
|
+
doc: leadingComment(node),
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
else if (exported && !value && decl.childForFieldName("type")) {
|
|
236
|
+
// Ambient `declare const X: T` — no initializer, but part of the typed API.
|
|
237
|
+
out.push(makeSymbol({
|
|
238
|
+
name,
|
|
239
|
+
kind: "const",
|
|
240
|
+
node: decl,
|
|
241
|
+
rawKind: `${node.type}>declare`,
|
|
242
|
+
signature: decl.text.replace(/\s+/g, " ").trim().slice(0, 120),
|
|
243
|
+
exported: true,
|
|
244
|
+
doc: leadingComment(node),
|
|
245
|
+
}));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return out;
|
|
249
|
+
}
|
|
250
|
+
// ─── Import extraction ────────────────────────────────────────────────────────
|
|
251
|
+
export function extractImportsTS(root, _source) {
|
|
252
|
+
const imports = [];
|
|
253
|
+
for (const child of namedChildren(root)) {
|
|
254
|
+
if (child.type === "import_statement")
|
|
255
|
+
parseImportStatement(child, imports);
|
|
256
|
+
// Re-exports: `export { X } from './foo'` or `export * from './foo'`
|
|
257
|
+
else if (child.type === "export_statement")
|
|
258
|
+
parseReExportStatement(child, imports);
|
|
259
|
+
}
|
|
260
|
+
collectDynamicImports(root, imports);
|
|
261
|
+
return imports;
|
|
262
|
+
}
|
|
263
|
+
/** First string-literal argument of a call's `arguments` node, or null. */
|
|
264
|
+
function firstStringArg(args) {
|
|
265
|
+
for (let i = 0; i < args.namedChildCount; i++) {
|
|
266
|
+
const a = args.namedChild(i);
|
|
267
|
+
if (a && a.type === "string") {
|
|
268
|
+
for (let j = 0; j < a.namedChildCount; j++) {
|
|
269
|
+
const frag = a.namedChild(j);
|
|
270
|
+
if (frag && frag.type === "string_fragment")
|
|
271
|
+
return frag.text;
|
|
272
|
+
}
|
|
273
|
+
return a.text.replace(/^['"`]|['"`]$/g, "");
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Walk the whole tree for dynamic `import("...")` and CommonJS `require("...")`
|
|
280
|
+
* calls (they can appear anywhere, not just at the top level). Only string-literal
|
|
281
|
+
* specifiers are captured; computed requires are skipped.
|
|
282
|
+
*/
|
|
283
|
+
function collectDynamicImports(node, out) {
|
|
284
|
+
if (node.type === "call_expression") {
|
|
285
|
+
const fn = node.childForFieldName("function");
|
|
286
|
+
const args = node.childForFieldName("arguments");
|
|
287
|
+
if (fn && args) {
|
|
288
|
+
const isImport = fn.type === "import";
|
|
289
|
+
const isRequire = fn.type === "identifier" && fn.text === "require";
|
|
290
|
+
if (isImport || isRequire) {
|
|
291
|
+
const spec = firstStringArg(args);
|
|
292
|
+
if (spec !== null) {
|
|
293
|
+
out.push({ symbol: "*", from: spec, isNamespaceImport: true, isDynamic: true });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
299
|
+
const c = node.namedChild(i);
|
|
300
|
+
if (c)
|
|
301
|
+
collectDynamicImports(c, out);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function parseReExportStatement(node, out) {
|
|
305
|
+
const source = extractModulePath(node.text);
|
|
306
|
+
if (!source)
|
|
307
|
+
return; // no `from` clause — local re-export, not an import
|
|
308
|
+
const isTypeOnly = /^export\s+type\b/.test(node.text);
|
|
309
|
+
// export * from './foo' or export * as Foo from './foo'
|
|
310
|
+
if (/^export\s+\*/.test(node.text)) {
|
|
311
|
+
out.push({ symbol: "*", from: source, isNamespaceImport: true });
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
// export { X, Y as Z } from './foo'
|
|
315
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
316
|
+
const c = node.namedChild(i);
|
|
317
|
+
if (!c || c.type !== "export_clause")
|
|
318
|
+
continue;
|
|
319
|
+
for (let j = 0; j < c.namedChildCount; j++) {
|
|
320
|
+
const spec = c.namedChild(j);
|
|
321
|
+
if (!spec || spec.type !== "export_specifier")
|
|
322
|
+
continue;
|
|
323
|
+
const nameNode = spec.childForFieldName("name");
|
|
324
|
+
const aliasNode = spec.childForFieldName("alias");
|
|
325
|
+
if (nameNode) {
|
|
326
|
+
const imp = { symbol: nameNode.text, from: source };
|
|
327
|
+
if (aliasNode)
|
|
328
|
+
imp.alias = aliasNode.text;
|
|
329
|
+
if (isTypeOnly)
|
|
330
|
+
imp.isTypeOnly = true;
|
|
331
|
+
out.push(imp);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function parseImportStatement(node, out) {
|
|
337
|
+
const isTypeOnly = /^import\s+type\b/.test(node.text);
|
|
338
|
+
const from = extractModulePath(node.text);
|
|
339
|
+
if (!from)
|
|
340
|
+
return;
|
|
341
|
+
let clauseNode = null;
|
|
342
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
343
|
+
const c = node.namedChild(i);
|
|
344
|
+
if (c && c.type === "import_clause") {
|
|
345
|
+
clauseNode = c;
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (!clauseNode) {
|
|
350
|
+
out.push({ symbol: "*", from, isSideEffect: true });
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
for (let i = 0; i < clauseNode.namedChildCount; i++) {
|
|
354
|
+
const c = clauseNode.namedChild(i);
|
|
355
|
+
if (!c)
|
|
356
|
+
continue;
|
|
357
|
+
if (c.type === "identifier") {
|
|
358
|
+
const imp = { symbol: c.text, from, isDefault: true };
|
|
359
|
+
if (isTypeOnly)
|
|
360
|
+
imp.isTypeOnly = true;
|
|
361
|
+
out.push(imp);
|
|
362
|
+
}
|
|
363
|
+
else if (c.type === "namespace_import") {
|
|
364
|
+
const id = c.namedChild(0);
|
|
365
|
+
if (id) {
|
|
366
|
+
const imp = { symbol: id.text, from, isNamespaceImport: true };
|
|
367
|
+
if (isTypeOnly)
|
|
368
|
+
imp.isTypeOnly = true;
|
|
369
|
+
out.push(imp);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
else if (c.type === "named_imports") {
|
|
373
|
+
for (let j = 0; j < c.namedChildCount; j++) {
|
|
374
|
+
const spec = c.namedChild(j);
|
|
375
|
+
if (!spec || spec.type !== "import_specifier")
|
|
376
|
+
continue;
|
|
377
|
+
const nameNode = spec.childForFieldName("name");
|
|
378
|
+
const aliasNode = spec.childForFieldName("alias");
|
|
379
|
+
if (nameNode) {
|
|
380
|
+
const imp = { symbol: nameNode.text, from };
|
|
381
|
+
if (aliasNode)
|
|
382
|
+
imp.alias = aliasNode.text;
|
|
383
|
+
if (isTypeOnly)
|
|
384
|
+
imp.isTypeOnly = true;
|
|
385
|
+
out.push(imp);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
function extractModulePath(importText) {
|
|
392
|
+
const m = importText.match(/from\s+['"`]([^'"`\n]+)['"`]/);
|
|
393
|
+
if (m)
|
|
394
|
+
return m[1];
|
|
395
|
+
const m2 = importText.match(/^import\s+(?:type\s+)?['"`]([^'"`\n]+)['"`]/);
|
|
396
|
+
return m2 ? m2[1] : null;
|
|
397
|
+
}
|
|
398
|
+
// ─── Member visibility ────────────────────────────────────────────────────────
|
|
399
|
+
function memberVisibility(node) {
|
|
400
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
401
|
+
const c = node.child(i);
|
|
402
|
+
if (c && c.type === "accessibility_modifier") {
|
|
403
|
+
return c.text === "private" || c.text === "protected" ? "private" : "public";
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
// `#name` ES private fields/methods
|
|
407
|
+
const name = node.childForFieldName("name");
|
|
408
|
+
if (name && name.type === "private_property_identifier")
|
|
409
|
+
return "private";
|
|
410
|
+
return "public";
|
|
411
|
+
}
|
|
412
|
+
// ─── React/TSX component prop extraction ──────────────────────────────────────
|
|
413
|
+
const JSX_NODES = new Set(["jsx_element", "jsx_self_closing_element", "jsx_fragment"]);
|
|
414
|
+
function firstNamed(node) {
|
|
415
|
+
return node.namedChildCount > 0 ? node.namedChild(0) : null;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Index every top-level (and exported) interface / object-type alias by name,
|
|
419
|
+
* mapping it to its prop fields. Used to resolve a component's named props type
|
|
420
|
+
* (e.g. `ButtonProps`) back to its individual props.
|
|
421
|
+
*/
|
|
422
|
+
function buildTypeIndex(root) {
|
|
423
|
+
const idx = new Map();
|
|
424
|
+
const visit = (nodes) => {
|
|
425
|
+
for (const n of nodes) {
|
|
426
|
+
if (n.type === "export_statement") {
|
|
427
|
+
visit(namedChildren(n));
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (n.type === "interface_declaration") {
|
|
431
|
+
const name = nameOf(n);
|
|
432
|
+
const body = n.childForFieldName("body");
|
|
433
|
+
if (name && body)
|
|
434
|
+
idx.set(name, propsFromMembers(body));
|
|
435
|
+
}
|
|
436
|
+
else if (n.type === "type_alias_declaration") {
|
|
437
|
+
const name = nameOf(n);
|
|
438
|
+
const val = n.childForFieldName("value");
|
|
439
|
+
if (name && val && val.type === "object_type")
|
|
440
|
+
idx.set(name, propsFromMembers(val));
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
visit(namedChildren(root));
|
|
445
|
+
return idx;
|
|
446
|
+
}
|
|
447
|
+
/** Read `property_signature` members out of an interface_body / object_type. */
|
|
448
|
+
function propsFromMembers(container) {
|
|
449
|
+
const props = [];
|
|
450
|
+
for (const m of namedChildren(container)) {
|
|
451
|
+
if (m.type !== "property_signature")
|
|
452
|
+
continue;
|
|
453
|
+
const nameNode = m.childForFieldName("name");
|
|
454
|
+
if (!nameNode)
|
|
455
|
+
continue;
|
|
456
|
+
const info = { name: nameNode.text };
|
|
457
|
+
const typeAnn = m.childForFieldName("type");
|
|
458
|
+
const typeNode = typeAnn ? firstNamed(typeAnn) : null;
|
|
459
|
+
if (typeNode)
|
|
460
|
+
info.type = typeNode.text.replace(/\s+/g, " ").trim();
|
|
461
|
+
const colon = m.text.indexOf(":");
|
|
462
|
+
const head = colon >= 0 ? m.text.slice(0, colon) : m.text;
|
|
463
|
+
if (head.includes("?"))
|
|
464
|
+
info.optional = true;
|
|
465
|
+
props.push(info);
|
|
466
|
+
}
|
|
467
|
+
return props;
|
|
468
|
+
}
|
|
469
|
+
/** Walk a function body looking for any JSX node (marks it a React component). */
|
|
470
|
+
function returnsJSX(node) {
|
|
471
|
+
if (!node)
|
|
472
|
+
return false;
|
|
473
|
+
let found = false;
|
|
474
|
+
const walk = (n) => {
|
|
475
|
+
if (found)
|
|
476
|
+
return;
|
|
477
|
+
if (JSX_NODES.has(n.type)) {
|
|
478
|
+
found = true;
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
482
|
+
const c = n.namedChild(i);
|
|
483
|
+
if (c)
|
|
484
|
+
walk(c);
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
walk(node);
|
|
488
|
+
return found;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* If `typeNode` is `FC<P>` / `React.FC<P>` / `FunctionComponent<P>` (or the
|
|
492
|
+
* React-qualified form), return the first type argument node (the props type).
|
|
493
|
+
*/
|
|
494
|
+
function fcTypeArgument(typeNode) {
|
|
495
|
+
if (!typeNode || typeNode.type !== "generic_type")
|
|
496
|
+
return null;
|
|
497
|
+
const base = typeNode.childForFieldName("name");
|
|
498
|
+
const baseText = base ? base.text : "";
|
|
499
|
+
if (!/(^|\.)(FC|FunctionComponent)$/.test(baseText))
|
|
500
|
+
return null;
|
|
501
|
+
for (let i = 0; i < typeNode.namedChildCount; i++) {
|
|
502
|
+
const c = typeNode.namedChild(i);
|
|
503
|
+
if (c && c.type === "type_arguments")
|
|
504
|
+
return firstNamed(c);
|
|
505
|
+
}
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Detect a React component (PascalCase + returns JSX, or typed as FC) and
|
|
510
|
+
* attach its props. `funcNode` is the function/arrow; `declNode` is the
|
|
511
|
+
* variable_declarator when the component is `const X: React.FC<P> = ...`.
|
|
512
|
+
*/
|
|
513
|
+
function attachComponentInfo(sym, funcNode, declNode, name, idx) {
|
|
514
|
+
if (!/^[A-Z]/.test(name))
|
|
515
|
+
return; // components are PascalCase
|
|
516
|
+
let propsTypeNode = null;
|
|
517
|
+
let fc = false;
|
|
518
|
+
if (declNode) {
|
|
519
|
+
const ta = declNode.childForFieldName("type");
|
|
520
|
+
const arg = ta ? fcTypeArgument(firstNamed(ta)) : null;
|
|
521
|
+
if (arg) {
|
|
522
|
+
propsTypeNode = arg;
|
|
523
|
+
fc = true;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (!fc && !returnsJSX(funcNode.childForFieldName("body")))
|
|
527
|
+
return; // not a component
|
|
528
|
+
if (!propsTypeNode) {
|
|
529
|
+
const params = funcNode.childForFieldName("parameters");
|
|
530
|
+
const first = params ? firstNamed(params) : null; // required/optional_parameter
|
|
531
|
+
const ta = first ? first.childForFieldName("type") : null;
|
|
532
|
+
if (ta)
|
|
533
|
+
propsTypeNode = firstNamed(ta);
|
|
534
|
+
}
|
|
535
|
+
if (!propsTypeNode)
|
|
536
|
+
return; // component, but untyped props — nothing to extract
|
|
537
|
+
if (propsTypeNode.type === "object_type") {
|
|
538
|
+
sym.props = propsFromMembers(propsTypeNode);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const typeName = propsTypeNode.text.replace(/\s+/g, " ").trim();
|
|
542
|
+
sym.propsType = typeName;
|
|
543
|
+
const resolved = idx.get(typeName);
|
|
544
|
+
if (resolved)
|
|
545
|
+
sym.props = resolved;
|
|
546
|
+
}
|
|
547
|
+
// ─── TS/JS decorators ─────────────────────────────────────────────────────────
|
|
548
|
+
/** Strip the leading `@` and collapse whitespace from a decorator node. */
|
|
549
|
+
function decoratorText(node) {
|
|
550
|
+
return node.text.replace(/^@\s*/, "").replace(/\s+/g, " ").trim();
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Attach decorators to a class/method symbol. TS decorators appear either as
|
|
554
|
+
* preceding sibling `decorator` nodes (classes, methods) or as leading child
|
|
555
|
+
* decorators (some grammars) — collect both.
|
|
556
|
+
*/
|
|
557
|
+
function attachDecorators(sym, node) {
|
|
558
|
+
const decs = [];
|
|
559
|
+
// leading child decorators
|
|
560
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
561
|
+
const c = node.namedChild(i);
|
|
562
|
+
if (c && c.type === "decorator")
|
|
563
|
+
decs.push(decoratorText(c));
|
|
564
|
+
else if (c && c.type !== "decorator")
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
// preceding sibling decorators (most common for classes/methods)
|
|
568
|
+
let prev = node.previousNamedSibling;
|
|
569
|
+
const lead = [];
|
|
570
|
+
while (prev && prev.type === "decorator") {
|
|
571
|
+
lead.unshift(decoratorText(prev));
|
|
572
|
+
prev = prev.previousNamedSibling;
|
|
573
|
+
}
|
|
574
|
+
const all = [...lead, ...decs].filter((t) => t.length > 0);
|
|
575
|
+
if (all.length > 0)
|
|
576
|
+
sym.decorators = all;
|
|
577
|
+
}
|
package/dist/fix.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// ─── Builder ──────────────────────────────────────────────────────────────────
|
|
2
|
+
export function buildFixSuggestions(opts) {
|
|
3
|
+
const suggestions = [];
|
|
4
|
+
// ── Dead exports → remove-dead-export (high confidence only) ─────────────
|
|
5
|
+
if (opts.dead) {
|
|
6
|
+
for (const dead of opts.dead) {
|
|
7
|
+
if (dead.confidence !== "high")
|
|
8
|
+
continue;
|
|
9
|
+
suggestions.push({
|
|
10
|
+
kind: "remove-dead-export",
|
|
11
|
+
file: dead.file,
|
|
12
|
+
symbol: dead.symbol,
|
|
13
|
+
description: `"${dead.symbol}" is exported but never imported within the scanned directory. Remove the export keyword to reduce surface area.`,
|
|
14
|
+
before: `export ${dead.kind} ${dead.symbol}`,
|
|
15
|
+
after: `${dead.kind} ${dead.symbol}`,
|
|
16
|
+
priority: 2,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
// ── Smells ────────────────────────────────────────────────────────────────
|
|
21
|
+
if (opts.smells) {
|
|
22
|
+
for (const smell of opts.smells) {
|
|
23
|
+
if (smell.smell === "long-method") {
|
|
24
|
+
suggestions.push({
|
|
25
|
+
kind: "extract-method",
|
|
26
|
+
file: smell.file,
|
|
27
|
+
line: smell.line,
|
|
28
|
+
symbol: smell.symbol,
|
|
29
|
+
description: smell.symbol
|
|
30
|
+
? `"${smell.symbol}" is too long. ${smell.message}. Extract cohesive blocks into smaller helper functions.`
|
|
31
|
+
: `${smell.message}. Extract cohesive blocks into smaller helper functions.`,
|
|
32
|
+
priority: 3,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
else if (smell.smell === "god-class") {
|
|
36
|
+
suggestions.push({
|
|
37
|
+
kind: "split-class",
|
|
38
|
+
file: smell.file,
|
|
39
|
+
line: smell.line,
|
|
40
|
+
symbol: smell.symbol,
|
|
41
|
+
description: smell.symbol
|
|
42
|
+
? `"${smell.symbol}" has too many responsibilities. ${smell.message}. Consider splitting into focused classes.`
|
|
43
|
+
: `${smell.message}. Consider splitting into focused classes.`,
|
|
44
|
+
priority: 2,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// ── Security issues ───────────────────────────────────────────────────────
|
|
50
|
+
if (opts.security) {
|
|
51
|
+
for (const issue of opts.security) {
|
|
52
|
+
if (issue.rule === "eval") {
|
|
53
|
+
suggestions.push({
|
|
54
|
+
kind: "remove-eval",
|
|
55
|
+
file: issue.file,
|
|
56
|
+
line: issue.line,
|
|
57
|
+
description: `${issue.message}. Replace eval() with a safer alternative such as JSON.parse() for data, or Function() with strict input validation.`,
|
|
58
|
+
before: `eval(userInput)`,
|
|
59
|
+
after: `JSON.parse(userInput) // or use a safe parser`,
|
|
60
|
+
priority: 1,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
else if (issue.rule === "http-url") {
|
|
64
|
+
// Try to extract the actual URL from the snippet for a more precise suggestion.
|
|
65
|
+
const urlMatch = issue.snippet.match(/http:\/\/[^\s'"`,)]+/);
|
|
66
|
+
const exampleUrl = urlMatch ? urlMatch[0] : "http://api.example.com";
|
|
67
|
+
const httpsUrl = exampleUrl.replace("http://", "https://");
|
|
68
|
+
suggestions.push({
|
|
69
|
+
kind: "use-https",
|
|
70
|
+
file: issue.file,
|
|
71
|
+
line: issue.line,
|
|
72
|
+
description: `${issue.message}. Switch to HTTPS to ensure data in transit is encrypted.`,
|
|
73
|
+
before: exampleUrl,
|
|
74
|
+
after: httpsUrl,
|
|
75
|
+
priority: 3,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
else if (issue.rule === "no-rate-limit") {
|
|
79
|
+
suggestions.push({
|
|
80
|
+
kind: "add-rate-limit",
|
|
81
|
+
file: issue.file,
|
|
82
|
+
line: issue.line,
|
|
83
|
+
description: `${issue.message}. Add rate-limit middleware (e.g. express-rate-limit) to prevent abuse.`,
|
|
84
|
+
before: `app.post('/api/endpoint', handler)`,
|
|
85
|
+
after: `app.post('/api/endpoint', rateLimiter, handler)`,
|
|
86
|
+
priority: 1,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return suggestions;
|
|
92
|
+
}
|