@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.
- package/dist/javascript/add-import.d.ts.map +1 -1
- package/dist/javascript/add-import.js +6 -26
- package/dist/javascript/add-import.js.map +1 -1
- package/dist/javascript/amd.d.ts.map +1 -1
- package/dist/javascript/amd.js +8 -3
- package/dist/javascript/amd.js.map +1 -1
- package/dist/javascript/scope.d.ts +5 -0
- package/dist/javascript/scope.d.ts.map +1 -1
- package/dist/javascript/scope.js +9 -0
- package/dist/javascript/scope.js.map +1 -1
- package/dist/rewrite-javascript-version.txt +1 -1
- package/dist/test/rewrite-test.d.ts +7 -1
- package/dist/test/rewrite-test.d.ts.map +1 -1
- package/dist/test/rewrite-test.js +31 -4
- package/dist/test/rewrite-test.js.map +1 -1
- package/package.json +1 -1
- package/src/javascript/add-import.ts +7 -30
- package/src/javascript/amd.ts +9 -4
- package/src/javascript/scope.ts +9 -0
- package/src/test/rewrite-test.ts +36 -8
|
@@ -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);
|
package/src/javascript/amd.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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.
|
package/src/javascript/scope.ts
CHANGED
|
@@ -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
|
package/src/test/rewrite-test.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
230
|
-
|
|
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);
|