@openrewrite/rewrite 8.92.17 → 8.92.18

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 (199) hide show
  1. package/README.md +48 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +2 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/java/formatting-utils.d.ts +2 -1
  6. package/dist/java/formatting-utils.d.ts.map +1 -1
  7. package/dist/java/formatting-utils.js +4 -3
  8. package/dist/java/formatting-utils.js.map +1 -1
  9. package/dist/java/rpc.d.ts.map +1 -1
  10. package/dist/java/rpc.js +2 -0
  11. package/dist/java/rpc.js.map +1 -1
  12. package/dist/java/tree.d.ts +8 -0
  13. package/dist/java/tree.d.ts.map +1 -1
  14. package/dist/java/tree.js +11 -1
  15. package/dist/java/tree.js.map +1 -1
  16. package/dist/java/type.d.ts +7 -1
  17. package/dist/java/type.d.ts.map +1 -1
  18. package/dist/java/type.js +16 -1
  19. package/dist/java/type.js.map +1 -1
  20. package/dist/javascript/add-import.d.ts +27 -21
  21. package/dist/javascript/add-import.d.ts.map +1 -1
  22. package/dist/javascript/add-import.js +303 -143
  23. package/dist/javascript/add-import.js.map +1 -1
  24. package/dist/javascript/amd.d.ts.map +1 -1
  25. package/dist/javascript/amd.js +25 -10
  26. package/dist/javascript/amd.js.map +1 -1
  27. package/dist/javascript/autodetect.js +36 -45
  28. package/dist/javascript/autodetect.js.map +1 -1
  29. package/dist/javascript/binding.d.ts +6 -4
  30. package/dist/javascript/binding.d.ts.map +1 -1
  31. package/dist/javascript/binding.js +54 -18
  32. package/dist/javascript/binding.js.map +1 -1
  33. package/dist/javascript/comparator.d.ts +22 -5
  34. package/dist/javascript/comparator.d.ts.map +1 -1
  35. package/dist/javascript/comparator.js +116 -38
  36. package/dist/javascript/comparator.js.map +1 -1
  37. package/dist/javascript/format/format.d.ts +2 -0
  38. package/dist/javascript/format/format.d.ts.map +1 -1
  39. package/dist/javascript/format/format.js +11 -1
  40. package/dist/javascript/format/format.js.map +1 -1
  41. package/dist/javascript/index.d.ts +4 -2
  42. package/dist/javascript/index.d.ts.map +1 -1
  43. package/dist/javascript/index.js +10 -1
  44. package/dist/javascript/index.js.map +1 -1
  45. package/dist/javascript/markers.d.ts.map +1 -1
  46. package/dist/javascript/markers.js +64 -0
  47. package/dist/javascript/markers.js.map +1 -1
  48. package/dist/javascript/method-matcher.d.ts +13 -2
  49. package/dist/javascript/method-matcher.d.ts.map +1 -1
  50. package/dist/javascript/method-matcher.js +32 -19
  51. package/dist/javascript/method-matcher.js.map +1 -1
  52. package/dist/javascript/package-name.d.ts +6 -0
  53. package/dist/javascript/package-name.d.ts.map +1 -0
  54. package/dist/javascript/package-name.js +47 -0
  55. package/dist/javascript/package-name.js.map +1 -0
  56. package/dist/javascript/preconditions.d.ts +4 -1
  57. package/dist/javascript/preconditions.d.ts.map +1 -1
  58. package/dist/javascript/preconditions.js +7 -4
  59. package/dist/javascript/preconditions.js.map +1 -1
  60. package/dist/javascript/recipes/change-method-target-to-import.d.ts +29 -0
  61. package/dist/javascript/recipes/change-method-target-to-import.d.ts.map +1 -0
  62. package/dist/javascript/recipes/change-method-target-to-import.js +164 -0
  63. package/dist/javascript/recipes/change-method-target-to-import.js.map +1 -0
  64. package/dist/javascript/recipes/index.d.ts +1 -0
  65. package/dist/javascript/recipes/index.d.ts.map +1 -1
  66. package/dist/javascript/recipes/index.js +1 -0
  67. package/dist/javascript/recipes/index.js.map +1 -1
  68. package/dist/javascript/scope.d.ts +60 -16
  69. package/dist/javascript/scope.d.ts.map +1 -1
  70. package/dist/javascript/scope.js +596 -131
  71. package/dist/javascript/scope.js.map +1 -1
  72. package/dist/javascript/search/uses-method.d.ts +1 -1
  73. package/dist/javascript/search/uses-method.d.ts.map +1 -1
  74. package/dist/javascript/search/uses-method.js +2 -2
  75. package/dist/javascript/search/uses-method.js.map +1 -1
  76. package/dist/javascript/search/uses-type.d.ts +14 -1
  77. package/dist/javascript/search/uses-type.d.ts.map +1 -1
  78. package/dist/javascript/search/uses-type.js +24 -9
  79. package/dist/javascript/search/uses-type.js.map +1 -1
  80. package/dist/javascript/templating/bindings.d.ts +1 -1
  81. package/dist/javascript/templating/bindings.d.ts.map +1 -1
  82. package/dist/javascript/templating/bindings.js +10 -50
  83. package/dist/javascript/templating/bindings.js.map +1 -1
  84. package/dist/javascript/templating/comparator.d.ts +8 -11
  85. package/dist/javascript/templating/comparator.d.ts.map +1 -1
  86. package/dist/javascript/templating/comparator.js +12 -110
  87. package/dist/javascript/templating/comparator.js.map +1 -1
  88. package/dist/javascript/templating/engine.d.ts +34 -24
  89. package/dist/javascript/templating/engine.d.ts.map +1 -1
  90. package/dist/javascript/templating/engine.js +200 -112
  91. package/dist/javascript/templating/engine.js.map +1 -1
  92. package/dist/javascript/templating/pattern.d.ts +11 -5
  93. package/dist/javascript/templating/pattern.d.ts.map +1 -1
  94. package/dist/javascript/templating/pattern.js +86 -20
  95. package/dist/javascript/templating/pattern.js.map +1 -1
  96. package/dist/javascript/templating/placeholder-replacement.d.ts +34 -3
  97. package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -1
  98. package/dist/javascript/templating/placeholder-replacement.js +126 -60
  99. package/dist/javascript/templating/placeholder-replacement.js.map +1 -1
  100. package/dist/javascript/templating/precedence.d.ts +3 -1
  101. package/dist/javascript/templating/precedence.d.ts.map +1 -1
  102. package/dist/javascript/templating/precedence.js +28 -8
  103. package/dist/javascript/templating/precedence.js.map +1 -1
  104. package/dist/javascript/templating/template.d.ts +7 -6
  105. package/dist/javascript/templating/template.d.ts.map +1 -1
  106. package/dist/javascript/templating/template.js +31 -28
  107. package/dist/javascript/templating/template.js.map +1 -1
  108. package/dist/javascript/templating/types.d.ts +6 -3
  109. package/dist/javascript/templating/types.d.ts.map +1 -1
  110. package/dist/javascript/templating/utils.d.ts +10 -1
  111. package/dist/javascript/templating/utils.d.ts.map +1 -1
  112. package/dist/javascript/templating/utils.js +22 -3
  113. package/dist/javascript/templating/utils.js.map +1 -1
  114. package/dist/javascript/tree-debug.d.ts +2 -0
  115. package/dist/javascript/tree-debug.d.ts.map +1 -1
  116. package/dist/javascript/tree-debug.js +6 -0
  117. package/dist/javascript/tree-debug.js.map +1 -1
  118. package/dist/javascript/tsconfig.d.ts.map +1 -1
  119. package/dist/javascript/tsconfig.js +7 -17
  120. package/dist/javascript/tsconfig.js.map +1 -1
  121. package/dist/javascript/type-mapping.d.ts +34 -16
  122. package/dist/javascript/type-mapping.d.ts.map +1 -1
  123. package/dist/javascript/type-mapping.js +253 -249
  124. package/dist/javascript/type-mapping.js.map +1 -1
  125. package/dist/javascript/type-report-cli.d.ts +7 -0
  126. package/dist/javascript/type-report-cli.d.ts.map +1 -0
  127. package/dist/javascript/type-report-cli.js +129 -0
  128. package/dist/javascript/type-report-cli.js.map +1 -0
  129. package/dist/javascript/type-report.d.ts +61 -0
  130. package/dist/javascript/type-report.d.ts.map +1 -0
  131. package/dist/javascript/type-report.js +488 -0
  132. package/dist/javascript/type-report.js.map +1 -0
  133. package/dist/markers.d.ts +4 -0
  134. package/dist/markers.d.ts.map +1 -1
  135. package/dist/markers.js +8 -0
  136. package/dist/markers.js.map +1 -1
  137. package/dist/recipe.d.ts +5 -0
  138. package/dist/recipe.d.ts.map +1 -1
  139. package/dist/recipe.js +5 -0
  140. package/dist/recipe.js.map +1 -1
  141. package/dist/rewrite-javascript-version.txt +1 -1
  142. package/dist/rpc/queue.d.ts +16 -0
  143. package/dist/rpc/queue.d.ts.map +1 -1
  144. package/dist/rpc/queue.js +27 -9
  145. package/dist/rpc/queue.js.map +1 -1
  146. package/dist/rpc/request/prepare-recipe.d.ts +3 -1
  147. package/dist/rpc/request/prepare-recipe.d.ts.map +1 -1
  148. package/dist/rpc/request/prepare-recipe.js +13 -6
  149. package/dist/rpc/request/prepare-recipe.js.map +1 -1
  150. package/dist/search/is-source-file.d.ts +1 -0
  151. package/dist/search/is-source-file.d.ts.map +1 -1
  152. package/dist/search/is-source-file.js +7 -2
  153. package/dist/search/is-source-file.js.map +1 -1
  154. package/dist/test/rewrite-test.d.ts +5 -0
  155. package/dist/test/rewrite-test.d.ts.map +1 -1
  156. package/dist/test/rewrite-test.js +14 -3
  157. package/dist/test/rewrite-test.js.map +1 -1
  158. package/package.json +4 -3
  159. package/src/index.ts +3 -1
  160. package/src/java/formatting-utils.ts +5 -4
  161. package/src/java/rpc.ts +2 -0
  162. package/src/java/tree.ts +15 -0
  163. package/src/java/type.ts +19 -2
  164. package/src/javascript/add-import.ts +338 -152
  165. package/src/javascript/amd.ts +28 -12
  166. package/src/javascript/autodetect.ts +32 -41
  167. package/src/javascript/binding.ts +64 -22
  168. package/src/javascript/comparator.ts +119 -43
  169. package/src/javascript/format/format.ts +9 -0
  170. package/src/javascript/index.ts +6 -3
  171. package/src/javascript/markers.ts +43 -1
  172. package/src/javascript/method-matcher.ts +34 -22
  173. package/src/javascript/package-name.ts +45 -0
  174. package/src/javascript/preconditions.ts +7 -4
  175. package/src/javascript/recipes/change-method-target-to-import.ts +178 -0
  176. package/src/javascript/recipes/index.ts +1 -0
  177. package/src/javascript/scope.ts +705 -169
  178. package/src/javascript/search/uses-method.ts +2 -2
  179. package/src/javascript/search/uses-type.ts +30 -9
  180. package/src/javascript/templating/bindings.ts +13 -38
  181. package/src/javascript/templating/comparator.ts +16 -127
  182. package/src/javascript/templating/engine.ts +229 -127
  183. package/src/javascript/templating/pattern.ts +107 -28
  184. package/src/javascript/templating/placeholder-replacement.ts +154 -62
  185. package/src/javascript/templating/precedence.ts +33 -11
  186. package/src/javascript/templating/template.ts +35 -30
  187. package/src/javascript/templating/types.ts +6 -3
  188. package/src/javascript/templating/utils.ts +24 -2
  189. package/src/javascript/tree-debug.ts +9 -0
  190. package/src/javascript/tsconfig.ts +7 -17
  191. package/src/javascript/type-mapping.ts +258 -252
  192. package/src/javascript/type-report-cli.ts +96 -0
  193. package/src/javascript/type-report.ts +575 -0
  194. package/src/markers.ts +8 -0
  195. package/src/recipe.ts +6 -0
  196. package/src/rpc/queue.ts +44 -5
  197. package/src/rpc/request/prepare-recipe.ts +16 -6
  198. package/src/search/is-source-file.ts +8 -3
  199. package/src/test/rewrite-test.ts +14 -3
