@openrewrite/rewrite 8.92.4 → 8.92.6

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.
@@ -1,8 +1,8 @@
1
1
  import {JavaScriptVisitor} from "./visitor";
2
- import {ElementRemovalFormatter, emptySpace, isIdentifier, J, NameTree, rightPadded, singleSpace, space, Statement, Type} from "../java";
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, markers} from "../markers";
5
+ import {emptyMarkers, findMarker, markers} from "../markers";
6
6
  import {getStyle, SpacesStyle, StyleKind} from "./style";
7
7
  import {bindingNames, compilationUnitOf, cursorOf, declarationsOf, deconflict, isValueReference, namesDeclaredIn, scopeOf, walk} from "./scope";
8
8
  import {create as produce, Draft} from "mutative";
@@ -940,7 +940,10 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
940
940
  // - trailingSpace: space before } (from last element's after)
941
941
  const firstElementPrefix = existingElements[0]?.element?.prefix ?? emptySpace;
942
942
  const lastIndex = existingElements.length - 1;
943
- const trailingSpace = existingElements[lastIndex].after;
943
+ // A trailing comma is a marker on the last element rather than padding, so with
944
+ // one present the space before `}` lives in the marker's suffix, not in `after`.
945
+ const trailingComma = findMarker<TrailingComma>(existingElements[lastIndex], J.Markers.TrailingComma);
946
+ const trailingSpace = trailingComma ? trailingComma.suffix : existingElements[lastIndex].after;
944
947
 
945
948
  // Build the new elements array with proper spacing
