@openrewrite/rewrite 8.91.1 → 8.91.3

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 (160) hide show
  1. package/dist/execution.d.ts +8 -0
  2. package/dist/execution.d.ts.map +1 -1
  3. package/dist/execution.js +31 -0
  4. package/dist/execution.js.map +1 -1
  5. package/dist/javascript/add-import.d.ts +127 -38
  6. package/dist/javascript/add-import.d.ts.map +1 -1
  7. package/dist/javascript/add-import.js +684 -195
  8. package/dist/javascript/add-import.js.map +1 -1
  9. package/dist/javascript/amd.d.ts +148 -0
  10. package/dist/javascript/amd.d.ts.map +1 -0
  11. package/dist/javascript/amd.js +802 -0
  12. package/dist/javascript/amd.js.map +1 -0
  13. package/dist/javascript/binding.d.ts +102 -0
  14. package/dist/javascript/binding.d.ts.map +1 -0
  15. package/dist/javascript/binding.js +308 -0
  16. package/dist/javascript/binding.js.map +1 -0
  17. package/dist/javascript/comparator.d.ts +2 -0
  18. package/dist/javascript/comparator.d.ts.map +1 -1
  19. package/dist/javascript/comparator.js +5 -1
  20. package/dist/javascript/comparator.js.map +1 -1
  21. package/dist/javascript/format/format.d.ts +5 -2
  22. package/dist/javascript/format/format.d.ts.map +1 -1
  23. package/dist/javascript/format/format.js +51 -11
  24. package/dist/javascript/format/format.js.map +1 -1
  25. package/dist/javascript/format/minimum-viable-spacing-visitor.d.ts +19 -0
  26. package/dist/javascript/format/minimum-viable-spacing-visitor.d.ts.map +1 -1
  27. package/dist/javascript/format/minimum-viable-spacing-visitor.js +180 -7
  28. package/dist/javascript/format/minimum-viable-spacing-visitor.js.map +1 -1
  29. package/dist/javascript/format/prettier-format.d.ts +2 -1
  30. package/dist/javascript/format/prettier-format.d.ts.map +1 -1
  31. package/dist/javascript/format/prettier-format.js +33 -23
  32. package/dist/javascript/format/prettier-format.js.map +1 -1
  33. package/dist/javascript/index.d.ts +8 -1
  34. package/dist/javascript/index.d.ts.map +1 -1
  35. package/dist/javascript/index.js +46 -1
  36. package/dist/javascript/index.js.map +1 -1
  37. package/dist/javascript/parser-utils.d.ts.map +1 -1
  38. package/dist/javascript/parser-utils.js +7 -9
  39. package/dist/javascript/parser-utils.js.map +1 -1
  40. package/dist/javascript/parser.d.ts +7 -1
  41. package/dist/javascript/parser.d.ts.map +1 -1
  42. package/dist/javascript/parser.js +55 -4
  43. package/dist/javascript/parser.js.map +1 -1
  44. package/dist/javascript/print.d.ts +1 -0
  45. package/dist/javascript/print.d.ts.map +1 -1
  46. package/dist/javascript/print.js +9 -0
  47. package/dist/javascript/print.js.map +1 -1
  48. package/dist/javascript/recipes/change-import.d.ts +4 -3
  49. package/dist/javascript/recipes/change-import.d.ts.map +1 -1
  50. package/dist/javascript/recipes/change-import.js +12 -504
  51. package/dist/javascript/recipes/change-import.js.map +1 -1
  52. package/dist/javascript/remove-import.d.ts +2 -28
  53. package/dist/javascript/remove-import.d.ts.map +1 -1
  54. package/dist/javascript/remove-import.js +19 -57
  55. package/dist/javascript/remove-import.js.map +1 -1
  56. package/dist/javascript/rpc.js +2 -0
  57. package/dist/javascript/rpc.js.map +1 -1
  58. package/dist/javascript/scope.d.ts +82 -0
  59. package/dist/javascript/scope.d.ts.map +1 -0
  60. package/dist/javascript/scope.js +476 -0
  61. package/dist/javascript/scope.js.map +1 -0
  62. package/dist/javascript/templating/bindings.d.ts +0 -11
  63. package/dist/javascript/templating/bindings.d.ts.map +1 -1
  64. package/dist/javascript/templating/bindings.js +2 -49
  65. package/dist/javascript/templating/bindings.js.map +1 -1
  66. package/dist/javascript/templating/comparator.d.ts +12 -0
  67. package/dist/javascript/templating/comparator.d.ts.map +1 -1
  68. package/dist/javascript/templating/comparator.js +68 -14
  69. package/dist/javascript/templating/comparator.js.map +1 -1
  70. package/dist/javascript/templating/engine.d.ts +22 -4
  71. package/dist/javascript/templating/engine.d.ts.map +1 -1
  72. package/dist/javascript/templating/engine.js +68 -24
  73. package/dist/javascript/templating/engine.js.map +1 -1
  74. package/dist/javascript/templating/index.d.ts +1 -1
  75. package/dist/javascript/templating/index.d.ts.map +1 -1
  76. package/dist/javascript/templating/index.js.map +1 -1
  77. package/dist/javascript/templating/pattern.d.ts.map +1 -1
  78. package/dist/javascript/templating/pattern.js +2 -2
  79. package/dist/javascript/templating/pattern.js.map +1 -1
  80. package/dist/javascript/templating/placeholder-replacement.d.ts +2 -0
  81. package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -1
  82. package/dist/javascript/templating/placeholder-replacement.js +10 -5
  83. package/dist/javascript/templating/placeholder-replacement.js.map +1 -1
  84. package/dist/javascript/templating/precedence.d.ts +12 -4
  85. package/dist/javascript/templating/precedence.d.ts.map +1 -1
  86. package/dist/javascript/templating/precedence.js +18 -11
  87. package/dist/javascript/templating/precedence.js.map +1 -1
  88. package/dist/javascript/templating/rewrite.d.ts.map +1 -1
  89. package/dist/javascript/templating/rewrite.js +7 -1
  90. package/dist/javascript/templating/rewrite.js.map +1 -1
  91. package/dist/javascript/templating/template.d.ts +7 -1
  92. package/dist/javascript/templating/template.d.ts.map +1 -1
  93. package/dist/javascript/templating/template.js +54 -28
  94. package/dist/javascript/templating/template.js.map +1 -1
  95. package/dist/javascript/templating/types.d.ts +15 -18
  96. package/dist/javascript/templating/types.d.ts.map +1 -1
  97. package/dist/javascript/templating/utils.d.ts +1 -1
  98. package/dist/javascript/templating/utils.d.ts.map +1 -1
  99. package/dist/javascript/templating/utils.js +3 -2
  100. package/dist/javascript/templating/utils.js.map +1 -1
  101. package/dist/javascript/tree.d.ts +1 -0
  102. package/dist/javascript/tree.d.ts.map +1 -1
  103. package/dist/javascript/tree.js.map +1 -1
  104. package/dist/javascript/type-mapping.d.ts.map +1 -1
  105. package/dist/javascript/type-mapping.js +14 -0
  106. package/dist/javascript/type-mapping.js.map +1 -1
  107. package/dist/javascript/visitor.d.ts.map +1 -1
  108. package/dist/javascript/visitor.js +1 -0
  109. package/dist/javascript/visitor.js.map +1 -1
  110. package/dist/parser.d.ts +6 -0
  111. package/dist/parser.d.ts.map +1 -1
  112. package/dist/parser.js +23 -1
  113. package/dist/parser.js.map +1 -1
  114. package/dist/print.d.ts.map +1 -1
  115. package/dist/print.js +5 -2
  116. package/dist/print.js.map +1 -1
  117. package/dist/rewrite-javascript-version.txt +1 -1
  118. package/dist/rpc/request/parse-project.d.ts +11 -1
  119. package/dist/rpc/request/parse-project.d.ts.map +1 -1
  120. package/dist/rpc/request/parse-project.js +7 -2
  121. package/dist/rpc/request/parse-project.js.map +1 -1
  122. package/dist/rpc/request/parse.d.ts +13 -1
  123. package/dist/rpc/request/parse.d.ts.map +1 -1
  124. package/dist/rpc/request/parse.js +8 -2
  125. package/dist/rpc/request/parse.js.map +1 -1
  126. package/package.json +2 -2
  127. package/src/execution.ts +23 -0
  128. package/src/javascript/add-import.ts +753 -238
  129. package/src/javascript/amd.ts +954 -0
  130. package/src/javascript/binding.ts +435 -0
  131. package/src/javascript/comparator.ts +6 -1
  132. package/src/javascript/format/format.ts +56 -12
  133. package/src/javascript/format/minimum-viable-spacing-visitor.ts +196 -7
  134. package/src/javascript/format/prettier-format.ts +35 -25
  135. package/src/javascript/index.ts +19 -1
  136. package/src/javascript/parser-utils.ts +7 -11
  137. package/src/javascript/parser.ts +78 -6
  138. package/src/javascript/print.ts +10 -0
  139. package/src/javascript/recipes/change-import.ts +13 -593
  140. package/src/javascript/remove-import.ts +20 -56
  141. package/src/javascript/rpc.ts +2 -0
  142. package/src/javascript/scope.ts +517 -0
  143. package/src/javascript/templating/bindings.ts +2 -55
  144. package/src/javascript/templating/comparator.ts +81 -16
  145. package/src/javascript/templating/engine.ts +107 -38
  146. package/src/javascript/templating/index.ts +0 -1
  147. package/src/javascript/templating/pattern.ts +4 -2
  148. package/src/javascript/templating/placeholder-replacement.ts +16 -6
  149. package/src/javascript/templating/precedence.ts +21 -13
  150. package/src/javascript/templating/rewrite.ts +9 -1
  151. package/src/javascript/templating/template.ts +68 -29
  152. package/src/javascript/templating/types.ts +17 -18
  153. package/src/javascript/templating/utils.ts +4 -2
  154. package/src/javascript/tree.ts +1 -0
  155. package/src/javascript/type-mapping.ts +15 -0
  156. package/src/javascript/visitor.ts +1 -0
  157. package/src/parser.ts +24 -1
  158. package/src/print.ts +5 -2
  159. package/src/rpc/request/parse-project.ts +6 -2
  160. package/src/rpc/request/parse.ts +7 -2
