@openrewrite/rewrite 8.67.0 → 8.67.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.
Files changed (77) hide show
  1. package/dist/index.d.ts.map +1 -1
  2. package/dist/index.js +2 -0
  3. package/dist/index.js.map +1 -1
  4. package/dist/java/tree.d.ts +7 -1
  5. package/dist/java/tree.d.ts.map +1 -1
  6. package/dist/java/tree.js +13 -3
  7. package/dist/java/tree.js.map +1 -1
  8. package/dist/javascript/add-import.d.ts.map +1 -1
  9. package/dist/javascript/add-import.js +200 -44
  10. package/dist/javascript/add-import.js.map +1 -1
  11. package/dist/javascript/cleanup/index.d.ts +2 -0
  12. package/dist/javascript/cleanup/index.d.ts.map +1 -0
  13. package/dist/javascript/cleanup/index.js +21 -0
  14. package/dist/javascript/cleanup/index.js.map +1 -0
  15. package/dist/javascript/cleanup/use-object-property-shorthand.d.ts +22 -0
  16. package/dist/javascript/cleanup/use-object-property-shorthand.d.ts.map +1 -0
  17. package/dist/javascript/cleanup/use-object-property-shorthand.js +144 -0
  18. package/dist/javascript/cleanup/use-object-property-shorthand.js.map +1 -0
  19. package/dist/javascript/comparator.d.ts +8 -0
  20. package/dist/javascript/comparator.d.ts.map +1 -1
  21. package/dist/javascript/comparator.js +12 -0
  22. package/dist/javascript/comparator.js.map +1 -1
  23. package/dist/javascript/dependency-workspace.d.ts +1 -0
  24. package/dist/javascript/dependency-workspace.d.ts.map +1 -1
  25. package/dist/javascript/dependency-workspace.js +44 -0
  26. package/dist/javascript/dependency-workspace.js.map +1 -1
  27. package/dist/javascript/parser.d.ts.map +1 -1
  28. package/dist/javascript/parser.js +21 -1
  29. package/dist/javascript/parser.js.map +1 -1
  30. package/dist/javascript/print.d.ts +1 -0
  31. package/dist/javascript/print.d.ts.map +1 -1
  32. package/dist/javascript/print.js +8 -0
  33. package/dist/javascript/print.js.map +1 -1
  34. package/dist/javascript/rpc.js +14 -0
  35. package/dist/javascript/rpc.js.map +1 -1
  36. package/dist/javascript/templating/pattern.d.ts +7 -0
  37. package/dist/javascript/templating/pattern.d.ts.map +1 -1
  38. package/dist/javascript/templating/pattern.js +10 -0
  39. package/dist/javascript/templating/pattern.js.map +1 -1
  40. package/dist/javascript/templating/rewrite.d.ts.map +1 -1
  41. package/dist/javascript/templating/rewrite.js +17 -16
  42. package/dist/javascript/templating/rewrite.js.map +1 -1
  43. package/dist/javascript/templating/types.d.ts +56 -28
  44. package/dist/javascript/templating/types.d.ts.map +1 -1
  45. package/dist/javascript/tree.d.ts +9 -0
  46. package/dist/javascript/tree.d.ts.map +1 -1
  47. package/dist/javascript/tree.js +1 -0
  48. package/dist/javascript/tree.js.map +1 -1
  49. package/dist/javascript/type-mapping.d.ts +13 -1
  50. package/dist/javascript/type-mapping.d.ts.map +1 -1
  51. package/dist/javascript/type-mapping.js +215 -8
  52. package/dist/javascript/type-mapping.js.map +1 -1
  53. package/dist/javascript/visitor.d.ts +1 -0
  54. package/dist/javascript/visitor.d.ts.map +1 -1
  55. package/dist/javascript/visitor.js +11 -0
  56. package/dist/javascript/visitor.js.map +1 -1
  57. package/dist/json/parser.js +18 -2
  58. package/dist/json/parser.js.map +1 -1
  59. package/dist/version.txt +1 -1
  60. package/package.json +1 -1
  61. package/src/index.ts +3 -0
  62. package/src/java/tree.ts +7 -1
  63. package/src/javascript/add-import.ts +211 -53
  64. package/src/javascript/cleanup/index.ts +17 -0
  65. package/src/javascript/cleanup/use-object-property-shorthand.ts +154 -0
  66. package/src/javascript/comparator.ts +11 -0
  67. package/src/javascript/dependency-workspace.ts +52 -0
  68. package/src/javascript/parser.ts +23 -1
  69. package/src/javascript/print.ts +7 -0
  70. package/src/javascript/rpc.ts +12 -0
  71. package/src/javascript/templating/pattern.ts +11 -0
  72. package/src/javascript/templating/rewrite.ts +19 -18
  73. package/src/javascript/templating/types.ts +60 -28
  74. package/src/javascript/tree.ts +10 -0
  75. package/src/javascript/type-mapping.ts +239 -9
  76. package/src/javascript/visitor.ts +10 -0
  77. package/src/json/parser.ts +16 -2
