@openrewrite/rewrite 8.96.1 → 8.96.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.
@@ -8,7 +8,7 @@ import {moduleName, packageNameOf, sameModule, spelledLike} from "./package-name
8
8
  import {emptyMarkers, findMarker, markers, MarkersKind} from "../markers";
9
9
  import {NamedStyles} from "../style";
10
10
  import {getStyle, SpacesStyle, StyleKind} from "./style";
11
- import {bindingNames, compilationUnitOf, cursorOf, declarationsOf, deconflict, namesDeclaredIn, resolve, scopeOf, walk} from "./scope";
11
+ import {bindingNames, compilationUnitOf, cursorOf, declarationsOf, deconflict, isFree, namesDeclaredIn, resolve, scopeOf, walk} from "./scope";
12
12
  import {create as produce, Draft} from "mutative";
13
13
  import {autoFormat} from "./format";
14
14
  import {getPrettierStyle} from "./format/prettier-format";
@@ -1352,7 +1352,7 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
1352
1352
 
1353
1353
  const collector = new class extends JavaScriptVisitor<void> {
1354
1354
  override async visitIdentifier(identifier: J.Identifier, p: void): Promise<J | undefined> {
1355
- if (identifier.simpleName === targetName) {
1355
+ if (!found && identifier.simpleName === targetName && isFree(this.cursor, identifier)) {
1356
1356
  const type = identifier.type;
1357
1357
  const fieldType = identifier.fieldType;
1358
1358
  if (expectedDeclaringType) {
@@ -1415,38 +1415,15 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
1415
1415
  found = true;
1416
1416
  }
1417
1417
  }
1418
+ // 4. References typed as the module's namespace object (e.g. `z` from zod)
1419
+ else if (type && Type.isClass(type) &&
1420
+ sameModule((type as Type.Class).fullyQualifiedName, targetModule)) {
1421
+ found = true;
1422
+ }
1418
1423
  }
1419
1424
  }
1420
1425
  return super.visitIdentifier(identifier, p);
1421
1426
  }
1422
-
1423
- override async visitMethodInvocation(methodInvocation: J.MethodInvocation, p: void): Promise<J | undefined> {
1424
- if (expectedDeclaringType && methodInvocation.methodType && methodInvocation.methodType.name === targetName &&
1425
- matchesModule(Type.FullyQualified.getFullyQualifiedName(methodInvocation.methodType.declaringType))) {
1426
- found = true;
1427
- }
1428
- return super.visitMethodInvocation(methodInvocation, p);
1429
- }
1430
-
1431
- override async visitFunctionCall(functionCall: JS.FunctionCall, p: void): Promise<J | undefined> {
1432
- if (expectedDeclaringType && functionCall.methodType && functionCall.methodType.name === targetName &&
1433
- matchesModule(Type.FullyQualified.getFullyQualifiedName(functionCall.methodType.declaringType))) {
1434
- found = true;
1435
- }
1436
- return super.visitFunctionCall(functionCall, p);
1437
- }
1438
-
1439
- override async visitFieldAccess(fieldAccess: J.FieldAccess, p: void): Promise<J | undefined> {
1440
- const type = fieldAccess.type;
1441
- if (type && Type.isMethod(type)) {
1442
- const methodType = type as Type.Method;
1443
- if (expectedDeclaringType && methodType.name === targetName &&
1444
- matchesModule(Type.FullyQualified.getFullyQualifiedName(methodType.declaringType))) {
1445
- found = true;
1446
- }
1447
- }
1448
- return super.visitFieldAccess(fieldAccess, p);
1449
- }
1450
1427
  };
1451
1428
 
1452
1429
  await collector.visit(compilationUnit, undefined);
@@ -29,7 +29,7 @@ import {
29
29
  } from "../java";
30
30
  import {JS} from "./tree";
31
31
  import {moduleNameOf} from "./add-import";
32
- import {cursorOf, deconflict, namesDeclaredWithin, scopeOf} from "./scope";
32
+ import {cursorOf, deconflict, namesDeclaredWithin, namesReferencedWithin, scopeOf} from "./scope";
33
33
  import {Cursor} from "../tree";
34
34
  import {JavaScriptVisitor} from "./visitor";
35
35
  import {ExecutionContext} from "../execution";
