@openrewrite/rewrite 8.91.0 → 8.91.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) 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 +39 -20
  10. package/dist/javascript/add-import.d.ts.map +1 -1
  11. package/dist/javascript/add-import.js +513 -243
  12. package/dist/javascript/add-import.js.map +1 -1
  13. package/dist/javascript/assertions.d.ts +7 -0
  14. package/dist/javascript/assertions.d.ts.map +1 -1
  15. package/dist/javascript/assertions.js +31 -5
  16. package/dist/javascript/assertions.js.map +1 -1
  17. package/dist/javascript/comparator.d.ts +5 -0
  18. package/dist/javascript/comparator.d.ts.map +1 -1
  19. package/dist/javascript/comparator.js +34 -9
  20. package/dist/javascript/comparator.js.map +1 -1
  21. package/dist/javascript/format/format.d.ts.map +1 -1
  22. package/dist/javascript/format/format.js +8 -9
  23. package/dist/javascript/format/format.js.map +1 -1
  24. package/dist/javascript/format/tabs-and-indents-visitor.d.ts +2 -0
  25. package/dist/javascript/format/tabs-and-indents-visitor.d.ts.map +1 -1
  26. package/dist/javascript/format/tabs-and-indents-visitor.js +17 -11
  27. package/dist/javascript/format/tabs-and-indents-visitor.js.map +1 -1
  28. package/dist/javascript/node-resolution-result.d.ts +19 -1
  29. package/dist/javascript/node-resolution-result.d.ts.map +1 -1
  30. package/dist/javascript/node-resolution-result.js +37 -2
  31. package/dist/javascript/node-resolution-result.js.map +1 -1
  32. package/dist/javascript/package-exported-types.d.ts.map +1 -1
  33. package/dist/javascript/package-exported-types.js +56 -72
  34. package/dist/javascript/package-exported-types.js.map +1 -1
  35. package/dist/javascript/parser.d.ts.map +1 -1
  36. package/dist/javascript/parser.js +10 -3
  37. package/dist/javascript/parser.js.map +1 -1
  38. package/dist/javascript/recipes/change-import.d.ts.map +1 -1
  39. package/dist/javascript/recipes/change-import.js +4 -8
  40. package/dist/javascript/recipes/change-import.js.map +1 -1
  41. package/dist/javascript/style.d.ts.map +1 -1
  42. package/dist/javascript/style.js +3 -10
  43. package/dist/javascript/style.js.map +1 -1
  44. package/dist/javascript/templating/bindings.d.ts +19 -0
  45. package/dist/javascript/templating/bindings.d.ts.map +1 -0
  46. package/dist/javascript/templating/bindings.js +121 -0
  47. package/dist/javascript/templating/bindings.js.map +1 -0
  48. package/dist/javascript/templating/comparator.d.ts.map +1 -1
  49. package/dist/javascript/templating/comparator.js +5 -6
  50. package/dist/javascript/templating/comparator.js.map +1 -1
  51. package/dist/javascript/templating/engine.d.ts +18 -7
  52. package/dist/javascript/templating/engine.d.ts.map +1 -1
  53. package/dist/javascript/templating/engine.js +70 -42
  54. package/dist/javascript/templating/engine.js.map +1 -1
  55. package/dist/javascript/templating/index.d.ts +1 -1
  56. package/dist/javascript/templating/index.d.ts.map +1 -1
  57. package/dist/javascript/templating/index.js.map +1 -1
  58. package/dist/javascript/templating/pattern.d.ts.map +1 -1
  59. package/dist/javascript/templating/pattern.js +13 -10
  60. package/dist/javascript/templating/pattern.js.map +1 -1
  61. package/dist/javascript/templating/placeholder-replacement.d.ts +2 -0
  62. package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -1
  63. package/dist/javascript/templating/placeholder-replacement.js +14 -5
  64. package/dist/javascript/templating/placeholder-replacement.js.map +1 -1
  65. package/dist/javascript/templating/rewrite.d.ts.map +1 -1
  66. package/dist/javascript/templating/rewrite.js +14 -18
  67. package/dist/javascript/templating/rewrite.js.map +1 -1
  68. package/dist/javascript/templating/template.d.ts +8 -0
  69. package/dist/javascript/templating/template.d.ts.map +1 -1
  70. package/dist/javascript/templating/template.js +43 -3
  71. package/dist/javascript/templating/template.js.map +1 -1
  72. package/dist/javascript/templating/types.d.ts +47 -1
  73. package/dist/javascript/templating/types.d.ts.map +1 -1
  74. package/dist/javascript/templating/utils.d.ts +5 -0
  75. package/dist/javascript/templating/utils.d.ts.map +1 -1
  76. package/dist/javascript/templating/utils.js +17 -0
  77. package/dist/javascript/templating/utils.js.map +1 -1
  78. package/dist/javascript/type-mapping.d.ts +1 -0
  79. package/dist/javascript/type-mapping.d.ts.map +1 -1
  80. package/dist/javascript/type-mapping.js +52 -2
  81. package/dist/javascript/type-mapping.js.map +1 -1
  82. package/dist/rewrite-javascript-version.txt +1 -1
  83. package/dist/style.d.ts +2 -0
  84. package/dist/style.d.ts.map +1 -1
  85. package/dist/style.js +5 -0
  86. package/dist/style.js.map +1 -1
  87. package/dist/test/rewrite-test.d.ts +12 -0
  88. package/dist/test/rewrite-test.d.ts.map +1 -1
  89. package/dist/test/rewrite-test.js +40 -3
  90. package/dist/test/rewrite-test.js.map +1 -1
  91. package/package.json +1 -1
  92. package/src/java/formatting-utils.ts +14 -10
  93. package/src/java/type.ts +5 -0
  94. package/src/javascript/add-import.ts +590 -230
  95. package/src/javascript/assertions.ts +33 -5
  96. package/src/javascript/comparator.ts +39 -10
  97. package/src/javascript/format/format.ts +9 -10
  98. package/src/javascript/format/tabs-and-indents-visitor.ts +18 -12
  99. package/src/javascript/node-resolution-result.ts +59 -4
  100. package/src/javascript/package-exported-types.ts +60 -74
  101. package/src/javascript/parser.ts +10 -3
  102. package/src/javascript/recipes/change-import.ts +4 -7
  103. package/src/javascript/style.ts +3 -11
  104. package/src/javascript/templating/bindings.ts +129 -0
  105. package/src/javascript/templating/comparator.ts +5 -6
  106. package/src/javascript/templating/engine.ts +82 -44
  107. package/src/javascript/templating/index.ts +2 -0
  108. package/src/javascript/templating/pattern.ts +16 -15
  109. package/src/javascript/templating/placeholder-replacement.ts +16 -5
  110. package/src/javascript/templating/rewrite.ts +16 -18
  111. package/src/javascript/templating/template.ts +45 -3
  112. package/src/javascript/templating/types.ts +53 -1
  113. package/src/javascript/templating/utils.ts +17 -0
  114. package/src/javascript/type-mapping.ts +54 -2
  115. package/src/style.ts +5 -0
  116. package/src/test/rewrite-test.ts +61 -3