package/src/rpc/queue.ts CHANGED
@@ -52,12 +52,27 @@ export interface RpcCodec<T> {
52
52
  rpcReceive(before: T, q: RpcReceiveQueue): Promise<T>;
53
53
  }
54
54
 
55
+ /**
56
+ * Converts a type that has no {@link RpcCodec} on either side, and so travels as one inline value,
57
+ * between its shape here and the one the peer's serializer gives it.
58
+ */
59
+ export interface RpcValueCodec<T> {
60
+ /**
61
+ * @param value The value as the peer serialized it, with its `kind` and without its top-level serializer keys.
62
+ */
63
+ fromValue(value: any): T;
64
+
65
+ toValue(after: T): any;
66
+ }
67
+
55
68
  /**
56
69
  * A registry for managing RPC codecs based on object types.
57
70
  */
58
71
  export class RpcCodecs {
59
72
  private static nonTreeCodecs = new Map<string, RpcCodec<any>>();
60
73
 
74
+ private static valueCodecs = new Map<string, RpcValueCodec<any>>();
75
+
61
76
  /**
62
77
  * The first key is on sourceFileType and the second on object type
63
78
  */
@@ -98,6 +113,14 @@ export class RpcCodecs {
98
113
  return this.nonTreeCodecs.get(type);
99
114
  }
100
115
 
116
+ static registerValueCodec(type: string, codec: RpcValueCodec<any>): void {
117
+ this.valueCodecs.set(type, codec);
118
+ }
119
+
120
+ static valueCodecForType(type: string): RpcValueCodec<any> | undefined {
121
+ return this.valueCodecs.get(type);
122
+ }
123
+
101
124
  /**
102
125
  * Determines the appropriate codec for an instance based on its `kind` property.
103
126
  *
@@ -204,7 +227,7 @@ export class RpcSendQueue {
204
227
  await this.add(after, onChange);
205
228
  } else {
206
229
  let afterCodec = onChange ? undefined : RpcCodecs.forInstance(after, this.sourceFileType);
207
- this.put({state: RpcObjectState.CHANGE, value: onChange || afterCodec ? undefined : after});
230
+ this.put(this.valueMessage(RpcObjectState.CHANGE, after, !onChange && !afterCodec));
208
231
  await this.doChange(after, before, onChange, afterCodec);
209
232
  }
210
233
  });
@@ -239,7 +262,7 @@ export class RpcSendQueue {
239
262
  const afterCodec = onChangeRun ? undefined : RpcCodecs.forInstance(anAfter, this.sourceFileType);
240
263
  // Without an onChange callback or codec, no property messages follow, so the
241
264
  // value must travel inline (as in send()) or the receiver keeps the stale element
242
- this.put({state: RpcObjectState.CHANGE, value: onChangeRun || afterCodec ? undefined : anAfter});
265
+ this.put(this.valueMessage(RpcObjectState.CHANGE, anAfter, !onChangeRun && !afterCodec));
243
266
  await this.doChange(anAfter, aBefore, onChangeRun, afterCodec);
244
267
  }
245
268
  }
@@ -288,14 +311,23 @@ export class RpcSendQueue {
288
311
  }
289
312
  let afterCodec = onChange ? undefined : RpcCodecs.forInstance(after, this.sourceFileType);
290
313
  this.put({
291
- state: RpcObjectState.ADD,
314
+ ...this.valueMessage(RpcObjectState.ADD, after, !onChange && !afterCodec),
292
315
  valueType: this.getValueType(after),
293
- value: onChange || afterCodec ? undefined : after,
294
316
  ref: ref
295
317
  });
296
318
  await this.doChange(after, undefined, onChange, afterCodec);
297
319
  }
298
320
 
321
+ // An inline value carries its type even in a CHANGE, as the peer needs it to decode the value
322
+ private valueMessage(state: RpcObjectState, after: any, inline: boolean): RpcObjectData {
323
+ if (!inline) {
324
+ return {state};
325
+ }
326
+ const valueType = this.getValueType(after);
327
+ const valueCodec = valueType === undefined ? undefined : RpcCodecs.valueCodecForType(valueType);
328
+ return {state, valueType, value: valueCodec ? valueCodec.toValue(after) : after};
329
+ }
330
+
299
331
  private async doChange(after: any, before: any, onChange?: () => Promise<void>, afterCodec?: RpcCodec<any>): Promise<void> {
300
332
  const lastBefore = this.before;
301
333
  this.before = before;
@@ -526,7 +558,7 @@ export class RpcReceiveQueue {
526
558
  after = await codec.rpcReceive(before, this);
527
559
  } else if (message.value !== undefined) {
528
560
  after = message.valueType ?
529
- {kind: this.internedStrings.internType(message.valueType), ...withoutSerializerKeys(message.value)} :
561
+ this.inlineValue(message.valueType, message.value) :
530
562
  typeof message.value === "string" ? this.internedStrings.internValue(message.value) : message.value;
531
563
  } else if (message.state === RpcObjectState.ADD && message.valueType) {
532
564
  throw new Error(
@@ -608,6 +640,13 @@ export class RpcReceiveQueue {
608
640
  }
609
641
  }
610
642
 
643
+ private inlineValue(valueType: string, value: any): any {
644
+ const kind = this.internedStrings.internType(valueType);
645
+ const valueCodec = RpcCodecs.valueCodecForType(kind);
646
+ const typed = {kind, ...withoutSerializerKeys(value)};
647
+ return valueCodec ? valueCodec.fromValue(typed) : typed;
648
+ }
649
+
611
650
  private newObj<T>(type: string): T {
612
651
  const codec = RpcCodecs.forType(type, this.sourceFileType);
613
652
  if (codec?.rpcNew) {
@@ -26,7 +26,10 @@ import {withMetrics} from "./metrics";
26
26
  import {RecipeMarketplace} from "../../marketplace";
27
27
 
28
28
  export class PrepareRecipe {
29
- constructor(private readonly id: string, private readonly options?: any) {
29
+ constructor(private readonly id: string,
30
+ private readonly options?: any,
31
+ // Older Java hosts reject unknown response fields, so causesAnotherCycle is only sent when asked.
32
+ private readonly acceptsCausesAnotherCycle?: boolean) {
30
33
  }
31
34
 
32
35
  static handle(connection: MessageConnection,
@@ -41,18 +44,19 @@ export class PrepareRecipe {
41
44
  metricsCsv,
42
45
  (context) => async (request) => {
43
46
  context.target = request.id;
47
+ const accepts = request.acceptsCausesAnotherCycle === true;
44
48
  const recipeCtor = marketplace.findRecipe(request.id);
45
49
  if (!recipeCtor) {
46
50
  // A miss means the host owns this recipe (e.g. a Java-delegate child a host
47
51
  // re-prepares by name), so answer with a delegatesTo stand-in for it to resolve.
48
52
  return await PrepareRecipe.prepareInstance(new DelegatingRecipe(request.id, request.options ?? {}),
49
- snowflake, preparedRecipes, marketplace);
53
+ snowflake, preparedRecipes, marketplace, accepts);
50
54
  }
51
55
  if (!recipeCtor[1]) {
52
56
  throw new Error(`Recipe ${request.id} was installed without a constructor`);
53
57
  }
54
58
  return await PrepareRecipe.prepareInstance(new recipeCtor[1](request.options),
55
- snowflake, preparedRecipes, marketplace);
59
+ snowflake, preparedRecipes, marketplace, accepts);
56
60
  }
57
61
  )
58
62
  );
@@ -68,7 +72,8 @@ export class PrepareRecipe {
68
72
  private static async prepareInstance(recipe: Recipe,
69
73
  snowflake: ReturnType<typeof SnowflakeId>,
70
74
  preparedRecipes: Map<String, Recipe>,
71
- marketplace: RecipeMarketplace): Promise<PrepareRecipeResponse> {
75
+ marketplace: RecipeMarketplace,
76
+ acceptsCausesAnotherCycle: boolean): Promise<PrepareRecipeResponse> {
72
77
  const id = snowflake.generate();
73
78
  const delegatesTo = PrepareRecipe.delegatesTo(recipe);
74
79
  if (delegatesTo) {
@@ -106,6 +111,9 @@ export class PrepareRecipe {
106
111
  scanVisitor: recipe instanceof ScanningRecipe ? `scan:${id}` : undefined,
107
112
  scanPreconditions: scanPreconditions
108
113
  };
114
+ if (acceptsCausesAnotherCycle && recipe.causesAnotherCycle === true) {
115
+ response.causesAnotherCycle = true;
116
+ }
109
117
 
110
118
  const childResponses: PrepareRecipeResponse[] = [];
111
119
  for (const child of await recipe.recipeList()) {
@@ -118,7 +126,7 @@ export class PrepareRecipe {
118
126
  }
119
127
  }
120
128
  childResponses.push(await PrepareRecipe.prepareInstance(new DelegatingRecipe(child.name, options),
121
- snowflake, preparedRecipes, marketplace));
129
+ snowflake, preparedRecipes, marketplace, acceptsCausesAnotherCycle));
122
130
  continue;
123
131
  }
124
132
  // Register a child that was instantiated in recipeList() but never installed, so a peer
@@ -126,7 +134,8 @@ export class PrepareRecipe {
126
134
  if (!PrepareRecipe.delegatesTo(child) && !marketplace.findRecipe(child.name)) {
127
135
  await marketplace.install(child.constructor as any, []);
128
136
  }
129
- childResponses.push(await PrepareRecipe.prepareInstance(child, snowflake, preparedRecipes, marketplace));
137
+ childResponses.push(await PrepareRecipe.prepareInstance(child, snowflake, preparedRecipes, marketplace,
138
+ acceptsCausesAnotherCycle));
130
139
  }
131
140
  response.recipeList = childResponses;
132
141
 
@@ -276,6 +285,7 @@ export interface PrepareRecipeResponse {
276
285
  * locally from these instead of re-preparing each child by name (the whole-tree optimization).
277
286
  */
278
287
  recipeList?: PrepareRecipeResponse[]
288
+ causesAnotherCycle?: boolean
279
289
  }