@@ -705,10 +705,15 @@ export function bindAmd(
705
705
  return undefined;
706
706
  }
707
707
  // A parameter is in scope across the whole factory, and the queue hands this name to later
708
- // requests at cursors inside it, so every name the factory declares is one it has to clear.
709
- const taken = [...bindings, ...namesDeclaredWithin(amd.block.factory), ...queued.map(v => v.binding)];
708
+ // requests at cursors inside it, so every name the factory declares is one it has to clear,
709
+ // as is every name it reads from further out, such as a global like `Element`, which it would capture.
710
+ // A pinned alias is exempt from the latter: its caller names a global on purpose to take it over.
711
+ const declaredWithin = namesDeclaredWithin(amd.block.factory);
712
+ const referencedWithin = pinned ? new Set<string>() : namesReferencedWithin(amd.block.factory);
713
+ const isTaken = (candidate: string) => declaredWithin.has(candidate) || referencedWithin.has(candidate) ||
714
+ bindings.includes(candidate) || queued.some(v => v.binding === candidate);
710
715
  const requested = preferredName ?? derivedBindingName(module)!;
711
- const binding = deconflict(requested, candidate => taken.includes(candidate));
716
+ const binding = deconflict(requested, isTaken);
712
717
  if (pinned && binding !== requested) {
713
718
  // An alias is bound verbatim or not at all, since the caller may already have emitted
714
719
  // code naming it, and a deconflicted spelling would leave that unbound.
@@ -1075,6 +1075,15 @@ export function resolve(cursor: Cursor, identifier: J.Identifier): J | undefined
1075
1075
  return reading && (bindingFrame(scopes, identifier.simpleName, reading)?.node as J | undefined);
1076
1076
  }
1077
1077
 
1078
+ /**
1079
+ * Whether the identifier reads a name nothing in the file binds where it stands, a global or nothing
1080
+ * at all. An identifier that names rather than reads is not free. The cursor is as {@link resolve} takes it.
1081
+ */
1082
+ export function isFree(cursor: Cursor, identifier: J.Identifier): boolean {
1083
+ const [scopes, reading] = position(pathTo(cursor, identifier), identifier);
1084
+ return reading !== undefined && bindingFrame(scopes, identifier.simpleName, reading) === undefined;
1085
+ }
1086
+
1078
1087
  /**
1079
1088
  * Whether the identifier is assigned to where it stands. An assignment's target, plain or compound,
1080
1089
  * is, and so are the operand of `++` or `--`, the head of a for-in or for-of declaring nothing, and
@@ -27,7 +27,7 @@ import {isNamedStyles} from "../style";
27
27
  import {JavaScriptVisitor} from "../javascript";
28
28
  import {attributionHint, dumpTypesIfRequested} from "../javascript/type-report";
29
29
  import {J, Type} from "../java";
30
- import {DATA_TABLE_STORE} from "../data-table";
30
+ import {DATA_TABLE_STORE, InMemoryDataTableStore} from "../data-table";
31
31
 
32
32
  export interface SourceSpec<T extends SourceFile> {
33
33
  kind: string,
@@ -49,7 +49,7 @@ interface RecipeSpecConfiguration {
49
49
  expectedCyclesThatMakeChanges?: number;
50
50
  recipe: Recipe;
51
51
  recipeExecutionContext: ExecutionContext;
52
- dataTableAssertions: { [key: string]: (rows: any[]) => void };
52
+ dataTableAssertions: { [key: string]: (rows: any[]) => void | Promise<void> };
53
53
  }
54
54
 
55
55
  export class RecipeSpec {
@@ -88,7 +88,7 @@ export class RecipeSpec {
88
88
  */
89
89
  recipeExecutionContext: ExecutionContext = this.executionContext;
90
90
 
91
- private dataTableAssertions: { [key: string]: (rows: any[]) => void } = {}
91
+ private dataTableAssertions: { [key: string]: (rows: any[]) => void | Promise<void> } = {}
92
92
 
93
93
  // A suite typically declares one spec and configures it per test, which makes the spec a
94
94
  // channel between tests unless each test starts from the same configuration. A consumer opts
@@ -101,7 +101,11 @@ export class RecipeSpec {
101
101
  RecipeSpec.live?.push(this);
102
102
  }
103
103
 
