@openrewrite/rewrite 8.92.14 → 8.92.16
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 +127 -52
- package/dist/javascript/add-import.js.map +1 -1
- package/dist/javascript/amd.d.ts.map +1 -1
- package/dist/javascript/amd.js +3 -2
- package/dist/javascript/amd.js.map +1 -1
- package/dist/javascript/binding.js +3 -3
- package/dist/javascript/binding.js.map +1 -1
- package/dist/javascript/index.d.ts +3 -0
- package/dist/javascript/index.d.ts.map +1 -1
- package/dist/javascript/index.js +3 -0
- package/dist/javascript/index.js.map +1 -1
- package/dist/javascript/migrate/es6/modernize-octal-escape-sequences.d.ts.map +1 -1
- package/dist/javascript/migrate/es6/modernize-octal-escape-sequences.js +28 -32
- package/dist/javascript/migrate/es6/modernize-octal-escape-sequences.js.map +1 -1
- package/dist/javascript/preconditions.d.ts +11 -0
- package/dist/javascript/preconditions.d.ts.map +1 -1
- package/dist/javascript/preconditions.js +14 -0
- package/dist/javascript/preconditions.js.map +1 -1
- package/dist/javascript/search/index.d.ts +1 -0
- package/dist/javascript/search/index.d.ts.map +1 -1
- package/dist/javascript/search/index.js +1 -0
- package/dist/javascript/search/index.js.map +1 -1
- package/dist/javascript/search/is-vendored-or-bundled.d.ts +13 -0
- package/dist/javascript/search/is-vendored-or-bundled.d.ts.map +1 -0
- package/dist/javascript/search/is-vendored-or-bundled.js +93 -0
- package/dist/javascript/search/is-vendored-or-bundled.js.map +1 -0
- package/dist/rewrite-javascript-version.txt +1 -1
- package/package.json +1 -1
- package/src/javascript/add-import.ts +135 -53
- package/src/javascript/amd.ts +3 -2
- package/src/javascript/binding.ts +3 -3
- package/src/javascript/index.ts +3 -0
- package/src/javascript/migrate/es6/modernize-octal-escape-sequences.ts +28 -36
- package/src/javascript/preconditions.ts +19 -1
- package/src/javascript/search/index.ts +1 -0
- package/src/javascript/search/is-vendored-or-bundled.ts +59 -0
|
@@ -2,7 +2,8 @@ import {JavaScriptVisitor} from "./visitor";
|
|
|
2
2
|
import {ElementRemovalFormatter, emptySpace, isIdentifier, J, NameTree, rightPadded, singleSpace, space, Statement, TrailingComma, Type} from "../java";
|
|
3
3
|
import {JS, JSX} from "./tree";
|
|
4
4
|
import {randomId, UUID} from "../uuid";
|
|
5
|
-
import {emptyMarkers, findMarker, markers} from "../markers";
|
|
5
|
+
import {emptyMarkers, findMarker, markers, MarkersKind} from "../markers";
|
|
6
|
+
import {NamedStyles} from "../style";
|
|
6
7
|
import {getStyle, SpacesStyle, StyleKind} from "./style";
|
|
7
8
|
import {bindingNames, compilationUnitOf, cursorOf, declarationsOf, deconflict, isValueReference, namesDeclaredIn, scopeOf, walk} from "./scope";
|
|
8
9
|
import {create as produce, Draft} from "mutative";
|
|
@@ -437,7 +438,7 @@ export function moduleNameOf(module: string | J.Literal): string {
|
|
|
437
438
|
}
|
|
438
439
|
restored += source.slice(cut);
|
|
439
440
|
const quote = restored.charAt(0);
|
|
440
|
-
return (quote === "'" || quote === '"') && restored.endsWith(quote) && restored.length > 1
|
|
441
|
+
return (quote === "'" || quote === '"' || quote === '`') && restored.endsWith(quote) && restored.length > 1
|
|
441
442
|
? restored.slice(1, -1)
|
|
442
443
|
: restored;
|
|
443
444
|
}
|
|
@@ -455,12 +456,34 @@ function quoteOf(expression: J | undefined): QuoteChar | undefined {
|
|
|
455
456
|
: undefined;
|
|
456
457
|
}
|
|
457
458
|
|
|
459
|
+
/**
|
|
460
|
+
* The value a Prettier configuration on `cu` gives `option`, or Prettier's own `fallback` where the
|
|
461
|
+
* configuration does not name it; `undefined` without a configuration in force. A configuration
|
|
462
|
+
* outranks the file's own evidence, since running Prettier would arrive at its answer anyway.
|
|
463
|
+
*/
|
|
464
|
+
function prettierOption<T>(cu: JS.CompilationUnit, option: string, fallback: T): T | undefined {
|
|
465
|
+
const prettier = getPrettierStyle(cu);
|
|
466
|
+
if (!prettier || prettier.ignored) {
|
|
467
|
+
return undefined;
|
|
468
|
+
}
|
|
469
|
+
const value = prettier.config[option];
|
|
470
|
+
return typeof value === typeof fallback ? value as T : fallback;
|
|
471
|
+
}
|
|
472
|
+
|
|
458
473
|
/**
|
|
459
474
|
* Pick the quote character for a module specifier being added to `cu`, so that it agrees
|
|
460
|
-
* with the file it lands in.
|
|
461
|
-
*
|
|
475
|
+
* with the file it lands in. A Prettier configuration settles it; short of one, the caller's
|
|
476
|
+
* `requested` quote, then an existing specifier as the most direct precedent.
|
|
462
477
|
*/
|
|
463
|
-
async function detectQuote(cu: JS.CompilationUnit): Promise<QuoteChar> {
|
|
478
|
+
async function detectQuote(cu: JS.CompilationUnit, requested?: QuoteChar): Promise<QuoteChar> {
|
|
479
|
+
const singleQuote = prettierOption(cu, 'singleQuote', false);
|
|
480
|
+
if (singleQuote !== undefined) {
|
|
481
|
+
return singleQuote ? "'" : '"';
|
|
482
|
+
}
|
|
483
|
+
if (requested) {
|
|
484
|
+
return requested;
|
|
485
|
+
}
|
|
486
|
+
|
|
464
487
|
// Static imports are top-level, so the highest-ranked signal needs no traversal to find.
|
|
465
488
|
for (const statement of cu.statements) {
|
|
466
489
|
const element = statement.element;
|
|
@@ -521,25 +544,87 @@ async function detectQuote(cu: JS.CompilationUnit): Promise<QuoteChar> {
|
|
|
521
544
|
return specifierQuote;
|
|
522
545
|
}
|
|
523
546
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
547
|
+
return double > single ? '"' : "'";
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** Top-level statements that end in `;` in a file that uses semicolons at all. */
|
|
551
|
+
const semicolonTerminated = new Set<string>([
|
|
552
|
+
JS.Kind.Import, JS.Kind.ExportDeclaration, JS.Kind.ExportAssignment, JS.Kind.TypeDeclaration,
|
|
553
|
+
JS.Kind.ExpressionStatement, J.Kind.VariableDeclarations, J.Kind.MethodInvocation, J.Kind.Assignment
|
|
554
|
+
]);
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Whether an import statement added to `cu` should end in `;`. A Prettier configuration settles
|
|
558
|
+
* it; short of one, the file's imports, then its other statements that would take one.
|
|
559
|
+
*/
|
|
560
|
+
function detectSemi(cu: JS.CompilationUnit): boolean {
|
|
561
|
+
const semi = prettierOption(cu, 'semi', true);
|
|
562
|
+
if (semi !== undefined) {
|
|
563
|
+
return semi;
|
|
564
|
+
}
|
|
565
|
+
const tally = (kinds: (kind: string) => boolean): number => {
|
|
566
|
+
let score = 0;
|
|
567
|
+
for (const statement of cu.statements) {
|
|
568
|
+
if (statement.element && kinds(statement.element.kind)) {
|
|
569
|
+
score += findMarker(statement, J.Markers.Semicolon) ? 1 : -1;
|
|
570
|
+
}
|
|
529
571
|
}
|
|
530
|
-
|
|
572
|
+
return score;
|
|
573
|
+
};
|
|
574
|
+
const score = cu.statements.some(s => s.element?.kind === JS.Kind.Import)
|
|
575
|
+
? tally(kind => kind === JS.Kind.Import)
|
|
576
|
+
: tally(kind => semicolonTerminated.has(kind));
|
|
577
|
+
return score >= 0;
|
|
578
|
+
}
|
|
531
579
|
|
|
532
|
-
|
|
580
|
+
/**
|
|
581
|
+
* Whether a brace list added to `cu` is written `{ x }` rather than `{x}`. A Prettier configuration
|
|
582
|
+
* settles it; short of one, the file's own single-line import lists, then the style in force.
|
|
583
|
+
*/
|
|
584
|
+
function detectBraceSpacing(cu: JS.CompilationUnit): boolean {
|
|
585
|
+
const bracketSpacing = prettierOption(cu, 'bracketSpacing', true);
|
|
586
|
+
if (bracketSpacing !== undefined) {
|
|
587
|
+
return bracketSpacing;
|
|
588
|
+
}
|
|
589
|
+
let score = 0;
|
|
590
|
+
for (const statement of cu.statements) {
|
|
591
|
+
const namedBindings = statement.element?.kind === JS.Kind.Import
|
|
592
|
+
? (statement.element as JS.Import).importClause?.namedBindings
|
|
593
|
+
: undefined;
|
|
594
|
+
if (namedBindings?.kind !== JS.Kind.NamedImports) {
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
const first = (namedBindings as JS.NamedImports).elements.elements[0]?.element;
|
|
598
|
+
// A list laid out one per line says nothing about how a one-line list is spaced.
|
|
599
|
+
if (first && !first.prefix.whitespace.includes('\n')) {
|
|
600
|
+
score += first.prefix.whitespace.length > 0 ? 1 : -1;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
if (score !== 0) {
|
|
604
|
+
return score > 0;
|
|
605
|
+
}
|
|
606
|
+
return (getStyle(StyleKind.SpacesStyle, cu) as SpacesStyle).within.es6ImportExportBraces;
|
|
533
607
|
}
|
|
534
608
|
|
|
535
609
|
/**
|
|
536
|
-
* Lays out the import statement `AddImport` wrote, so that
|
|
537
|
-
*
|
|
610
|
+
* Lays out the import statement `AddImport` wrote, so that a Prettier configuration's print width,
|
|
611
|
+
* say, reaches it. Its quote, brace spacing and semicolon were already chosen to match the file.
|
|
538
612
|
*/
|
|
539
|
-
async function formatImport<P>(cu: JS.CompilationUnit, id: UUID, p: P): Promise<JS.CompilationUnit> {
|
|
613
|
+
async function formatImport<P>(cu: JS.CompilationUnit, id: UUID, p: P, braceSpacing?: boolean): Promise<JS.CompilationUnit> {
|
|
614
|
+
// The formatter would otherwise re-space the braces by the style in force, over the file's own evidence.
|
|
615
|
+
const spaces = getStyle(StyleKind.SpacesStyle, cu) as SpacesStyle;
|
|
616
|
+
const styles: NamedStyles<string>[] | undefined =
|
|
617
|
+
braceSpacing === undefined || braceSpacing === spaces.within.es6ImportExportBraces ? undefined : [{
|
|
618
|
+
kind: MarkersKind.NamedStyles,
|
|
619
|
+
id: randomId(),
|
|
620
|
+
name: "org.openrewrite.javascript.AddImport",
|
|
621
|
+
displayName: "Brace spacing detected by AddImport",
|
|
622
|
+
tags: [],
|
|
623
|
+
styles: [{...spaces, within: {...spaces.within, es6ImportExportBraces: braceSpacing}} as SpacesStyle]
|
|
624
|
+
}];
|
|
540
625
|
return await new class extends JavaScriptVisitor<P> {
|
|
541
626
|
override async visitImportDeclaration(jsImport: JS.Import, p: P): Promise<J | undefined> {
|
|
542
|
-
return jsImport.id === id ? await autoFormat(jsImport, p, undefined, this.cursor.parent) : jsImport;
|
|
627
|
+
return jsImport.id === id ? await autoFormat(jsImport, p, undefined, this.cursor.parent, styles) : jsImport;
|
|
543
628
|
}
|
|
544
629
|
}().visit(cu, p) as JS.CompilationUnit;
|
|
545
630
|
}
|
|
@@ -820,15 +905,13 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
|
|
|
820
905
|
|
|
821
906
|
const withImport = await this.produceJavaScript(compilationUnit, p, async draft => {
|
|
822
907
|
// Insert the import at the appropriate position
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
id: randomId()
|
|
827
|
-
});
|
|
908
|
+
const semicolonMarkers = detectSemi(compilationUnit)
|
|
909
|
+
? markers({kind: J.Markers.Semicolon, id: randomId()})
|
|
910
|
+
: emptyMarkers;
|
|
828
911
|
|
|
829
912
|
if (insertionIndex === 0) {
|
|
830
913
|
// Insert at the beginning
|
|
831
|
-
// The `after` space should be empty since semicolon is printed after it
|
|
914
|
+
// The `after` space should be empty since any semicolon is printed after it
|
|
832
915
|
// The spacing comes from updating the next statement's prefix
|
|
833
916
|
const updatedStatements = compilationUnit.statements.length > 0
|
|
834
917
|
? [
|
|
@@ -876,7 +959,7 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
|
|
|
876
959
|
}
|
|
877
960
|
});
|
|
878
961
|
|
|
879
|
-
return formatImport(withImport, newImport.id, p);
|
|
962
|
+
return formatImport(withImport, newImport.id, p, detectBraceSpacing(compilationUnit));
|
|
880
963
|
}
|
|
881
964
|
|
|
882
965
|
/**
|
|
@@ -1150,6 +1233,9 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
|
|
|
1150
1233
|
* or extracting it from the FQN.
|
|
1151
1234
|
*/
|
|
1152
1235
|
private getModuleFromClassType(classType: Type.Class): string | undefined {
|
|
1236
|
+
if (Type.isFunctionType(classType)) {
|
|
1237
|
+
return undefined;
|
|
1238
|
+
}
|
|
1153
1239
|
// Traverse owningClass chain to find the root
|
|
1154
1240
|
let current: Type.Class = classType;
|
|
1155
1241
|
while (current.owningClass && Type.isClass(current.owningClass)) {
|
|
@@ -1272,6 +1358,8 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
|
|
|
1272
1358
|
const targetModule = this.module;
|
|
1273
1359
|
let found = false;
|
|
1274
1360
|
const self = this;
|
|
1361
|
+
const matchesModule = (name: string | undefined) =>
|
|
1362
|
+
name !== undefined && (name === expectedDeclaringType || name === targetModule);
|
|
1275
1363
|
|
|
1276
1364
|
// If no existing imports from this module, look for unresolved references
|
|
1277
1365
|
// If there ARE existing imports, look for references with the expected declaring type
|
|
@@ -1285,12 +1373,13 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
|
|
|
1285
1373
|
// We have an expected declaring type - check for exact match
|
|
1286
1374
|
if (type && Type.isMethod(type)) {
|
|
1287
1375
|
const declaringTypeName = Type.FullyQualified.getFullyQualifiedName((type as Type.Method).declaringType);
|
|
1288
|
-
if (declaringTypeName
|
|
1376
|
+
if (matchesModule(declaringTypeName)) {
|
|
1289
1377
|
found = true;
|
|
1290
1378
|
}
|
|
1291
1379
|
}
|
|
1292
1380
|
else if (type && Type.isClass(type)) {
|
|
1293
|
-
if (self.classTypeMatchesModule(type as Type.Class, expectedDeclaringType)
|
|
1381
|
+
if (self.classTypeMatchesModule(type as Type.Class, expectedDeclaringType) ||
|
|
1382
|
+
self.classTypeMatchesModule(type as Type.Class, targetModule)) {
|
|
1294
1383
|
found = true;
|
|
1295
1384
|
}
|
|
1296
1385
|
}
|
|
@@ -1298,7 +1387,7 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
|
|
|
1298
1387
|
const ownerTypeName = (fieldType as Type.Variable).owner
|
|
1299
1388
|
? Type.FullyQualified.getFullyQualifiedName((fieldType as Type.Variable).owner!)
|
|
1300
1389
|
: undefined;
|
|
1301
|
-
if (ownerTypeName
|
|
1390
|
+
if (matchesModule(ownerTypeName)) {
|
|
1302
1391
|
found = true;
|
|
1303
1392
|
}
|
|
1304
1393
|
}
|
|
@@ -1346,25 +1435,17 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
|
|
|
1346
1435
|
}
|
|
1347
1436
|
|
|
1348
1437
|
override async visitMethodInvocation(methodInvocation: J.MethodInvocation, p: void): Promise<J | undefined> {
|
|
1349
|
-
if (methodInvocation.methodType && methodInvocation.methodType.name === targetName
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
if (declaringTypeName === expectedDeclaringType) {
|
|
1353
|
-
found = true;
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1438
|
+
if (expectedDeclaringType && methodInvocation.methodType && methodInvocation.methodType.name === targetName &&
|
|
1439
|
+
matchesModule(Type.FullyQualified.getFullyQualifiedName(methodInvocation.methodType.declaringType))) {
|
|
1440
|
+
found = true;
|
|
1356
1441
|
}
|
|
1357
1442
|
return super.visitMethodInvocation(methodInvocation, p);
|
|
1358
1443
|
}
|
|
1359
1444
|
|
|
1360
1445
|
override async visitFunctionCall(functionCall: JS.FunctionCall, p: void): Promise<J | undefined> {
|
|
1361
|
-
if (functionCall.methodType && functionCall.methodType.name === targetName
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
if (declaringTypeName === expectedDeclaringType) {
|
|
1365
|
-
found = true;
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1446
|
+
if (expectedDeclaringType && functionCall.methodType && functionCall.methodType.name === targetName &&
|
|
1447
|
+
matchesModule(Type.FullyQualified.getFullyQualifiedName(functionCall.methodType.declaringType))) {
|
|
1448
|
+
found = true;
|
|
1368
1449
|
}
|
|
1369
1450
|
return super.visitFunctionCall(functionCall, p);
|
|
1370
1451
|
}
|
|
@@ -1373,13 +1454,9 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
|
|
|
1373
1454
|
const type = fieldAccess.type;
|
|
1374
1455
|
if (type && Type.isMethod(type)) {
|
|
1375
1456
|
const methodType = type as Type.Method;
|
|
1376
|
-
if (methodType.name === targetName
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
if (declaringTypeName === expectedDeclaringType) {
|
|
1380
|
-
found = true;
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
1457
|
+
if (expectedDeclaringType && methodType.name === targetName &&
|
|
1458
|
+
matchesModule(Type.FullyQualified.getFullyQualifiedName(methodType.declaringType))) {
|
|
1459
|
+
found = true;
|
|
1383
1460
|
}
|
|
1384
1461
|
}
|
|
1385
1462
|
return super.visitFieldAccess(fieldAccess, p);
|
|
@@ -1403,7 +1480,7 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
|
|
|
1403
1480
|
// Note: value is the unquoted module name; valueSource and unicodeEscapes are its printed form
|
|
1404
1481
|
let valueSource = this.moduleValueSource;
|
|
1405
1482
|
if (valueSource === undefined) {
|
|
1406
|
-
const quote =
|
|
1483
|
+
const quote = await detectQuote(compilationUnit, this.quoteStyle);
|
|
1407
1484
|
valueSource = `${quote}${this.module}${quote}`;
|
|
1408
1485
|
}
|
|
1409
1486
|
const moduleSpecifier: J.Literal = {
|
|
@@ -1490,9 +1567,7 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
|
|
|
1490
1567
|
} else {
|
|
1491
1568
|
// Named import: import { member } from 'module'
|
|
1492
1569
|
|
|
1493
|
-
|
|
1494
|
-
const spacesStyle = getStyle(StyleKind.SpacesStyle, compilationUnit) as SpacesStyle;
|
|
1495
|
-
const braceSpace = spacesStyle.within.es6ImportExportBraces ? singleSpace : emptySpace;
|
|
1570
|
+
const braceSpace = detectBraceSpacing(compilationUnit) ? singleSpace : emptySpace;
|
|
1496
1571
|
|
|
1497
1572
|
const importSpec = this.createImportSpecifier();
|
|
1498
1573
|
// Apply brace spacing: the space after { is in the specifier's prefix,
|
|
@@ -1672,7 +1747,7 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
|
|
|
1672
1747
|
*/
|
|
1673
1748
|
function importBinds(jsImport: JS.Import, module: string, member: string | undefined): string | undefined {
|
|
1674
1749
|
const specifier = jsImport.moduleSpecifier?.element;
|
|
1675
|
-
if (specifier?.kind !== J.Kind.Literal || (specifier as J.Literal)
|
|
1750
|
+
if (specifier?.kind !== J.Kind.Literal || moduleNameOf(specifier as J.Literal) !== module) {
|
|
1676
1751
|
return undefined;
|
|
1677
1752
|
}
|
|
1678
1753
|
const importClause = jsImport.importClause;
|
|
@@ -1822,7 +1897,7 @@ function aliasing(local: J.Identifier, member: string): JS.Alias {
|
|
|
1822
1897
|
* Drops the binding for `member` from `jsImport`'s clause — the default, the namespace alias, or
|
|
1823
1898
|
* one entry of the named list, whichever `member` names — keeping everything else the clause
|
|
1824
1899
|
* binds. `ElementRemovalFormatter` carries the dropped binding's prefix onto whatever prints
|
|
1825
|
-
* next,
|
|
1900
|
+
* next, and a dropped last entry's trailing space onto the new last, as `RemoveImport` does.
|
|
1826
1901
|
*/
|
|
1827
1902
|
function removeBinding(jsImport: JS.Import, member: string | undefined): JS.Import {
|
|
1828
1903
|
const importClause = jsImport.importClause;
|
|
@@ -1876,6 +1951,12 @@ function removeBinding(jsImport: JS.Import, member: string | undefined): JS.Impo
|
|
|
1876
1951
|
// last member; the caller drops the whole statement when no default remains either.
|
|
1877
1952
|
return {...jsImport, importClause: {...importClause, namedBindings: undefined}};
|
|
1878
1953
|
}
|
|
1954
|
+
// The space before `}` and any trailing comma belong to the last entry, so they move onto the
|
|
1955
|
+
// new last entry when the old one is dropped.
|
|
1956
|
+
const originalLast = namedImports.elements.elements[namedImports.elements.elements.length - 1];
|
|
1957
|
+
if (namedSpecifierImports(originalLast.element.specifier, key)) {
|
|
1958
|
+
kept[kept.length - 1] = {...kept[kept.length - 1], after: originalLast.after, markers: originalLast.markers};
|
|
1959
|
+
}
|
|
1879
1960
|
const updatedNamedImports: JS.NamedImports = {...namedImports, elements: {...namedImports.elements, elements: kept}};
|
|
1880
1961
|
return {...jsImport, importClause: {...importClause, namedBindings: updatedNamedImports}};
|
|
1881
1962
|
}
|
|
@@ -1948,6 +2029,7 @@ export class RebindImport<P> extends JavaScriptVisitor<P> {
|
|
|
1948
2029
|
const originalSource = literal.valueSource || `"${this.from.module}"`;
|
|
1949
2030
|
const quoteChar = originalSource.startsWith("'") ? "'" : '"';
|
|
1950
2031
|
literal.valueSource = `${quoteChar}${this.to.module}${quoteChar}`;
|
|
2032
|
+
literal.unicodeEscapes = undefined;
|
|
1951
2033
|
|
|
1952
2034
|
// A default or namespace import carries its local name on the clause itself; a named
|
|
1953
2035
|
// one states the member alongside it, in the specifier.
|
package/src/javascript/amd.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
TrailingComma
|
|
30
30
|
} from "../java";
|
|
31
31
|
import {JS} from "./tree";
|
|
32
|
+
import {moduleNameOf} from "./add-import";
|
|
32
33
|
import {cursorOf, deconflict, namesDeclaredWithin, scopeOf} from "./scope";
|
|
33
34
|
import {Cursor} from "../tree";
|
|
34
35
|
import {JavaScriptVisitor} from "./visitor";
|
|
@@ -183,7 +184,7 @@ function foldDeclarationTrailingSpace(entry: J.RightPadded<J>): J.RightPadded<J>
|
|
|
183
184
|
export function dependencyNames(block: AmdBlock): string[] {
|
|
184
185
|
return elementsOf(block).map(padded => {
|
|
185
186
|
const element = padded.element;
|
|
186
|
-
return isLiteral(element) && typeof element.value === "string" ? element
|
|
187
|
+
return isLiteral(element) && typeof element.value === "string" ? moduleNameOf(element) : "";
|
|
187
188
|
});
|
|
188
189
|
}
|
|
189
190
|
|
|
@@ -519,7 +520,7 @@ export function withDependencyModuleAt(
|
|
|
519
520
|
const elements = [...elementsOf(block)];
|
|
520
521
|
const entry = elements[index];
|
|
521
522
|
const literal = entry.element as J.Literal;
|
|
522
|
-
const updated: J.Literal = {...literal, value: module, valueSource: `${quote}${module}${quote}
|
|
523
|
+
const updated: J.Literal = {...literal, value: module, valueSource: `${quote}${module}${quote}`, unicodeEscapes: undefined};
|
|
523
524
|
elements[index] = {...entry, element: updated};
|
|
524
525
|
const dependencies = withElements(block.dependencies, elements);
|
|
525
526
|
return withParts(call, block, dependencies, block.factory);
|
|
@@ -164,7 +164,7 @@ function dynamicallyImportedModule(declaration: J.VariableDeclarations): string
|
|
|
164
164
|
}
|
|
165
165
|
const argument = call.arguments.elements[0]?.element;
|
|
166
166
|
return argument?.kind === J.Kind.Literal && typeof (argument as J.Literal).value === "string"
|
|
167
|
-
? (argument as J.Literal)
|
|
167
|
+
? moduleNameOf(argument as J.Literal)
|
|
168
168
|
: undefined;
|
|
169
169
|
}
|
|
170
170
|
|
|
@@ -200,10 +200,10 @@ function moduleObjectBindings(cu: JS.CompilationUnit): ModuleObjectBinding[] {
|
|
|
200
200
|
if (specifier?.kind !== J.Kind.Literal) {
|
|
201
201
|
continue;
|
|
202
202
|
}
|
|
203
|
-
|
|
204
|
-
if (typeof module !== "string") {
|
|
203
|
+
if (typeof (specifier as J.Literal).value !== "string") {
|
|
205
204
|
continue;
|
|
206
205
|
}
|
|
206
|
+
const module = moduleNameOf(specifier as J.Literal);
|
|
207
207
|
const clause = jsImport.importClause;
|
|
208
208
|
const typeOnly = clause?.typeOnly ?? false;
|
|
209
209
|
if (clause?.name?.element?.kind === J.Kind.Identifier) {
|
package/src/javascript/index.ts
CHANGED
|
@@ -54,6 +54,9 @@ export * from "./remove-import";
|
|
|
54
54
|
export * from "./cleanup/index";
|
|
55
55
|
export * from "./recipes/index";
|
|
56
56
|
export * from "./search/index";
|
|
57
|
+
export * from "./migrate/index";
|
|
58
|
+
export * from "./migrate/es6/index";
|
|
59
|
+
export * from "./migrate/typescript/index";
|
|
57
60
|
|
|
58
61
|
import "./print";
|
|
59
62
|
import "./rpc";
|
|
@@ -19,12 +19,13 @@ import {TreeVisitor} from "../../../visitor";
|
|
|
19
19
|
import {ExecutionContext} from "../../../execution";
|
|
20
20
|
import {JavaScriptVisitor} from "../../visitor";
|
|
21
21
|
import {J} from "../../../java";
|
|
22
|
+
import {JS} from "../../tree";
|
|
22
23
|
import {create as produce} from "mutative";
|
|
23
24
|
|
|
24
25
|
export class ModernizeOctalEscapeSequences extends Recipe {
|
|
25
26
|
name = "org.openrewrite.javascript.migrate.es6.modernize-octal-escape-sequences";
|
|
26
27
|
displayName = "Modernize octal escape sequences";
|
|
27
|
-
description = "Convert
|
|
28
|
+
description = "Convert legacy octal escape sequences in string literals (e.g., `\\1`, `\\123`) to hex escape sequences (e.g., `\\x01`, `\\x53`) or Unicode escape sequences (e.g., `\\u0001`, `\\u0053`). The `\\0` escape is left alone unless a digit follows it.";
|
|
28
29
|
|
|
29
30
|
@Option({
|
|
30
31
|
displayName: "Use Unicode escapes",
|
|
@@ -41,50 +42,41 @@ export class ModernizeOctalEscapeSequences extends Recipe {
|
|
|
41
42
|
|
|
42
43
|
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
43
44
|
const useUnicode = this.useUnicodeEscapes;
|
|
45
|
+
const modernize = (source: string) => source.replace(
|
|
46
|
+
/\\([0-3][0-7]{0,2}|[4-7][0-7]?|[\s\S])/g,
|
|
47
|
+
(escape: string, body: string, offset: number) => {
|
|
48
|
+
if (!/^[0-7]/.test(body) || (body === '0' && !/[0-9]/.test(source[offset + 2] ?? ''))) {
|
|
49
|
+
return escape;
|
|
50
|
+
}
|
|
51
|
+
const code = parseInt(body, 8);
|
|
52
|
+
return useUnicode ?
|
|
53
|
+
`\\u${code.toString(16).padStart(4, '0')}` :
|
|
54
|
+
`\\x${code.toString(16).padStart(2, '0')}`;
|
|
55
|
+
});
|
|
44
56
|
return new class extends JavaScriptVisitor<ExecutionContext> {
|
|
45
57
|
|
|
46
58
|
protected async visitLiteral(literal: J.Literal, _ctx: ExecutionContext): Promise<J | undefined> {
|
|
47
|
-
// Only process string literals
|
|
48
|
-
if (typeof literal.value !== 'string') {
|
|
49
|
-
return literal;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
59
|
const valueSource = literal.valueSource;
|
|
53
|
-
if (!valueSource)
|
|
60
|
+
if (!valueSource || (valueSource[0] !== '"' && valueSource[0] !== "'") ||
|
|
61
|
+
this.cursor.parentTree()?.value.kind === JS.Kind.JsxAttribute) {
|
|
54
62
|
return literal;
|
|
55
63
|
}
|
|
56
64
|
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// Replace all octal escape sequences with hex or Unicode equivalents
|
|
67
|
-
modernized = valueSource.replace(octalEscapePattern, (match, octalDigits) => {
|
|
68
|
-
hasOctalEscapes = true;
|
|
69
|
-
// Convert octal string to decimal number
|
|
70
|
-
const decimalValue = parseInt(octalDigits, 8);
|
|
71
|
-
|
|
72
|
-
if (useUnicode) {
|
|
73
|
-
// Convert to Unicode escape sequence (4 hex digits, zero-padded)
|
|
74
|
-
return `\\u${decimalValue.toString(16).padStart(4, '0')}`;
|
|
75
|
-
} else {
|
|
76
|
-
// Convert to hex escape sequence (2 hex digits, zero-padded)
|
|
77
|
-
return `\\x${decimalValue.toString(16).padStart(2, '0')}`;
|
|
78
|
-
}
|
|
65
|
+
// Surrogate escapes are held outside valueSource, so each run between them is rewritten on its own
|
|
66
|
+
// and the escapes are moved to where their runs now end.
|
|
67
|
+
let modernized = '';
|
|
68
|
+
let cut = 0;
|
|
69
|
+
const unicodeEscapes = literal.unicodeEscapes?.map(escape => {
|
|
70
|
+
modernized += modernize(valueSource.slice(cut, escape.valueSourceIndex));
|
|
71
|
+
cut = escape.valueSourceIndex;
|
|
72
|
+
return {...escape, valueSourceIndex: modernized.length};
|
|
79
73
|
});
|
|
74
|
+
modernized += modernize(valueSource.slice(cut));
|
|
80
75
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return literal;
|
|
76
|
+
return modernized === valueSource ? literal : produce(literal, draft => {
|
|
77
|
+
draft.valueSource = modernized;
|
|
78
|
+
draft.unicodeEscapes = unicodeEscapes;
|
|
79
|
+
});
|
|
88
80
|
}
|
|
89
81
|
}
|
|
90
82
|
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import {RecipeRef} from "../preconditions";
|
|
17
17
|
import {IsSourceFile} from "../search";
|
|
18
|
-
import {UsesMethod, UsesType} from "./search";
|
|
18
|
+
import {IsVendoredOrBundled, UsesMethod, UsesType} from "./search";
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Match source files by path glob.
|
|
@@ -40,6 +40,24 @@ export function hasSourcePath(filePattern: string): RecipeRef {
|
|
|
40
40
|
);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Match vendored, bundled and build-output JavaScript and TypeScript sources,
|
|
45
|
+
* typically negated with {@link not} so that a recipe leaves them alone.
|
|
46
|
+
*
|
|
47
|
+
* Returns a {@link RecipeRef} placeholder bundled with a native
|
|
48
|
+
* {@link IsVendoredOrBundled} visitor for in-process evaluation; see
|
|
49
|
+
* {@link hasSourcePath} for the introspection / lazy-evaluation pattern.
|
|
50
|
+
*
|
|
51
|
+
* Delegates to {@code org.openrewrite.javascript.search.FindVendoredOrBundled}.
|
|
52
|
+
*/
|
|
53
|
+
export function isVendoredOrBundled(): RecipeRef {
|
|
54
|
+
return new RecipeRef(
|
|
55
|
+
"org.openrewrite.javascript.search.FindVendoredOrBundled",
|
|
56
|
+
{},
|
|
57
|
+
new IsVendoredOrBundled(),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
43
61
|
/**
|
|
44
62
|
* Match files using a specific method.
|
|
45
63
|
*
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 the original author or authors.
|
|
3
|
+
* <p>
|
|
4
|
+
* Licensed under the Moderne Source Available License (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
* <p>
|
|
8
|
+
* https://docs.moderne.io/licensing/moderne-source-available-license
|
|
9
|
+
* <p>
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import * as picomatch from "picomatch";
|
|
17
|
+
import {ExecutionContext} from "../../execution";
|
|
18
|
+
import {TreeVisitor} from "../../visitor";
|
|
19
|
+
import {foundSearchResult} from "../../markers";
|
|
20
|
+
import {J, TextComment} from "../../java";
|
|
21
|
+
import {JS} from "../tree";
|
|
22
|
+
|
|
23
|
+
const PATH_PATTERNS = [
|
|
24
|
+
"**/node_modules/**",
|
|
25
|
+
"**/bower_components/**",
|
|
26
|
+
"**/vendor/**",
|
|
27
|
+
"**/dist/**",
|
|
28
|
+
"**/*.min.*",
|
|
29
|
+
"**/*.bundle.*"
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const SOURCE_MAPPING_URL = /^[#@] sourceMappingURL=/;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Marks JavaScript and TypeScript sources that are vendored, bundled or build output.
|
|
36
|
+
* In-process counterpart of {@code org.openrewrite.javascript.search.FindVendoredOrBundled};
|
|
37
|
+
* keep the two in sync.
|
|
38
|
+
*/
|
|
39
|
+
export class IsVendoredOrBundled extends TreeVisitor<any, ExecutionContext> {
|
|
40
|
+
private readonly matcher: picomatch.Matcher =
|
|
41
|
+
picomatch.default ? picomatch.default(PATH_PATTERNS) : (picomatch as any)(PATH_PATTERNS);
|
|
42
|
+
|
|
43
|
+
protected async preVisit(tree: any, _: ExecutionContext): Promise<any> {
|
|
44
|
+
this.stopAfterPreVisit();
|
|
45
|
+
if (tree.kind === JS.Kind.CompilationUnit && this.isVendoredOrBundled(tree as JS.CompilationUnit)) {
|
|
46
|
+
return foundSearchResult(tree);
|
|
47
|
+
}
|
|
48
|
+
return tree;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private isVendoredOrBundled(cu: JS.CompilationUnit): boolean {
|
|
52
|
+
if (this.matcher(cu.sourcePath.replace(/\\/g, '/'))) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
const comments = cu.eof.comments;
|
|
56
|
+
const last = comments[comments.length - 1];
|
|
57
|
+
return last?.kind === J.Kind.TextComment && SOURCE_MAPPING_URL.test((last as TextComment).text.trim());
|
|
58
|
+
}
|
|
59
|
+
}
|