280
290
 
281
291
  /**
@@ -5,12 +5,17 @@ import {foundSearchResult} from "../markers";
5
5
  import * as picomatch from "picomatch";
6
6
 
7
7
  export class IsSourceFile extends TreeVisitor<any, ExecutionContext> {
8
- private readonly matcher: picomatch.Matcher;
8
+ private readonly matcher: (path: string) => boolean;
9
9
 
10
+ /** Reads and matches `filePattern` the way Java's FindSourceFiles does, so the precondition agrees on both hosts. */
10
11
  constructor(filePattern: string) {
11
12
  super();
12
- // Create a picomatch matcher for the pattern
13
- this.matcher = picomatch.default ? picomatch.default(filePattern) : (picomatch as any)(filePattern);
13
+ const patterns = filePattern.split(";")
14
+ .map(p => p.trim().replace(/^\.?[/\\]/, ""))
15
+ .filter(p => p.length > 0);
16
+ const options = {dot: true};
17
+ this.matcher = patterns.length === 0 ? () => true :
18
+ picomatch.default ? picomatch.default(patterns, options) : (picomatch as any)(patterns, options);
14
19
  }
15
20
 
16
21
  protected async preVisit(tree: any, _: ExecutionContext): Promise<any> {
@@ -24,6 +24,7 @@ import {mapAsync, trimIndent} from "../util";
24
24
  import {ParseErrorKind} from "../parse-error";
25
25
  import {MarkersKind, ParseExceptionResult} from "../markers";
26
26
  import {JavaScriptVisitor} from "../javascript";
27
+ import {attributionHint, dumpTypesIfRequested} from "../javascript/type-report";
27
28
  import {J} from "../java";
28
29
 
29
30
  export interface SourceSpec<T extends SourceFile> {
@@ -117,6 +118,11 @@ export class RecipeSpec {
117
118
  RecipeSpec.live &&= [];
118
119
  }
119
120
 
121
+ /**
122
+ * Setting `REWRITE_JAVASCRIPT_DUMP_TYPES` prints each parsed file's type attribution, and a
123
+ * recipe that produces no change names the nodes that carry none. See the README section
124
+ * "Inspecting type attribution".
125
+ */
120
126
  async rewriteRun(...sourceSpecs: (SourceSpec<any> | Generator<SourceSpec<any>, void, unknown> | AsyncGenerator<SourceSpec<any>, void, unknown>)[]): Promise<void> {
121
127
  // Flatten generators into a list of sourceSpecs
122
128
  const flattenedSpecs: SourceSpec<any>[] = [];
@@ -152,6 +158,9 @@ export class RecipeSpec {
152
158
  const specs = specsByKind[kind];
153
159
  const parsed = await this.parse(specs);
154
160
  await this.expectNoParseFailures(parsed);
161
+ for (const [_, sourceFile] of parsed) {
162
+ await dumpTypesIfRequested(sourceFile);
163
+ }
155
164
  await this.expectWhitespaceNotToContainNonwhitespaceCharacters(parsed);
156
165
  this.checkParsePrintIdempotence && await this.expectParsePrintIdempotence(parsed);
157
166
  allParsed.push(...parsed);
@@ -245,7 +254,7 @@ export class RecipeSpec {
245
254
  await spec.afterRecipe(matchingSpec![1]);
246
255
  }
247
256
  } else {
248
- await this.expectAfter(spec, after);
257
+ await this.expectAfter(spec, after, matchingSpec?.[1]);
249
258
  }
250
259
  }
251
260
  }
@@ -263,7 +272,7 @@ export class RecipeSpec {
263
272
  }
264
273
  }
265
274
 
266
- private async expectAfter(spec: SourceSpec<any>, after?: SourceFile) {
275
+ private async expectAfter(spec: SourceSpec<any>, after?: SourceFile, before?: SourceFile) {
267
276
  if (!after) {
268
277
  throw new Error('Expected for recipe to have produced a change for file:\n' + trimIndent(spec.before))
269
278
  }
@@ -272,9 +281,11 @@ export class RecipeSpec {
272
281
  const afterSource = typeof spec.after === "function" ?
273
282
  (spec.after as (actual: string) => string)(actualAfter) : spec.after as string;
274
283
  if (actualAfter !== afterSource) {
284
+ // A tree the recipe returned unchanged usually means it never fired.
285
+ const hint = after === before ? await attributionHint(after) : "";
275
286
  throw new Error(
276
287
  `Recipe output does not match expected.\n` +
277
- `Expected:\n${afterSource}\n\nActual:\n${actualAfter}`
288
+ `Expected:\n${afterSource}\n\nActual:\n${actualAfter}${hint}`
278
289
  );
279
290
  }
280
291
  if (spec.afterRecipe) {