universal-ast-mapper 2.0.0 → 2.0.2

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 (75) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +261 -12
  3. package/dist/ai-refactor.js +185 -0
  4. package/dist/ai-testgen.js +105 -0
  5. package/dist/analysis.js +134 -0
  6. package/dist/arch-rules.js +82 -0
  7. package/dist/callgraph.js +467 -0
  8. package/dist/check.js +112 -0
  9. package/dist/cli.js +2284 -0
  10. package/dist/complexity.js +98 -0
  11. package/dist/config.js +53 -0
  12. package/dist/contextpack.js +79 -0
  13. package/dist/coupling.js +35 -0
  14. package/dist/covmerge.js +176 -0
  15. package/dist/crosslang.js +425 -0
  16. package/dist/dashboard.js +259 -0
  17. package/dist/diagram.js +264 -0
  18. package/dist/diskcache.js +97 -0
  19. package/dist/docgen.js +156 -0
  20. package/dist/embeddings.js +136 -0
  21. package/dist/explain.js +123 -0
  22. package/dist/explorer.js +123 -0
  23. package/dist/extractors/c.js +204 -0
  24. package/dist/extractors/common.js +56 -0
  25. package/dist/extractors/cpp.js +272 -0
  26. package/dist/extractors/csharp.js +209 -0
  27. package/dist/extractors/go.js +212 -0
  28. package/dist/extractors/java.js +152 -0
  29. package/dist/extractors/kotlin.js +159 -0
  30. package/dist/extractors/php.js +208 -0
  31. package/dist/extractors/python.js +153 -0
  32. package/dist/extractors/ruby.js +146 -0
  33. package/dist/extractors/rust.js +249 -0
  34. package/dist/extractors/swift.js +192 -0
  35. package/dist/extractors/typescript.js +577 -0
  36. package/dist/fix.js +92 -0
  37. package/dist/gitdiff.js +178 -0
  38. package/dist/graph-analysis.js +279 -0
  39. package/dist/graph.js +165 -0
  40. package/dist/history.js +36 -0
  41. package/dist/html.js +658 -0
  42. package/dist/incremental.js +122 -0
  43. package/dist/index.js +1945 -0
  44. package/dist/indexstore.js +105 -0
  45. package/dist/layers.js +36 -0
  46. package/dist/lsp.js +238 -0
  47. package/dist/modulecoupling.js +0 -0
  48. package/dist/parser.js +84 -0
  49. package/dist/patch.js +199 -0
  50. package/dist/plugins.js +88 -0
  51. package/dist/pool.js +114 -0
  52. package/dist/prompts.js +67 -0
  53. package/dist/registry.js +87 -0
  54. package/dist/report.js +441 -0
  55. package/dist/resolver.js +222 -0
  56. package/dist/roots.js +47 -0
  57. package/dist/search.js +68 -0
  58. package/dist/security.js +178 -0
  59. package/dist/semantic.js +365 -0
  60. package/dist/serve.js +328 -0
  61. package/dist/sfc.js +27 -0
  62. package/dist/similar.js +98 -0
  63. package/dist/skeleton.js +132 -0
  64. package/dist/smells.js +285 -0
  65. package/dist/sourcemap.js +60 -0
  66. package/dist/testgen.js +280 -0
  67. package/dist/testmap.js +167 -0
  68. package/dist/tsconfig.js +212 -0
  69. package/dist/typeflow.js +124 -0
  70. package/dist/types.js +5 -0
  71. package/dist/unused-params.js +127 -0
  72. package/dist/webapp.js +646 -0
  73. package/dist/worker.js +27 -0
  74. package/dist/workspace.js +330 -0
  75. 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
+ }