@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.
Files changed (37) hide show
  1. package/dist/javascript/add-import.d.ts.map +1 -1
  2. package/dist/javascript/add-import.js +127 -52
  3. package/dist/javascript/add-import.js.map +1 -1
  4. package/dist/javascript/amd.d.ts.map +1 -1
  5. package/dist/javascript/amd.js +3 -2
  6. package/dist/javascript/amd.js.map +1 -1
  7. package/dist/javascript/binding.js +3 -3
  8. package/dist/javascript/binding.js.map +1 -1
  9. package/dist/javascript/index.d.ts +3 -0
  10. package/dist/javascript/index.d.ts.map +1 -1
  11. package/dist/javascript/index.js +3 -0
  12. package/dist/javascript/index.js.map +1 -1
  13. package/dist/javascript/migrate/es6/modernize-octal-escape-sequences.d.ts.map +1 -1
  14. package/dist/javascript/migrate/es6/modernize-octal-escape-sequences.js +28 -32
  15. package/dist/javascript/migrate/es6/modernize-octal-escape-sequences.js.map +1 -1
  16. package/dist/javascript/preconditions.d.ts +11 -0
  17. package/dist/javascript/preconditions.d.ts.map +1 -1
  18. package/dist/javascript/preconditions.js +14 -0
  19. package/dist/javascript/preconditions.js.map +1 -1
  20. package/dist/javascript/search/index.d.ts +1 -0
  21. package/dist/javascript/search/index.d.ts.map +1 -1
  22. package/dist/javascript/search/index.js +1 -0
  23. package/dist/javascript/search/index.js.map +1 -1
  24. package/dist/javascript/search/is-vendored-or-bundled.d.ts +13 -0
  25. package/dist/javascript/search/is-vendored-or-bundled.d.ts.map +1 -0
  26. package/dist/javascript/search/is-vendored-or-bundled.js +93 -0
  27. package/dist/javascript/search/is-vendored-or-bundled.js.map +1 -0
  28. package/dist/rewrite-javascript-version.txt +1 -1
  29. package/package.json +1 -1
  30. package/src/javascript/add-import.ts +135 -53
  31. package/src/javascript/amd.ts +3 -2
  32. package/src/javascript/binding.ts +3 -3
  33. package/src/javascript/index.ts +3 -0
  34. package/src/javascript/migrate/es6/modernize-octal-escape-sequences.ts +28 -36
  35. package/src/javascript/preconditions.ts +19 -1
  36. package/src/javascript/search/index.ts +1 -0
  37. 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. An existing specifier is the most direct precedent; a Prettier
461
- * config states intent even where the file itself has not been formatted to match.
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
- const prettier = getPrettierStyle(cu);
525
- if (prettier && !prettier.ignored) {
526
- const singleQuote = prettier.config.singleQuote;
527
- if (typeof singleQuote === 'boolean') {
528
- return singleQuote ? "'" : '"';
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
- return double > single ? '"' : "'";
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 the file's own style — a Prettier
537
- * configuration's brace spacing and print width, say — reaches it.
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
- // Create semicolon marker for the import statement
824
- const semicolonMarkers = markers({
825
- kind: J.Markers.Semicolon,
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 === expectedDeclaringType) {
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 === expectedDeclaringType) {
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
- if (expectedDeclaringType) {
1351
- const declaringTypeName = Type.FullyQualified.getFullyQualifiedName(methodInvocation.methodType.declaringType);
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
- if (expectedDeclaringType) {
1363
- const declaringTypeName = Type.FullyQualified.getFullyQualifiedName(functionCall.methodType.declaringType);
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
- if (expectedDeclaringType) {
1378
- const declaringTypeName = Type.FullyQualified.getFullyQualifiedName(methodType.declaringType);
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 = this.quoteStyle ?? await detectQuote(compilationUnit);
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
- // Get the spaces style for brace spacing
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).value !== module) {
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, the same way `RemoveImport` keeps formatting sane when trimming a list.
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.
@@ -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.value : "";
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).value as string
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
- const module = (specifier as J.Literal).value;
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) {
@@ -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 old-style octal escape sequences (e.g., `\\0`, `\\123`) to modern hex escape sequences (e.g., `\\x00`, `\\x53`) or Unicode escape sequences (e.g., `\\u0000`, `\\u0053`).";
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
- // Check if this string contains octal escape sequences
58
- // Octal escape sequences: \0 through \377 (1-3 octal digits)
59
- // Pattern: backslash followed by 1-3 octal digits (0-7)
60
- // We need to be careful not to match already escaped sequences
61
- const octalEscapePattern = /\\([0-7]{1,3})/g;
62
-
63
- let hasOctalEscapes = false;
64
- let modernized = valueSource;
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
- if (hasOctalEscapes) {
82
- return produce(literal, draft => {
83
- draft.valueSource = modernized;
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
  *
@@ -1,6 +1,7 @@
1
1
  import {CategoryDescriptor, JavaScript} from "../../marketplace";
2
2
 
3
3
  export * from './find-dependency';
4
+ export * from './is-vendored-or-bundled';
4
5
  export * from './uses-method';
5
6
  export * from './uses-type';
6
7
 
@@ -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
+ }