@@ -15,37 +15,7 @@
15
15
  */
16
16
  import {J, Type} from '../../java';
17
17
  import {JavaScriptVisitor} from '../visitor';
18
- import {Cursor} from '../../tree';
19
- import {ModuleBinding} from './types';
20
-
21
- /**
22
- * Whether the template's dependencies bring a workspace that could resolve `module`.
23
- */
24
- export function isResolvable(module: string, dependencies: Record<string, string>): boolean {
25
- const segments = module.split('/');
26
- const pkg = module.startsWith('@') ? segments.slice(0, 2).join('/') : segments[0];
27
- return Object.prototype.hasOwnProperty.call(dependencies, pkg);
28
- }
29
-
30
- /**
31
- * What a declared binding's name is parsed against, ahead of the template and so out of its output.
32
- * An import is what carries attribution, and costs a module resolution to get it; a declaration
33
- * names the binding for a module no workspace could have resolved anyway.
34
- */
35
- export function bindingContextStatement(name: string, binding: ModuleBinding, dependencies: Record<string, string>): string {
36
- if (!isResolvable(binding.module, dependencies)) {
37
- return binding.typeOnly ? `type ${name} = any;` : `declare const ${name}: any;`;
38
- }
39
- const type = binding.typeOnly ? 'type ' : '';
40
- if (binding.member === '*') {
41
- return `import ${type}* as ${name} from '${binding.module}';`;
42
- }
43
- if (binding.member === undefined || binding.member === 'default') {
44
- return `import ${type}${name} from '${binding.module}';`;
45
- }
46
- const specifier = binding.member === name ? name : `${binding.member} as ${name}`;
47
- return `import ${type}{${specifier}} from '${binding.module}';`;
48
- }
18
+ import {isValueReference} from '../scope';
49
19
 
