@openrewrite/rewrite 8.67.0 → 8.67.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/java/tree.d.ts +7 -1
- package/dist/java/tree.d.ts.map +1 -1
- package/dist/java/tree.js +13 -3
- package/dist/java/tree.js.map +1 -1
- package/dist/javascript/add-import.d.ts.map +1 -1
- package/dist/javascript/add-import.js +200 -44
- package/dist/javascript/add-import.js.map +1 -1
- package/dist/javascript/cleanup/index.d.ts +2 -0
- package/dist/javascript/cleanup/index.d.ts.map +1 -0
- package/dist/javascript/cleanup/index.js +21 -0
- package/dist/javascript/cleanup/index.js.map +1 -0
- package/dist/javascript/cleanup/use-object-property-shorthand.d.ts +22 -0
- package/dist/javascript/cleanup/use-object-property-shorthand.d.ts.map +1 -0
- package/dist/javascript/cleanup/use-object-property-shorthand.js +144 -0
- package/dist/javascript/cleanup/use-object-property-shorthand.js.map +1 -0
- package/dist/javascript/comparator.d.ts +8 -0
- package/dist/javascript/comparator.d.ts.map +1 -1
- package/dist/javascript/comparator.js +12 -0
- package/dist/javascript/comparator.js.map +1 -1
- package/dist/javascript/dependency-workspace.d.ts +1 -0
- package/dist/javascript/dependency-workspace.d.ts.map +1 -1
- package/dist/javascript/dependency-workspace.js +44 -0
- package/dist/javascript/dependency-workspace.js.map +1 -1
- package/dist/javascript/parser.d.ts.map +1 -1
- package/dist/javascript/parser.js +21 -1
- package/dist/javascript/parser.js.map +1 -1
- package/dist/javascript/print.d.ts +1 -0
- package/dist/javascript/print.d.ts.map +1 -1
- package/dist/javascript/print.js +8 -0
- package/dist/javascript/print.js.map +1 -1
- package/dist/javascript/rpc.js +14 -0
- package/dist/javascript/rpc.js.map +1 -1
- package/dist/javascript/templating/pattern.d.ts +7 -0
- package/dist/javascript/templating/pattern.d.ts.map +1 -1
- package/dist/javascript/templating/pattern.js +10 -0
- package/dist/javascript/templating/pattern.js.map +1 -1
- package/dist/javascript/templating/rewrite.d.ts.map +1 -1
- package/dist/javascript/templating/rewrite.js +17 -16
- package/dist/javascript/templating/rewrite.js.map +1 -1
- package/dist/javascript/templating/types.d.ts +56 -28
- package/dist/javascript/templating/types.d.ts.map +1 -1
- package/dist/javascript/tree.d.ts +9 -0
- package/dist/javascript/tree.d.ts.map +1 -1
- package/dist/javascript/tree.js +1 -0
- package/dist/javascript/tree.js.map +1 -1
- package/dist/javascript/type-mapping.d.ts +13 -1
- package/dist/javascript/type-mapping.d.ts.map +1 -1
- package/dist/javascript/type-mapping.js +215 -8
- package/dist/javascript/type-mapping.js.map +1 -1
- package/dist/javascript/visitor.d.ts +1 -0
- package/dist/javascript/visitor.d.ts.map +1 -1
- package/dist/javascript/visitor.js +11 -0
- package/dist/javascript/visitor.js.map +1 -1
- package/dist/json/parser.js +18 -2
- package/dist/json/parser.js.map +1 -1
- package/dist/version.txt +1 -1
- package/package.json +1 -1
- package/src/index.ts +3 -0
- package/src/java/tree.ts +7 -1
- package/src/javascript/add-import.ts +211 -53
- package/src/javascript/cleanup/index.ts +17 -0
- package/src/javascript/cleanup/use-object-property-shorthand.ts +154 -0
- package/src/javascript/comparator.ts +11 -0
- package/src/javascript/dependency-workspace.ts +52 -0
- package/src/javascript/parser.ts +23 -1
- package/src/javascript/print.ts +7 -0
- package/src/javascript/rpc.ts +12 -0
- package/src/javascript/templating/pattern.ts +11 -0
- package/src/javascript/templating/rewrite.ts +19 -18
- package/src/javascript/templating/types.ts +60 -28
- package/src/javascript/tree.ts +10 -0
- package/src/javascript/type-mapping.ts +239 -9
- package/src/javascript/visitor.ts +10 -0
- package/src/json/parser.ts +16 -2
package/src/java/tree.ts
CHANGED
|
@@ -37,6 +37,7 @@ export namespace J {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export interface Expression extends J {
|
|
40
|
+
readonly type?: Type;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
export interface MethodCall extends Expression {
|
|
@@ -834,15 +835,20 @@ export interface DocComment extends Comment {
|
|
|
834
835
|
// TODO implement me!
|
|
835
836
|
}
|
|
836
837
|
|
|
837
|
-
export const
|
|
838
|
+
export const isBinary = (n: any): n is J.Binary => n.kind === J.Kind.Binary;
|
|
838
839
|
export const isBlock = (n: any): n is J.Block => n.kind === J.Kind.Block;
|
|
840
|
+
export const isClassDeclaration = (n: any): n is J.ClassDeclaration => n.kind === J.Kind.ClassDeclaration;
|
|
841
|
+
export const isFieldAccess = (n: any): n is J.FieldAccess => n.kind === J.Kind.FieldAccess;
|
|
839
842
|
export const isIdentifier = (tree: any): tree is J.Identifier => tree["kind"] === J.Kind.Identifier;
|
|
840
843
|
export const isIf = (tree: any): tree is J.If => tree["kind"] === J.Kind.If;
|
|
844
|
+
export const isImport = (tree: any): tree is J.Import => tree["kind"] === J.Kind.Import;
|
|
841
845
|
export const isLambda = (tree: any): tree is J.Lambda => tree["kind"] === J.Kind.Lambda;
|
|
842
846
|
export const isLiteral = (tree: any): tree is J.Literal => tree["kind"] === J.Kind.Literal;
|
|
843
847
|
export const isMethodDeclaration = (n: any): n is J.MethodDeclaration => n.kind === J.Kind.MethodDeclaration;
|
|
844
848
|
export const isMethodInvocation = (n: any): n is J.MethodInvocation => n.kind === J.Kind.MethodInvocation;
|
|
849
|
+
export const isNamedVariable = (n: any): n is J.VariableDeclarations.NamedVariable => n.kind === J.Kind.NamedVariable;
|
|
845
850
|
export const isNewClass = (n: any): n is J.NewClass => n.kind === J.Kind.NewClass;
|
|
851
|
+
export const isReturn = (n: any): n is J.Return => n.kind === J.Kind.Return;
|
|
846
852
|
export const isVariableDeclarations = (n: any): n is J.VariableDeclarations => n.kind === J.Kind.VariableDeclarations;
|
|
847
853
|
|
|
848
854
|
export function rightPadded<T extends J | boolean>(t: T, trailing: J.Space, markers?: Markers): J.RightPadded<T> {
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import {JavaScriptVisitor} from "./visitor";
|
|
2
|
-
import {
|
|
2
|
+
import {emptySpace, J, rightPadded, singleSpace, space, Statement, Type} from "../java";
|
|
3
3
|
import {JS} from "./tree";
|
|
4
4
|
import {randomId} from "../uuid";
|
|
5
5
|
import {emptyMarkers, markers} from "../markers";
|
|
6
|
-
import {ExecutionContext} from "../execution";
|
|
7
6
|
|
|
8
7
|
export enum ImportStyle {
|
|
9
8
|
ES6Named, // import { x } from 'module'
|
|
@@ -449,21 +448,52 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
|
|
|
449
448
|
// We found a matching import with named bindings - merge into it
|
|
450
449
|
return this.produceJavaScript<JS.CompilationUnit>(compilationUnit, p, async draft => {
|
|
451
450
|
const namedImports = importClause.namedBindings as JS.NamedImports;
|
|
451
|
+
const existingElements = namedImports.elements.elements;
|
|
452
|
+
|
|
453
|
+
// Find the correct insertion position (alphabetical, case-insensitive)
|
|
454
|
+
const newName = (this.alias || this.member!).toLowerCase();
|
|
455
|
+
let insertIndex = existingElements.findIndex(elem => {
|
|
456
|
+
if (elem.element?.kind === JS.Kind.ImportSpecifier) {
|
|
457
|
+
const name = this.getImportAlias(elem.element) || this.getImportName(elem.element);
|
|
458
|
+
return newName.localeCompare(name.toLowerCase()) < 0;
|
|
459
|
+
}
|
|
460
|
+
return false;
|
|
461
|
+
});
|
|
462
|
+
if (insertIndex === -1) insertIndex = existingElements.length;
|
|
452
463
|
|
|
453
|
-
//
|
|
454
|
-
const newSpecifierBase = this.createImportSpecifier();
|
|
455
|
-
const newSpecifier = {...newSpecifierBase, prefix: singleSpace};
|
|
456
|
-
|
|
457
|
-
// Add the new specifier to the elements
|
|
464
|
+
// Build the new elements array with proper spacing
|
|
458
465
|
const updatedNamedImports: JS.NamedImports = await this.produceJavaScript<JS.NamedImports>(
|
|
459
466
|
namedImports, p, async namedDraft => {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
]
|
|
466
|
-
|
|
467
|
+
const lastIndex = existingElements.length - 1;
|
|
468
|
+
const trailingSpace = existingElements[lastIndex].after;
|
|
469
|
+
const newSpecifier = this.createImportSpecifier();
|
|
470
|
+
|
|
471
|
+
const newElements = existingElements.flatMap((elem, j) => {
|
|
472
|
+
const results: J.RightPadded<JS.ImportSpecifier>[] = [];
|
|
473
|
+
if (j === insertIndex) {
|
|
474
|
+
// Insert new element here; first element gets no prefix, others get space
|
|
475
|
+
const prefix = j === 0 ? emptySpace : singleSpace;
|
|
476
|
+
results.push(rightPadded({...newSpecifier, prefix}, emptySpace));
|
|
477
|
+
}
|
|
478
|
+
// Adjust existing element: first after insertion gets space prefix
|
|
479
|
+
let adjusted = elem;
|
|
480
|
+
if (j === 0 && insertIndex === 0 && elem.element) {
|
|
481
|
+
adjusted = {...elem, element: {...elem.element, prefix: singleSpace}};
|
|
482
|
+
}
|
|
483
|
+
// Last element before a new trailing element loses its trailing space
|
|
484
|
+
if (j === lastIndex && insertIndex > lastIndex) {
|
|
485
|
+
adjusted = {...adjusted, after: emptySpace};
|
|
486
|
+
}
|
|
487
|
+
results.push(adjusted);
|
|
488
|
+
return results;
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
// Append at end if inserting after all existing elements
|
|
492
|
+
if (insertIndex > lastIndex) {
|
|
493
|
+
newElements.push(rightPadded({...newSpecifier, prefix: singleSpace}, trailingSpace));
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
namedDraft.elements = {...namedImports.elements, elements: newElements};
|
|
467
497
|
}
|
|
468
498
|
);
|
|
469
499
|
|
|
@@ -660,41 +690,176 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
|
|
|
660
690
|
* Check if the identifier is actually referenced in the file
|
|
661
691
|
*/
|
|
662
692
|
private async checkIdentifierReferenced(compilationUnit: JS.CompilationUnit): Promise<boolean> {
|
|
663
|
-
//
|
|
664
|
-
//
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
693
|
+
// For namespace imports, we cannot use type attribution to detect usage
|
|
694
|
+
// because the namespace itself is used as an identifier, not individual members.
|
|
695
|
+
// For simplicity, we skip the onlyIfReferenced check for namespace imports.
|
|
696
|
+
if (this.member === '*') {
|
|
697
|
+
// TODO: Implement proper namespace usage detection by checking if alias identifier is used
|
|
698
|
+
return true;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Step 1: Find the expected declaring type by examining existing imports from the same module
|
|
702
|
+
let expectedDeclaringType: string | undefined;
|
|
703
|
+
|
|
704
|
+
for (const stmt of compilationUnit.statements) {
|
|
705
|
+
const statement = stmt.element;
|
|
706
|
+
|
|
707
|
+
if (statement?.kind === JS.Kind.Import) {
|
|
708
|
+
const jsImport = statement as JS.Import;
|
|
709
|
+
const moduleSpecifier = jsImport.moduleSpecifier?.element;
|
|
710
|
+
|
|
711
|
+
if (!moduleSpecifier) {
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const moduleName = this.getModuleName(moduleSpecifier);
|
|
716
|
+
if (moduleName !== this.module) {
|
|
717
|
+
continue; // Not the module we're interested in
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Found an existing import from our target module
|
|
721
|
+
// Extract the declaring type from any imported member with type attribution
|
|
722
|
+
const importClause = jsImport.importClause;
|
|
723
|
+
if (importClause?.namedBindings?.kind === JS.Kind.NamedImports) {
|
|
724
|
+
const namedImports = importClause.namedBindings as JS.NamedImports;
|
|
725
|
+
for (const elem of namedImports.elements.elements) {
|
|
726
|
+
const specifier = elem.element;
|
|
727
|
+
if (specifier?.kind === JS.Kind.ImportSpecifier) {
|
|
728
|
+
const importSpec = specifier as JS.ImportSpecifier;
|
|
729
|
+
let identifier: J.Identifier | undefined;
|
|
730
|
+
if (importSpec.specifier?.kind === J.Kind.Identifier) {
|
|
731
|
+
identifier = importSpec.specifier as J.Identifier;
|
|
732
|
+
} else if (importSpec.specifier?.kind === JS.Kind.Alias) {
|
|
733
|
+
const aliasSpec = importSpec.specifier as JS.Alias;
|
|
734
|
+
if (aliasSpec.alias?.kind === J.Kind.Identifier) {
|
|
735
|
+
identifier = aliasSpec.alias as J.Identifier;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Try to extract declaring type from either type or fieldType
|
|
740
|
+
if (identifier?.type && Type.isMethod(identifier.type)) {
|
|
741
|
+
const methodType = identifier.type as Type.Method;
|
|
742
|
+
expectedDeclaringType = Type.FullyQualified.getFullyQualifiedName(methodType.declaringType);
|
|
743
|
+
if (expectedDeclaringType) {
|
|
744
|
+
break; // Found it!
|
|
745
|
+
}
|
|
746
|
+
} else if (identifier?.fieldType?.kind === Type.Kind.Variable) {
|
|
747
|
+
const variableType = identifier.fieldType as Type.Variable;
|
|
748
|
+
if (variableType.owner) {
|
|
749
|
+
expectedDeclaringType = Type.FullyQualified.getFullyQualifiedName(variableType.owner);
|
|
750
|
+
if (expectedDeclaringType) {
|
|
751
|
+
break; // Found it!
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
if (expectedDeclaringType) {
|
|
760
|
+
break; // No need to scan more imports
|
|
673
761
|
}
|
|
674
|
-
usedImports.get(moduleName)!.add(methodType.name);
|
|
675
762
|
}
|
|
676
|
-
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Step 2: Look for references that match
|
|
766
|
+
const targetName = this.alias || this.member;
|
|
767
|
+
const targetModule = this.module;
|
|
768
|
+
let found = false;
|
|
769
|
+
|
|
770
|
+
// If no existing imports from this module, look for unresolved references
|
|
771
|
+
// If there ARE existing imports, look for references with the expected declaring type
|
|
677
772
|
|
|
678
|
-
// Create a visitor to collect used identifiers with their type attribution
|
|
679
773
|
const collector = new class extends JavaScriptVisitor<void> {
|
|
680
774
|
override async visitIdentifier(identifier: J.Identifier, p: void): Promise<J | undefined> {
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
775
|
+
if (identifier.simpleName === targetName) {
|
|
776
|
+
const type = identifier.type;
|
|
777
|
+
const fieldType = identifier.fieldType;
|
|
778
|
+
if (expectedDeclaringType) {
|
|
779
|
+
// We have an expected declaring type - check for exact match
|
|
780
|
+
// Check method type (for functions)
|
|
781
|
+
if (type && Type.isMethod(type)) {
|
|
782
|
+
const methodType = type as Type.Method;
|
|
783
|
+
const declaringTypeName = Type.FullyQualified.getFullyQualifiedName(methodType.declaringType);
|
|
784
|
+
if (declaringTypeName === expectedDeclaringType) {
|
|
785
|
+
found = true;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
// Also check field type (for objects, variables, etc.)
|
|
789
|
+
else if (fieldType?.kind === Type.Kind.Variable) {
|
|
790
|
+
const variableType = fieldType as Type.Variable;
|
|
791
|
+
// For variables, the owner is the declaring module/namespace
|
|
792
|
+
if (variableType.owner) {
|
|
793
|
+
const ownerTypeName = Type.FullyQualified.getFullyQualifiedName(variableType.owner);
|
|
794
|
+
if (ownerTypeName === expectedDeclaringType) {
|
|
795
|
+
found = true;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
// Even with expectedDeclaringType, also check for unresolved references
|
|
800
|
+
// This handles the case where the member isn't imported yet
|
|
801
|
+
else if (!type && !fieldType) {
|
|
802
|
+
found = true;
|
|
803
|
+
}
|
|
804
|
+
} else {
|
|
805
|
+
// No existing imports from this module - look for references that match
|
|
806
|
+
// 1. Unresolved references (no type/unknown type and no fieldType)
|
|
807
|
+
const isUnknownType = !type || type.kind === Type.Kind.Unknown;
|
|
808
|
+
if (isUnknownType && !fieldType) {
|
|
809
|
+
found = true;
|
|
810
|
+
}
|
|
811
|
+
// 2. References with fieldType matching the target module
|
|
812
|
+
else if (fieldType?.kind === Type.Kind.Variable) {
|
|
813
|
+
const variableType = fieldType as Type.Variable;
|
|
814
|
+
if (variableType.owner && Type.isClass(variableType.owner)) {
|
|
815
|
+
// Traverse owningClass chain to find the root module (handles nested namespaces)
|
|
816
|
+
// For example: React.forwardRef -> owner is "React" namespace -> owningClass is "react" module
|
|
817
|
+
let current: Type.Class = variableType.owner as Type.Class;
|
|
818
|
+
|
|
819
|
+
// Walk up the owningClass chain until we reach the root
|
|
820
|
+
while (current.owningClass && Type.isClass(current.owningClass)) {
|
|
821
|
+
current = current.owningClass as Type.Class;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const moduleName = Type.FullyQualified.getFullyQualifiedName(current);
|
|
825
|
+
if (moduleName === targetModule) {
|
|
826
|
+
found = true;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
// 3. References with method type matching the target module
|
|
831
|
+
else if (type && Type.isMethod(type)) {
|
|
832
|
+
const methodType = type as Type.Method;
|
|
833
|
+
const declaringTypeName = Type.FullyQualified.getFullyQualifiedName(methodType.declaringType);
|
|
834
|
+
if (declaringTypeName === targetModule) {
|
|
835
|
+
found = true;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
684
839
|
}
|
|
685
840
|
return super.visitIdentifier(identifier, p);
|
|
686
841
|
}
|
|
687
842
|
|
|
688
843
|
override async visitMethodInvocation(methodInvocation: J.MethodInvocation, p: void): Promise<J | undefined> {
|
|
689
|
-
if (methodInvocation.methodType) {
|
|
690
|
-
|
|
844
|
+
if (methodInvocation.methodType && methodInvocation.methodType.name === targetName) {
|
|
845
|
+
if (expectedDeclaringType) {
|
|
846
|
+
const declaringTypeName = Type.FullyQualified.getFullyQualifiedName(methodInvocation.methodType.declaringType);
|
|
847
|
+
if (declaringTypeName === expectedDeclaringType) {
|
|
848
|
+
found = true;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
691
851
|
}
|
|
692
852
|
return super.visitMethodInvocation(methodInvocation, p);
|
|
693
853
|
}
|
|
694
854
|
|
|
695
855
|
override async visitFunctionCall(functionCall: JS.FunctionCall, p: void): Promise<J | undefined> {
|
|
696
|
-
if (functionCall.methodType) {
|
|
697
|
-
|
|
856
|
+
if (functionCall.methodType && functionCall.methodType.name === targetName) {
|
|
857
|
+
if (expectedDeclaringType) {
|
|
858
|
+
const declaringTypeName = Type.FullyQualified.getFullyQualifiedName(functionCall.methodType.declaringType);
|
|
859
|
+
if (declaringTypeName === expectedDeclaringType) {
|
|
860
|
+
found = true;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
698
863
|
}
|
|
699
864
|
return super.visitFunctionCall(functionCall, p);
|
|
700
865
|
}
|
|
@@ -702,7 +867,15 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
|
|
|
702
867
|
override async visitFieldAccess(fieldAccess: J.FieldAccess, p: void): Promise<J | undefined> {
|
|
703
868
|
const type = fieldAccess.type;
|
|
704
869
|
if (type && Type.isMethod(type)) {
|
|
705
|
-
|
|
870
|
+
const methodType = type as Type.Method;
|
|
871
|
+
if (methodType.name === targetName) {
|
|
872
|
+
if (expectedDeclaringType) {
|
|
873
|
+
const declaringTypeName = Type.FullyQualified.getFullyQualifiedName(methodType.declaringType);
|
|
874
|
+
if (declaringTypeName === expectedDeclaringType) {
|
|
875
|
+
found = true;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
706
879
|
}
|
|
707
880
|
return super.visitFieldAccess(fieldAccess, p);
|
|
708
881
|
}
|
|
@@ -710,23 +883,7 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
|
|
|
710
883
|
|
|
711
884
|
await collector.visit(compilationUnit, undefined);
|
|
712
885
|
|
|
713
|
-
|
|
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
|
-
}
|
|
721
|
-
|
|
722
|
-
// Check if our target import is used based on type attribution
|
|
723
|
-
const moduleMembers = usedImports.get(this.module);
|
|
724
|
-
if (!moduleMembers) {
|
|
725
|
-
return false;
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
// For specific members, check if that member is used; otherwise check if any member is used
|
|
729
|
-
return this.member ? moduleMembers.has(this.member) : moduleMembers.size > 0;
|
|
886
|
+
return found;
|
|
730
887
|
}
|
|
731
888
|
|
|
732
889
|
/**
|
|
@@ -740,12 +897,13 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
|
|
|
740
897
|
// For side-effect imports, use emptySpace since space comes from LeftPadded.before
|
|
741
898
|
// For regular imports with import clause, use emptySpace since space comes from LeftPadded.before
|
|
742
899
|
// However, the printer expects the space after 'from' in the literal's prefix
|
|
900
|
+
// Note: value contains the unquoted string, valueSource contains the quoted version for printing
|
|
743
901
|
const moduleSpecifier: J.Literal = {
|
|
744
902
|
id: randomId(),
|
|
745
903
|
kind: J.Kind.Literal,
|
|
746
904
|
prefix: this.sideEffectOnly ? emptySpace : singleSpace,
|
|
747
905
|
markers: emptyMarkers,
|
|
748
|
-
value:
|
|
906
|
+
value: this.module,
|
|
749
907
|
valueSource: `'${this.module}'`,
|
|
750
908
|
unicodeEscapes: [],
|
|
751
909
|
type: undefined
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 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
|
+
|
|
17
|
+
export {UseObjectPropertyShorthand} from "./use-object-property-shorthand";
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 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
|
+
|
|
17
|
+
import {Recipe} from "../../recipe";
|
|
18
|
+
import {TreeVisitor} from "../../visitor";
|
|
19
|
+
import {ExecutionContext} from "../../execution";
|
|
20
|
+
import {JavaScriptVisitor} from "../visitor";
|
|
21
|
+
import {J} from "../../java";
|
|
22
|
+
import {JS} from "../tree";
|
|
23
|
+
import {produce} from "immer";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Simplifies object properties where the key and value have the same name,
|
|
27
|
+
* in both destructuring patterns and object literals.
|
|
28
|
+
*
|
|
29
|
+
* Destructuring examples:
|
|
30
|
+
* - `const { x: x } = obj` becomes `const { x } = obj`
|
|
31
|
+
* - `function({ ref: ref, ...props })` becomes `function({ ref, ...props })`
|
|
32
|
+
*
|
|
33
|
+
* Object literal examples:
|
|
34
|
+
* - `{ x: x }` becomes `{ x }`
|
|
35
|
+
* - `{ foo: foo, bar: bar }` becomes `{ foo, bar }`
|
|
36
|
+
*/
|
|
37
|
+
export class UseObjectPropertyShorthand extends Recipe {
|
|
38
|
+
name = "org.openrewrite.javascript.cleanup.use-object-property-shorthand";
|
|
39
|
+
displayName = "Use object property shorthand";
|
|
40
|
+
description = "Simplifies object properties where the property name and value/variable name are the same (e.g., `{ x: x }` becomes `{ x }`). Applies to both destructuring patterns and object literals.";
|
|
41
|
+
|
|
42
|
+
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
43
|
+
return new class extends JavaScriptVisitor<ExecutionContext> {
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Handle object binding patterns (destructuring): const { x: x } = obj
|
|
47
|
+
*/
|
|
48
|
+
protected async visitObjectBindingPattern(pattern: JS.ObjectBindingPattern, p: ExecutionContext): Promise<J | undefined> {
|
|
49
|
+
const visited = await super.visitObjectBindingPattern(pattern, p) as JS.ObjectBindingPattern;
|
|
50
|
+
|
|
51
|
+
let hasChanges = false;
|
|
52
|
+
|
|
53
|
+
const simplifiedBindings = visited.bindings.elements.map(right => {
|
|
54
|
+
const element = right.element;
|
|
55
|
+
|
|
56
|
+
if (element.kind === JS.Kind.BindingElement) {
|
|
57
|
+
const binding = element as JS.BindingElement;
|
|
58
|
+
|
|
59
|
+
if (binding.propertyName?.element.kind === J.Kind.Identifier) {
|
|
60
|
+
const propName = (binding.propertyName.element as J.Identifier).simpleName;
|
|
61
|
+
|
|
62
|
+
if (binding.name?.kind === J.Kind.Identifier) {
|
|
63
|
+
const bindingName = (binding.name as J.Identifier).simpleName;
|
|
64
|
+
|
|
65
|
+
if (propName === bindingName) {
|
|
66
|
+
hasChanges = true;
|
|
67
|
+
return {
|
|
68
|
+
...right,
|
|
69
|
+
element: {
|
|
70
|
+
...binding,
|
|
71
|
+
propertyName: undefined,
|
|
72
|
+
name: {
|
|
73
|
+
...binding.name,
|
|
74
|
+
prefix: binding.propertyName.element.prefix
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} as J.RightPadded<JS.BindingElement>;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return right;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (!hasChanges) {
|
|
87
|
+
return visited;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return produce(visited, draft => {
|
|
91
|
+
draft.bindings.elements = simplifiedBindings;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Handle object literals: { x: x }
|
|
97
|
+
* Object literals are represented as J.NewClass with a body containing JS.PropertyAssignment statements.
|
|
98
|
+
*/
|
|
99
|
+
protected async visitNewClass(newClass: J.NewClass, ctx: ExecutionContext): Promise<J | undefined> {
|
|
100
|
+
const visited = await super.visitNewClass(newClass, ctx) as J.NewClass;
|
|
101
|
+
|
|
102
|
+
// Only process object literals (NewClass with body but no class or arguments)
|
|
103
|
+
if (!visited.body || visited.class || (visited.arguments?.elements && visited.arguments.elements.length > 0)) {
|
|
104
|
+
return visited;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const statements = visited.body.statements;
|
|
108
|
+
if (!statements || statements.length === 0) {
|
|
109
|
+
return visited;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let hasChanges = false;
|
|
113
|
+
|
|
114
|
+
const simplifiedStatements = statements.map(stmt => {
|
|
115
|
+
if (stmt.element.kind === JS.Kind.PropertyAssignment) {
|
|
116
|
+
const prop = stmt.element as JS.PropertyAssignment;
|
|
117
|
+
|
|
118
|
+
// Check if the property name is an identifier
|
|
119
|
+
if (prop.name.element.kind === J.Kind.Identifier) {
|
|
120
|
+
const propName = (prop.name.element as J.Identifier).simpleName;
|
|
121
|
+
|
|
122
|
+
// Check if the initializer is also an identifier with the same name
|
|
123
|
+
if (prop.initializer?.kind === J.Kind.Identifier) {
|
|
124
|
+
const initName = (prop.initializer as J.Identifier).simpleName;
|
|
125
|
+
|
|
126
|
+
if (propName === initName) {
|
|
127
|
+
hasChanges = true;
|
|
128
|
+
// Remove the initializer to use shorthand syntax
|
|
129
|
+
return {
|
|
130
|
+
...stmt,
|
|
131
|
+
element: {
|
|
132
|
+
...prop,
|
|
133
|
+
initializer: undefined
|
|
134
|
+
}
|
|
135
|
+
} as J.RightPadded<JS.PropertyAssignment>;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return stmt;
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (!hasChanges) {
|
|
145
|
+
return visited;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return produce(visited, draft => {
|
|
149
|
+
draft.body!.statements = simplifiedStatements;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -840,6 +840,17 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
|
|
|
840
840
|
return this.visitElement(scopedVariableDeclarations, other as JS.ScopedVariableDeclarations);
|
|
841
841
|
}
|
|
842
842
|
|
|
843
|
+
/**
|
|
844
|
+
* Overrides the visitShebang method to compare shebangs.
|
|
845
|
+
*
|
|
846
|
+
* @param shebang The shebang to visit
|
|
847
|
+
* @param other The other shebang to compare with
|
|
848
|
+
* @returns The visited shebang, or undefined if the visit was aborted
|
|
849
|
+
*/
|
|
850
|
+
override async visitShebang(shebang: JS.Shebang, other: J): Promise<J | undefined> {
|
|
851
|
+
return this.visitElement(shebang, other as JS.Shebang);
|
|
852
|
+
}
|
|
853
|
+
|
|
843
854
|
/**
|
|
844
855
|
* Overrides the visitStatementExpression method to compare statement expressions.
|
|
845
856
|
*
|
|
@@ -47,6 +47,42 @@ export class DependencyWorkspace {
|
|
|
47
47
|
// Create/update workspace in target directory
|
|
48
48
|
fs.mkdirSync(targetDir, {recursive: true});
|
|
49
49
|
|
|
50
|
+
// Check if we can reuse a cached workspace by symlinking node_modules
|
|
51
|
+
const hash = this.hashDependencies(dependencies);
|
|
52
|
+
const cachedWorkspaceDir = path.join(this.WORKSPACE_BASE, hash);
|
|
53
|
+
const cachedNodeModules = path.join(cachedWorkspaceDir, 'node_modules');
|
|
54
|
+
|
|
55
|
+
if (fs.existsSync(cachedNodeModules) && this.isWorkspaceValid(cachedWorkspaceDir, dependencies)) {
|
|
56
|
+
// Symlink node_modules from cached workspace
|
|
57
|
+
try {
|
|
58
|
+
const targetNodeModules = path.join(targetDir, 'node_modules');
|
|
59
|
+
|
|
60
|
+
// Remove existing node_modules if present (might be invalid)
|
|
61
|
+
if (fs.existsSync(targetNodeModules)) {
|
|
62
|
+
fs.rmSync(targetNodeModules, {recursive: true, force: true});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Create symlink to cached node_modules
|
|
66
|
+
fs.symlinkSync(cachedNodeModules, targetNodeModules, 'dir');
|
|
67
|
+
|
|
68
|
+
// Write package.json
|
|
69
|
+
const packageJson = {
|
|
70
|
+
name: "openrewrite-template-workspace",
|
|
71
|
+
version: "1.0.0",
|
|
72
|
+
private: true,
|
|
73
|
+
dependencies: dependencies
|
|
74
|
+
};
|
|
75
|
+
fs.writeFileSync(
|
|
76
|
+
path.join(targetDir, 'package.json'),
|
|
77
|
+
JSON.stringify(packageJson, null, 2)
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return targetDir;
|
|
81
|
+
} catch (symlinkError) {
|
|
82
|
+
// Symlink failed (e.g., cross-device, permissions) - fall through to npm install
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
50
86
|
try {
|
|
51
87
|
const packageJson = {
|
|
52
88
|
name: "openrewrite-template-workspace",
|
|
@@ -214,6 +250,7 @@ export class DependencyWorkspace {
|
|
|
214
250
|
|
|
215
251
|
/**
|
|
216
252
|
* Checks if a workspace is valid (has node_modules and matching package.json).
|
|
253
|
+
* Handles both real node_modules directories and symlinks to cached workspaces.
|
|
217
254
|
*
|
|
218
255
|
* @param workspaceDir Directory to check
|
|
219
256
|
* @param expectedDependencies Optional dependencies to check against package.json
|
|
@@ -222,10 +259,25 @@ export class DependencyWorkspace {
|
|
|
222
259
|
const nodeModules = path.join(workspaceDir, 'node_modules');
|
|
223
260
|
const packageJsonPath = path.join(workspaceDir, 'package.json');
|
|
224
261
|
|
|
262
|
+
// Check node_modules exists (as directory or symlink)
|
|
225
263
|
if (!fs.existsSync(nodeModules) || !fs.existsSync(packageJsonPath)) {
|
|
226
264
|
return false;
|
|
227
265
|
}
|
|
228
266
|
|
|
267
|
+
// If node_modules is a symlink, verify the target still exists
|
|
268
|
+
try {
|
|
269
|
+
const stats = fs.lstatSync(nodeModules);
|
|
270
|
+
if (stats.isSymbolicLink()) {
|
|
271
|
+
const target = fs.readlinkSync(nodeModules);
|
|
272
|
+
const absoluteTarget = path.isAbsolute(target) ? target : path.resolve(path.dirname(nodeModules), target);
|
|
273
|
+
if (!fs.existsSync(absoluteTarget)) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
} catch {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
|
|
229
281
|
// If dependencies provided, check if they match
|
|
230
282
|
if (expectedDependencies) {
|
|
231
283
|
try {
|