@openrewrite/rewrite 8.91.2 → 8.91.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.
Files changed (124) hide show
  1. package/dist/execution.d.ts +8 -0
  2. package/dist/execution.d.ts.map +1 -1
  3. package/dist/execution.js +31 -0
  4. package/dist/execution.js.map +1 -1
  5. package/dist/javascript/add-import.d.ts +64 -9
  6. package/dist/javascript/add-import.d.ts.map +1 -1
  7. package/dist/javascript/add-import.js +398 -71
  8. package/dist/javascript/add-import.js.map +1 -1
  9. package/dist/javascript/amd.d.ts +11 -1
  10. package/dist/javascript/amd.d.ts.map +1 -1
  11. package/dist/javascript/amd.js +43 -9
  12. package/dist/javascript/amd.js.map +1 -1
  13. package/dist/javascript/binding.d.ts +10 -2
  14. package/dist/javascript/binding.d.ts.map +1 -1
  15. package/dist/javascript/binding.js +44 -71
  16. package/dist/javascript/binding.js.map +1 -1
  17. package/dist/javascript/comparator.d.ts +2 -0
  18. package/dist/javascript/comparator.d.ts.map +1 -1
  19. package/dist/javascript/comparator.js +5 -1
  20. package/dist/javascript/comparator.js.map +1 -1
  21. package/dist/javascript/format/format.d.ts +5 -2
  22. package/dist/javascript/format/format.d.ts.map +1 -1
  23. package/dist/javascript/format/format.js +51 -11
  24. package/dist/javascript/format/format.js.map +1 -1
  25. package/dist/javascript/format/minimum-viable-spacing-visitor.d.ts +19 -0
  26. package/dist/javascript/format/minimum-viable-spacing-visitor.d.ts.map +1 -1
  27. package/dist/javascript/format/minimum-viable-spacing-visitor.js +180 -7
  28. package/dist/javascript/format/minimum-viable-spacing-visitor.js.map +1 -1
  29. package/dist/javascript/format/prettier-format.d.ts +2 -1
  30. package/dist/javascript/format/prettier-format.d.ts.map +1 -1
  31. package/dist/javascript/format/prettier-format.js +33 -23
  32. package/dist/javascript/format/prettier-format.js.map +1 -1
  33. package/dist/javascript/index.d.ts +3 -3
  34. package/dist/javascript/index.d.ts.map +1 -1
  35. package/dist/javascript/index.js +21 -1
  36. package/dist/javascript/index.js.map +1 -1
  37. package/dist/javascript/parser-utils.d.ts.map +1 -1
  38. package/dist/javascript/parser-utils.js +5 -9
  39. package/dist/javascript/parser-utils.js.map +1 -1
  40. package/dist/javascript/parser.d.ts.map +1 -1
  41. package/dist/javascript/parser.js +48 -3
  42. package/dist/javascript/parser.js.map +1 -1
  43. package/dist/javascript/print.d.ts +1 -0
  44. package/dist/javascript/print.d.ts.map +1 -1
  45. package/dist/javascript/print.js +9 -0
  46. package/dist/javascript/print.js.map +1 -1
  47. package/dist/javascript/recipes/change-import.d.ts +6 -3
  48. package/dist/javascript/recipes/change-import.d.ts.map +1 -1
  49. package/dist/javascript/recipes/change-import.js +17 -277
  50. package/dist/javascript/recipes/change-import.js.map +1 -1
  51. package/dist/javascript/rpc.js +2 -0
  52. package/dist/javascript/rpc.js.map +1 -1
  53. package/dist/javascript/scope.d.ts +40 -7
  54. package/dist/javascript/scope.d.ts.map +1 -1
  55. package/dist/javascript/scope.js +182 -30
  56. package/dist/javascript/scope.js.map +1 -1
  57. package/dist/javascript/templating/bindings.js +2 -20
  58. package/dist/javascript/templating/bindings.js.map +1 -1
  59. package/dist/javascript/templating/comparator.d.ts +2 -0
  60. package/dist/javascript/templating/comparator.d.ts.map +1 -1
  61. package/dist/javascript/templating/comparator.js +25 -0
  62. package/dist/javascript/templating/comparator.js.map +1 -1
  63. package/dist/javascript/templating/engine.d.ts +5 -0
  64. package/dist/javascript/templating/engine.d.ts.map +1 -1
  65. package/dist/javascript/templating/engine.js +30 -12
  66. package/dist/javascript/templating/engine.js.map +1 -1
  67. package/dist/javascript/templating/template.d.ts.map +1 -1
  68. package/dist/javascript/templating/template.js +6 -0
  69. package/dist/javascript/templating/template.js.map +1 -1
  70. package/dist/javascript/templating/types.d.ts +5 -0
  71. package/dist/javascript/templating/types.d.ts.map +1 -1
  72. package/dist/javascript/tree.d.ts +1 -0
  73. package/dist/javascript/tree.d.ts.map +1 -1
  74. package/dist/javascript/tree.js.map +1 -1
  75. package/dist/javascript/type-mapping.d.ts.map +1 -1
  76. package/dist/javascript/type-mapping.js +14 -0
  77. package/dist/javascript/type-mapping.js.map +1 -1
  78. package/dist/javascript/visitor.d.ts.map +1 -1
  79. package/dist/javascript/visitor.js +1 -0
  80. package/dist/javascript/visitor.js.map +1 -1
  81. package/dist/parser.d.ts +6 -0
  82. package/dist/parser.d.ts.map +1 -1
  83. package/dist/parser.js +23 -1
  84. package/dist/parser.js.map +1 -1
  85. package/dist/print.d.ts.map +1 -1
  86. package/dist/print.js +5 -2
  87. package/dist/print.js.map +1 -1
  88. package/dist/rewrite-javascript-version.txt +1 -1
  89. package/dist/rpc/request/parse-project.d.ts +11 -1
  90. package/dist/rpc/request/parse-project.d.ts.map +1 -1
  91. package/dist/rpc/request/parse-project.js +7 -2
  92. package/dist/rpc/request/parse-project.js.map +1 -1
  93. package/dist/rpc/request/parse.d.ts +13 -1
  94. package/dist/rpc/request/parse.d.ts.map +1 -1
  95. package/dist/rpc/request/parse.js +8 -2
  96. package/dist/rpc/request/parse.js.map +1 -1
  97. package/package.json +1 -1
  98. package/src/execution.ts +23 -0
  99. package/src/javascript/add-import.ts +449 -78
  100. package/src/javascript/amd.ts +49 -10
  101. package/src/javascript/binding.ts +60 -74
  102. package/src/javascript/comparator.ts +6 -1
  103. package/src/javascript/format/format.ts +56 -12
  104. package/src/javascript/format/minimum-viable-spacing-visitor.ts +196 -7
  105. package/src/javascript/format/prettier-format.ts +35 -25
  106. package/src/javascript/index.ts +10 -3
  107. package/src/javascript/parser-utils.ts +5 -11
  108. package/src/javascript/parser.ts +65 -6
  109. package/src/javascript/print.ts +10 -0
  110. package/src/javascript/recipes/change-import.ts +19 -349
  111. package/src/javascript/rpc.ts +2 -0
  112. package/src/javascript/scope.ts +210 -31
  113. package/src/javascript/templating/bindings.ts +2 -25
  114. package/src/javascript/templating/comparator.ts +29 -2
  115. package/src/javascript/templating/engine.ts +41 -14
  116. package/src/javascript/templating/template.ts +6 -0
  117. package/src/javascript/templating/types.ts +6 -0
  118. package/src/javascript/tree.ts +1 -0
  119. package/src/javascript/type-mapping.ts +15 -0
  120. package/src/javascript/visitor.ts +1 -0
  121. package/src/parser.ts +24 -1
  122. package/src/print.ts +5 -2
  123. package/src/rpc/request/parse-project.ts +6 -2
  124. package/src/rpc/request/parse.ts +7 -2