@@ -366,6 +366,26 @@ export class JavaScriptParserVisitor {
366
366
  }
367
367
  }
368
368
 
369
+ let shebangStatement: J.RightPadded<JS.Shebang> | undefined;
370
+ if (prefix.whitespace?.startsWith('#!')) {
371
+ const newlineIndex = prefix.whitespace.indexOf('\n');
372
+ const shebangText = newlineIndex === -1 ? prefix.whitespace : prefix.whitespace.slice(0, newlineIndex);
373
+ const afterShebang = newlineIndex === -1 ? '' : '\n';
374
+ const remainingWhitespace = newlineIndex === -1 ? '' : prefix.whitespace.slice(newlineIndex + 1);
375
+
376
+ shebangStatement = this.rightPadded<JS.Shebang>({
377
+ kind: JS.Kind.Shebang,
378
+ id: randomId(),
379
+ prefix: emptySpace,
380
+ markers: emptyMarkers,
381
+ text: shebangText
382
+ }, {kind: J.Kind.Space, whitespace: afterShebang, comments: []}, emptyMarkers);
383
+
384
+ prefix = produce(prefix, draft => {
385
+ draft.whitespace = remainingWhitespace;
386
+ });
387
+ }
388
+
369
389
  return {
370
390
  kind: JS.Kind.CompilationUnit,
371
391
  id: randomId(),
@@ -374,7 +394,9 @@ export class JavaScriptParserVisitor {
374
394
  sourcePath: this.sourcePath,
375
395
  charsetName: bomAndTextEncoding.encoding,
376
396
  charsetBomMarked: bomAndTextEncoding.hasBom,
377
- statements: this.semicolonPaddedStatementList(node.statements),
397
+ statements: shebangStatement
398
+ ? [shebangStatement, ...this.semicolonPaddedStatementList(node.statements)]
399
+ : this.semicolonPaddedStatementList(node.statements),
378
400
  eof: this.prefix(node.endOfFileToken)
379
401
  };
380
402
  }
@@ -459,6 +459,13 @@ export class JavaScriptPrinter extends JavaScriptVisitor<PrintOutputCapture> {
459
459
  return variableDeclarations;
460
460
  }
461
461
 
