@openrewrite/rewrite 8.91.0 → 8.91.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/dist/java/formatting-utils.d.ts +8 -5
  2. package/dist/java/formatting-utils.d.ts.map +1 -1
  3. package/dist/java/formatting-utils.js +14 -10
  4. package/dist/java/formatting-utils.js.map +1 -1
  5. package/dist/java/type.d.ts +2 -0
  6. package/dist/java/type.d.ts.map +1 -1
  7. package/dist/java/type.js +5 -0
  8. package/dist/java/type.js.map +1 -1
  9. package/dist/javascript/add-import.d.ts +96 -43
  10. package/dist/javascript/add-import.d.ts.map +1 -1
  11. package/dist/javascript/add-import.js +668 -236
  12. package/dist/javascript/add-import.js.map +1 -1
  13. package/dist/javascript/amd.d.ts +138 -0
  14. package/dist/javascript/amd.d.ts.map +1 -0
  15. package/dist/javascript/amd.js +768 -0
  16. package/dist/javascript/amd.js.map +1 -0
  17. package/dist/javascript/assertions.d.ts +7 -0
  18. package/dist/javascript/assertions.d.ts.map +1 -1
  19. package/dist/javascript/assertions.js +31 -5
  20. package/dist/javascript/assertions.js.map +1 -1
  21. package/dist/javascript/binding.d.ts +94 -0
  22. package/dist/javascript/binding.d.ts.map +1 -0
  23. package/dist/javascript/binding.js +335 -0
  24. package/dist/javascript/binding.js.map +1 -0
  25. package/dist/javascript/comparator.d.ts +5 -0
  26. package/dist/javascript/comparator.d.ts.map +1 -1
  27. package/dist/javascript/comparator.js +34 -9
  28. package/dist/javascript/comparator.js.map +1 -1
  29. package/dist/javascript/format/format.d.ts.map +1 -1
  30. package/dist/javascript/format/format.js +8 -9
  31. package/dist/javascript/format/format.js.map +1 -1
  32. package/dist/javascript/format/tabs-and-indents-visitor.d.ts +2 -0
  33. package/dist/javascript/format/tabs-and-indents-visitor.d.ts.map +1 -1
  34. package/dist/javascript/format/tabs-and-indents-visitor.js +17 -11
  35. package/dist/javascript/format/tabs-and-indents-visitor.js.map +1 -1
  36. package/dist/javascript/index.d.ts +8 -1
  37. package/dist/javascript/index.d.ts.map +1 -1
  38. package/dist/javascript/index.js +26 -1
  39. package/dist/javascript/index.js.map +1 -1
  40. package/dist/javascript/node-resolution-result.d.ts +19 -1
  41. package/dist/javascript/node-resolution-result.d.ts.map +1 -1
  42. package/dist/javascript/node-resolution-result.js +37 -2
  43. package/dist/javascript/node-resolution-result.js.map +1 -1
  44. package/dist/javascript/package-exported-types.d.ts.map +1 -1
  45. package/dist/javascript/package-exported-types.js +56 -72
  46. package/dist/javascript/package-exported-types.js.map +1 -1
  47. package/dist/javascript/parser-utils.d.ts.map +1 -1
  48. package/dist/javascript/parser-utils.js +2 -0
  49. package/dist/javascript/parser-utils.js.map +1 -1
  50. package/dist/javascript/parser.d.ts +7 -1
  51. package/dist/javascript/parser.d.ts.map +1 -1
  52. package/dist/javascript/parser.js +17 -4
  53. package/dist/javascript/parser.js.map +1 -1
  54. package/dist/javascript/recipes/change-import.d.ts +2 -4
  55. package/dist/javascript/recipes/change-import.d.ts.map +1 -1
  56. package/dist/javascript/recipes/change-import.js +8 -244
  57. package/dist/javascript/recipes/change-import.js.map +1 -1
  58. package/dist/javascript/remove-import.d.ts +2 -28
  59. package/dist/javascript/remove-import.d.ts.map +1 -1
  60. package/dist/javascript/remove-import.js +19 -57
  61. package/dist/javascript/remove-import.js.map +1 -1
  62. package/dist/javascript/scope.d.ts +49 -0
  63. package/dist/javascript/scope.d.ts.map +1 -0
  64. package/dist/javascript/scope.js +324 -0
  65. package/dist/javascript/scope.js.map +1 -0
  66. package/dist/javascript/style.d.ts.map +1 -1
  67. package/dist/javascript/style.js +3 -10
  68. package/dist/javascript/style.js.map +1 -1
  69. package/dist/javascript/templating/bindings.d.ts +8 -0
  70. package/dist/javascript/templating/bindings.d.ts.map +1 -0
  71. package/dist/javascript/templating/bindings.js +92 -0
  72. package/dist/javascript/templating/bindings.js.map +1 -0
  73. package/dist/javascript/templating/comparator.d.ts +10 -0
  74. package/dist/javascript/templating/comparator.d.ts.map +1 -1
  75. package/dist/javascript/templating/comparator.js +48 -20
  76. package/dist/javascript/templating/comparator.js.map +1 -1
  77. package/dist/javascript/templating/engine.d.ts +34 -10
  78. package/dist/javascript/templating/engine.d.ts.map +1 -1
  79. package/dist/javascript/templating/engine.js +109 -55
  80. package/dist/javascript/templating/engine.js.map +1 -1
  81. package/dist/javascript/templating/index.d.ts +1 -1
  82. package/dist/javascript/templating/index.d.ts.map +1 -1
  83. package/dist/javascript/templating/index.js.map +1 -1
  84. package/dist/javascript/templating/pattern.d.ts.map +1 -1
  85. package/dist/javascript/templating/pattern.js +13 -10
  86. package/dist/javascript/templating/pattern.js.map +1 -1
  87. package/dist/javascript/templating/placeholder-replacement.d.ts +4 -0
  88. package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -1
  89. package/dist/javascript/templating/placeholder-replacement.js +24 -10
  90. package/dist/javascript/templating/placeholder-replacement.js.map +1 -1
  91. package/dist/javascript/templating/precedence.d.ts +12 -4
  92. package/dist/javascript/templating/precedence.d.ts.map +1 -1
  93. package/dist/javascript/templating/precedence.js +18 -11
  94. package/dist/javascript/templating/precedence.js.map +1 -1
  95. package/dist/javascript/templating/rewrite.d.ts.map +1 -1
  96. package/dist/javascript/templating/rewrite.js +19 -17
  97. package/dist/javascript/templating/rewrite.js.map +1 -1
  98. package/dist/javascript/templating/template.d.ts +14 -0
  99. package/dist/javascript/templating/template.d.ts.map +1 -1
  100. package/dist/javascript/templating/template.js +63 -3
  101. package/dist/javascript/templating/template.js.map +1 -1
  102. package/dist/javascript/templating/types.d.ts +39 -1
  103. package/dist/javascript/templating/types.d.ts.map +1 -1
  104. package/dist/javascript/templating/utils.d.ts +6 -1
  105. package/dist/javascript/templating/utils.d.ts.map +1 -1
  106. package/dist/javascript/templating/utils.js +20 -2
  107. package/dist/javascript/templating/utils.js.map +1 -1
  108. package/dist/javascript/type-mapping.d.ts +1 -0
  109. package/dist/javascript/type-mapping.d.ts.map +1 -1
  110. package/dist/javascript/type-mapping.js +52 -2
  111. package/dist/javascript/type-mapping.js.map +1 -1
  112. package/dist/rewrite-javascript-version.txt +1 -1
  113. package/dist/style.d.ts +2 -0
  114. package/dist/style.d.ts.map +1 -1
  115. package/dist/style.js +5 -0
  116. package/dist/style.js.map +1 -1
  117. package/dist/test/rewrite-test.d.ts +12 -0
  118. package/dist/test/rewrite-test.d.ts.map +1 -1
  119. package/dist/test/rewrite-test.js +40 -3
  120. package/dist/test/rewrite-test.js.map +1 -1
  121. package/package.json +2 -2
  122. package/src/java/formatting-utils.ts +14 -10
  123. package/src/java/type.ts +5 -0
  124. package/src/javascript/add-import.ts +751 -247
  125. package/src/javascript/amd.ts +915 -0
  126. package/src/javascript/assertions.ts +33 -5
  127. package/src/javascript/binding.ts +449 -0
  128. package/src/javascript/comparator.ts +39 -10
  129. package/src/javascript/format/format.ts +9 -10
  130. package/src/javascript/format/tabs-and-indents-visitor.ts +18 -12
  131. package/src/javascript/index.ts +12 -1
  132. package/src/javascript/node-resolution-result.ts +59 -4
  133. package/src/javascript/package-exported-types.ts +60 -74
  134. package/src/javascript/parser-utils.ts +2 -0
  135. package/src/javascript/parser.ts +23 -3
  136. package/src/javascript/recipes/change-import.ts +10 -263
  137. package/src/javascript/remove-import.ts +20 -56
  138. package/src/javascript/scope.ts +338 -0
  139. package/src/javascript/style.ts +3 -11
  140. package/src/javascript/templating/bindings.ts +99 -0
  141. package/src/javascript/templating/comparator.ts +57 -20
  142. package/src/javascript/templating/engine.ts +148 -68
  143. package/src/javascript/templating/index.ts +1 -0
  144. package/src/javascript/templating/pattern.ts +19 -16
  145. package/src/javascript/templating/placeholder-replacement.ts +32 -11
  146. package/src/javascript/templating/precedence.ts +21 -13
  147. package/src/javascript/templating/rewrite.ts +23 -17
  148. package/src/javascript/templating/template.ts +84 -9
  149. package/src/javascript/templating/types.ts +46 -1
  150. package/src/javascript/templating/utils.ts +21 -2
  151. package/src/javascript/type-mapping.ts +54 -2
  152. package/src/style.ts +5 -0
  153. package/src/test/rewrite-test.ts +61 -3
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import {Cursor, ExecutionContext, Recipe, TreeVisitor} from '../..';
17
17
  import {J, Statement} from '../../java';