@@ -29,7 +29,8 @@ import {
29
29
  TrailingComma
30
30
  } from "../java";
31
31
  import {JS} from "./tree";
32
- import {cursorOf, deconflict, namesDeclaredWithin} from "./scope";
32
+ import {cursorOf, deconflict, namesDeclaredWithin, scopeOf} from "./scope";
33
+ import {Cursor} from "../tree";
33
34
  import {JavaScriptVisitor} from "./visitor";
34
35
  import {ExecutionContext} from "../execution";
35
36
 
@@ -374,17 +375,19 @@ function withParameters(
374
375
  }
375
376
 
376
377
  /**
377
- * A bare arrow parameter has no closing delimiter of its own — the space before `=>` sits on
378
- * the parameter itself. Wrapping it in parens turns that into the space before the new `)`,
379
- * so it has to move to `arrow` instead, where it will actually print before `=>` again.
378
+ * A bare arrow parameter has no closing delimiter of its own — the space before `=>` is the
379
+ * parameter's trailing padding, so parenthesizing moves its whitespace to `arrow` and leaves any
380
+ * comment with the parameter. The padding comes from the original list, since `elements` stands
381
+ * in for an emptied list with a placeholder that carries none.
380
382
  */
381
383
  function parenthesize(lambda: J.Lambda, elements: J.RightPadded<J>[]): J.Lambda {
384
+ const original = normalizeArrowParameters(lambda.parameters.parameters);
385
+ const trailing = original[original.length - 1]?.after ?? lambda.arrow;
382
386
  const last = elements[elements.length - 1];
383
- const arrowSpace = last.after.whitespace === "" ? lambda.arrow : last.after;
384
387
  return {
385
388
  ...lambda,
386
389
  parameters: {...lambda.parameters, parenthesized: true, parameters: [...elements.slice(0, -1), {...last, after: emptySpace}]},
387
- arrow: arrowSpace
390
+ arrow: space(trailing.whitespace)
388
391
  };
389
392
  }