@@ -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 {bindingContextStatement, isResolvable} from './bindings';
20
+ import {maybeAddImport} from '../add-import';
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';
@@ -246,7 +249,11 @@ export class Template {
246
249
  // Generate cache key for global lookup
247
250
  // For raw() parameters, we need to include their code values in the key
248
251
  // since they're spliced at construction time, not application time
249
- const contextStatements = this.options.context || this.options.imports || [];
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
+ ];
250
257
  const parametersKey = this.parameters.map((p, i) => {
251
258
  const value = p.value;
252
259
  // Include raw code values in the cache key using the symbol
@@ -258,7 +265,8 @@ export class Template {
258
265
  const cacheKey = generateCacheKey(
259
266
  this.templateParts,
260
267
  parametersKey,
261
- contextStatements,
268
+ // As in Pattern.getAstPattern: a parameter's type reaches the parse as a declaration
269
+ [...contextStatements, ...TemplateEngine.parameterPreamble(this.parameters)],
262
270
  this.options.dependencies || {}
263
271
  );
264
272
 
@@ -284,6 +292,24 @@ 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
+ resolveBindings(visitor: JavaScriptVisitor<any>): Record<string, string> {
302
+ 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
+ }
310
+ return resolved;
311
+ }
312
+
287
313
  /**
288
314
  * Applies this template and returns the resulting tree.
289
315
  *
@@ -346,6 +372,19 @@ export class Template {
346
372
  }
347
373
  }
348
374
 
375
+ const declared = this.options.bindings ?? {};
376
+ const renames: Record<string, string> = {};
377
+ 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().`);
383
+ }
384
+ renames[name] = bound;
385
+ modules[name] = binding.module;
386
+ }
387
+
349
388
  // Use instance-level cache to get the template tree
350
389
  const ast = await this.getTemplateTree();
351
390
 
@@ -359,7 +398,10 @@ export class Template {
359
398
  mode: JavaCoordinates.Mode.Replace
360
399
  },
361
400
  normalizedValues,
362
- wrappersMap
401
+ wrappersMap,
402
+ options?.format ?? true,
403
+ renames,
404
+ modules
363
405
  );
364
406
  }
365
407
  }
@@ -18,6 +18,8 @@ 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
+ import type {JavaScriptVisitor} from "../visitor";
21
23
 
22
24
  /**
23
25
  * Options for variadic captures that match zero or more nodes in a sequence.
@@ -481,8 +483,29 @@ export interface TemplateOptions {
481
483
  * The template engine will create a package.json with these dependencies.
482
484
  */
483
485
  dependencies?: Record<string, string>;
486
+
487
+ /**
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.
491
+ *
492
+ * @example
493
+ * ```typescript
494
+ * template`Theming.setTheme(${capture('theme')})`
495
+ * .configure({bindings: {Theming: {module: 'sap/ui/core/Theming', member: 'default'}}})
496
+ * ```
497
+ */
498
+ bindings?: Record<string, ModuleBinding>;
484
499
  }
485
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 };
508
+
486
509
  /**
487
510
  * Options for template application.
488
511
  */
