@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,7 +15,7 @@
15
15
  */
16
16
  import {isTree} from '../..';
17
17
  import {emptySpace, J} from '../../java';
18
- import {emptyMarkers, Marker, markers} from '../../markers';
18
+ import {emptyMarkers, Marker, markers, Markers} from '../../markers';
19
19
  import {randomId} from '../../uuid';
20
20
  import {JS} from '..';
21
21
 
@@ -319,9 +319,12 @@ export function requiredPrecedence(parent: J, childId: string): number | undefin
319
319
  return slotConstraints(parent, childId)?.precedence;
320
320
  }
321
321
 
322
- /** Parenthesizes `expression` if the slot of `parent` holding `childId` would otherwise reparse it. */
322
+ /**
323
+ * Parenthesizes `expression` if the slot of `parent` holding `childId` would otherwise reparse it.
324
+ * `slotMarkers` are the markers the slot contributed; {@link parenthesize} says what they decide.
325
+ */
323
326
  export function maybeParenthesize(parent: J | undefined, childId: string, expression: J,
324
- slotOwnsTrailingMarkers: boolean = false): J {
327
+ slotMarkers?: Markers): J {
325
328
  if (!parent) {
326
329
  return expression;
327
330
  }
@@ -334,10 +337,10 @@ export function maybeParenthesize(parent: J | undefined, childId: string, expres
334
337
  // A statement wrapper is transparent here: the parentheses belong around the expression
335
338
  if (expression.kind === JS.Kind.ExpressionStatement) {
336
339
  const inner = (expression as JS.ExpressionStatement).expression;
337
- const wrapped = wrapIfNeeded(parent, childId, constraints, inner, slotOwnsTrailingMarkers);
340
+ const wrapped = wrapIfNeeded(parent, childId, constraints, inner, slotMarkers);
338
341
  return wrapped === inner ? expression : {...expression, expression: wrapped} as JS.ExpressionStatement;
339
342
  }
340
- return wrapIfNeeded(parent, childId, constraints, expression, slotOwnsTrailingMarkers);
343
+ return wrapIfNeeded(parent, childId, constraints, expression, slotMarkers);
341
344
  }
342
345
 
343
346
  /** The nearest enclosing LST node in a cursor path, skipping the padding wrappers visitors push. */
@@ -352,18 +355,23 @@ export function enclosingTree(cursor: { value: any, parent?: any } | undefined):
352
355
  return undefined;
353
356
  }
354
357
 