18
- import {PostMatchContext, PreMatchContext, RewriteConfig, RewriteRule} from './types';
18
+ import {PostMatchContext, PreMatchContext, RewriteConfig, RewriteRule, TryOnOptions} from './types';
19
19
  import {MatchResult, Pattern} from './pattern';
20
20
  import {Template} from './template';
21
21
  import {JavaScriptVisitor} from '../visitor';
@@ -28,11 +28,12 @@ class RewriteRuleImpl implements RewriteRule {
28
28
  private readonly before: Pattern[],
29
29
  private readonly after: Template | ((match: MatchResult) => Template),
30
30
  private readonly preMatch?: (node: J, context: PreMatchContext) => boolean | Promise<boolean>,
31
- private readonly postMatch?: (node: J, context: PostMatchContext) => boolean | Promise<boolean>
31
+ private readonly postMatch?: (node: J, context: PostMatchContext) => boolean | Promise<boolean>,
32
+ private readonly format?: boolean
32
33
  ) {
33
34
  }
34
35
 
35
- async tryOn(cursor: Cursor, node: J): Promise<J | undefined> {
36
+ async tryOn(cursor: Cursor, node: J, options?: TryOnOptions): Promise<J | undefined> {
36
37
  // Evaluate preMatch before attempting any pattern matching
37
38
  if (this.preMatch) {
38
39
  const preMatchResult = await this.preMatch(node, { cursor });
@@ -56,14 +57,18 @@ class RewriteRuleImpl implements RewriteRule {
56
57
  // Apply transformation
57
58
  let result: J | undefined;
58
59
 
59
- if (typeof this.after === 'function') {
60
- // Call the function to get a template, then apply it
61
- const template = this.after(match);
62
- result = await template.apply(node, cursor, { values: match });
63
- } else {
64
- // Use template.apply() as before
65
- result = await this.after.apply(node, cursor, { values: match });
60
+ const template = typeof this.after === 'function' ? this.after(match) : this.after;
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.");
66
69
  }
70
+ result = await template.apply(node, cursor,
71
+ { values: match, format: this.format, bindings: bindings || undefined });
67
72
 
68
73
  if (result) {
69
74
  return result;
@@ -84,10 +89,10 @@ class RewriteRuleImpl implements RewriteRule {
84
89
  super([], () => undefined as unknown as Template);
85
90
  }
86
91
 
87
- async tryOn(cursor: Cursor, node: J): Promise<J | undefined> {
88
- const firstResult = await first.tryOn(cursor, node);
92
+ async tryOn(cursor: Cursor, node: J, options?: TryOnOptions): Promise<J | undefined> {
93
+ const firstResult = await first.tryOn(cursor, node, options);
89
94
  if (firstResult !== undefined) {
90
- const secondResult = await next.tryOn(cursor, firstResult);
95
+ const secondResult = await next.tryOn(cursor, firstResult, options);
91
96
  return secondResult ?? firstResult;
92
97
  }
93
98
  return undefined;
@@ -104,12 +109,12 @@ class RewriteRuleImpl implements RewriteRule {
104
109
  super([], () => undefined as unknown as Template);
105
110
  }
106
111
 
107
- async tryOn(cursor: Cursor, node: J): Promise<J | undefined> {
108
- const firstResult = await first.tryOn(cursor, node);
112
+ async tryOn(cursor: Cursor, node: J, options?: TryOnOptions): Promise<J | undefined> {
113
+ const firstResult = await first.tryOn(cursor, node, options);
109
114
  if (firstResult !== undefined) {
110
115
  return firstResult;
111
116
  }
112
- return await alternative.tryOn(cursor, node);
117
+ return await alternative.tryOn(cursor, node, options);
113
118
  }
114
119
  })();
115
120
  }
@@ -171,7 +176,8 @@ export function rewrite(
171
176
  Array.isArray(config.before) ? config.before : [config.before],
172
177
  config.after,
173
178
  config.preMatch,
174
- config.postMatch
179
+ config.postMatch,
180
+ config.format
175
181
  );
176
182
  }
177
183
 
@@ -16,6 +16,9 @@
16
16
  import {Cursor, Tree} from '../..';
17
17
  import {J} from '../../java';
18
18
  import {ApplyOptions, Parameter, TemplateOptions, TemplateParameter} from './types';
19
+ import {maybeBind} from '../binding';
20
+ import {ContextBinding} from './engine';
21
+ import {JavaScriptVisitor} from '../visitor';
19
22
  import {MatchResult} from './pattern';
20
23
  import {generateCacheKey, globalAstCache, WRAPPERS_MAP_SYMBOL} from './utils';
21
24
  import {CAPTURE_NAME_SYMBOL, RAW_CODE_SYMBOL} from './capture';
@@ -174,6 +177,7 @@ export class TemplateBuilder {
174
177
  export class Template {
175
178
  private options: TemplateOptions = {};
176
179
  private _cachedTemplate?: J;
180
+ private _contextBindings?: Promise<ContextBinding[]>;
177
181
 
178
182
  /**
179
183
  * Creates a new template.
@@ -221,6 +225,7 @@ export class Template {
221
225
  this.options = {...this.options, ...options};
222
226
  // Invalidate cache when configuration changes
223
227
  this._cachedTemplate = undefined;
228
+ this._contextBindings = undefined;
224
229
  return this;
225
230
  }
226
231
 
@@ -237,10 +242,10 @@ export class Template {
237
242
  * @returns The cached or newly computed template tree
238
243
  * @internal
239
244
  */
240
- private async getTemplateTree(): Promise<JS.CompilationUnit> {
245
+ private async getTemplateTree(): Promise<J> {
241
246
  // Level 1: Instance cache (fastest path)
242
247
  if (this._cachedTemplate) {
243
- return this._cachedTemplate as JS.CompilationUnit;
248
+ return this._cachedTemplate;
244
249
  }
245
250
 
246
251
  // Generate cache key for global lookup
@@ -258,15 +263,17 @@ export class Template {
258
263
  const cacheKey = generateCacheKey(
259
264
  this.templateParts,
260
265
  parametersKey,
261
- contextStatements,
262
- this.options.dependencies || {}
266
+ // As in Pattern.getAstPattern: a parameter's type reaches the parse as a declaration
267
+ [...contextStatements, ...TemplateEngine.parameterPreamble(this.parameters)],
268
+ this.options.dependencies || {},
269
+ this.options.types
263
270
  );
264
271
 
265
272
  // Level 2: Global cache (fast path - shared with Pattern)
266
273
  const cached = globalAstCache.get(cacheKey);
267
274
  if (cached) {
268
- this._cachedTemplate = cached as JS.CompilationUnit;
269
- return cached as JS.CompilationUnit;
275
+ this._cachedTemplate = cached;
276
+ return cached;
270
277
  }
271
278
 
272
279
  // Level 3: Compute via TemplateEngine (slow path)
@@ -274,8 +281,9 @@ export class Template {
274
281
  this.templateParts,
275
282
  this.parameters,
276
283
  contextStatements,
277
- this.options.dependencies || {}
278
- ) as JS.CompilationUnit;
284
+ this.options.dependencies || {},
285
+ this.options.types
286
+ );
279
287
 
280
288
  // Cache in both levels
281
289
  globalAstCache.set(cacheKey, result);
@@ -284,6 +292,53 @@ export class Template {
284
292
  return result;
285
293
  }
286
294
 
295
+ /**
296
+ * Binds every module this template declares in the file `visitor` is traversing, and returns
297
+ * the local names to hand back through {@link ApplyOptions.bindings}. A module the dependencies
298
+ * cannot resolve is bound whether or not the template goes on to reference it, so call this
299
+ * where the template is known to apply — {@link RewriteRule.tryOn} does, once a pattern matched.
300
+ */
301
+ async resolveBindings(visitor: JavaScriptVisitor<any>): Promise<Record<string, string>> {
302
+ const resolved: Record<string, string> = {};
303
+ for (const binding of await this.contextBindings()) {
304
+ // Recognising the reference the template splices in takes attribution, so a module the
305
+ // workspace could not resolve is bound whether or not the template turns out to use it.
306
+ const onlyIfReferenced = binding.attributed;
307
+ const bound = maybeBind(visitor, {
308
+ module: binding.module!,
309
+ member: binding.member,
310
+ typeOnly: binding.typeOnly,
311
+ preferredName: binding.name,
312
+ onlyIfReferenced
313
+ });
314
+ // An unresolved binding is left out rather than recorded as `undefined`, so `apply()`'s
315
+ // own "applied without a local name" check catches it, same as a caller-omitted one.
316
+ if (bound !== undefined) {
317
+ resolved[binding.name] = bound;
318
+ }
319
+ }
320
+ return resolved;
321
+ }
322
+
323
+ /** Whether the context binds a module, which applying this template therefore has to resolve. */
324
+ async bindsModules(): Promise<boolean> {
325
+ return (await this.contextBindings()).length > 0;
326
+ }
327
+
328
+ /** What this template's context statements bind, which is what it needs bound in the target file. */
329
+ private contextBindings(): Promise<ContextBinding[]> {
330
+ return this._contextBindings ??= this.deriveContextBindings();
331
+ }
332
+
333
+ private deriveContextBindings(): Promise<ContextBinding[]> {
334
+ return TemplateEngine.getContextBindings(
335
+ this.templateParts, this.parameters,
336
+ this.options.context || this.options.imports || [],
337
+ this.options.dependencies || {},
338
+ this.options.types
339
+ );
340
+ }
341
+
287
342
  /**
288
343
  * Applies this template and returns the resulting tree.
289
344
  *
@@ -346,6 +401,23 @@ export class Template {
346
401
  }
347
402
  }
348
403
 
404
+ const renames: Record<string, string> = {};
405
+ const modules: Record<string, string> = {};
406
+ // Supplying names is what asks for the context's modules to be bound in the file being
407
+ // edited; without them a context import only types the template.
408
+ if (options?.bindings !== undefined) {
409
+ for (const binding of await this.contextBindings()) {
410
+ const bound = options.bindings[binding.name];
411
+ if (bound === undefined) {
412
+ throw new Error(`Template binds '${binding.module}' in its context, but no local name was ` +
413
+ `given for '${binding.name}'. Either pass bindings from resolveBindings(visitor), or — if it ` +
414
+ `already did — binding was refused, which an AMD block or a file requiring its modules can do.`);
415
+ }
416
+ renames[binding.name] = bound;
417
+ modules[binding.name] = binding.module!;
418
+ }
419
+ }
420
+
349
421
  // Use instance-level cache to get the template tree
350
422
  const ast = await this.getTemplateTree();
351
423
 
@@ -359,7 +431,10 @@ export class Template {
359
431
  mode: JavaCoordinates.Mode.Replace
360
432
  },
361
433
  normalizedValues,
362
- wrappersMap
434
+ wrappersMap,
435
+ options?.format ?? true,
436
+ renames,
437
+ modules
363
438
  );
364
439
  }
365
440
  }
@@ -18,6 +18,7 @@ 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 {JavaScriptVisitor} from "../visitor";
21
22
 
22
23
  /**
23
24
  * Options for variadic captures that match zero or more nodes in a sequence.
@@ -348,6 +349,12 @@ export interface PatternOptions {
348
349
  */
349
350
  dependencies?: Record<string, string>;
350
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
+
351
358
  /**
352
359
  * When true, allows patterns without type annotations to match code with type annotations.
353
360
  * This enables more flexible pattern matching during development or when full type attribution
@@ -481,6 +488,15 @@ export interface TemplateOptions {
481
488
  * The template engine will create a package.json with these dependencies.
482
489
  */
483
490
  dependencies?: Record<string, string>;
491
+
492
+ /**
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.
496
+ *
497
+ * @example `{dependencies: {'@sapui5/types': '^1.120.0'}, types: ['@sapui5/types']}`
498
+ */
499
+ types?: string[];
484
500
  }
485
501
 
486
502
  /**
@@ -509,6 +525,32 @@ export interface ApplyOptions {
509
525
  * ```
510
526
  */
511
527
  values?: Map<Capture | string, J> | MatchResult | Record<string, J>;
528
+
529
+ /**
530
+ * Whether the result is fitted to where it lands. Defaults to `true`; pass `false` to assemble
531
+ * several results into one subtree and format that subtree once, rather than once per application.
532
+ * The anchor supplies the result's prefix either way.
533
+ */
534
+ format?: boolean;
535
+
536
+ /**
537
+ * Local names for the bindings the template declares, keyed as they are declared — normally
538
+ * {@link Template.resolveBindings}. Every declared binding needs one.
539
+ */
540
+ bindings?: Record<string, string>;
541
+ }
542
+
543
+ /** Options a caller supplies per node, as against the ones {@link RewriteConfig} fixes for the rule. */
544
+ export interface TryOnOptions {
545
+ /**
546
+ * The visitor to bind the applied template's declared modules in. The rule holds that template,
547
+ * so it resolves them itself, and only once a pattern has matched — a rule that does not fire
548
+ * leaves the file's imports alone. {@link TryOnOptions.bindings} overrides this.
549
+ */
550
+ visitor?: JavaScriptVisitor<any>;
551
+
552
+ /** As {@link ApplyOptions.bindings}, for a caller that resolves the modules some other way. */
553
+ bindings?: Record<string, string>;
512
554
  }
513
555
 
514
556
  /**
@@ -524,7 +566,7 @@ export interface RewriteRule {
524
566
  * When using in a visitor, always use the `|| node` pattern to return the original
525
567
  * node when there's no match: `return await rule.tryOn(this.cursor, node) || node;`
526
568
  */
527
- tryOn(cursor: Cursor, node: J): Promise<J | undefined>;
569
+ tryOn(cursor: Cursor, node: J, options?: TryOnOptions): Promise<J | undefined>;
528
570
 
529
571
  /**
530
572
  * Chains this rule with another rule, creating a composite rule that applies both transformations sequentially.
@@ -624,6 +666,9 @@ export interface RewriteConfig {
624
666
  before: Pattern | Pattern[];
625
667
  after: Template | ((match: MatchResult) => Template);
626
668
 
669
+ /** As {@link ApplyOptions.format}, applied to every node the rule rewrites. */
670
+ format?: boolean;
671
+
627
672
  /**
628
673
  * Optional predicate evaluated BEFORE pattern matching.
629
674
  * Use for efficient early filtering based on AST context when captures aren't needed.
@@ -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
 
@@ -182,6 +184,23 @@ export function retainIds<T extends J>(tree: T, retainable: ReadonlySet<string>)
182
184
  return new RetainIdVisitor(retainable).visit(tree, null) as Promise<T>;
183
185
  }
184
186
 
187
+ /**
188
+ * Strips the indentation a template carries from the recipe source it was written in. Its first
189
+ * line starts at the opening backtick, so the common indent is the one shared by the lines below.
190
+ */
191
+ export function dedentTemplate(code: string): string {
192
+ const lines = code.split("\n");
193
+ let indent = Infinity;
194
+ for (const line of lines.slice(1)) {
195
+ if (line.trim().length > 0) {
196
+ indent = Math.min(indent, line.length - line.trimStart().length);
197
+ }
198
+ }
199
+ return indent === Infinity || indent === 0 ?
200
+ code :
201
+ [lines[0], ...lines.slice(1).map(line => line.slice(indent))].join("\n");
202
+ }
203
+
185
204
  /**
186
205
  * Marker that stores capture metadata on pattern AST nodes.
187
206
  * This avoids the need to parse capture names from identifiers during matching.
@@ -17,6 +17,7 @@ import ts from "typescript";
17
17
  import * as path from "path";
18
18
  import {Type} from "../java";
19
19
  import FUNCTION_TYPE_NAME = Type.FUNCTION_TYPE_NAME;
20
+ import OBJECT_TYPE_NAME = Type.OBJECT_TYPE_NAME;
20
21
 
21
22
  export class JavaScriptTypeMapping {
22
23
  // Primary cache: Use type signatures (preferring type.id) as cache keys
@@ -461,14 +462,25 @@ export class JavaScriptTypeMapping {
461
462
  return functionType;
462
463
  }
463
464
 
464
- // For anonymous object types that could have circular references
465
+ // A structural type has no nominal name, so every object literal `{a: 1}` and type
466
+ // literal `{a: number}` shares the synthetic FQN `{}`, with `members` carrying the shape.
465
467
  if (type.flags & ts.TypeFlags.Object) {
466
468
  const objectFlags = (type as ts.ObjectType).objectFlags;
467
469
  if (objectFlags & ts.ObjectFlags.Anonymous) {
468
- return Type.unknownType;
470
+ const objectType = this.createEmptyObjectType();
471
+ this.typeCache.set(signature, objectType);
472
+ this.populateClassType(objectType, type);
473
+ return objectType;
469
474
  }
470
475
  }
471
476
 
477
+ // The `object` keyword: an object of unknown shape.
478
+ if (type.flags & ts.TypeFlags.NonPrimitive) {
479
+ const objectType = this.createEmptyObjectType();
480
+ this.typeCache.set(signature, objectType);
481
+ return objectType;
482
+ }
483
+
472
484
  // Pre-cache as unknownType before resolving type aliases to prevent infinite
473
485
  // recursion when alias resolution cycles back through getType with different type.ids
474
486
  this.typeCache.set(signature, Type.unknownType);
@@ -820,6 +832,29 @@ export class JavaScriptTypeMapping {
820
832
  }
821
833
 
822
834
  methodType(node: ts.Node): Type.Method | undefined {
835
+ // An object literal is a `J.NewClass`, and `J.NewClass.getType()` reads
836
+ // `constructorType.returnType`, as it does for a Java anonymous class.
837
+ if (ts.isObjectLiteralExpression(node)) {
838
+ const objectType = this.type(node);
839
+ if (!Type.isFullyQualified(objectType)) {
840
+ return undefined;
841
+ }
842
+ return {
843
+ kind: Type.Kind.Method,
844
+ flags: 0,
845
+ declaringType: objectType,
846
+ name: '<constructor>',
847
+ returnType: objectType,
848
+ parameterNames: [],
849
+ parameterTypes: [],
850
+ thrownExceptions: [],
851
+ annotations: [],
852
+ declaredFormalTypeNames: [],
853
+ toJSON: function () {
854
+ return Type.signature(this);
855
+ }
856
+ } as Type.Method;
857
+ }
823
858
 
824
859
  let signature: ts.Signature | undefined;
825
860
  let methodName: string;
@@ -1644,6 +1679,23 @@ export class JavaScriptTypeMapping {
1644
1679
  return gtv;
1645
1680
  }
1646
1681
 
1682
+ private createEmptyObjectType(): Type.Class {
1683
+ return {
1684
+ kind: Type.Kind.Class,
1685
+ flags: 0,
1686
+ classKind: Type.Class.Kind.Interface,
1687
+ fullyQualifiedName: OBJECT_TYPE_NAME,
1688
+ typeParameters: [],
1689
+ annotations: [],
1690
+ interfaces: [],
1691
+ members: [],
1692
+ methods: [],
1693
+ toJSON: function () {
1694
+ return Type.signature(this);
1695
+ }
1696
+ } as Type.Class;
1697
+ }
1698
+
1647
1699
  /**
1648
1700
  * Create an empty function type shell with FQN 𝑓.
1649
1701
  * The shell will be populated later to handle circular references.
package/src/style.ts CHANGED
@@ -20,6 +20,11 @@ export interface Style {
20
20
  readonly kind: string
21
21
  }
22
22
 
23
+ /** Style markers carry their own kind (e.g. Autodetect, PrettierStyle), so `styles` is what identifies one. */
24
+ export function isNamedStyles(marker: Marker): marker is NamedStyles<string> {
25
+ return Array.isArray((marker as Partial<NamedStyles<string>>).styles);
26
+ }
27
+
23
28
  export interface NamedStyles<K extends string = typeof MarkersKind.NamedStyles> extends Marker {
24
29
  readonly kind: K
25
30
  readonly name: string;
@@ -20,7 +20,6 @@ import {Parser} from "../parser";
20
20
  import {TreePrinters} from "../print";
21
21
  import {SourceFile} from "../tree";
22
22
  import {Result, scheduleRun} from "../run";
23
- import {SnowflakeId} from "@akashrajpurohit/snowflake-id";
24
23
  import {mapAsync, trimIndent} from "../util";
25
24
  import {ParseErrorKind} from "../parse-error";
26
25
  import {MarkersKind, ParseExceptionResult} from "../markers";
@@ -38,6 +37,15 @@ export interface SourceSpec<T extends SourceFile> {
38
37
  ext: string
39
38
  }
40
39
 
40
+ /** The fields a test configures on a spec, as opposed to the machinery it runs with. */
41
+ interface RecipeSpecConfiguration {
42
+ checkParsePrintIdempotence: boolean;
43
+ allowEmptyDiff: boolean;
44
+ recipe: Recipe;
45
+ recipeExecutionContext: ExecutionContext;
46
+ dataTableAssertions: { [key: string]: (rows: any[]) => void };
47
+ }
48
+
41
49
  export class RecipeSpec {
42
50
  checkParsePrintIdempotence: boolean = true
43
51
  allowEmptyDiff: boolean = false
@@ -57,10 +65,58 @@ export class RecipeSpec {
57
65
 
58
66
  private dataTableAssertions: { [key: string]: (rows: any[]) => void } = {}
59
67
 
68
+ // A suite typically declares one spec and configures it per test, which makes the spec a
69
+ // channel between tests unless each test starts from the same configuration. A consumer opts
70
+ // into that reset by calling `trackSuiteConfiguration` from a vitest setup file, as
71
+ // `test/setup.ts` does; until it does, `live` is undefined and no spec is retained here.
72
+ private static live?: RecipeSpec[];
73
+ private suiteConfiguration?: RecipeSpecConfiguration;
74
+
75
+ constructor() {
76
+ RecipeSpec.live?.push(this);
77
+ }
78
+
60
79
  dataTable<Row>(name: string, allRows: (rows: Row[]) => void) {
61
80
  this.dataTableAssertions[name] = allRows;
62
81
  }
63
82
 
83
+ private configuration(): RecipeSpecConfiguration {
84
+ return {
85
+ checkParsePrintIdempotence: this.checkParsePrintIdempotence,
86
+ allowEmptyDiff: this.allowEmptyDiff,
87
+ recipe: this.recipe,
88
+ recipeExecutionContext: this.recipeExecutionContext,
89
+ dataTableAssertions: {...this.dataTableAssertions}
90
+ };
91
+ }
92
+
93
+ /** Starts recording specs, so that the calls below have something to act on. */
94
+ static trackSuiteConfiguration(): void {
95
+ RecipeSpec.live = [];
96
+ }
97
+
98
+ /** Records the configuration each test in the current suite starts from. */
99
+ static captureSuiteConfiguration(): void {
100
+ for (const spec of RecipeSpec.live ?? []) {
101
+ spec.suiteConfiguration = spec.configuration();
102
+ }
103
+ }
104
+
105
+ /** Undoes whatever the previous test configured. */
106
+ static restoreSuiteConfiguration(): void {
107
+ for (const spec of RecipeSpec.live ?? []) {
108
+ if (spec.suiteConfiguration) {
109
+ Object.assign(spec, spec.suiteConfiguration);
110
+ spec.dataTableAssertions = {...spec.suiteConfiguration.dataTableAssertions};
111
+ }
112
+ }
113
+ }
114
+
115
+ /** Bounds the registry to the suite that is running, which is all it has to span. */
116
+ static forgetSuiteSpecs(): void {
117
+ RecipeSpec.live &&= [];
118
+ }
119
+
64
120
  async rewriteRun(...sourceSpecs: (SourceSpec<any> | Generator<SourceSpec<any>, void, unknown> | AsyncGenerator<SourceSpec<any>, void, unknown>)[]): Promise<void> {
65
121
  // Flatten generators into a list of sourceSpecs
66
122
  const flattenedSpecs: SourceSpec<any>[] = [];
@@ -230,11 +286,13 @@ export class RecipeSpec {
230
286
  * Parse the whole group together so the sources can reference once another.
231
287
  */
232
288
  private async parse(specs: SourceSpec<any>[]): Promise<[SourceSpec<any>, SourceFile][]> {
233
- let snowflake = SnowflakeId();
289
+ // The path is a function of position, so a test parses under the same path on every run,
290
+ // which is what lets a parser pooled across specs reuse its program.
291
+ let unnamed = 0;
234
292
  const before: [SourceSpec<any>, { text: string, sourcePath: string }][] = [];
235
293
  for (const spec of specs) {
236
294
  if (spec.before) {
237
- const sourcePath = spec.path || `${snowflake.generate()}.${spec.ext}`;
295
+ const sourcePath = spec.path || `source-${unnamed++}.${spec.ext}`;
238
296
  before.push([spec, {text: dedent(spec.before), sourcePath: sourcePath}]);
239
297
  }
240
298
  }