@@ -509,6 +532,32 @@ export interface ApplyOptions {
509
532
  * ```
510
533
  */
511
534
  values?: Map<Capture | string, J> | MatchResult | Record<string, J>;
535
+
536
+ /**
537
+ * Whether the result is fitted to where it lands. Defaults to `true`; pass `false` to assemble
538
+ * several results into one subtree and format that subtree once, rather than once per application.
539
+ * The anchor supplies the result's prefix either way.
540
+ */
541
+ format?: boolean;
542
+
543
+ /**
544
+ * Local names for the bindings the template declares, keyed as they are declared — normally
545
+ * {@link Template.resolveBindings}. Every declared binding needs one.
546
+ */
547
+ bindings?: Record<string, string>;
548
+ }
549
+
550
+ /** Options a caller supplies per node, as against the ones {@link RewriteConfig} fixes for the rule. */
551
+ export interface TryOnOptions {
552
+ /**
553
+ * The visitor to bind the applied template's declared modules in. The rule holds that template,
554
+ * so it resolves them itself, and only once a pattern has matched — a rule that does not fire
555
+ * leaves the file's imports alone. {@link TryOnOptions.bindings} overrides this.
556
+ */
557
+ visitor?: JavaScriptVisitor<any>;
558
+
559
+ /** As {@link ApplyOptions.bindings}, for a caller that resolves the modules some other way. */
560
+ bindings?: Record<string, string>;
512
561
  }
513
562
 
514
563
  /**
@@ -524,7 +573,7 @@ export interface RewriteRule {
524
573
  * When using in a visitor, always use the `|| node` pattern to return the original
525
574
  * node when there's no match: `return await rule.tryOn(this.cursor, node) || node;`
526
575
  */
527
- tryOn(cursor: Cursor, node: J): Promise<J | undefined>;
576
+ tryOn(cursor: Cursor, node: J, options?: TryOnOptions): Promise<J | undefined>;
528
577
 
529
578
  /**
530
579
  * Chains this rule with another rule, creating a composite rule that applies both transformations sequentially.
@@ -624,6 +673,9 @@ export interface RewriteConfig {
624
673
  before: Pattern | Pattern[];
625
674
  after: Template | ((match: MatchResult) => Template);
626
675
 
676
+ /** As {@link ApplyOptions.format}, applied to every node the rule rewrites. */
677
+ format?: boolean;
678
+
627
679
  /**
628
680
  * Optional predicate evaluated BEFORE pattern matching.
629
681
  * Use for efficient early filtering based on AST context when captures aren't needed.
@@ -182,6 +182,23 @@ export function retainIds<T extends J>(tree: T, retainable: ReadonlySet<string>)
182
182
  return new RetainIdVisitor(retainable).visit(tree, null) as Promise<T>;
183
183
  }
184
184
 
185
+ /**
186
+ * Strips the indentation a template carries from the recipe source it was written in. Its first
187
+ * line starts at the opening backtick, so the common indent is the one shared by the lines below.
188
+ */
189
+ export function dedentTemplate(code: string): string {
190
+ const lines = code.split("\n");
191
+ let indent = Infinity;
192
+ for (const line of lines.slice(1)) {
193
+ if (line.trim().length > 0) {
194
+ indent = Math.min(indent, line.length - line.trimStart().length);
195
+ }
196
+ }
197
+ return indent === Infinity || indent === 0 ?
198
+ code :
199
+ [lines[0], ...lines.slice(1).map(line => line.slice(indent))].join("\n");
200
+ }
201
+
185
202
  /**
186
203
  * Marker that stores capture metadata on pattern AST nodes.
187
204
  * 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
  }