50
20
  /**
51
21
  * Renames the identifiers a template uses for its declared bindings to the names the file
@@ -73,35 +43,12 @@ class RenameBindingsVisitor extends JavaScriptVisitor<undefined> {
73
43
  const resolved = resolvedModule(identifier);
74
44
  const refersToBinding = resolved !== undefined
75
45
  ? resolved === this.modules[identifier.simpleName]
76
- : !namesItsParent(this.cursor, identifier);
46
+ : isValueReference(this.cursor, identifier);
77
47
 
78
48
  return refersToBinding ? {...identifier, simpleName: renamed} as J.Identifier : identifier;
79
49
  }
80
50
  }
81
51
 
82
- /** Whether the parent is naming this identifier — as a property, a method, a declaration — rather than referencing it. */
83
- function namesItsParent(cursor: Cursor, identifier: J.Identifier): boolean {
84
- let c: Cursor | undefined = cursor.parent;
85
- while (c && isPadding(c.value)) {
86
- c = c.parent;
87
- }
88
- const parent = c?.value as { kind?: string; name?: unknown; select?: unknown } | undefined;
89
-
90
- // A call names a member of whatever it selects from. With nothing selected there is no member,
91
- // and its `name` is a reference to the function being called.
92
- if (parent?.kind === J.Kind.MethodInvocation && !parent.select) {
93
- return false;
94
- }
95
-
96
- const name = parent?.name;
97
- return name === identifier || (name as { element?: unknown } | undefined)?.element === identifier;
98
- }
99
-
100
- function isPadding(value: unknown): boolean {
101
- const kind = (value as { kind?: string } | undefined)?.kind;
102
- return kind === J.Kind.RightPadded || kind === J.Kind.LeftPadded || kind === J.Kind.Container;
103
- }
104
-
105
52
  /** The module an identifier's attribution traces back to, following the owning-class chain to its root. */
106
53
  function resolvedModule(identifier: J.Identifier): string | undefined {
107
54
  const fieldType = identifier.fieldType;
@@ -13,8 +13,8 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import {Cursor, Tree} from '../..';
17
- import {J} from '../../java';
16
+ import {Cursor, Markers, Tree} from '../..';
17
+ import {J, Type} from '../../java';
18
18
  import {JS} from '../index';
19
19
  import {JavaScriptSemanticComparatorVisitor} from '../comparator';
20
20
  import {CaptureMarker, CaptureStorageValue, PlaceholderUtils} from './utils';
@@ -85,6 +85,17 @@ class CaptureMapImpl implements CaptureMap {
85
85
  }
86
86
  }
87
87
 