462
+ override async visitShebang(shebang: JS.Shebang, p: PrintOutputCapture): Promise<J | undefined> {
463
+ await this.beforeSyntax(shebang, p);
464
+ p.append(shebang.text);
465
+ await this.afterSyntax(shebang, p);
466
+ return shebang;
467
+ }
468
+
462
469
  override async visitVariableDeclarations(multiVariable: J.VariableDeclarations, p: PrintOutputCapture): Promise<J | undefined> {
463
470
  await this.beforeSyntax(multiVariable, p);
464
471
  await this.visitNodes(multiVariable.leadingAnnotations, p);
@@ -247,6 +247,11 @@ class JavaScriptSender extends JavaScriptVisitor<RpcSendQueue> {
247
247
  return scopedVariableDeclarations;
248
248
  }
249
249
 
250
+ override async visitShebang(shebang: JS.Shebang, q: RpcSendQueue): Promise<J | undefined> {
251
+ await q.getAndSend(shebang, el => el.text);
252
+ return shebang;
253
+ }
254
+
250
255
  override async visitStatementExpression(statementExpression: JS.StatementExpression, q: RpcSendQueue): Promise<J | undefined> {
251
256
  await q.getAndSend(statementExpression, el => el.statement, el => this.visit(el, q));
252
257
  return statementExpression;
@@ -828,6 +833,13 @@ class JavaScriptReceiver extends JavaScriptVisitor<RpcReceiveQueue> {
828
833
  return updateIfChanged(scopedVariableDeclarations, updates);
829
834
  }
830
835
 
836
+ override async visitShebang(shebang: JS.Shebang, q: RpcReceiveQueue): Promise<J | undefined> {
837
+ const updates = {
838
+ text: await q.receive(shebang.text)
839
+ };
840
+ return updateIfChanged(shebang, updates);
841
+ }
842
+
831
843
  override async visitStatementExpression(statementExpression: JS.StatementExpression, q: RpcReceiveQueue): Promise<J | undefined> {
832
844
  const updates = {
833
845
  statement: await q.receive(statementExpression.statement, el => this.visitDefined<Statement>(el, q))
@@ -616,6 +616,17 @@ export class MatchResult implements IMatchResult {
616
616
  return this.extractElements(value);
617
617
  }
618
618
 
619
+ /**
620
+ * Checks if a capture has been matched.
621
+ *
622
+ * @param capture The capture name (string) or Capture object
623
+ * @returns true if the capture exists in the match result
624
+ */
625
+ has(capture: Capture | string): boolean {
626
+ const name = typeof capture === "string" ? capture : ((capture as any)[CAPTURE_NAME_SYMBOL] || capture.getName());
627
+ return this.storage.has(name);
628
+ }
629
+
619
630
  /**
620
631
  * Extracts semantic elements from storage value.
621
632
  * For wrappers, extracts the .element; for arrays, returns array of elements.
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import {Cursor, ExecutionContext, Recipe} from '../..';
17
17
  import {J} from '../../java';
18
- import {RewriteRule, RewriteConfig} from './types';
18
+ import {RewriteRule, RewriteConfig, PreMatchContext, PostMatchContext} from './types';
19
19
  import {Pattern, MatchResult} from './pattern';
20
20
  import {Template} from './template';
21
21
 
@@ -26,28 +26,29 @@ class RewriteRuleImpl implements RewriteRule {
26
26
  constructor(
27
27
  private readonly before: Pattern[],
28
28
  private readonly after: Template | ((match: MatchResult) => Template),
29
- private readonly where?: (node: J, cursor: Cursor) => boolean | Promise<boolean>,
30
- private readonly whereNot?: (node: J, cursor: Cursor) => boolean | Promise<boolean>
29
+ private readonly preMatch?: (node: J, context: PreMatchContext) => boolean | Promise<boolean>,
30
+ private readonly postMatch?: (node: J, context: PostMatchContext) => boolean | Promise<boolean>
31
31
  ) {
32
32
  }
33
33
 
34
34
  async tryOn(cursor: Cursor, node: J): Promise<J | undefined> {
35
+ // Evaluate preMatch before attempting any pattern matching
36
+ if (this.preMatch) {
37
+ const preMatchResult = await this.preMatch(node, { cursor });
38
+ if (!preMatchResult) {
39
+ return undefined; // Early exit - don't attempt pattern matching
40
+ }
41
+ }
42
+
35
43
  for (const pattern of this.before) {
36
44
  // Pass cursor to pattern.match() for context-aware capture constraints
37
45
  const match = await pattern.match(node, cursor);
38
46
  if (match) {
39
- // Evaluate context predicates after structural match
40
- if (this.where) {
41
- const whereResult = await this.where(node, cursor);
42
- if (!whereResult) {
43
- continue; // Pattern matched but context doesn't, try next pattern
44
- }
45
- }
46
-
47
- if (this.whereNot) {
48
- const whereNotResult = await this.whereNot(node, cursor);
49
- if (whereNotResult) {
50
- continue; // Pattern matched but context is excluded, try next pattern
47
+ // Evaluate postMatch after structural match succeeds
48
+ if (this.postMatch) {
49
+ const postMatchResult = await this.postMatch(node, { cursor, captures: match });
50
+ if (!postMatchResult) {
51
+ continue; // Pattern matched but postMatch failed, try next pattern
51
52
  }
52
53
  }
53
54
 
@@ -69,7 +70,7 @@ class RewriteRuleImpl implements RewriteRule {
69
70
  }
70
71
  }
71
72
 
72
- // Return undefined if no patterns match or all context checks failed
73
+ // Return undefined if no patterns match or all postMatch checks failed
73
74
  return undefined;
74
75
  }
75
76
 
@@ -168,8 +169,8 @@ export function rewrite(
168
169
  return new RewriteRuleImpl(
169
170
  Array.isArray(config.before) ? config.before : [config.before],
170
171
  config.after,
171
- config.where,
172
- config.whereNot
172
+ config.preMatch,
173
+ config.postMatch
173
174
  );
174
175
  }
175
176
 
@@ -590,6 +590,33 @@ export interface RewriteRule {
590
590
  orElse(alternative: RewriteRule): RewriteRule;
591
591
  }
592
592
 
593
+ /**
594
+ * Context for preMatch predicate - only has cursor, no captures yet.
595
+ */
596
+ export interface PreMatchContext {
597
+ /**
598
+ * The cursor pointing to the node being considered for matching.
599
+ * Allows navigating the AST (parent, root, etc.).
600
+ */
601
+ cursor: Cursor;
602
+ }
603
+
604
+ /**
605
+ * Context for postMatch predicate - has cursor and captured values.
606
+ */
607
+ export interface PostMatchContext {
608
+ /**
609
+ * The cursor pointing to the matched node.
610
+ * Allows navigating the AST (parent, root, etc.).
611
+ */
612
+ cursor: Cursor;
613
+
614
+ /**
615
+ * Values captured during pattern matching.
616
+ */
617
+ captures: CaptureMap;
618
+ }
619
+
593
620
  /**
594
621
  * Configuration for a replacement rule.
595
622
  */
@@ -598,55 +625,52 @@ export interface RewriteConfig {
598
625
  after: Template | ((match: MatchResult) => Template);
599
626
 
600
627
  /**
601
- * Optional context predicate that must evaluate to true for the transformation to be applied.
602
- * Evaluated after the pattern matches structurally but before applying the template.
603
- * Provides access to both the matched node and the cursor for context inspection.
628
+ * Optional predicate evaluated BEFORE pattern matching.
629
+ * Use for efficient early filtering based on AST context when captures aren't needed.
630
+ * If this returns false, pattern matching is skipped entirely.
604
631
  *
605
- * @param node The matched AST node
606
- * @param cursor The cursor at the matched node, providing access to ancestors and context
607
- * @returns true if the transformation should be applied, false otherwise
632
+ * @param node The AST node being considered for matching
633
+ * @param context Context providing cursor for AST navigation
634
+ * @returns true to proceed with pattern matching, false to skip this node
608
635
  *
609
636
  * @example
610
637
  * ```typescript
611
638
  * rewrite(() => ({
612
- * before: pattern`await ${_('promise')}`,
613
- * after: template`await ${_('promise')}.catch(handleError)`,
614
- * where: (node, cursor) => {
615
- * // Only apply inside async functions
616
- * const method = cursor.firstEnclosing((n: any): n is J.MethodDeclaration =>
617
- * n.kind === J.Kind.MethodDeclaration
618
- * );
619
- * return method?.modifiers.some(m => m.type === 'async') || false;
639
+ * before: pattern`console.log(${_('msg')})`,
640
+ * after: template`logger.info(${_('msg')})`,
641
+ * preMatch: (node, {cursor}) => {
642
+ * // Only attempt matching inside functions named 'handleError'
643
+ * const method = cursor.firstEnclosing(isMethodDeclaration);
644
+ * return method?.name.simpleName === 'handleError';
620
645
  * }
621
646
  * }));
622
647
  * ```
623
648
  */
624
- where?: (node: J, cursor: Cursor) => boolean | Promise<boolean>;
649
+ preMatch?: (node: J, context: PreMatchContext) => boolean | Promise<boolean>;
625
650
 
626
651
  /**
627
- * Optional context predicate that must evaluate to false for the transformation to be applied.
628
- * Evaluated after the pattern matches structurally but before applying the template.
629
- * Provides access to both the matched node and the cursor for context inspection.
652
+ * Optional predicate evaluated AFTER pattern matching succeeds.
653
+ * Use when you need access to captured values to decide whether to apply the transformation.
654
+ * If this returns false, the transformation is not applied.
630
655
  *
631
656
  * @param node The matched AST node
632
- * @param cursor The cursor at the matched node, providing access to ancestors and context
633
- * @returns true if the transformation should NOT be applied, false if it should proceed
657
+ * @param context Context providing cursor for AST navigation and captured values
658
+ * @returns true to apply the transformation, false to skip
634
659
  *
635
660
  * @example
636
661
  * ```typescript
637
662
  * rewrite(() => ({
638
- * before: pattern`await ${_('promise')}`,
639
- * after: template`await ${_('promise')}.catch(handleError)`,
640
- * whereNot: (node, cursor) => {
641
- * // Don't apply inside try-catch blocks
642
- * return cursor.firstEnclosing((n: any): n is J.Try =>
643
- * n.kind === J.Kind.Try
644
- * ) !== undefined;
663
+ * before: pattern`${_('a')} + ${_('b')}`,
664
+ * after: template`${_('b')} + ${_('a')}`,
665
+ * postMatch: (node, {cursor, captures}) => {
666
+ * // Only swap if 'a' is a literal number
667
+ * const a = captures.get('a');
668
+ * return a?.kind === J.Kind.Literal && typeof a.value === 'number';
645
669
  * }
646
670
  * }));
647
671
  * ```
648
672
  */
649
- whereNot?: (node: J, cursor: Cursor) => boolean | Promise<boolean>;
673
+ postMatch?: (node: J, context: PostMatchContext) => boolean | Promise<boolean>;
650
674
  }
651
675
 
652
676
  /**
@@ -755,6 +779,14 @@ export interface MatchResult {
755
779
  get(capture: string): any;
756
780
 
757
781
  get<T>(capture: Capture<T>): T | undefined;
782
+
783
+ /**
784
+ * Checks if a capture has been matched.
785
+ *
786
+ * @param capture The capture name (string) or Capture object
787
+ * @returns true if the capture exists in the match result
788
+ */
789
+ has(capture: Capture | string): boolean;
758
790
  }
759
791
 
760
792
  /**
@@ -89,6 +89,7 @@ export namespace JS {
89
89
  PropertyAssignment: "org.openrewrite.javascript.tree.JS$PropertyAssignment",
90
90
  SatisfiesExpression: "org.openrewrite.javascript.tree.JS$SatisfiesExpression",
91
91
  ScopedVariableDeclarations: "org.openrewrite.javascript.tree.JS$ScopedVariableDeclarations",
92
+ Shebang: "org.openrewrite.javascript.tree.JS$Shebang",
92
93
  StatementExpression: "org.openrewrite.javascript.tree.JS$StatementExpression",
93
94
  TaggedTemplateExpression: "org.openrewrite.javascript.tree.JS$TaggedTemplateExpression",
94
95
  TemplateExpression: "org.openrewrite.javascript.tree.JS$TemplateExpression",
@@ -449,6 +450,15 @@ export namespace JS {
449
450
  readonly variables: J.RightPadded<J>[];
450
451
  }
451
452
 
453
+ /**
454
+ * Represents a shebang line at the beginning of a script.
455
+ * @example #!/usr/bin/env node
456
+ */
457
+ export interface Shebang extends JS, Statement {
458
+ readonly kind: typeof Kind.Shebang;
459
+ readonly text: string;
460
+ }
461
+
452
462
  /**
453
463
  * Represents a statement used as an expression. The example shows a function expressions.
454
464
  * @example const greet = function (name: string) : string { return name; };
@@ -65,6 +65,16 @@ export class JavaScriptTypeMapping {
65
65
  }
66
66
 
67
67
  type(node: ts.Node): Type | undefined {
68
+ // For identifiers, check if this references a variable
69
+ // This enables fieldType attribution for variable references
70
+ if (ts.isIdentifier(node)) {
71
+ const variableType = this.variableType(node);
72
+ if (variableType) {
73
+ return variableType;
74
+ }
75
+ // Fall through to regular type checking if not a variable
76
+ }
77
+
68
78
  let type: ts.Type | undefined;
69
79
  if (ts.isExpression(node)) {
70
80
  type = this.checker.getTypeAtLocation(node);
@@ -115,6 +125,14 @@ export class JavaScriptTypeMapping {
115
125
  return existing;
116
126
  }
117
127
 
128
+ // TypeScript represents `boolean` as a union of `false | true`, but the union
129
+ // type still has the Boolean flag set. Check this early to return Primitive.Boolean
130
+ // before we process it as a generic union type.
131
+ if (type.flags & ts.TypeFlags.Boolean) {
132
+ this.typeCache.set(signature, Type.Primitive.Boolean);
133
+ return Type.Primitive.Boolean;
134
+ }
135
+
118
136
  // Get symbol for later use in type detection
119
137
  const symbol = type.getSymbol?.();
120
138
 
@@ -248,8 +266,7 @@ export class JavaScriptTypeMapping {
248
266
 
249
267
  // Check for union types (e.g., string | number)
250
268
  if (type.flags & ts.TypeFlags.Union) {
251
- const unionType = type as ts.UnionType;
252
- return this.createUnionType(unionType, signature);
269
+ return this.createUnionType(type as ts.UnionType, signature);
253
270
  }
254
271
 
255
272
  // Check for intersection types (e.g., A & B)
@@ -318,16 +335,229 @@ export class JavaScriptTypeMapping {
318
335
  return Type.isPrimitive(type) ? type : Type.Primitive.None;
319
336
  }
320
337
 
321
- variableType(node: ts.NamedDeclaration): Type.Variable | undefined {
338
+ variableType(node: ts.Node): Type.Variable | undefined {
339
+ let symbol: ts.Symbol | undefined;
340
+ let location: ts.Node = node;
341
+
342
+ // Get the symbol depending on node type
322
343
  if (ts.isVariableDeclaration(node)) {
323
- const symbol = this.checker.getSymbolAtLocation(node.name);
324
- if (symbol) {
325
- // TODO: Implement in Phase 6
326
- // const type = this.checker.getTypeOfSymbolAtLocation(symbol, node);
327
- // return JavaType.Variable with proper mapping
344
+ symbol = this.checker.getSymbolAtLocation(node.name);
345
+ } else if (ts.isParameter(node)) {
346
+ symbol = this.checker.getSymbolAtLocation(node.name);
347
+ } else if (ts.isIdentifier(node)) {
348
+ // For identifier references (like 'vi' in 'vi.fn()')
349
+ symbol = this.checker.getSymbolAtLocation(node);
350
+ } else if (ts.isPropertyDeclaration(node) || ts.isPropertySignature(node)) {
351
+ symbol = this.checker.getSymbolAtLocation(node.name);
352
+ } else {
353
+ // Not a variable/parameter/property we can handle
354
+ return undefined;
355
+ }
356
+
357
+ if (!symbol) {
358
+ return undefined;
359
+ }
360
+
361
+ // Get the variable declaration (resolve aliases if needed)
362
+ let actualSymbol = symbol;
363
+ if (symbol.flags & ts.SymbolFlags.Alias) {
364
+ actualSymbol = this.checker.getAliasedSymbol(symbol);
365
+ }
366
+
367
+ // Check if this symbol represents a variable, parameter, or property
368
+ // Exclude functions, classes, interfaces, namespaces, type aliases
369
+ const isExcluded = actualSymbol.flags & (
370
+ ts.SymbolFlags.Function |
371
+ ts.SymbolFlags.Class |
372
+ ts.SymbolFlags.Interface |
373
+ ts.SymbolFlags.Enum |
374
+ ts.SymbolFlags.ValueModule |
375
+ ts.SymbolFlags.NamespaceModule |
376
+ ts.SymbolFlags.TypeAlias |
377
+ ts.SymbolFlags.TypeParameter
378
+ );
379
+
380
+ if (isExcluded) {
381
+ // Not a variable - it's a type, function, class, namespace, etc.
382
+ return undefined;
383
+ }
384
+
385
+ const isVariable = actualSymbol.flags & (
386
+ ts.SymbolFlags.Variable |
387
+ ts.SymbolFlags.Property |
388
+ ts.SymbolFlags.FunctionScopedVariable |
389
+ ts.SymbolFlags.BlockScopedVariable
390
+ );
391
+
392
+ if (!isVariable) {
393
+ // Not a variable we recognize
394
+ return undefined;
395
+ }
396
+
397
+ // Get the type of the variable
398
+ const variableType = this.checker.getTypeOfSymbolAtLocation(actualSymbol, location);
399
+ const mappedType = this.getType(variableType);
400
+
401
+ // Get the owner (declaring type) for the variable
402
+ let ownerType: Type | undefined;
403
+
404
+ // Check if the variable is imported
405
+ if (symbol.flags & ts.SymbolFlags.Alias) {
406
+ // For imported variables, find the module specifier
407
+ const declarations = symbol.declarations;
408
+ if (declarations && declarations.length > 0) {
409
+ let importNode: ts.Node | undefined = declarations[0];
410
+
411
+ // Traverse up to find the ImportDeclaration
412
+ while (importNode && !ts.isImportDeclaration(importNode)) {
413
+ importNode = importNode.parent;
414
+ }
415
+
416
+ if (importNode && ts.isImportDeclaration(importNode)) {
417
+ const importDecl = importNode as ts.ImportDeclaration;
418
+ if (ts.isStringLiteral(importDecl.moduleSpecifier)) {
419
+ const moduleSpecifier = importDecl.moduleSpecifier.text;
420
+ // Create a Type.Class representing the module
421
+ ownerType = Object.assign(new NonDraftableType(), {
422
+ kind: Type.Kind.Class,
423
+ flags: 0,
424
+ classKind: Type.Class.Kind.Interface,
425
+ fullyQualifiedName: moduleSpecifier,
426
+ typeParameters: [],
427
+ annotations: [],
428
+ interfaces: [],
429
+ members: [],
430
+ methods: [],
431
+ toJSON: function () {
432
+ return Type.signature(this);
433
+ }
434
+ }) as Type.Class;
435
+ }
436
+ }
437
+ }
438
+ } else {
439
+ // For non-imported variables, check if they belong to a class/interface/namespace
440
+ const parentSymbol = (actualSymbol as any).parent as ts.Symbol | undefined;
441
+ if (parentSymbol) {
442
+ const parentType = this.checker.getDeclaredTypeOfSymbol(parentSymbol);
443
+ if (parentType) {
444
+ ownerType = this.getType(parentType);
445
+
446
+ // If the parent is a namespace, try to find the module it came from
447
+ // This handles cases like React.forwardRef where the namespace is React
448
+ // but the module is "react"
449
+ if (parentSymbol.flags & ts.SymbolFlags.ValueModule ||
450
+ parentSymbol.flags & ts.SymbolFlags.NamespaceModule) {
451
+ // Check if this namespace was imported
452
+ const parentDeclarations = parentSymbol.declarations;
453
+ if (parentDeclarations && parentDeclarations.length > 0) {
454
+ const firstDecl = parentDeclarations[0];
455
+ const sourceFile = firstDecl.getSourceFile();
456
+ // If it's from node_modules or a .d.ts file, try to extract the module name
457
+ if (sourceFile.isDeclarationFile) {
458
+ const fileName = sourceFile.fileName;
459
+ const moduleName = this.extractModuleNameFromPath(fileName);
460
+ if (moduleName) {
461
+ // Store the module as the owningClass for now
462
+ // (This is a bit of a hack, but works with the current type system)
463
+ if (Type.isClass(ownerType)) {
464
+ (ownerType as any).owningClass = Object.assign(new NonDraftableType(), {
465
+ kind: Type.Kind.Class,
466
+ flags: 0,
467
+ classKind: Type.Class.Kind.Interface,
468
+ fullyQualifiedName: moduleName,
469
+ typeParameters: [],
470
+ annotations: [],
471
+ interfaces: [],
472
+ members: [],
473
+ methods: [],
474
+ toJSON: function () {
475
+ return Type.signature(this);
476
+ }
477
+ }) as Type.Class;
478
+ }
479
+ }
480
+ }
481
+ }
482
+ }
483
+ }
328
484
  }
329
485
  }
330
- return undefined;
486
+
487
+ // Create the Type.Variable
488
+ const variable = Object.assign(new NonDraftableType(), {
489
+ kind: Type.Kind.Variable,
490
+ name: actualSymbol.getName(),
491
+ owner: ownerType,
492
+ type: mappedType,
493
+ annotations: [],
494
+ toJSON: function () {
495
+ return Type.signature(this);
496
+ }
497
+ }) as Type.Variable;
498
+
499
+ return variable;
500
+ }
501
+
502
+ /**
503
+ * Extract the npm module name from a file path.
504
+ * Handles various package manager layouts:
505
+ * - Standard: /path/node_modules/react/index.d.ts -> react
506
+ * - Scoped: /path/node_modules/@types/react/index.d.ts -> react
507
+ * - Scoped with __ encoding: /path/node_modules/@types/testing-library__react/index.d.ts -> @testing-library/react
508
+ * - Nested node_modules: /path/node_modules/pkg/node_modules/dep/index.d.ts -> dep
509
+ * - pnpm: /path/node_modules/.pnpm/react@18.2.0/node_modules/react/index.d.ts -> react
510
+ *
511
+ * @returns The module name, or undefined if not from node_modules
512
+ */
513
+ private extractModuleNameFromPath(fileName: string): string | undefined {
514
+ if (!fileName.includes('node_modules/')) {
515
+ return undefined;
516
+ }
517
+
518
+ // Find the last occurrence of node_modules/ to handle nested dependencies
519
+ // This also correctly handles pnpm's .pnpm structure
520
+ const lastNodeModulesIndex = fileName.lastIndexOf('node_modules/');
521
+ const afterNodeModules = fileName.substring(lastNodeModulesIndex + 'node_modules/'.length);
522
+
523
+ // Split by '/' to get path segments
524
+ const segments = afterNodeModules.split('/');
525
+ if (segments.length === 0) {
526
+ return undefined;
527
+ }
528
+
529
+ let moduleName: string;
530
+
531
+ // Handle scoped packages (@scope/package)
532
+ if (segments[0].startsWith('@') && segments.length > 1) {
533
+ moduleName = `${segments[0]}/${segments[1]}`;
534
+ } else {
535
+ moduleName = segments[0];
536
+ }
537
+
538
+ // Skip pnpm's .pnpm directory - it contains versioned package paths
539
+ // In pnpm, the actual package is in: .pnpm/pkg@version/node_modules/pkg
540
+ // So we already handled this by using lastIndexOf above
541
+ if (moduleName === '.pnpm') {
542
+ return undefined;
543
+ }
544
+
545
+ // Remove @types/ prefix and decode DefinitelyTyped scoped package encoding
546
+ // DefinitelyTyped encodes scoped packages using __ instead of /
547
+ // Example: @types/testing-library__react -> @testing-library/react
548
+ if (moduleName.startsWith('@types/')) {
549
+ moduleName = moduleName.substring('@types/'.length);
550
+ // Decode __ encoding for scoped packages
551
+ // testing-library__react -> @testing-library/react
552
+ if (moduleName.includes('__')) {
553
+ const parts = moduleName.split('__');
554
+ if (parts.length === 2) {
555
+ moduleName = `@${parts[0]}/${parts[1]}`;
556
+ }
557
+ }
558
+ }
559
+
560
+ return moduleName;
331
561
  }
332
562
 
333
563
  /**
@@ -588,6 +588,14 @@ export class JavaScriptVisitor<P> extends JavaVisitor<P> {
588
588
  return updateIfChanged(scopedVariableDeclarations, updates);
589
589
  }
590
590
 
591
+ protected async visitShebang(shebang: JS.Shebang, p: P): Promise<J | undefined> {
592
+ const updates: any = {
593
+ prefix: await this.visitSpace(shebang.prefix, p),
594
+ markers: await this.visitMarkers(shebang.markers, p)
595
+ };
596
+ return updateIfChanged(shebang, updates);
597
+ }
598
+
591
599
  protected async visitStatementExpression(statementExpression: JS.StatementExpression, p: P): Promise<J | undefined> {
592
600
  const expression = await this.visitExpression(statementExpression, p);
593
601
  if (!expression?.kind || expression.kind !== JS.Kind.StatementExpression) {
@@ -1158,6 +1166,8 @@ export class JavaScriptVisitor<P> extends JavaVisitor<P> {
1158
1166
  return this.visitSatisfiesExpression(tree as unknown as JS.SatisfiesExpression, p);
1159
1167
  case JS.Kind.ScopedVariableDeclarations:
1160
1168
  return this.visitScopedVariableDeclarations(tree as unknown as JS.ScopedVariableDeclarations, p);
1169
+ case JS.Kind.Shebang:
1170
+ return this.visitShebang(tree as unknown as JS.Shebang, p);
1161
1171
  case JS.Kind.StatementExpression:
1162
1172
  return this.visitStatementExpression(tree as unknown as JS.StatementExpression, p);
1163
1173
  case JS.Kind.TaggedTemplateExpression:
@@ -90,11 +90,25 @@ class ParseJsonReader extends ParserSourceReader {
90
90
  })
91
91
  } satisfies Json.Object as Json.Object;
92
92
  } else if (typeof parsed === "string") {
93
- this.cursor += parsed.length + 2;
93
+ // Extract original source to preserve escape sequences
94
+ const sourceStart = this.cursor;
95
+ this.cursor++; // skip opening quote
96
+ while (this.cursor < this.source.length) {
97
+ const char = this.source[this.cursor];
98
+ if (char === '\\') {
99
+ this.cursor += 2; // skip escape sequence
100
+ } else if (char === '"') {
101
+ this.cursor++; // skip closing quote
102
+ break;
103
+ } else {
104
+ this.cursor++;
105
+ }
106
+ }
107
+ const source = this.source.slice(sourceStart, this.cursor);
94
108
  return {
95
109
  kind: Json.Kind.Literal,
96
110
  ...base,
97
- source: `"${parsed}"`,
111
+ source,
98
112
  value: parsed
99
113
  } satisfies Json.Literal as Json.Literal;
100
114
  } else if (typeof parsed === "number") {