104
- dataTable<Row>(name: string, allRows: (rows: Row[]) => void) {
104
+ /**
105
+ * Asserts on the rows the first cycle of each subsequent {@link rewriteRun} writes to the named data
106
+ * table, failing the run when the recipe wrote none.
107
+ */
108
+ dataTable<Row>(name: string, allRows: (rows: Row[]) => void | Promise<void>) {
105
109
  this.dataTableAssertions[name] = allRows;
106
110
  }
107
111
 
@@ -212,11 +216,13 @@ export class RecipeSpec {
212
216
  }
213
217
  }
214
218
 
219
+ this.recipeExecutionContext.messages[DATA_TABLE_STORE] = new InMemoryDataTableStore();
215
220
  const expectedCycles = this.expectedCyclesThatMakeChanges ?? 1;
216
221
  const cycles = this.cycles ?? Math.max(2, expectedCycles + 1);
217
222
  const {changeset, unsettled} = await this.runCycles(allParsed.map(([_, sourceFile]) => sourceFile),
218
223
  cycles, expectedCycles);
219
224
 
225
+ this.expectNoUnexpectedGeneratedFiles(flattenedSpecs, changeset);
220
226
  this.checkReportingMarkersOnly && await this.expectNoAddedMarkers(flattenedSpecs, allParsed, changeset);
221
227
  await this.expectResultsToMatchAfter(flattenedSpecs, changeset, allParsed);
222
228
  await this.expectGeneratedFiles(flattenedSpecs, changeset);
@@ -225,10 +231,20 @@ export class RecipeSpec {
225
231
  `Expected recipe to complete in ${pluralCycles(expectedCycles)}, but cycle ${unsettled.cycle} changed`);
226
232
  }
227
233
  cycles > expectedCycles && await this.expectStableOnFreshParse(flattenedSpecs, allParsed, changeset);
234
+ await this.expectDataTables();
235
+ }
228
236
 
229
- // for (const [name, assertion] of Object.entries(this.dataTableAssertions)) {
230
- // assertion(getRows(name, this.recipeExecutionContext));
231
- // }
237
+ private async expectDataTables() {
238
+ const store: InMemoryDataTableStore = this.recipeExecutionContext.messages[DATA_TABLE_STORE];
239
+ const dataTables = store.getDataTables();
240
+ for (const [name, assertion] of Object.entries(this.dataTableAssertions)) {
241
+ const dataTable = dataTables.find(dt => dt.descriptor.name === name);
242
+ if (!dataTable) {
243
+ throw new Error(`No data table named ${name}` + (dataTables.length === 0 ? "" :
244
+ `\nFound data tables: ${[...new Set(dataTables.map(dt => dt.descriptor.name))].join(", ")}`));
245
+ }
246
+ await assertion(store.getRows(name, dataTable.group));
247
+ }
232
248
  }
233
249
 
234
250
  private async runCycles(sources: SourceFile[], cycles: number, expectedCycles: number):
@@ -496,9 +512,21 @@ export class RecipeSpec {
496
512
  }
497
513
  }
498
514
 
515
+ private expectNoUnexpectedGeneratedFiles(specs: SourceSpec<any>[], changeset: Result[]) {
516
+ const expectedPaths = new Set(specs.filter(spec => !spec.before).map(spec => spec.path));
517
+ const unexpected = changeset.filter(c => !c.before && c.after && !expectedPaths.has(c.after.sourcePath));
518
+ if (unexpected.length > 0) {
519
+ throw new Error([
520
+ "The recipe generated source files the test did not expect:",
521
+ ...unexpected.map(c => ` ${c.after!.sourcePath}`),
522
+ "Describe each with a spec that has a null before and the path of the file."
523
+ ].join("\n"));
524
+ }
525
+ }
526
+
499
527
  private async expectAfter(spec: SourceSpec<any>, after?: SourceFile, before?: SourceFile) {
500
528
  if (!after) {
501
- throw new Error('Expected for recipe to have produced a change for file:\n' + trimIndent(spec.before))
529
+ throw new Error('Expected for recipe to have produced a change for file:\n' + (trimIndent(spec.before) || spec.path))
502
530
  }
503
531
  await new ValidateWhitespaceVisitor().visit(after, this.executionContext);
504
532
  const actualAfter = await TreePrinters.print(after);