88
+ /** A capture found in a pattern, and the target node it binds to. */
89
+ interface CaptureAt {
90
+ marker: CaptureMarker;
91
+ target: J;
92
+ }
93
+
94
+ /** Parentheses that `JavaScriptSemanticComparatorVisitor.unwrap()` sees through. */
95
+ function isParenthesized(j: any): boolean {
96
+ return j?.kind === J.Kind.Parentheses || j?.kind === J.Kind.ControlParentheses;
97
+ }
98
+
88
99
  /**
89
100
  * A comparator for pattern matching that is lenient about optional properties.
90
101
  * Allows patterns without type annotations to match actual code with type annotations.
@@ -112,33 +123,59 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit
112
123
  };
113
124
  }
114
125
 
126
+ /** The capture this pattern node stands for, paired with the target node it should bind to. */
127
+ protected captureAt(j: Tree, p: J): CaptureAt | undefined {
128
+ const marker = PlaceholderUtils.getCaptureMarker(j as J);
129
+ return marker ? {marker, target: p} : this.captureBehindParentheses(j, p);
130
+ }
131
+
132
+ /** The marker `attachCaptureMarkers` moved onto a parenthesized capture's `J.RightPadded`, which `unwrap()` skips past. */
133
+ private captureBehindParentheses(j: Tree, p: J): CaptureAt | undefined {
134
+ let pattern: any = j;
135
+ let target: any = p;
136
+ while (isParenthesized(pattern)) {
137
+ const padded = pattern.tree as J.RightPadded<J> | undefined;
138
+ if (!padded) {
139
+ return undefined;
140
+ }
141
+ target = isParenthesized(target) ? target.tree.element : target;
142
+ const marker = PlaceholderUtils.getCaptureMarker(padded);
143
+ if (marker) {
144
+ return {marker, target: target as J};
145
+ }
146
+ pattern = padded.element;
147
+ }
148
+ return undefined;
149
+ }
150
+
115
151
  override async visit<R extends J>(j: Tree, p: J, parent?: Cursor): Promise<R | undefined> {
116
152
  // Check if the pattern node is a capture - this handles unwrapped captures
117
153
  // (Wrapped captures in J.RightPadded are handled by visitRightPadded override)
118
154
  // Note: targetCursor will be pushed by parent's visit() method after this check
119
- const captureMarker = PlaceholderUtils.getCaptureMarker(j)!;
120
- if (captureMarker) {
155
+ const capture = this.captureAt(j, p);
156
+ if (capture) {
157
+ const {marker: captureMarker, target} = capture;
121
158
 
122
159
  // Push targetCursor to position it at the captured node for constraint evaluation
123
160
  // Only create cursor if targetCursor was initialized (meaning user provided one)
124
161
  const savedTargetCursor = this.targetCursor;
125
162
  const cursorAtCapturedNode = this.targetCursor !== undefined
126
- ? new Cursor(p, this.targetCursor)
127
- : new Cursor(p);
163
+ ? new Cursor(target, this.targetCursor)
164
+ : new Cursor(target);
128
165
  this.targetCursor = cursorAtCapturedNode;
129
166
  try {
130
167
  // Evaluate constraint with context (cursor + previous captures)
131
168
  // Skip constraint for variadic captures - they're evaluated in matchSequence with the full array
132
169
  if (captureMarker.constraint && !captureMarker.variadicOptions) {
133
170
  const context = this.buildConstraintContext(cursorAtCapturedNode);
134
- if (!captureMarker.constraint(p, context)) {
171
+ if (!captureMarker.constraint(target, context)) {
135
172
  const captureName = captureMarker.captureName || 'unnamed';
136
- const targetKind = (p as any).kind || 'unknown';
173
+ const targetKind = (target as any).kind || 'unknown';
137
174
  return this.constraintFailed(captureName, targetKind) as R;
138
175
  }
139
176
  }
140
177
 
141
- const success = this.matcher.handleCapture(captureMarker, p, undefined);
178
+ const success = this.matcher.handleCapture(captureMarker, target, undefined);
142
179
  if (!success) {
143
180
  const captureName = captureMarker.captureName || 'unnamed';
144
181
  return this.captureConflict(captureName) as R;
@@ -157,6 +194,11 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit
157
194
  return await super.visit(j, p, parent);
158
195
  }
159
196
 
197
+ /** A capture in the receiver has to bind, so a matching `methodType` cannot stand in for it. */
198
+ protected override selectMustBeVisited(method: J.MethodInvocation): boolean {
199
+ return method.select !== undefined && containsCapture(method.select);
200
+ }
201
+
160
202
  protected hasSameKind(j: J, other: J): boolean {
161
203
  return super.hasSameKind(j, other) ||
162
204
  (j.kind == J.Kind.Identifier && PlaceholderUtils.isCapture(j as J.Identifier));
@@ -896,26 +938,27 @@ export class DebugPatternMatchingComparator extends PatternMatchingComparator {
896
938
  }
897
939
 
898
940
  override async visit<R extends J>(j: Tree, p: J, parent?: Cursor): Promise<R | undefined> {
899
- const captureMarker = PlaceholderUtils.getCaptureMarker(j)!;
900
- if (captureMarker) {
941
+ const capture = this.captureAt(j, p);
942
+ if (capture) {
943
+ const {marker: captureMarker, target} = capture;
901
944
  const savedTargetCursor = this.targetCursor;
902
945
  const cursorAtCapturedNode = this.targetCursor !== undefined
903
- ? new Cursor(p, this.targetCursor)
904
- : new Cursor(p);
946
+ ? new Cursor(target, this.targetCursor)
947
+ : new Cursor(target);
905
948
  this.targetCursor = cursorAtCapturedNode;
906
949
  try {
907
950
  if (captureMarker.constraint && !captureMarker.variadicOptions) {
908
951
  this.debug.log('debug', 'constraint', `Evaluating constraint for capture: ${captureMarker.captureName}`);
909
- const constraintResult = captureMarker.constraint(p, this.buildConstraintContext(cursorAtCapturedNode));
952
+ const constraintResult = captureMarker.constraint(target, this.buildConstraintContext(cursorAtCapturedNode));
910
953
  if (!constraintResult) {
911
954
  this.debug.log('info', 'constraint', `Constraint failed for capture: ${captureMarker.captureName}`);
912
- this.debug.setExplanation('constraint-failed', `Capture ${captureMarker.captureName} with valid constraint`, `Constraint failed for ${(p as any).kind}`, `Constraint evaluation returned false`);
955
+ this.debug.setExplanation('constraint-failed', `Capture ${captureMarker.captureName} with valid constraint`, `Constraint failed for ${(target as any).kind}`, `Constraint evaluation returned false`);
913
956
  return this.abort(j) as R;
914
957
  }
915
958
  this.debug.log('debug', 'constraint', `Constraint passed for capture: ${captureMarker.captureName}`);
916
959
  }
917
960
 
918
- const success = this.matcher.handleCapture(captureMarker, p, undefined);
961
+ const success = this.matcher.handleCapture(captureMarker, target, undefined);
919
962
  if (!success) {
920
963
  return this.abort(j) as R;
921
964
  }
@@ -1419,3 +1462,25 @@ export class DebugPatternMatchingComparator extends PatternMatchingComparator {
1419
1462
  }
1420
1463
  }
1421
1464
  }
1465
+
1466
+ /** Whether anything under `node`, the padding wrappers a marker moves onto included, is a capture. */
1467
+ function containsCapture(node: unknown): boolean {
1468
+ if (node === null || typeof node !== 'object') {
1469
+ return false;
1470
+ }
1471
+ if (Array.isArray(node)) {
1472
+ return node.some(containsCapture);
1473
+ }
1474
+ const record = node as Record<string, unknown>;
1475
+ if (typeof record.kind !== 'string') {
1476
+ return false;
1477
+ }
1478
+ // Attribution is a cyclic graph, and a capture is never reachable through one
1479
+ if (Type.isType(node)) {
1480
+ return false;
1481
+ }
1482
+ if (record.markers && PlaceholderUtils.getCaptureMarker(node as { markers: Markers })) {
1483
+ return true;
1484
+ }
1485
+ return Object.entries(record).some(([key, value]) => key !== 'markers' && containsCapture(value));
1486
+ }
@@ -28,6 +28,15 @@ import {isExpression, isStatement} from '../parser-utils';
28
28
  import {randomId} from '../../uuid';
29
29
  import ts from "typescript";
30
30
  import {DependencyWorkspace} from "../dependency-workspace";
31
+ import {ModuleScopeBinding, moduleScopeBindings} from '../add-import';
32
+ import {walk} from '../scope';
33
+ import {isIdentifier} from '../../java';
34
+ import {findMarker, MarkersKind, ParseExceptionResult} from '../../markers';
35
+
36
+ /** A module a template's context binds, and whether the parse resolved it well enough to attribute. */
37
+ export interface ContextBinding extends ModuleScopeBinding {
38
+ attributed: boolean;
39
+ }
31
40
  import {Parameter} from "./types";
32
41
 
33
42
  /**
@@ -90,11 +99,13 @@ export function setTemplateSourceFileCache(cache?: Map<string, ts.SourceFile>):
90
99
  // from one compile to the next; see `JavaScriptParser.parse`.
91
100
  const templateParsers: Map<string, JavaScriptParser> = new Map();
92
101
 
93
- function templateParser(workspaceDir?: string): JavaScriptParser {
94
- const key = workspaceDir ?? "";
102
+ function templateParser(workspaceDir?: string, types?: string[]): JavaScriptParser {
103
+ // `types` changes what the compiler loads, so parsers cannot be shared across differing sets.
104
+ // An empty list loads nothing where absence loads the defaults, so the two encode apart.
105
+ const key = `${workspaceDir ?? ""}::${JSON.stringify(types ?? null)}`;
95
106
  let parser = templateParsers.get(key);
96
107
  if (!parser) {
97
- parser = new JavaScriptParser({relativeTo: workspaceDir, sourceFileCache: templateSourceFileCache});
108
+ parser = new JavaScriptParser({relativeTo: workspaceDir, sourceFileCache: templateSourceFileCache, types});
98
109
  templateParsers.set(key, parser);
99
110
  }
100
111
  return parser;
@@ -115,7 +126,8 @@ class TemplateCache {
115
126
  templateString: string,
116
127
  captures: (Capture | Any)[],
117
128
  contextStatements: string[],
118
- dependencies: Record<string, string>
129
+ dependencies: Record<string, string>,
130
+ types: string[] | undefined
119
131
  ): string {
120
132
  // Use the actual template string (with placeholders) as the primary key
121
133
  const templateKey = templateString;
@@ -129,7 +141,7 @@ class TemplateCache {
129
141
  // Dependencies
130
142
  const depsKey = JSON.stringify(dependencies || {});
131
143
 
132
- return `${templateKey}::${capturesKey}::${contextKey}::${depsKey}`;
144
+ return `${templateKey}::${capturesKey}::${contextKey}::${depsKey}::${JSON.stringify(types ?? null)}`;
133
145
  }
134
146
 
135
147
  /**
@@ -139,9 +151,10 @@ class TemplateCache {
139
151
  templateString: string,
140
152
  captures: (Capture | Any)[],
141
153
  contextStatements: string[],
142
- dependencies: Record<string, string>
154
+ dependencies: Record<string, string>,
155
+ types?: string[]
143
156
  ): Promise<JS.CompilationUnit> {
144
- const key = this.generateKey(templateString, captures, contextStatements, dependencies);
157
+ const key = this.generateKey(templateString, captures, contextStatements, dependencies, types);
145
158
 
146
159
  let cu = this.cache.get(key);
147
160
  if (cu) {
@@ -163,7 +176,7 @@ class TemplateCache {
163
176
 
164
177
  // Parse and cache (workspace only needed during parsing)
165
178
  // Use templateSourceFileCache if configured for ~3.2x speedup on dependency file parsing
166
- const parser = templateParser(workspaceDir);
179
+ const parser = templateParser(workspaceDir, types);
167
180
  const parseGenerator = parser.parse({text: fullTemplateString, sourcePath: 'template.tsx'});
168
181
  cu = (await parseGenerator.next()).value as JS.CompilationUnit;
169
182
 
@@ -193,6 +206,20 @@ export function clearTemplateCache(): void {
193
206
  templateCache.clear();
194
207
  }
195
208
 
209
+ /** Whether a name reads as a TypeScript type reference: a dotted chain of identifiers. */
210
+ function isTypeReference(name: string): boolean {
211
+ return /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)*$/.test(name);
212
+ }
213
+
214
+ /**
215
+ * The compiler's diagnostic for code that did not parse, as one line: the marker states it ahead
216
+ * of a stack, and its positions index the whole parsed text, context statements included.
217
+ */
218
+ function parseFailureReason(cu: JS.CompilationUnit): string {
219
+ const failure = findMarker<ParseExceptionResult>(cu, MarkersKind.ParseExceptionResult);
220
+ return failure ? failure.message.split('\n')[0].replace(/:$/, '') : 'no statements';
221
+ }
222
+
196
223
  /**
197
224
  * Internal template engine - handles the core templating logic.
198
225
  * Not exported from index, so only visible within the templating module.
@@ -208,35 +235,77 @@ export class TemplateEngine {
208
235
  * @param dependencies NPM dependencies for type attribution
209
236
  * @returns A Promise resolving to the extracted template AST
210
237
  */
211
- static async getTemplateTree(
238
+ /** The template parsed with its context, which is what gives its code types to attribute against. */
239
+ private static async parseWithContext(
212
240
  templateParts: TemplateStringsArray,
213
241
  parameters: Parameter[],
214
- contextStatements: string[] = [],
215
- dependencies: Record<string, string> = {}
216
- ): Promise<J> {
217
- // Generate type preamble for captures/parameters with types
242
+ contextStatements: string[],
243
+ dependencies: Record<string, string>,
244
+ types: string[] | undefined
245
+ ): Promise<JS.CompilationUnit> {
246
+ // A capture's declared type reaches the parse as a declaration, so it belongs with context.
218
247
  const preamble = TemplateEngine.parameterPreamble(parameters);
219
-
220
- // Build the template string with parameter placeholders
221
248
  const templateString = TemplateEngine.buildTemplateString(templateParts, parameters);
222
-
223
- // Add preamble to context statements (so they're skipped during extraction)
224
249
  const contextWithPreamble = preamble.length > 0
225
250
  ? [...contextStatements, ...preamble]
226
251
  : contextStatements;
252
+ return TemplateEngine.parseOrThrow('template', templateString, [], contextWithPreamble, dependencies, types);
253
+ }
227
254
 
228
- // Use cache to get or parse the compilation unit
229
- const cu = await templateCache.getOrParse(
230
- templateString,
231
- [],
232
- contextWithPreamble,
233
- dependencies
234
- );
235
-
236
- // Check if there are any statements
255
+ /**
256
+ * The parse behind every template and pattern. Code that fails to parse yields a compilation
257
+ * unit with no statements, which every caller goes on to read, so it fails here naming the code.
258
+ */
259
+ private static async parseOrThrow(
260
+ kind: 'template' | 'pattern',
261
+ templateString: string,
262
+ captures: (Capture | Any)[],
263
+ contextStatements: string[],
264
+ dependencies: Record<string, string>,
265
+ types: string[] | undefined
266
+ ): Promise<JS.CompilationUnit> {
267
+ const cu = await templateCache.getOrParse(templateString, captures, contextStatements, dependencies, types);
237
268
  if (!cu.statements || cu.statements.length === 0) {
238
- throw new Error(`Failed to parse template code (no statements):\n${templateString}`);
269
+ throw new Error(`Failed to parse ${kind} code (${parseFailureReason(cu)}):\n${templateString}`);
239
270
  }
271
+ return cu;
272
+ }
273
+
274
+ /**
275
+ * The modules the template's context binds, for the caller to bind in the file being edited.
276
+ * An `import` or `require` states one; anything else — a `declare`, a helper signature — types
277
+ * the template without asking for a binding.
278
+ */
279
+ static async getContextBindings(
280
+ templateParts: TemplateStringsArray,
281
+ parameters: Parameter[],
282
+ contextStatements: string[] = [],
283
+ dependencies: Record<string, string> = {},
284
+ types?: string[]
285
+ ): Promise<ContextBinding[]> {
286
+ const cu = await TemplateEngine.parseWithContext(templateParts, parameters, contextStatements, dependencies, types);
287
+ // The template's own code is the last statement, so everything ahead of it is context.
288
+ const context = {...cu, statements: cu.statements.slice(0, -1)};
289
+ const attributed = new Set<string>();
290
+ walk(context.statements, node => {
291
+ if (isIdentifier(node) && (node.type !== undefined || node.fieldType !== undefined)) {
292
+ attributed.add(node.simpleName);
293
+ }
294
+ return true;
295
+ });
296
+ return moduleScopeBindings(context)
297
+ .filter(b => b.module !== undefined)
298
+ .map(b => ({...b, attributed: attributed.has(b.name)}));
299
+ }
300
+
301
+ static async getTemplateTree(
302
+ templateParts: TemplateStringsArray,
303
+ parameters: Parameter[],
304
+ contextStatements: string[] = [],
305
+ dependencies: Record<string, string> = {},
306
+ types?: string[]
307
+ ): Promise<J> {
308
+ const cu = await TemplateEngine.parseWithContext(templateParts, parameters, contextStatements, dependencies, types);
240
309
 
241
310
  // The template code is always the last statement (after context + preamble)
242
311
  const lastStatement = cu.statements[cu.statements.length - 1].element;
@@ -263,7 +332,7 @@ export class TemplateEngine {
263
332
  * @returns A Promise resolving to the generated AST node
264
333
  */
265
334
  static async applyTemplateFromAst(
266
- ast: JS.CompilationUnit,
335
+ ast: J,
267
336
  parameters: Parameter[],
268
337
  cursor: Cursor,
269
338
  coordinates: JavaCoordinates,
@@ -439,7 +508,10 @@ export class TemplateEngine {
439
508
  // Handle Type.Class and Type.ShallowClass - return their fully qualified names
440
509
  if (type.kind === Type.Kind.Class || type.kind === Type.Kind.ShallowClass) {
441
510
  const classType = type as Type.Class;
442
- return classType.fullyQualifiedName;
511
+ // A module qualifies its types by where they come from — `src/a.Foo` — and a path that
512
+ // is no type reference cannot be declared for. Naming a module's type takes a `context`
513
+ // statement of the caller's own, which `type` then names.
514
+ return isTypeReference(classType.fullyQualifiedName) ? classType.fullyQualifiedName : 'any';
443
515
  }
444
516
 
445
517
  // Handle Type.Primitive - map to TypeScript primitive types
@@ -490,7 +562,8 @@ export class TemplateEngine {
490
562
  templateParts: TemplateStringsArray,
491
563
  captures: (Capture | Any | RawCode)[],
492
564
  contextStatements: string[] = [],
493
- dependencies: Record<string, string> = {}
565
+ dependencies: Record<string, string> = {},
566
+ types?: string[]
494
567
  ): Promise<J> {
495
568
  const preamble = TemplateEngine.capturePreamble(captures);
496
569
 
@@ -526,19 +599,15 @@ export class TemplateEngine {
526
599
  !(c instanceof RawCode || (c && typeof c === 'object' && (c as any)[RAW_CODE_SYMBOL]))
527
600
  ) as (Capture | Any)[];
528
601
 
529
- // Use cache to get or parse the compilation unit
530
- const cu = await templateCache.getOrParse(
602
+ const cu = await TemplateEngine.parseOrThrow(
603
+ 'pattern',
531
604
  templateString,
532
605
  actualCaptures,
533
606
  contextWithPreamble,
534
- dependencies
607
+ dependencies,
608
+ types
535
609
  );
536
610
 
537
- // Check if there are any statements
538
- if (!cu.statements || cu.statements.length === 0) {
539
- throw new Error(`Failed to parse pattern code (no statements):\n${templateString}`);
540
- }
541
-
542
611
  // The pattern code is always the last statement (after context + preamble)
543
612
  const lastStatement = cu.statements[cu.statements.length - 1].element;
544
613
 
@@ -25,7 +25,6 @@ export type {
25
25
  MatchOptions,
26
26
  TemplateParameter,
27
27
  TemplateOptions,
28
- ModuleBinding,
29
28
  RewriteRule,
30
29
  TryOnOptions,
31
30
  RewriteConfig,
@@ -236,7 +236,8 @@ export class Pattern {
236
236
  // A capture's type reaches the parse as a declaration, so it shapes the tree the same
237
237
  // way an explicit context statement does and belongs in the key alongside one.
238
238
  [...contextStatements, ...TemplateEngine.capturePreamble(this.captures)],
239
- this._options.dependencies || {}
239
+ this._options.dependencies || {},
240
+ this._options.types
240
241
  );
241
242
 
242
243
  // Level 2: Global cache (fast path - shared with Template)
@@ -247,7 +248,8 @@ export class Pattern {
247
248
  this.templateParts,
248
249
  this.captures,
249
250
  contextStatements,
250
- this._options.dependencies || {}
251
+ this._options.dependencies || {},
252
+ this._options.types
251
253
  );
252
254
  globalAstCache.set(cacheKey, tree);
253
255
  }
@@ -13,7 +13,7 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import {Cursor, isTree} from '../..';
16
+ import {Cursor, isTree, Markers} from '../..';
17
17
  import {J} from '../../java';
18
18
  import {JS} from '..';
19
19
  import {JavaScriptVisitor} from '../visitor';
@@ -42,7 +42,7 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
42
42
  const replacement = this.replacePlaceholder(tree);
43
43
  if (replacement !== tree) {
44
44
  // `this.cursor` is still the enclosing template node: `super.visit()` has not pushed this one
45
- return maybeParenthesize(enclosingTree(parent ?? this.cursor), tree.id, replacement, true) as R;
45
+ return maybeParenthesize(enclosingTree(parent ?? this.cursor), tree.id, replacement, tree.markers) as R;
46
46
  }
47
47
  }
48
48
 
@@ -181,6 +181,16 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
181
181
  sourcePrefix : this.mergePrefix(sourcePrefix, templatePrefix);
182
182
  }
183
183
 
184
+ /** As `mergePrefix`, for markers: everything the value was matched with, plus the kinds only the slot wrote. */
185
+ private mergeMarkers(sourceMarkers: Markers, templateMarkers: Markers): Markers {
186
+ const added = templateMarkers.markers.filter(
187
+ template => !sourceMarkers.markers.some(source => source.kind === template.kind));
188
+ return added.length === 0 ? sourceMarkers : {
189
+ ...sourceMarkers,
190
+ markers: [...sourceMarkers.markers, ...added]
191
+ };
192
+ }
193
+
184
194
  /**
185
195
  * Expands variadic placeholders in a list of elements.
186
196
  *
@@ -386,7 +396,7 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
386
396
  if (isTree(propertyValue)) {
387
397
  const propValueAsJ = propertyValue as J;
388
398
  return produce(propValueAsJ, draft => {
389
- draft.markers = placeholder.markers;
399
+ draft.markers = this.mergeMarkers(propValueAsJ.markers, placeholder.markers);
390
400
  draft.prefix = this.mergePrefix(propValueAsJ.prefix, placeholder.prefix);
391
401
  });
392
402
  }
@@ -421,7 +431,7 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
421
431
  const matchedNode = this.values.get(name);
422
432
  if (matchedNode && !Array.isArray(matchedNode)) {
423
433
  return produce(matchedNode, draft => {
424
- draft.markers = placeholder.markers;
434
+ draft.markers = this.mergeMarkers(matchedNode.markers, placeholder.markers);
425
435
  draft.prefix = this.mergePrefix(matchedNode.prefix, placeholder.prefix);
426
436
  });
427
437
  }
@@ -438,7 +448,7 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
438
448
  // Extract the element from the J.RightPadded wrapper
439
449
  const element = param.value.element as J;
440
450
  return produce(element, draft => {
441
- draft.markers = placeholder.markers;
451
+ draft.markers = this.mergeMarkers(element.markers, placeholder.markers);
442
452
  draft.prefix = this.mergePrefix(element.prefix, placeholder.prefix);
443
453
  });
444
454
  }
@@ -458,7 +468,7 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
458
468
  if (isTree(param.value)) {
459
469
  // Return the AST node, preserving comments from the source
460
470
  return produce(param.value as J, draft => {
461
- draft.markers = placeholder.markers;
471
+ draft.markers = this.mergeMarkers(param.value.markers, placeholder.markers);
462
472
  draft.prefix = this.mergePrefix(param.value.prefix, placeholder.prefix);
463
473
  });
464
474
  }