@openrewrite/rewrite 8.67.0-20251119-202228 → 8.67.0-20251120-093147

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.
@@ -13,23 +13,32 @@ export enum ImportStyle {
13
13
  }
14
14
 
15
15
  export interface AddImportOptions {
16
- /** The module name (e.g., 'fs') to import from */
17
- target: string;
16
+ /** The module name (e.g., 'fs', 'react') to import from */
17
+ module: string;
18
18
 
19
19
  /** Optionally, the specific member to import from the module.
20
20
  * If not specified, adds a default import or namespace import.
21
21
  * Special values:
22
- * - 'default': Adds a default import from the target module.
23
- * When using 'default', the `alias` parameter is required. */
22
+ * - 'default': Adds a default import from the module.
23
+ * When using 'default', the `alias` parameter is required.
24
+ * - '*': Adds a namespace import (import * as alias from 'module').
25
+ * When using '*', the `alias` parameter is required.
26
+ * Cannot be combined with `sideEffectOnly`. */
24
27
  member?: string;
25
28
 
26
29
  /** Optional alias for the imported member.
27
- * Required when member is 'default'. */
30
+ * Required when member is 'default' or '*'.
31
+ * Cannot be combined with `sideEffectOnly`. */
28
32
  alias?: string;
29
33
 
30
- /** If true, only add the import if the member is actually used in the file. Default: true */
34
+ /** If true, only add the import if the member is actually used in the file. Default: true
35
+ * Cannot be combined with `sideEffectOnly`. */
31
36
  onlyIfReferenced?: boolean;
32
37
 
38
+ /** If true, adds a side-effect import without bindings (e.g., `import 'module'` or `require('module')`).
39
+ * Cannot be combined with `member`, `alias`, or `onlyIfReferenced`. */
40
+ sideEffectOnly?: boolean;
41
+
33
42
  /** Optional import style to use. If not specified, auto-detects from file and existing imports */
34
43
  style?: ImportStyle;
35
44
  }