946
949
  const updatedNamedImports: JS.NamedImports = await this.produceJavaScript(
@@ -961,9 +964,18 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
961
964
  if (j === 0 && insertIndex === 0 && elem.element) {
962
965
  adjusted = {...elem, element: {...elem.element, prefix: singleSpace}};
963
966
  }
964
- // Last element before a new trailing element loses its trailing space
967
+ // Last element before a new trailing element loses its trailing space,
968
+ // and hands off its trailing comma to the element that becomes last.
965
969
  if (j === lastIndex && insertIndex > lastIndex) {
966
- adjusted = {...adjusted, after: emptySpace};
970
+ adjusted = trailingComma ?
971
+ {
972
+ ...adjusted,
973
+ markers: {
974
+ ...adjusted.markers,
975
+ markers: adjusted.markers.markers.filter(m => m !== trailingComma)
976
+ }
977
+ } :
978
+ {...adjusted, after: emptySpace};
967
979
  }
968
980
  results.push(adjusted);
969
981
  return results;
@@ -971,7 +983,12 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
971
983
 
972
984
  // Append at end if inserting after all existing elements
973
985
  if (insertIndex > lastIndex) {
974
- newElements.push(rightPadded({...newSpecifier, prefix: singleSpace}, trailingSpace));
986
+ const appended = rightPadded(
987
+ {...newSpecifier, prefix: singleSpace},
988
+ trailingComma ? emptySpace : trailingSpace);
989
+ newElements.push(trailingComma ?
990
+ {...appended, markers: markers(trailingComma)} :
991
+ appended);
975
992
  }
976
993
 
977
994
  namedDraft.elements = {...namedImports.elements, elements: newElements};
@@ -693,6 +693,38 @@ export class JavaScriptTypeMapping {
693
693
  return variable;
694
694
  }
695
695
 
696
+ /**
697
+ * The module specifier of a `require('module')` call, or undefined for anything else.
698
+ *
699
+ * `require` is declared to return `any`, so the checker carries no module type through it and
700
+ * the specifier in the call is the only thing identifying the module.
701
+ */
702
+ private requiredModuleSpecifier(node: ts.Expression): string | undefined {
703
+ if (!ts.isCallExpression(node) || !ts.isIdentifier(node.expression) ||
704
+ node.expression.text !== 'require' || node.arguments.length === 0) {
705
+ return undefined;
706
+ }
707
+ const moduleArg = node.arguments[0];
708
+ return ts.isStringLiteral(moduleArg) ? moduleArg.text : undefined;
709
+ }
710
+
711
+ /**
712
+ * The module an expression evaluates to where that is a `require()` result — either the call
713
+ * itself (`require('x').fn()`) or an identifier bound to one (`const x = require('x')`).
714
+ */
715
+ private requiredModuleOfExpression(node: ts.Expression): string | undefined {
716
+ const direct = this.requiredModuleSpecifier(node);
717
+ if (direct) {
718
+ return direct;
719
+ }
720
+ if (!ts.isIdentifier(node)) {
721
+ return undefined;
722
+ }
723
+ const valueDecl = this.checker.getSymbolAtLocation(node)?.valueDeclaration;
724
+ return valueDecl && ts.isVariableDeclaration(valueDecl) && valueDecl.initializer ?
725
+ this.requiredModuleSpecifier(valueDecl.initializer) : undefined;
726
+ }
727
+
696
728
  /**
697
729
  * Extract the npm module name from a file path.
698
730
  * Handles various package manager layouts:
@@ -891,41 +923,14 @@ export class JavaScriptTypeMapping {
891
923
  // where the module is typed as 'any' but methods still have signatures
892
924
 
893
925
  // Try to trace back through the AST to find the require() call
894
- let inferredDeclaringType: Type.FullyQualified | undefined;
895
- const objExpr = node.expression.expression;
896
-
897
- if (ts.isIdentifier(objExpr)) {
898
- // Look for the variable declaration that assigns the require() result
899
- const objSymbol = this.checker.getSymbolAtLocation(objExpr);
900
-
901
- if (objSymbol && objSymbol.valueDeclaration) {
902
- const valueDecl = objSymbol.valueDeclaration;
903
- if (ts.isVariableDeclaration(valueDecl) && valueDecl.initializer) {
904
- // Check if it's a require() call
905
- if (ts.isCallExpression(valueDecl.initializer)) {
906
- const callExpr = valueDecl.initializer;
907
- if (ts.isIdentifier(callExpr.expression) &&
908
- callExpr.expression.getText() === 'require' &&
909
- callExpr.arguments.length > 0) {
910
- // Extract the module name from require('module-name')
911
- const moduleArg = callExpr.arguments[0];
912
- if (ts.isStringLiteral(moduleArg)) {
913
- const moduleName = moduleArg.text;
914
-
915
- inferredDeclaringType = {
916
- kind: Type.Kind.Class,
917
- flags: 0, // TODO - determine flags
918
- fullyQualifiedName: moduleName
919
- } as Type.FullyQualified;
920
- }
921
- }
922
- }
923
- }
924
- }
925
- }
926
+ const moduleName = this.requiredModuleOfExpression(node.expression.expression);
926
927
 
927
928
  // Use the inferred type or fall back to unknown
928
- declaringType = inferredDeclaringType || Type.unknownType as Type.FullyQualified;
929
+ declaringType = moduleName ? {
930
+ kind: Type.Kind.Class,
931
+ flags: 0, // TODO - determine flags
932
+ fullyQualifiedName: moduleName
933
+ } as Type.FullyQualified : Type.unknownType as Type.FullyQualified;
929
934
 
930
935
  // Create the method type using the helper
931
936
  return this.createMethodType(signature, node, declaringType, methodName);
@@ -985,6 +990,19 @@ export class JavaScriptTypeMapping {
985
990
  declaringType = Type.unknownType as Type.FullyQualified;
986
991
  }
987
992
 
993
+ // A `require()` result is typed `any`, so nothing above identifies the module; the
994
+ // specifier in the call does.
995
+ if (declaringType === Type.unknownType) {
996
+ const moduleName = this.requiredModuleOfExpression(node.expression.expression);
997
+ if (moduleName) {
998
+ declaringType = {
999
+ kind: Type.Kind.Class,
1000
+ flags: 0, // TODO - determine flags
1001
+ fullyQualifiedName: moduleName
1002
+ } as Type.FullyQualified;
1003
+ }
1004
+ }
1005
+
988
1006
  // For string methods like 'hello'.split(), ensure we have a proper declaring type for primitives
989
1007
  if (!isImport && declaringType === Type.unknownType) {
990
1008
  // If the expression type is a primitive string, use String as declaring type
@@ -1294,9 +1312,14 @@ export class JavaScriptTypeMapping {
1294
1312
  // file's node_modules path) match the names used for directly imported types.
1295
1313
  packageName = this.normalizePackageName(packageName);
1296
1314
 
1297
- // Find the symbol name (everything after the last dot in the original cleaned name)
1315
+ // The symbol name is appended to the declaration file's path, so it is whatever
1316
+ // follows the last dot of the final path segment. Directory names carry dots of
1317
+ // their own — `.pnpm`, `.yarn`, a hidden parent such as `.claude`, a versioned
1318
+ // pnpm directory — so a search over the whole path would split there instead and
1319
+ // fold most of the path into the name.
1320
+ const lastSegmentIndex = cleanedName.lastIndexOf('/') + 1;
1298
1321
  const lastDotIndex = cleanedName.lastIndexOf('.');
1299
- if (lastDotIndex > 0) {
1322
+ if (lastDotIndex > lastSegmentIndex) {
1300
1323
  const symbolName = cleanedName.substring(lastDotIndex + 1);
1301
1324
  cleanedName = `${packageName}.${symbolName}`;
1302
1325
  } else {
package/src/rpc/index.ts CHANGED
@@ -16,6 +16,7 @@
16
16
  import {Checksum, FileAttributes, TreeKind} from "../tree";
17
17
  import {RpcCodecs, RpcReceiveQueue, RpcSendQueue} from "./queue";
18
18
  import {
19
+ emptyMarkers,
19
20
  Markers,
20
21
  MarkersKind,
21
22
  MarkupDebug,
@@ -76,10 +77,12 @@ RpcCodecs.registerCodec(TreeKind.FileAttributes, {
76
77
 
77
78
  RpcCodecs.registerCodec(MarkersKind.Markers, {
78
79
  async rpcReceive(before: Markers, q: RpcReceiveQueue): Promise<Markers> {
79
- return updateIfChanged(before, {
80
- id: await q.receive(before.id),
81
- markers: (await q.receiveList(before.markers))!,
82
- });
80
+ const id = await q.receive(before.id);
81
+ const markers = (await q.receiveList(before.markers))!;
82
+ if (markers.length === 0) {
83
+ return emptyMarkers;
84
+ }
85
+ return updateIfChanged(before, {id, markers});
83
86
  },
84
87
 
85
88
  async rpcSend(after: Markers, q: RpcSendQueue): Promise<void> {
package/src/rpc/queue.ts CHANGED
@@ -19,6 +19,8 @@ import {saveTrace, trace} from "./trace";
19
19
  import {updateIfChanged} from "../util";
20
20
  import {isRef, ReferenceMap} from "../reference";
21
21
 
22
+ const EMPTY_LIST: readonly never[] = Object.freeze([]);
23
+
22
24
  /**
23
25
  * Interface representing an RPC codec that defines methods
24
26
  * for sending and receiving objects in an RPC communication.
@@ -424,10 +426,12 @@ export class RpcReceiveQueue {
424
426
  }
425
427
  return this.receive(markers, async m => {
426
428
  return saveTrace(this.trace, async () => {
427
- return updateIfChanged(markers!, {
428
- id: await this.receive(m.id),
429
- markers: (await this.receiveList(m.markers))!,
430
- });
429
+ const id = await this.receive(m.id);
430
+ const markerList = (await this.receiveList(m.markers))!;
431
+ if (markerList.length === 0) {
432
+ return emptyMarkers;
433
+ }
434
+ return updateIfChanged(markers!, {id, markers: markerList});
431
435
  })
432
436
  })
433
437
  }
@@ -516,7 +520,7 @@ export class RpcReceiveQueue {
516
520
  before: T[] | undefined,
517
521
  onChange?: (before: T) => T | Promise<T | undefined> | undefined
518
522
  ): Promise<T[]> {
519
- return (await this.receiveList(before, onChange)) ?? [];
523
+ return (await this.receiveList(before, onChange)) ?? (EMPTY_LIST as unknown as T[]);
520
524
  }
521
525
 
522
526
  receiveList<T>(
@@ -554,6 +558,9 @@ export class RpcReceiveQueue {
554
558
  if (!positions) {
555
559
  throw new Error(`Expected positions array but got: ${JSON.stringify(d)}`);
556
560
  }
561
+ if (positions.length === 0) {
562
+ return EMPTY_LIST as unknown as T[];
563
+ }
557
564
  const after: T[] = new Array(positions.length);
558
565
  for (let i = 0; i < positions.length; i++) {
559
566
  const beforeIdx = positions[i];
package/src/util.ts CHANGED
@@ -50,16 +50,66 @@ export function trimIndent(str: string | null | undefined): string {
50
50
  .trim();
51
51
  }
52
52
 
53
+ /**
54
+ * A compiled `(original, updates) => merged` builder that names every resulting property in an
55
+ * object literal. V8 sizes a literal's in-object storage to the properties it names, so the merged
56
+ * node keeps its fields inline; the `{...original, ...updates}` spread of a shared, megamorphic
57
+ * function instead lands in an out-of-object `system / PropertyArray` (millions of them across an
58
+ * RPC-received LST forest). Builders are keyed and cached by the pair of property-name lists so a
59
+ * given shape compiles once.
60
+ */
61
+ type MergeBuilder = (original: any, updates: any) => any;
62
+
63
+ const mergeBuilders = new Map<string, MergeBuilder>();
64
+
65
+ function mergeBuilderFor(originalKeys: string[], updateKeys: string[]): MergeBuilder | undefined {
66
+ const cacheKey = JSON.stringify(originalKeys) + JSON.stringify(updateKeys);
67
+ let builder = mergeBuilders.get(cacheKey);
68
+ if (builder === undefined) {
69
+ const fromUpdates = new Set(updateKeys);
70
+ const names = originalKeys.slice();
71
+ for (const key of updateKeys) {
72
+ if (!names.includes(key)) {
73
+ names.push(key);
74
+ }
75
+ }
76
+ const body = "return {" + names
77
+ .map(name => `${JSON.stringify(name)}:(${fromUpdates.has(name) ? "u" : "o"})[${JSON.stringify(name)}]`)
78
+ .join(",") + "};";
79
+ try {
80
+ builder = new Function("o", "u", body) as MergeBuilder;
81
+ } catch {
82
+ builder = (o, u) => ({...o, ...u});
83
+ }
84
+ mergeBuilders.set(cacheKey, builder);
85
+ }
86
+ return builder;
87
+ }
88
+
53
89
  /**
54
90
  * Helper function to create a new object only if any properties have changed.
55
91
  * Compares each property in updates with the original object.
56
92
  * Returns the original object if nothing changed, or a new object with updates applied.
57
93
  */
58
94
  export function updateIfChanged<O extends object>(original: O, updates: Partial<O>): O {
95
+ let changed = false;
59
96
  for (const key in updates) {
60
97
  if (updates[key] !== original[key]) {
61
- return { ...original, ...updates };
98
+ changed = true;
99
+ break;
100
+ }
101
+ }
102
+ if (!changed) {
103
+ return original;
104
+ }
105
+ // A merged node built through a shared spread overflows into a PropertyArray; a compiled
106
+ // literal builder keeps its fields inline. Symbol-keyed originals fall back to the spread,
107
+ // since only string keys survive the JSON-serialized builder body.
108
+ if (typeof original === "object" && Object.getOwnPropertySymbols(original).length === 0) {
109
+ const builder = mergeBuilderFor(Object.keys(original), Object.keys(updates as object));
110
+ if (builder !== undefined) {
111
+ return builder(original, updates);
62
112
  }
63
113
  }
64
- return original;
114
+ return {...original, ...updates};
65
115
  }