390
393
 
@@ -468,6 +471,26 @@ export function withDependency(
468
471
  return withParts(call, block, dependencies, factory);
469
472
  }
470
473
 
474
+ /**
475
+ * Adds a dependency the factory takes no parameter for, as AMD allows for a module loaded only
476
+ * for its side effects. Refuses where parameters outnumber dependencies, since the appended one
477
+ * would land on a surplus parameter and bind after all — as it would in `define(factory)`, whose
478
+ * parameters come from the loader.
479
+ */
480
+ export function withUnboundDependency(
481
+ call: J.MethodInvocation,
482
+ block: AmdBlock,
483
+ module: string
484
+ ): J.MethodInvocation | undefined {
485
+ if (parametersOf(block).length > elementsOf(block).length) {
486
+ return undefined;
487
+ }
488
+ const dependencies = withElements(
489
+ block.dependencies,
490
+ appendEntry(elementsOf(block), dependencyLiteral(module, quoteOf(block)), dependencySlot));
491
+ return withParts(call, block, dependencies, block.factory);
492
+ }
493
+
471
494
  export function withoutDependencyAt(
472
495
  call: J.MethodInvocation,
473
496
  block: AmdBlock,
@@ -549,11 +572,11 @@ export function calleesOf(options?: AmdCalleeOptions): readonly string[] {
549
572
 
550
573
  /** The nearest AMD block the cursor sits inside, which is the one a binding belongs to. */
551
574
  export function enclosingAmdBlock(
552
- visitor: JavaScriptVisitor<any>,
575
+ from: Cursor | JavaScriptVisitor<any>,
553
576
  options?: AmdCalleeOptions
554
577
  ): {call: J.MethodInvocation, block: AmdBlock} | undefined {
555
578
  const callees = calleesOf(options);
556
- let cursor = cursorOf(visitor);
579
+ let cursor = from instanceof Cursor ? from : cursorOf(from);
557
580
  while (cursor !== undefined) {
558
581
  const value = cursor.value as J | undefined;
559
582
  if (value?.kind === J.Kind.MethodInvocation) {
@@ -591,7 +614,12 @@ const RESERVED_WORDS = new Set([
591
614
  */
592
615
  export function derivedBindingName(module: string): string | undefined {
593
616
  const segment = lastSegment(module);
594
- return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(segment) && !RESERVED_WORDS.has(segment) ? segment : undefined;
617
+ return isBindableName(segment) ? segment : undefined;
618
+ }
619
+
620
+ /** Whether `name` is one a binding can take. ASCII identifiers only, so a legal name may be refused. */
621
+ export function isBindableName(name: string): boolean {
622
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) && !RESERVED_WORDS.has(name);
595
623
  }
596
624
 
597
625
  /** Queued reservations already targeting this block: the shared source for both dedup and deconfliction. */
@@ -619,7 +647,18 @@ export function bindAmd(
619
647
 
620
648
  const declared = module === "" ? -1 : modules.indexOf(module);
621
649
  if (declared >= 0) {
622
- return bindings[declared];
650
+ const bound = bindings[declared];
651
+ if (bound === undefined) {
652
+ // A parameter that is not a plain name gives the module no binding to hand back.
653
+ return undefined;
654
+ }
655
+ // Only code the factory encloses can shadow its parameter, so a cursor elsewhere in the
656
+ // block — on the `define` call, or on its dependency array — has nothing to measure.
657
+ const cursor = cursorOf(visitor);
658
+ const withinFactory = cursor?.firstEnclosing((v): v is J => v === amd.block.factory);
659
+ if (withinFactory === undefined || scopeOf(cursor!).declaringScope(bound) === amd.block.factory) {
660
+ return bound;
661
+ }
623
662
  }
624
663
 
625
664
  // Two calls naming the same module at the same block are the same request (ADR 0013),
@@ -16,12 +16,15 @@
16
16
  import {J} from "../java";
17
17
  import {JS} from "./tree";
18
18
  import {JavaScriptVisitor} from "./visitor";
19
- import {compilationUnitOf, cursorOf, declarationsOf, walk} from "./scope";
20
- import {AddImportOptions, bindImport, existingImportBinding, memberName, moduleNameOf, RebindImport, requiredModuleOf} from "./add-import";
19
+ import {compilationUnitOf, cursorOf, declarationsOf, namesUsedIn, scopeOf} from "./scope";
20
+ import {
21
+ AddImportOptions, bindImport, existingImportBinding, ExistingImportBinding, hasEsmSyntax, isCommonJs, memberName,
22
+ moduleNameOf, nameTaken, RebindImport, requiredModuleOfDeclaration
23
+ } from "./add-import";
21
24
  import {RemoveImport} from "./remove-import";
22
25
  import {
23
26
  AmdCalleeOptions, amdBlockOf, bindAmd, calleesOf, dependencyNames, derivedBindingName, enclosingAmdBlock,
24
- parameterNames, RebindAmdDependency, RemoveAmdDependency
27
+ isBindableName, parameterNames, RebindAmdDependency, RemoveAmdDependency
25
28
  } from "./amd";
26
29
 
27
30
  /**
@@ -48,7 +51,14 @@ export interface MaybeUnbindOptions {
48
51
 
49
52
  export interface MaybeRebindOptions {
50
53
  from: {module: string; member?: string};
51
- to: {module: string; member?: string};
54
+
55
+ /**
56
+ * `alias` names the moved binding, taken verbatim. Left unset, an alias on the `from` side
57
+ * survives the move — `{Old as X}` becomes `{New as X}`, since `X` is a name the file chose —
58
+ * while an unaliased binding takes the new member's name where that name is free. Pin `alias`
59
+ * to the old local name to keep it. See CLAUDE.md: Which name a rebind binds.
60
+ */
61
+ to: {module: string; member?: string; alias?: string};
52
62
 
53
63
  /** Callees that introduce an AMD block. UI5 writes `sap.ui.define`, RequireJS and Dojo `define`. */
54
64
  amdCallee?: string | readonly string[];
@@ -117,40 +127,6 @@ export function isAmdBlock(node: J, options?: AmdCalleeOptions): boolean {
117
127
  amdBlockOf(node as J.MethodInvocation, calleesOf(options)) !== undefined;
118
128
  }
119
129
 
120
- /** Whether a top-level statement already marks the file as a module: an import or any export form. */
121
- function hasEsmSyntax(cu: JS.CompilationUnit): boolean {
122
- return cu.statements.some(stmt => {
123
- const element = stmt.element;
124
- // `export {a, b}`, `export * from` and `export default` are their own statement kinds;
125
- // `export class`/`function`/`const` instead carry `export` as a modifier on the
126
- // declaration itself, the same way TypeScript's own AST models it.
127
- const modifiers = (element as {modifiers?: J.Modifier[]} | undefined)?.modifiers;
128
- return element?.kind === JS.Kind.Import ||
129
- element?.kind === JS.Kind.ExportDeclaration ||
130
- element?.kind === JS.Kind.ExportAssignment ||
131
- (modifiers?.some(m => m.keyword === "export") ?? false);
132
- }) || hasTopLevelAwait(cu);
133
- }
134
-
135
- /**
136
- * Whether a statement holds an `await` outside any function of its own — legal only at module
137
- * top level, unlike an `await` inside an `async function`, which says nothing about the file.
138
- */
139
- function hasTopLevelAwait(cu: JS.CompilationUnit): boolean {
140
- let found = false;
141
- walk(cu.statements, node => {
142
- if (found) {
143
- return false;
144
- }
145
- if (node.kind === JS.Kind.Await) {
146
- found = true;
147
- return false;
148
- }
149
- return node.kind !== J.Kind.MethodDeclaration && node.kind !== J.Kind.Lambda;
150
- });
151
- return found;
152
- }
153
-
154
130
  interface ModuleObjectBinding {
155
131
  name: string;
156
132
  module: string;
@@ -167,33 +143,6 @@ interface ModuleObjectBinding {
167
143
  typeOnly: boolean;
168
144
  }
169
145
 
170
- /** Whether the file binds its modules with `require`, which decides whether a create is possible. */
171
- function isCommonJs(cu: JS.CompilationUnit): boolean {
172
- if (cu.sourcePath.endsWith(".cjs") || cu.sourcePath.endsWith(".cts")) {
173
- return true;
174
- }
175
- // Node treats these as ES modules regardless of what they contain, the same way
176
- // `AddImport`'s own `determineImportStyle` reads them as ES6-preferring; `.js`/`.ts`/`.tsx`
177
- // stay ambiguous and fall through to the statements below.
178
- if (cu.sourcePath.endsWith(".mjs") || cu.sourcePath.endsWith(".mts")) {
179
- return false;
180
- }
181
- if (hasEsmSyntax(cu)) {
182
- return false;
183
- }
184
- return cu.statements.some(stmt =>
185
- declarationsOf(stmt.element).some(d => requiredModule(d) !== undefined));
186
- }
187
-
188
- /** The module a `const X = require("m")` declaration names, for the one variable it declares. */
189
- function requiredModule(declaration: J.VariableDeclarations): string | undefined {
190
- const variables = declaration.variables;
191
- const initializer = variables.length === 1 ? variables[0].element?.initializer?.element : undefined;
192
- return initializer?.kind === J.Kind.MethodInvocation
193
- ? requiredModuleOf(initializer as J.MethodInvocation)
194
- : undefined;
195
- }
196
-
197
146
  /**
198
147
  * The module a `const X = await import("m")` declaration names, for the one variable it declares.
199
148
  * A dynamic import resolves to the module namespace object, the same value `import * as X`
@@ -270,7 +219,7 @@ function moduleObjectBindings(cu: JS.CompilationUnit): ModuleObjectBinding[] {
270
219
  }
271
220
  }
272
221
  }
273
- bindings.push(...wholeModuleBindingsVia(cu, requiredModule, "require"));
222
+ bindings.push(...wholeModuleBindingsVia(cu, requiredModuleOfDeclaration, "require"));
274
223
  bindings.push(...wholeModuleBindingsVia(cu, dynamicallyImportedModule, "namespace"));
275
224
  return bindings;
276
225
  }
@@ -312,12 +261,14 @@ export function maybeBind(
312
261
 
313
262
  const cu = compilationUnitOf(visitor);
314
263
  const isWholeModule = !options.sideEffectOnly && (key === undefined || key === "*");
315
- if (isWholeModule) {
316
- const bound = cu && moduleObjectBindings(cu).find(b =>
264
+ if (isWholeModule && cu) {
265
+ const scope = scopeOf(cursorOf(visitor)!);
266
+ const bound = moduleObjectBindings(cu).find(b =>
317
267
  b.module === module && answersWholeModuleRequest(b, key === "*", options.typeOnly ?? false) &&
318
268
  // A pinned alias asks for a binding of that name, so another name for the same
319
269
  // module does not answer it; `bindImport`'s own lookup applies the same rule.
320
- (options.alias === undefined || b.name === options.alias));
270
+ (options.alias === undefined || b.name === options.alias) &&
271
+ scope.declaringScope(b.name) === cu);
321
272
  if (bound !== undefined) {
322
273
  return bound.name;
323
274
  }
@@ -374,9 +325,10 @@ function bindingShape(member: string | undefined): "default" | "namespace" | "na
374
325
  }
375
326
 
376
327
  /**
377
- * Moves the binding for `from` to `to`, keeping the local name it already had — the primitive
328
+ * Moves the binding for `from` to `to` and answers with the name it now carries — the primitive
378
329
  * behind a member rename or a module move. Returns `undefined`, changing nothing, where the move
379
- * is not safely expressible; the refusals are listed in CLAUDE.md: JavaScript module bindings.
330
+ * is not safely expressible; the refusals and the choice of name are listed in CLAUDE.md:
331
+ * JavaScript module bindings.
380
332
  */
381
333
  export function maybeRebind(visitor: JavaScriptVisitor<any>, options: MaybeRebindOptions): string | undefined {
382
334
  const amd = enclosingAmdBlock(visitor, options);
@@ -386,7 +338,10 @@ export function maybeRebind(visitor: JavaScriptVisitor<any>, options: MaybeRebin
386
338
  }
387
339
  const index = dependencyNames(amd.block).indexOf(options.from.module);
388
340
  const binding = index < 0 ? undefined : parameterNames(amd.block)[index];
389
- if (binding === undefined) {
341
+ if (binding === undefined ||
342
+ // An AMD binding's name is its factory parameter, so naming it something else means
343
+ // rewriting the factory's references, which this lane's dependency swap does not reach.
344
+ (options.to.alias !== undefined && options.to.alias !== binding)) {
390
345
  return undefined;
391
346
  }
392
347
  const callees = calleesOf(options);
@@ -412,8 +367,39 @@ export function maybeRebind(visitor: JavaScriptVisitor<any>, options: MaybeRebin
412
367
  if (!existing.onlyMemberOfStatement && isCommonJs(cu)) {
413
368
  return undefined;
414
369
  }
415
- visitor.afterVisit.push(new RebindImport(options.from, options.to, existing.localName));
416
- return existing.localName;
370
+ const boundName = rebindingName(visitor, cu, options, existing);
371
+ if (boundName === undefined) {
372
+ return undefined;
373
+ }
374
+ if (!(visitor.afterVisit || []).some(v => v instanceof RebindImport &&
375
+ v.from.module === options.from.module && v.from.member === options.from.member &&
376
+ v.localName === existing.localName)) {
377
+ visitor.afterVisit.push(new RebindImport(options.from, options.to, existing.localName, boundName));
378
+ }
379
+ return boundName;
380
+ }
381
+
382
+ /**
383
+ * The name the moved binding takes, or undefined where a pinned alias cannot be bound verbatim,
384
+ * since deconflicting it would leave the references the caller emits unbound. The rule is
385
+ * CLAUDE.md: Which name a rebind binds.
386
+ */
387
+ function rebindingName(
388
+ visitor: JavaScriptVisitor<any>,
389
+ cu: JS.CompilationUnit,
390
+ options: MaybeRebindOptions,
391
+ existing: ExistingImportBinding
392
+ ): string | undefined {
393
+ const taken = (name: string) => nameTaken(name, namesUsedIn(cu), visitor);
394
+ const alias = options.to.alias;
395
+ if (alias !== undefined) {
396
+ return isBindableName(alias) && (alias === existing.localName || !taken(alias)) ? alias : undefined;
397
+ }
398
+ const member = memberName(options.to.member);
399
+ if (existing.aliased || member === undefined || member === '*' || member === existing.localName) {
400
+ return existing.localName;
401
+ }
402
+ return taken(member) ? existing.localName : member;
417
403
  }
418
404
 
419
405
  /**
@@ -2347,6 +2347,11 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2347
2347
  return true;
2348
2348
  }
2349
2349
 
2350
+ /** Whether the receiver has to be walked even where attribution alone settles the call. */
2351
+ protected selectMustBeVisited(_method: J.MethodInvocation): boolean {
2352
+ return false;
2353
+ }
2354
+
2350
2355
  /**
2351
2356
  * Override method invocation comparison to include type attribution checking.
2352
2357
  * When types match semantically, we allow matching even if one has a receiver
@@ -2432,7 +2437,7 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2432
2437
 
2433
2438
  // When types match (canSkipNameCheck = true), we can skip select comparison entirely.
2434
2439
  // This allows matching forwardRef() vs React.forwardRef() where types indicate same method.
2435
- if (!canSkipNameCheck) {
2440
+ if (!canSkipNameCheck || this.selectMustBeVisited(method)) {
2436
2441
  // Types didn't provide a match - must compare receivers structurally
2437
2442
  if ((method.select === undefined) !== (otherMethod.select === undefined)) {
2438
2443
  return this.structuralMismatch('select');
@@ -18,7 +18,7 @@ import {JavaScriptVisitor} from "../visitor";
18
18
  import {Comment, J, lastWhitespace, replaceLastWhitespace, Statement} from "../../java";
19
19
  import {create as produce, Draft} from "mutative";
20
20
  import {Cursor, isScope, isSourceFile, Tree} from "../../tree";
21
- import {BlankLinesStyle, getStyle, SpacesStyle, StyleKind, TabsAndIndentsStyle, WrappingAndBracesStyle} from "../style";
21
+ import {BlankLinesStyle, getStyle, PrettierStyle, SpacesStyle, StyleKind, TabsAndIndentsStyle, WrappingAndBracesStyle} from "../style";
22
22
  import {NamedStyles} from "../../style";
23
23
  import {produceAsync} from "../../visitor";
24
24
  import {Generator} from "../markers";
@@ -55,8 +55,9 @@ export const autoFormat = async <J2 extends J, P>(
55
55
  * 2. Styles from source file markers (NamedStyles)
56
56
  * 3. IntelliJ defaults
57
57
  *
58
- * When a PrettierStyle is present (either in the styles array or as a marker on the source file),
59
- * Prettier is used for formatting. Otherwise, built-in formatting visitors are used.
58
+ * A PrettierStyle, in the styles array or as a marker on the source file, formats through Prettier.
59
+ * The built-in visitors lay out a generated subtree Prettier cannot carry back, and any file
60
+ * without such a style.
60
61
  */
61
62
  export class AutoformatVisitor<P> extends JavaScriptVisitor<P> {
62
63
  private readonly styles?: NamedStyles<string>[];
@@ -67,23 +68,27 @@ export class AutoformatVisitor<P> extends JavaScriptVisitor<P> {
67
68
  }
68
69
 
69
70
  async visit<R extends J>(tree: Tree, p: P, cursor?: Cursor): Promise<R | undefined> {
70
- // Check for PrettierStyle in styles array or as marker on source file
71
- // If found, delegate entirely to Prettier (skip other formatting visitors)
72
71
  const prettierStyle = getPrettierStyle(tree, cursor, this.styles);
73
72
  if (prettierStyle) {
74
- return applyPrettierFormatting(tree as R, prettierStyle, p, cursor, this.stopAfter);
73
+ const formatted = await applyPrettierFormatting(tree as R, prettierStyle, p, cursor, this.stopAfter);
74
+ if (formatted !== undefined) {
75
+ return formatted;
76
+ }
75
77
  }
76
78
 
77
79
  // Style markers live on the source file, which is `tree` itself only when a whole file is being formatted
78
80
  const styleSource = (isSourceFile(tree) ? tree : cursor?.firstEnclosing(isSourceFile)) ?? tree;
81
+ const tabsAndIndents = getStyle(StyleKind.TabsAndIndentsStyle, styleSource, this.styles) as TabsAndIndentsStyle;
82
+ const spaces = getStyle(StyleKind.SpacesStyle, styleSource, this.styles) as SpacesStyle;
79
83
 
80
84
  const visitors = [
81
85
  new NormalizeWhitespaceVisitor(this.stopAfter),
82
86
  new MinimumViableSpacingVisitor(this.stopAfter),
83
87
  new BlankLinesVisitor(getStyle(StyleKind.BlankLinesStyle, styleSource, this.styles) as BlankLinesStyle, this.stopAfter),
84
88
  new WrappingAndBracesVisitor(getStyle(StyleKind.WrappingAndBracesStyle, styleSource, this.styles) as WrappingAndBracesStyle, this.stopAfter),
85
- new SpacesVisitor(getStyle(StyleKind.SpacesStyle, styleSource, this.styles) as SpacesStyle, this.stopAfter),
86
- new TabsAndIndentsVisitor(getStyle(StyleKind.TabsAndIndentsStyle, styleSource, this.styles) as TabsAndIndentsStyle, this.stopAfter),
89
+ new SpacesVisitor(prettierStyle ? spacesFrom(prettierStyle, spaces) : spaces, this.stopAfter),
90
+ new TabsAndIndentsVisitor(
91
+ prettierStyle ? tabsAndIndentsFrom(prettierStyle, tabsAndIndents) : tabsAndIndents, this.stopAfter),
87
92
  ]
88
93
 
89
94
  let t: R | undefined = tree as R;
@@ -98,6 +103,22 @@ export class AutoformatVisitor<P> extends JavaScriptVisitor<P> {
98
103
  }
99
104
  }
100
105
 
106
+ /** The brace spacing a Prettier configuration states, over the style the rest of the spacing comes from. */
107
+ function spacesFrom(prettierStyle: PrettierStyle, fallback: SpacesStyle): SpacesStyle {
108
+ // Prettier's default, in force for a configuration that does not name it
109
+ const bracketSpacing = prettierStyle.config.bracketSpacing !== false;
110
+ return {...fallback, within: {...fallback.within, objectLiteralBraces: bracketSpacing, es6ImportExportBraces: bracketSpacing}};
111
+ }
112
+
113
+ /** The widths a Prettier configuration states, over the style the rest of the layout comes from. */
114
+ function tabsAndIndentsFrom(prettierStyle: PrettierStyle, fallback: TabsAndIndentsStyle): TabsAndIndentsStyle {
115
+ const config = prettierStyle.config;
116
+ // Prettier's defaults, in force for a configuration that names neither
117
+ const tabWidth = typeof config.tabWidth === 'number' ? config.tabWidth : 2;
118
+ const useTabCharacter = config.useTabs === true;
119
+ return {...fallback, useTabCharacter, tabSize: tabWidth, indentSize: tabWidth, continuationIndent: tabWidth};
120
+ }
121
+
101
122
  export class SpacesVisitor<P> extends JavaScriptVisitor<P> {
102
123
  constructor(private style: SpacesStyle, private stopAfter?: Tree) {
103
124
  super();
@@ -279,6 +300,14 @@ export class SpacesVisitor<P> extends JavaScriptVisitor<P> {
279
300
  })
280
301
  }
281
302
 
303
+ protected async visitExportSpecifier(exportSpecifier: JS.ExportSpecifier, p: P): Promise<J | undefined> {
304
+ const ret = await super.visitExportSpecifier(exportSpecifier, p) as JS.ExportSpecifier;
305
+ return produce(ret, draft => {
306
+ // `type` is separated from the name it qualifies by that name's own prefix
307
+ draft.specifier.prefix.whitespace = draft.typeOnly.element ? " " : "";
308
+ });
309
+ }
310
+
282
311
  protected async visitForLoop(forLoop: J.ForLoop, p: P): Promise<J | undefined> {
283
312
  const ret = await super.visitForLoop(forLoop, p) as J.ForLoop;
284
313
  return produceAsync(ret, async draft => {
@@ -337,12 +366,16 @@ export class SpacesVisitor<P> extends JavaScriptVisitor<P> {
337
366
  : "";
338
367
  }
339
368
  if (draft.importClause.namedBindings) {
340
- const hasDefaultImport = !!draft.importClause.name;
341
- if (hasDefaultImport || draft.importClause.typeOnly) {
342
- draft.importClause.namedBindings.prefix.whitespace = " ";
343
- }
369
+ // `type`, or a default name and its comma, stands between keyword and bindings
370
+ const afterClause = draft.importClause.name || draft.importClause.typeOnly ? " " : "";
344
371
  if (draft.importClause.namedBindings.kind == JS.Kind.NamedImports) {
345
372
  const ni = draft.importClause.namedBindings as Draft<JS.NamedImports>;
373
+ // For named imports the space before `{` sits in the container, as the parser
374
+ // writes it; a newline there is layout, which spacing does not decide
375
+ if (!ni.prefix.whitespace.includes("\n") && !ni.elements.before.whitespace.includes("\n")) {
376
+ ni.prefix.whitespace = "";
377
+ ni.elements.before.whitespace = afterClause;
378
+ }
346
379
  const isMultiLine = ni.elements.elements.some(e => e.element.prefix.whitespace.includes("\n"));
347
380
  if (!isMultiLine) {
348
381
  const braceSpace = this.style.within.es6ImportExportBraces ? " " : "";
@@ -355,6 +388,8 @@ export class SpacesVisitor<P> extends JavaScriptVisitor<P> {
355
388
  lastAfter.whitespace = this.style.other.beforeComma ? " " : "";
356
389
  }
357
390
  }
391
+ } else if (afterClause) {
392
+ draft.importClause.namedBindings.prefix.whitespace = afterClause;
358
393
  }
359
394
  }
360
395
  }
@@ -365,6 +400,15 @@ export class SpacesVisitor<P> extends JavaScriptVisitor<P> {
365
400
  })
366
401
  }
367
402
 
403
+ protected async visitImportSpecifier(importSpecifier: JS.ImportSpecifier, p: P): Promise<J | undefined> {
404
+ const ret = await super.visitImportSpecifier(importSpecifier, p) as JS.ImportSpecifier;
405
+ return produce(ret, draft => {
406
+ // The specifier's leading space is the brace and comma rules'; this is the
407
+ // gap between `type` and the name, which the name's own prefix carries
408
+ draft.specifier.prefix.whitespace = draft.importType.element ? " " : "";
409
+ });
410
+ }
411
+
368
412
  protected async visitIndexSignatureDeclaration(indexSignatureDeclaration: JS.IndexSignatureDeclaration, p: P): Promise<J | undefined> {
369
413
  const ret = await super.visitIndexSignatureDeclaration(indexSignatureDeclaration, p) as JS.IndexSignatureDeclaration;
370
414
  return produce(ret, draft => {