@@ -41,15 +50,23 @@ export interface AddImportOptions {
41
50
  *
42
51
  * @example
43
52
  * // Add a named import
44
- * maybeAddImport(visitor, { target: 'fs', member: 'readFile' });
53
+ * maybeAddImport(visitor, { module: 'fs', member: 'readFile' });
45
54
  *
46
55
  * @example
47
56
  * // Add a default import using the 'default' member specifier
48
- * maybeAddImport(visitor, { target: 'react', member: 'default', alias: 'React' });
57
+ * maybeAddImport(visitor, { module: 'react', member: 'default', alias: 'React' });
49
58
  *
50
59
  * @example
51
60
  * // Add a default import (legacy way, without specifying member)
52
- * maybeAddImport(visitor, { target: 'react', alias: 'React' });
61
+ * maybeAddImport(visitor, { module: 'react', alias: 'React' });
62
+ *
63
+ * @example
64
+ * // Add a namespace import
65
+ * maybeAddImport(visitor, { module: 'crypto', member: '*', alias: 'crypto' });
66
+ *
67
+ * @example
68
+ * // Add a side-effect import
69
+ * maybeAddImport(visitor, { module: 'core-js/stable', sideEffectOnly: true });
53
70
  */
54
71
  export function maybeAddImport(
55
72
  visitor: JavaScriptVisitor<any>,
@@ -57,9 +74,10 @@ export function maybeAddImport(
57
74
  ) {
58
75
  for (const v of visitor.afterVisit || []) {
59
76
  if (v instanceof AddImport &&
60
- v.target === options.target &&
77
+ v.module === options.module &&
61
78
  v.member === options.member &&
62
- v.alias === options.alias) {
79
+ v.alias === options.alias &&
80
+ v.sideEffectOnly === (options.sideEffectOnly ?? false)) {
63
81
  return;
64
82
  }
65
83
  }
@@ -67,10 +85,11 @@ export function maybeAddImport(
67
85
  }
68
86
 
69
87
  export class AddImport<P> extends JavaScriptVisitor<P> {
70
- readonly target: string;
88
+ readonly module: string;
71
89
  readonly member?: string;
72
90
  readonly alias?: string;
73
91
  readonly onlyIfReferenced: boolean;
92
+ readonly sideEffectOnly: boolean;
74
93
  readonly style?: ImportStyle;
75
94
 
76
95
  constructor(options: AddImportOptions) {
@@ -81,10 +100,29 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
81
100
  throw new Error("When member is 'default', the alias parameter is required");
82
101
  }
83
102
 
84
- this.target = options.target;
103
+ // Validate that alias is provided when member is '*' (namespace import)
104
+ if (options.member === '*' && !options.alias) {
105
+ throw new Error("When member is '*', the alias parameter is required");
106
+ }
107
+
108
+ // Validate that sideEffectOnly is not combined with incompatible options
109
+ if (options.sideEffectOnly) {
110
+ if (options.member !== undefined) {
111
+ throw new Error("Cannot combine sideEffectOnly with member");
112
+ }
113
+ if (options.alias !== undefined) {
114
+ throw new Error("Cannot combine sideEffectOnly with alias");
115
+ }
116
+ if (options.onlyIfReferenced !== undefined) {
117
+ throw new Error("Cannot combine sideEffectOnly with onlyIfReferenced");
118
+ }
119
+ }
120
+
121
+ this.module = options.module;
85
122
  this.member = options.member;
86
123
  this.alias = options.alias;
87
124
  this.onlyIfReferenced = options.onlyIfReferenced ?? true;
125
+ this.sideEffectOnly = options.sideEffectOnly ?? false;
88
126
  this.style = options.style;
89
127
  }
90
128
 
@@ -238,7 +276,7 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
238
276
  if (moduleSpecifier) {
239
277
  const moduleName = this.getModuleName(moduleSpecifier);
240
278
 
241
- if (moduleName === this.target) {
279
+ if (moduleName === this.module) {
242
280
  const importClause = jsImport.importClause;
243
281
  if (importClause?.namedBindings) {
244
282
  if (importClause.namedBindings.kind === JS.Kind.NamedImports) {
@@ -264,7 +302,7 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
264
302
  if (initializer?.kind === J.Kind.MethodInvocation &&
265
303
  this.isRequireCall(initializer as J.MethodInvocation)) {
266
304
  const moduleName = this.getModuleNameFromRequire(initializer as J.MethodInvocation);
267
- if (moduleName === this.target) {
305
+ if (moduleName === this.module) {
268
306
  return ImportStyle.CommonJS;
269
307
  }
270
308
  }
@@ -283,7 +321,8 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
283
321
  }
284
322
 
285
323
  // If onlyIfReferenced is true, check if the identifier is actually used
286
- if (this.onlyIfReferenced) {
324
+ // Skip this check for side-effect imports
325
+ if (!this.sideEffectOnly && this.onlyIfReferenced) {
287
326
  const isReferenced = await this.checkIdentifierReferenced(compilationUnit);
288
327
  if (!isReferenced) {
289
328
  return compilationUnit;
@@ -294,8 +333,8 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
294
333
  const importStyle = this.determineImportStyle(compilationUnit);
295
334
 
296
335
  // For ES6 named imports, check if we can merge into an existing import from the same module
297
- // Don't try to merge default imports (member === 'default')
298
- if (importStyle === ImportStyle.ES6Named && this.member !== undefined && this.member !== 'default') {
336
+ // Don't try to merge default imports (member === 'default'), side-effect imports, or namespace imports (member === '*')
337
+ if (!this.sideEffectOnly && importStyle === ImportStyle.ES6Named && this.member !== undefined && this.member !== 'default' && this.member !== '*') {
299
338
  const mergedCu = await this.tryMergeIntoExistingImport(compilationUnit, p);
300
339
  if (mergedCu !== compilationUnit) {
301
340
  return mergedCu;
@@ -393,7 +432,7 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
393
432
  const moduleName = this.getModuleName(moduleSpecifier);
394
433
 
395
434
  // Check if this is an import from our target module
396
- if (moduleName !== this.target) {
435
+ if (moduleName !== this.module) {
397
436
  continue;
398
437
  }
399
438
 
@@ -487,17 +526,44 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
487
526
  }
488
527
 
489
528
  const moduleName = this.getModuleName(moduleSpecifier);
490
- if (moduleName !== this.target) {
529
+ if (moduleName !== this.module) {
491
530
  return false;
492
531
  }
493
532
 
494
533
  const importClause = jsImport.importClause;
534
+
535
+ // Handle side-effect imports (no import clause)
495
536
  if (!importClause) {
537
+ // If we're trying to add a side-effect import and one already exists, it's a match
538
+ return this.sideEffectOnly;
539
+ }
540
+
541
+ // If we're adding a side-effect import but there's an existing import with bindings,
542
+ // it's not a match (side-effect import should be separate)
543
+ if (this.sideEffectOnly) {
496
544
  return false;
497
545
  }
498
546
 
499
547
  // Check if the specific member or default import already exists
500
- if (this.member === undefined || this.member === 'default') {
548
+ if (this.member === '*') {
549
+ // We're adding a namespace import, check if one exists
550
+ const namedBindings = importClause.namedBindings;
551
+ if (!namedBindings) {
552
+ return false;
553
+ }
554
+
555
+ // Namespace imports can be represented as J.Identifier or JS.Alias
556
+ if (namedBindings.kind === J.Kind.Identifier) {
557
+ const identifier = namedBindings as J.Identifier;
558
+ return identifier.simpleName === this.alias;
559
+ } else if (namedBindings.kind === JS.Kind.Alias) {
560
+ const alias = namedBindings as JS.Alias;
561
+ if (alias.alias?.kind === J.Kind.Identifier) {
562
+ return (alias.alias as J.Identifier).simpleName === this.alias;
563
+ }
564
+ }
565
+ return false;
566
+ } else if (this.member === undefined || this.member === 'default') {
501
567
  // We're adding a default import, check if one exists
502
568
  // For member === 'default', also verify the alias matches if specified
503
569
  if (importClause.name === undefined) {
@@ -559,7 +625,7 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
559
625
  }
560
626
 
561
627
  const moduleName = this.getModuleNameFromRequire(methodInv);
562
- if (moduleName !== this.target) {
628
+ if (moduleName !== this.module) {
563
629
  return false;
564
630
  }
565
631
 
@@ -610,8 +676,8 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
610
676
  };
611
677
 
612
678
  // Create a visitor to collect used identifiers with their type attribution
613
- const collector = new class extends JavaScriptVisitor<ExecutionContext> {
614
- override async visitIdentifier(identifier: J.Identifier, p: ExecutionContext): Promise<J | undefined> {
679
+ const collector = new class extends JavaScriptVisitor<void> {
680
+ override async visitIdentifier(identifier: J.Identifier, p: void): Promise<J | undefined> {
615
681
  const type = identifier.type;
616
682
  if (type && Type.isMethod(type)) {
617
683
  recordMethodUsage(type as Type.Method);
@@ -619,21 +685,21 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
619
685
  return super.visitIdentifier(identifier, p);
620
686
  }
621
687
 
622
- override async visitMethodInvocation(methodInvocation: J.MethodInvocation, p: ExecutionContext): Promise<J | undefined> {
688
+ override async visitMethodInvocation(methodInvocation: J.MethodInvocation, p: void): Promise<J | undefined> {
623
689
  if (methodInvocation.methodType) {
624
690
  recordMethodUsage(methodInvocation.methodType);
625
691
  }
626
692
  return super.visitMethodInvocation(methodInvocation, p);
627
693
  }
628
694
 
629
- override async visitFunctionCall(functionCall: JS.FunctionCall, p: ExecutionContext): Promise<J | undefined> {
695
+ override async visitFunctionCall(functionCall: JS.FunctionCall, p: void): Promise<J | undefined> {
630
696
  if (functionCall.methodType) {
631
697
  recordMethodUsage(functionCall.methodType);
632
698
  }
633
699
  return super.visitFunctionCall(functionCall, p);
634
700
  }
635
701
 
636
- override async visitFieldAccess(fieldAccess: J.FieldAccess, p: ExecutionContext): Promise<J | undefined> {
702
+ override async visitFieldAccess(fieldAccess: J.FieldAccess, p: void): Promise<J | undefined> {
637
703
  const type = fieldAccess.type;
638
704
  if (type && Type.isMethod(type)) {
639
705
  recordMethodUsage(type as Type.Method);
@@ -642,10 +708,19 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
642
708
  }
643
709
  };
644
710
 
645
- await collector.visit(compilationUnit, new ExecutionContext());
711
+ await collector.visit(compilationUnit, undefined);
712
+
713
+ // For namespace imports (member === '*'), we cannot use type attribution to detect usage
714
+ // because the namespace itself is used as an identifier, not individual members.
715
+ // We would need to traverse the AST looking for the alias identifier.
716
+ // For simplicity, we skip the onlyIfReferenced check for namespace imports.
717
+ if (this.member === '*') {
718
+ // TODO: Implement proper namespace usage detection by checking if alias identifier is used
719
+ return true;
720
+ }
646
721
 
647
722
  // Check if our target import is used based on type attribution
648
- const moduleMembers = usedImports.get(this.target);
723
+ const moduleMembers = usedImports.get(this.module);
649
724
  if (!moduleMembers) {
650
725
  return false;
651
726
  }
@@ -662,20 +737,68 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
662
737
  const prefix = this.determineImportPrefix(compilationUnit, insertionIndex);
663
738
 
664
739
  // Create the module specifier
740
+ // For side-effect imports, use emptySpace since space comes from LeftPadded.before
741
+ // For regular imports with import clause, use emptySpace since space comes from LeftPadded.before
742
+ // However, the printer expects the space after 'from' in the literal's prefix
665
743
  const moduleSpecifier: J.Literal = {
666
744
  id: randomId(),
667
745
  kind: J.Kind.Literal,
668
- prefix: singleSpace,
746
+ prefix: this.sideEffectOnly ? emptySpace : singleSpace,
669
747
  markers: emptyMarkers,
670
- value: `'${this.target}'`,
671
- valueSource: `'${this.target}'`,
748
+ value: `'${this.module}'`,
749
+ valueSource: `'${this.module}'`,
672
750
  unicodeEscapes: [],
673
751
  type: undefined
674
752
  };
675
753
 
676
754
  let importClause: JS.ImportClause | undefined;
677
755
 
678
- if (this.member === undefined || this.member === 'default') {
756
+ if (this.sideEffectOnly) {
757
+ // Side-effect import: import 'module'
758
+ importClause = undefined;
759
+ } else if (this.member === '*') {
760
+ // Namespace import: import * as alias from 'module'
761
+ const propertyName: J.Identifier = {
762
+ id: randomId(),
763
+ kind: J.Kind.Identifier,
764
+ prefix: emptySpace,
765
+ markers: emptyMarkers,
766
+ annotations: [],
767
+ simpleName: '*',
768
+ type: undefined,
769
+ fieldType: undefined
770
+ };
771
+
772
+ const aliasIdentifier: J.Identifier = {
773
+ id: randomId(),
774
+ kind: J.Kind.Identifier,
775
+ prefix: singleSpace,
776
+ markers: emptyMarkers,
777
+ annotations: [],
778
+ simpleName: this.alias!,
779
+ type: undefined,
780
+ fieldType: undefined
781
+ };
782
+
783
+ const namespaceBinding: JS.Alias = {
784
+ id: randomId(),
785
+ kind: JS.Kind.Alias,
786
+ prefix: singleSpace,
787
+ markers: emptyMarkers,
788
+ propertyName: rightPadded(propertyName, singleSpace),
789
+ alias: aliasIdentifier
790
+ };
791
+
792
+ importClause = {
793
+ id: randomId(),
794
+ kind: JS.Kind.ImportClause,
795
+ prefix: emptySpace,
796
+ markers: emptyMarkers,
797
+ typeOnly: false,
798
+ name: undefined,
799
+ namedBindings: namespaceBinding
800
+ };
801
+ } else if (this.member === undefined || this.member === 'default') {
679
802
  // Default import: import target from 'module'
680
803
  // or: import alias from 'module' (when member === 'default')
681
804
  const defaultName: J.Identifier = {
@@ -684,7 +807,7 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
684
807
  prefix: singleSpace,
685
808
  markers: emptyMarkers,
686
809
  annotations: [],
687
- simpleName: this.alias || this.target,
810
+ simpleName: this.alias || this.module,
688
811
  type: undefined,
689
812
  fieldType: undefined
690
813
  };
@@ -6,16 +6,16 @@ import {ElementRemovalFormatter} from "../java/formatting-utils";
6
6
 
7
7
  /**
8
8
  * @param visitor The visitor to add the import removal to
9
- * @param target Either the module name (e.g., 'fs') to remove specific members from,
10
- * or the name of the import to remove entirely
9
+ * @param module The module name (e.g., 'fs', 'react') to remove imports from
11
10
  * @param member Optionally, the specific member to remove from the import.
12
- * If not specified, removes the import matching `target`.
11
+ * If not specified, removes all unused imports from the module.
13
12
  * Special values:
14
- * - 'default': Removes the default import from the target module if unused,
13
+ * - 'default': Removes the default import from the module if unused,
15
14
  * regardless of its local name (e.g., `import React from 'react'`)
15
+ * - '*': Removes the namespace import if unused (e.g., `import * as fs from 'fs'`)
16
16
  *
17
17
  * @example
18
- * // Remove a named import if unused
18
+ * // Remove a specific named import if unused
19
19
  * maybeRemoveImport(visitor, 'fs', 'readFile');
20
20
  *
21
21
  * @example
@@ -23,16 +23,20 @@ import {ElementRemovalFormatter} from "../java/formatting-utils";
23
23
  * maybeRemoveImport(visitor, 'react', 'default');
24
24
  *
25
25
  * @example
26
- * // Remove an import by name if unused (no module specified)
27
- * maybeRemoveImport(visitor, 'fs');
26
+ * // Remove all unused imports from 'react' module
27
+ * maybeRemoveImport(visitor, 'react');
28
+ *
29
+ * @example
30
+ * // Remove namespace import if unused
31
+ * maybeRemoveImport(visitor, 'fs', '*');
28
32
  */
29
- export function maybeRemoveImport(visitor: JavaScriptVisitor<any>, target: string, member?: string) {
33
+ export function maybeRemoveImport(visitor: JavaScriptVisitor<any>, module: string, member?: string) {
30
34
  for (const v of visitor.afterVisit || []) {
31
- if (v instanceof RemoveImport && v.target === target && v.member === member) {
35
+ if (v instanceof RemoveImport && v.module === module && v.member === member) {
32
36
  return;
33
37
  }
34
38
  }
35
- visitor.afterVisit.push(new RemoveImport(target, member));
39
+ visitor.afterVisit.push(new RemoveImport(module, member));
36
40
  }
37
41
 
38
42
  // Type alias for RightPadded elements to simplify type signatures
@@ -45,15 +49,15 @@ type RightPaddedElement<T extends J> = {
45
49
 
46
50
  export class RemoveImport<P> extends JavaScriptVisitor<P> {
47
51
  /**
48
- * @param target Either the module name (e.g., 'fs') to remove specific members from,
49
- * or the name of the import to remove entirely
52
+ * @param module The module name (e.g., 'fs', 'react') to remove imports from
50
53
  * @param member Optionally, the specific member to remove from the import.
51
- * If not specified, removes the import matching `target`.
54
+ * If not specified, removes all unused imports from the module.
52
55
  * Special values:
53
- * - 'default': Removes the default import from the target module if unused,
56
+ * - 'default': Removes the default import from the module if unused,
54
57
  * regardless of its local name
58
+ * - '*': Removes the namespace import if unused
55
59
  */
56
- constructor(readonly target: string,
60
+ constructor(readonly module: string,
57
61
  readonly member?: string) {
58
62
  super();
59
63
  }
@@ -245,7 +249,7 @@ export class RemoveImport<P> extends JavaScriptVisitor<P> {
245
249
  const name = identifier.simpleName;
246
250
 
247
251
  // Check if we should remove this default import
248
- let shouldRemove = false;
252
+ let shouldRemove: boolean;
249
253
  if (this.member === 'default') {
250
254
  // Special case: member 'default' means remove any default import from the target module if unused
251
255
  shouldRemove = !usedIdentifiers.has(name) && !usedTypes.has(name);
@@ -425,22 +429,17 @@ export class RemoveImport<P> extends JavaScriptVisitor<P> {
425
429
  * Check if the module name matches the target module
426
430
  */
427
431
  private matchesTargetModule(moduleName: string): boolean {
428
- return this.member === undefined ? moduleName === this.target : moduleName === this.target;
432
+ return moduleName === this.module;
429
433
  }
430
434
 
431
435
  /**
432
436
  * Check if an identifier should be removed based on usage
433
437
  */
434
438
  private shouldRemoveIdentifier(name: string, usedIdentifiers: Set<string>, usedTypes: Set<string>): boolean {
435
- // If member is specified, we're removing a specific member
436
- if (this.member !== undefined) {
437
- // Only remove if the identifier is not used
438
- return !usedIdentifiers.has(name) && !usedTypes.has(name);
439
- } else {
440
- // We're removing based on the target name
441
- // Check if the name matches and is not used
442
- return this.target === name && !usedIdentifiers.has(name) && !usedTypes.has(name);
443
- }
439
+ // For CommonJS and import-equals-require, we're removing the entire import
440
+ // if the identifier is not used (member is typically undefined for these cases,
441
+ // or we're checking if a specific binding is used)
442
+ return !usedIdentifiers.has(name) && !usedTypes.has(name);
444
443
  }
445
444
 
446
445
  private async processNamedImports(
@@ -681,40 +680,32 @@ export class RemoveImport<P> extends JavaScriptVisitor<P> {
681
680
  usedIdentifiers: Set<string>,
682
681
  usedTypes: Set<string>
683
682
  ): boolean {
684
- // If member is specified, we're removing a specific member from a module
683
+ // If member is specified, we're removing a specific member from the module
685
684
  if (this.member !== undefined) {
686
685
  // Only remove if this is the specific member we're looking for
687
686
  if (this.member !== name) {
688
687
  return false;
689
688
  }
690
- } else {
691
- // If no member specified, we're removing based on the import name itself
692
- if (this.target !== name) {
693
- return false;
694
- }
695
689
  }
690
+ // If no member specified, we're removing all unused imports from the module
691
+ // So we check if this particular import is unused
696
692
 
697
693
  // Check if it's used
698
694
  return !(usedIdentifiers.has(name) || usedTypes.has(name));
699
695
  }
700
696
 
701
697
  private isTargetModule(jsImport: JS.Import): boolean {
702
- // If member is specified, we're looking for imports from a specific module
703
- if (this.member !== undefined) {
704
- const moduleSpecifier = jsImport.moduleSpecifier?.element;
705
- if (!moduleSpecifier || moduleSpecifier.kind !== J.Kind.Literal) {
706
- return false;
707
- }
708
-
709
- const literal = moduleSpecifier as J.Literal;
710
- const moduleName = literal.value?.toString().replace(/['"`]/g, '');
711
-
712
- // Match the module name
713
- return moduleName === this.target;
698
+ // Always check if the import is from the specified module
699
+ const moduleSpecifier = jsImport.moduleSpecifier?.element;
700
+ if (!moduleSpecifier || moduleSpecifier.kind !== J.Kind.Literal) {
701
+ return false;
714
702
  }
715
703
 
716
- // If no member specified, we process all imports to check their names
717
- return true;
704
+ const literal = moduleSpecifier as J.Literal;
705
+ const moduleName = literal.value?.toString().replace(/['"`]/g, '');
706
+
707
+ // Match the module name
708
+ return moduleName === this.module;
718
709
  }
719
710
 
720
711
  /**