355
- /** Wraps in `J.Parentheses`, moving the prefix out so the surrounding whitespace survives. */
356
- export function parenthesize(expression: J, slotOwnsTrailingMarkers: boolean = false): J.Parentheses<J> {
357
- const trailing = slotOwnsTrailingMarkers ? expression.markers.markers.filter(isTrailingMarker) : [];
358
- const inner = trailing.length === 0 ? expression : {
358
+ /**
359
+ * Wraps in `J.Parentheses`, moving the prefix out so the surrounding whitespace survives. A trailing marker
360
+ * `slotMarkers` also carries belongs to the slot around the expression, so it moves out too: `${x}!` with `x`
361
+ * bound to `a + b` gives `(a + b)!`, while a bare `${x}` bound to `a!` gives `(a!)`.
362
+ */
363
+ export function parenthesize(expression: J, slotMarkers?: Markers): J.Parentheses<J> {
364
+ const hoisted = expression.markers.markers.filter(
365
+ m => isTrailingMarker(m) && slotMarkers?.markers.some(slot => slot.kind === m.kind));
366
+ const inner = hoisted.length === 0 ? expression : {
359
367
  ...expression,
360
- markers: markers(...expression.markers.markers.filter(m => !isTrailingMarker(m)))
368
+ markers: markers(...expression.markers.markers.filter(m => !hoisted.includes(m)))
361
369
  };
362
370
  return {
363
371
  kind: J.Kind.Parentheses,
364
372
  id: randomId(),
365
373
  prefix: expression.prefix,
366
- markers: trailing.length === 0 ? emptyMarkers : markers(...trailing),
374
+ markers: hoisted.length === 0 ? emptyMarkers : markers(...hoisted),
367
375
  tree: {
368
376
  kind: J.Kind.RightPadded,
369
377
  element: {...inner, prefix: emptySpace},
@@ -374,7 +382,7 @@ export function parenthesize(expression: J, slotOwnsTrailingMarkers: boolean = f
374
382
  }
375
383
 
376
384
  function wrapIfNeeded(parent: J, childId: string, constraints: SlotConstraints, expression: J,
377
- slotOwnsTrailingMarkers: boolean): J {
385
+ slotMarkers: Markers | undefined): J {
378
386
  if (precedenceOf(expression) < constraints.precedence ||
379
387
  (constraints.noCallShape && isCallShaped(expression)) ||
380
388
  (constraints.noOptionalChain && hasOptionalChain(expression)) ||
@@ -383,7 +391,7 @@ function wrapIfNeeded(parent: J, childId: string, constraints: SlotConstraints,
383
391
  (constraints.followedByDot && isDotAdjacentNumber(expression)) ||
384
392
  mixesNullishWithLogical(parent, expression) ||
385
393
  wouldFuseSigns(parent, childId, expression)) {
386
- return parenthesize(expression, slotOwnsTrailingMarkers);
394
+ return parenthesize(expression, slotMarkers);
387
395
  }
388
396
  return expression;
389
397
  }
@@ -58,7 +58,15 @@ class RewriteRuleImpl implements RewriteRule {
58
58
  let result: J | undefined;
59
59
 
60
60
  const template = typeof this.after === 'function' ? this.after(match) : this.after;
61
- const bindings = options?.bindings ?? (options?.visitor && template.resolveBindings(options.visitor));
61
+ const bindings = options?.bindings ??
62
+ (options?.visitor ? await template.resolveBindings(options.visitor) : undefined);
63
+ // Applying without them would splice the context's own names in unbound, which
64
+ // reads as a working edit and is not one.
65
+ if (bindings === undefined && await template.bindsModules()) {
66
+ throw new Error(
67
+ "Template binds modules in its context, so applying it needs their local names. " +
68
+ "Pass {visitor: this} to tryOn, or bindings you resolved yourself.");
69
+ }
62
70
  result = await template.apply(node, cursor,
63
71
  { values: match, format: this.format, bindings: bindings || undefined });
64
72
 
@@ -16,8 +16,8 @@
16
16
  import {Cursor, Tree} from '../..';
17
17
  import {J} from '../../java';
18
18
  import {ApplyOptions, Parameter, TemplateOptions, TemplateParameter} from './types';
19
- import {bindingContextStatement, isResolvable} from './bindings';
20
- import {maybeAddImport} from '../add-import';
19
+ import {maybeBind} from '../binding';
20
+ import {ContextBinding} from './engine';
21
21
  import {JavaScriptVisitor} from '../visitor';
22
22
  import {MatchResult} from './pattern';
23
23
  import {generateCacheKey, globalAstCache, WRAPPERS_MAP_SYMBOL} from './utils';
@@ -177,6 +177,7 @@ export class TemplateBuilder {
177
177
  export class Template {
178
178
  private options: TemplateOptions = {};
179
179
  private _cachedTemplate?: J;
180
+ private _contextBindings?: Promise<ContextBinding[]>;
180
181
 
181
182
  /**
182
183
  * Creates a new template.
@@ -221,9 +222,16 @@ export class Template {
221
222
  * })
222
223
  */
223
224
  configure(options: TemplateOptions): Template {
225
+ // The `never` applies only where a caller typechecks against it, so JavaScript and pre-#8685
226
+ // typings reach here, where a silently dropped option leaves the template's names unbound.
227
+ if ((options as { bindings?: unknown }).bindings !== undefined) {
228
+ throw new Error("Template option 'bindings' is now 'context': name each module as the " +
229
+ "import it stands for, e.g. context: [`import {vi} from 'vitest';`].");
230
+ }
224
231
  this.options = {...this.options, ...options};
225
232
  // Invalidate cache when configuration changes
226
233
  this._cachedTemplate = undefined;
234
+ this._contextBindings = undefined;
227
235
  return this;
228
236
  }
229
237
 
@@ -240,20 +248,16 @@ export class Template {
240
248
  * @returns The cached or newly computed template tree
241
249
  * @internal
242
250
  */
243
- private async getTemplateTree(): Promise<JS.CompilationUnit> {
251
+ private async getTemplateTree(): Promise<J> {
244
252
  // Level 1: Instance cache (fastest path)
245
253
  if (this._cachedTemplate) {
246
- return this._cachedTemplate as JS.CompilationUnit;
254
+ return this._cachedTemplate;
247
255
  }
248
256
 
249
257
  // Generate cache key for global lookup
250
258
  // For raw() parameters, we need to include their code values in the key
251
259
  // since they're spliced at construction time, not application time
252
- const contextStatements = [
253
- ...(this.options.context || this.options.imports || []),
254
- ...Object.entries(this.options.bindings ?? {})
255
- .map(([name, b]) => bindingContextStatement(name, b, this.options.dependencies ?? {}))
256
- ];
260
+ const contextStatements = this.options.context || this.options.imports || [];
257
261
  const parametersKey = this.parameters.map((p, i) => {
258
262
  const value = p.value;
259
263
  // Include raw code values in the cache key using the symbol
@@ -267,14 +271,15 @@ export class Template {
267
271
  parametersKey,
268
272
  // As in Pattern.getAstPattern: a parameter's type reaches the parse as a declaration
269
273
  [...contextStatements, ...TemplateEngine.parameterPreamble(this.parameters)],
270
- this.options.dependencies || {}
274
+ this.options.dependencies || {},
275
+ this.options.types
271
276
  );
272
277
 
273
278
  // Level 2: Global cache (fast path - shared with Pattern)
274
279
  const cached = globalAstCache.get(cacheKey);
275
280
  if (cached) {
276
- this._cachedTemplate = cached as JS.CompilationUnit;
277
- return cached as JS.CompilationUnit;
281
+ this._cachedTemplate = cached;
282
+ return cached;
278
283
  }
279
284
 
280
285
  // Level 3: Compute via TemplateEngine (slow path)
@@ -282,8 +287,9 @@ export class Template {
282
287
  this.templateParts,
283
288
  this.parameters,
284
289
  contextStatements,
285
- this.options.dependencies || {}
286
- ) as JS.CompilationUnit;
290
+ this.options.dependencies || {},
291
+ this.options.types
292
+ );
287
293
 
288
294
  // Cache in both levels
289
295
  globalAstCache.set(cacheKey, result);
@@ -298,18 +304,47 @@ export class Template {
298
304
  * cannot resolve is bound whether or not the template goes on to reference it, so call this
299
305
  * where the template is known to apply — {@link RewriteRule.tryOn} does, once a pattern matched.
300
306
  */
301
- resolveBindings(visitor: JavaScriptVisitor<any>): Record<string, string> {
307
+ async resolveBindings(visitor: JavaScriptVisitor<any>): Promise<Record<string, string>> {
302
308
  const resolved: Record<string, string> = {};
303
- const dependencies = this.options.dependencies ?? {};
304
- for (const [name, binding] of Object.entries(this.options.bindings ?? {})) {
305
- // Recognising the reference the template splices in takes attribution, which only the
306
- // import form of a context statement carries. Without one there is nothing to look for.
307
- const onlyIfReferenced = isResolvable(binding.module, dependencies);
308
- resolved[name] = maybeAddImport(visitor, {...binding, preferredName: name, onlyIfReferenced});
309
+ for (const binding of await this.contextBindings()) {
310
+ // Recognising the reference the template splices in takes attribution, so a module the
311
+ // workspace could not resolve is bound whether or not the template turns out to use it.
312
+ const onlyIfReferenced = binding.attributed;
313
+ const bound = maybeBind(visitor, {
314
+ module: binding.module!,
315
+ member: binding.member,
316
+ typeOnly: binding.typeOnly,
317
+ preferredName: binding.name,
318
+ onlyIfReferenced
319
+ });
320
+ // An unresolved binding is left out rather than recorded as `undefined`, so `apply()`'s
321
+ // own "applied without a local name" check catches it, same as a caller-omitted one.
322
+ if (bound !== undefined) {
323
+ resolved[binding.name] = bound;
324
+ }
309
325
  }
310
326
  return resolved;
311
327
  }
312
328
 
329
+ /** Whether the context binds a module, which applying this template therefore has to resolve. */
330
+ async bindsModules(): Promise<boolean> {
331
+ return (await this.contextBindings()).length > 0;
332
+ }
333
+
334
+ /** What this template's context statements bind, which is what it needs bound in the target file. */
335
+ private contextBindings(): Promise<ContextBinding[]> {
336
+ return this._contextBindings ??= this.deriveContextBindings();
337
+ }
338
+
339
+ private deriveContextBindings(): Promise<ContextBinding[]> {
340
+ return TemplateEngine.getContextBindings(
341
+ this.templateParts, this.parameters,
342
+ this.options.context || this.options.imports || [],
343
+ this.options.dependencies || {},
344
+ this.options.types
345
+ );
346
+ }
347
+
313
348
  /**
314
349
  * Applies this template and returns the resulting tree.
315
350
  *
@@ -372,17 +407,21 @@ export class Template {
372
407
  }
373
408
  }
374
409
 
375
- const declared = this.options.bindings ?? {};
376
410
  const renames: Record<string, string> = {};
377
411
  const modules: Record<string, string> = {};
378
- for (const [name, binding] of Object.entries(declared)) {
379
- const bound = options?.bindings?.[name];
380
- if (bound === undefined) {
381
- throw new Error(`Template declares a binding for '${name}' but was applied without a local name for it. ` +
382
- `Pass bindings: template.resolveBindings(visitor) to apply().`);
412
+ // Supplying names is what asks for the context's modules to be bound in the file being
413
+ // edited; without them a context import only types the template.
414
+ if (options?.bindings !== undefined) {
415
+ for (const binding of await this.contextBindings()) {
416
+ const bound = options.bindings[binding.name];
417
+ if (bound === undefined) {
418
+ throw new Error(`Template binds '${binding.module}' in its context, but no local name was ` +
419
+ `given for '${binding.name}'. Either pass bindings from resolveBindings(visitor), or — if it ` +
420
+ `already did — binding was refused, which an AMD block or a file requiring its modules can do.`);
421
+ }
422
+ renames[binding.name] = bound;
423
+ modules[binding.name] = binding.module!;
383
424
  }
384
- renames[name] = bound;
385
- modules[name] = binding.module;
386
425
  }
387
426
 
388
427
  // Use instance-level cache to get the template tree
@@ -18,7 +18,6 @@ import {J, Type} from '../../java';
18
18
  import type {Pattern} from "./pattern";
19
19
  import type {Template} from "./template";
20
20
  import type {CaptureValue, RawCode} from "./capture";
21
- import type {AddImportOptions} from "../add-import";
22
21
  import type {JavaScriptVisitor} from "../visitor";
23
22
 
24
23
  /**
@@ -350,6 +349,12 @@ export interface PatternOptions {
350
349
  */
351
350
  dependencies?: Record<string, string>;
352
351
 
352
+ /**
353
+ * Type packages to load whose declarations nothing imports, as {@link TemplateOptions.types}.
354
+ * A pattern matches on attribution, so a module typed only ambiently needs this to match.
355
+ */
356
+ types?: string[];
357
+
353
358
  /**
354
359
  * When true, allows patterns without type annotations to match code with type annotations.
355
360
  * This enables more flexible pattern matching during development or when full type attribution
@@ -485,26 +490,20 @@ export interface TemplateOptions {
485
490
  dependencies?: Record<string, string>;
486
491
 
487
492
  /**
488
- * Modules the template's code refers to, keyed by the identifier its source uses for each.
489
- * The key is a preferred name: {@link Template.resolveBindings} deconflicts it against the
490
- * file, and applying the template rewrites the template's references to whatever it settled on.
493
+ * Type packages to load whose declarations nothing imports. TypeScript reads `@types/*` on its
494
+ * own and everything else only when named here, so a package declaring its modules ambiently —
495
+ * rather than at a path matching the specifier — resolves only with this set.
491
496
  *
492
- * @example
493
- * ```typescript
494
- * template`Theming.setTheme(${capture('theme')})`
495
- * .configure({bindings: {Theming: {module: 'sap/ui/core/Theming', member: 'default'}}})
496
- * ```
497
+ * @example `{dependencies: {'@sapui5/types': '^1.120.0'}, types: ['@sapui5/types']}`
497
498
  */
498
- bindings?: Record<string, ModuleBinding>;
499
- }
499
+ types?: string[];
500
500
 
501
- /**
502
- * A module a template's code depends on. The local name comes from the key it is declared under,
503
- * and {@link Template.resolveBindings} settles the rest. `member` and `typeOnly` shape the import
504
- * it creates, so a caller passing {@link ApplyOptions.bindings} of its own reads neither.
505
- */
506
- export type ModuleBinding = Omit<AddImportOptions,
507
- 'module' | 'alias' | 'preferredName' | 'onlyIfReferenced' | 'sideEffectOnly'> & { module: string };
501
+ /**
502
+ * @deprecated Name the module in {@link TemplateOptions.context} as the import it stands for:
503
+ * `bindings: {vi: {module: 'vitest', member: 'vi'}}` is `context: ["import {vi} from 'vitest';"]`.
504
+ */
505
+ bindings?: never;
506
+ }
508
507
 
509
508
  /**
510
509
  * Options for template application.
@@ -107,13 +107,15 @@ export function generateCacheKey(
107
107
  templateParts: string[] | TemplateStringsArray,
108
108
  itemsKey: string,
109
109
  contextStatements: string[],
110
- dependencies: Record<string, string>
110
+ dependencies: Record<string, string>,
111
+ types?: string[]
111
112
  ): string {
112
113
  return [
113
114
  Array.from(templateParts).join('|'),
114
115
  itemsKey,
115
116
  contextStatements.join(';'),
116
- JSON.stringify(dependencies)
117
+ JSON.stringify(dependencies),
118
+ JSON.stringify(types ?? null)
117
119
  ].join('::');
118
120
  }
119
121
 
@@ -417,6 +417,7 @@ export namespace JS {
417
417
  */
418
418
  export interface PropertyAssignment extends JS, Statement, TypedTree {
419
419
  readonly kind: typeof Kind.PropertyAssignment;
420
+ readonly modifiers: J.Modifier[];
420
421
  readonly name: J.RightPadded<Expression>;
421
422
  readonly assigmentToken: PropertyAssignment.Token;
422
423
  readonly initializer?: Expression;
@@ -19,6 +19,20 @@ import {Type} from "../java";
19
19
  import FUNCTION_TYPE_NAME = Type.FUNCTION_TYPE_NAME;
20
20
  import OBJECT_TYPE_NAME = Type.OBJECT_TYPE_NAME;
21
21
 
22
+ /**
23
+ * TypeScript orders union constituents by internal type id, which varies with what the checker resolved
24
+ * earlier in the run; ordering by signature keeps a union's signature the same across parses. Intersections
25
+ * keep the order they were written in and are left alone. Call once `bounds` is attached to its owner, so
26
+ * a constituent that reaches back into it reads the whole constituent list rather than an empty one.
27
+ */
28
+ function sortBySignature(bounds: Type[]): void {
29
+ const keys = new Map<Type, string>(bounds.map(b => [b, Type.signature(b)]));
30
+ bounds.sort((a, b) => {
31
+ const ka = keys.get(a)!, kb = keys.get(b)!;
32
+ return ka < kb ? -1 : ka > kb ? 1 : 0;
33
+ });
34
+ }
35
+
22
36
  export class JavaScriptTypeMapping {
23
37
  // Primary cache: Use type signatures (preferring type.id) as cache keys
24
38
  // TypeScript assigns stable IDs to all types, so we don't need secondary caches
@@ -1610,6 +1624,7 @@ export class JavaScriptTypeMapping {
1610
1624
 
1611
1625
  // Update the bounds in the union we created
1612
1626
  (union as any).bounds = bounds;
1627
+ sortBySignature(bounds);
1613
1628
 
1614
1629
  return union;
1615
1630
  }
@@ -551,6 +551,7 @@ export class JavaScriptVisitor<P> extends JavaVisitor<P> {
551
551
  const updates: any = {
552
552
  prefix: await this.visitSpace(propertyAssignment.prefix, p),
553
553
  markers: await this.visitMarkers(propertyAssignment.markers, p),
554
+ modifiers: await mapAsync(propertyAssignment.modifiers, m => this.visitDefined<J.Modifier>(m, p)),
554
555
  name: await this.visitRightPadded(propertyAssignment.name, p),
555
556
  initializer: propertyAssignment.initializer && await this.visitDefined<Expression>(propertyAssignment.initializer, p)
556
557
  };
package/src/parser.ts CHANGED
@@ -13,8 +13,10 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
+ import {createTwoFilesPatch} from "diff";
16
17
  import {ExecutionContext} from "./execution";
17
18
  import {SourceFile} from "./tree";
19
+ import {TreePrinters} from "./print";
18
20
  import fs, {readFileSync} from "node:fs";
19
21
  import {isAbsolute, relative} from "path";
20
22
  import {ParseError, ParseErrorKind} from "./parse-error";
@@ -79,6 +81,27 @@ export abstract class Parser {
79
81
  return isAbsolute(path) && this.relativeTo ? relative(this.relativeTo, path) : path;
80
82
  }
81
83
 
84
+ /**
85
+ * A parse result that does not print back to its input has silently lost source, so it is reported as
86
+ * a `ParseError` carrying the tree that lost it. Mirrors `org.openrewrite.Parser`, down to the message.
87
+ * A parser whose LST cannot hold some byte of its input passes the form it can hold as `source`.
88
+ */
89
+ protected async requirePrintEqualsInput(sourceFile: SourceFile, input: ParserInput,
90
+ source: string = parserInputRead(input)): Promise<SourceFile> {
91
+ if (!this.ctx.getBoolean(ExecutionContext.REQUIRE_PRINT_EQUALS_INPUT, true)) {
92
+ return sourceFile;
93
+ }
94
+ const printed = await TreePrinters.print(sourceFile);
95
+ if (printed === source) {
96
+ return sourceFile;
97
+ }
98
+ const path = this.relativePath(input);
99
+ const diff = createTwoFilesPatch(path, path, source, printed, "", "", {context: 3});
100
+ const error = this.error(input, new Error(`${sourceFile.sourcePath} is not print idempotent. \n${diff}`));
101
+ const withErroneous: ParseError = {...error, erroneous: sourceFile};
102
+ return withErroneous;
103
+ }
104
+
82
105
  protected error(input: ParserInput, e: Error): ParseError {
83
106
  return {
84
107
  kind: ParseErrorKind,
@@ -88,7 +111,7 @@ export abstract class Parser {
88
111
  id: randomId(),
89
112
  parserType: this.constructor.name,
90
113
  exceptionType: e.name,
91
- message: e.message + ':\n' + e.stack,
114
+ message: e.stack ?? e.message,
92
115
  } satisfies ParseExceptionResult as ParseExceptionResult),
93
116
  text: parserInputRead(input),
94
117
  sourcePath: this.relativePath(input),
package/src/print.ts CHANGED
@@ -162,8 +162,11 @@ export class TreePrinters {
162
162
  return this._registry.get(sourceFileKind)!;
163
163
  }
164
164
 
165
- static print(sourceFile: SourceFile): Promise<string> {
166
- return this.printer(sourceFile).print(sourceFile);
165
+ static async print(sourceFile: SourceFile): Promise<string> {
166
+ const printed = await this.printer(sourceFile).print(sourceFile);
167
+ // The BOM is a property of the file's encoding rather than of its syntax, so it is held on the
168
+ // source file and put back here. Mirrors `org.openrewrite.Tree.print`.
169
+ return sourceFile.charsetBomMarked && printed.charAt(0) !== "\uFEFF" ? "\uFEFF" + printed : printed;
167
170
  }
168
171
 
169
172
  static async printTrimmed(sourceFile: SourceFile): Promise<string> {
@@ -55,7 +55,11 @@ export class ParseProject {
55
55
  * If not specified, paths are relative to projectPath.
56
56
  * Use this when parsing a subdirectory but wanting paths relative to the repository root.
57
57
  */
58
- private readonly relativeTo?: string
58
+ private readonly relativeTo?: string,
59
+ /**
60
+ * Parser options, as on the `Parse` request.
61
+ */
62
+ private readonly options?: { [key: string]: string }
59
63
  ) {}
60
64
 
61
65
  static handle(
@@ -85,7 +89,7 @@ export class ParseProject {
85
89
  const prettierLoader = await projectParser.createPrettierLoader();
86
90
 
87
91
  const resultItems: ParseProjectResponseItem[] = [];
88
- const ctx = new ExecutionContext();
92
+ const ctx = new ExecutionContext({...request.options});
89
93
 
90
94
  // Parse package.json files (these get NodeResolutionResult markers)
91
95
  if (discovered.packageJsonFiles.length > 0) {
@@ -23,7 +23,12 @@ import * as path from "path";
23
23
 
24
24
  export class Parse {
25
25
  constructor(private readonly inputs: ParserInput[],
26
- private readonly relativeTo?: string) {
26
+ private readonly relativeTo?: string,
27
+ /**
28
+ * Parser options from the peer. Keys this handler does not recognize are ignored, and a
29
+ * peer that sends none gets the parser's own defaults.
30
+ */
31
+ private readonly options?: { [key: string]: string }) {
27
32
  }
28
33
 
29
34
  /**
@@ -58,7 +63,7 @@ export class Parse {
58
63
  : "javascript";
59
64
 
60
65
  const parser = Parsers.createParser(parserType, {
61
- ctx: new ExecutionContext(),
66
+ ctx: new ExecutionContext({...request.options}),
62
67
  relativeTo: request.relativeTo
63
68
  })!;
64
69