@openrewrite/rewrite 8.92.5 → 8.92.7
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/javascript/add-import.d.ts.map +1 -1
- package/dist/javascript/add-import.js +16 -7
- package/dist/javascript/add-import.js.map +1 -1
- package/dist/javascript/type-mapping.d.ts +25 -0
- package/dist/javascript/type-mapping.d.ts.map +1 -1
- package/dist/javascript/type-mapping.js +145 -56
- package/dist/javascript/type-mapping.js.map +1 -1
- package/dist/rewrite-javascript-version.txt +1 -1
- package/package.json +1 -1
- package/src/javascript/add-import.ts +29 -8
- package/src/javascript/type-mapping.ts +147 -56
|
@@ -693,6 +693,96 @@ 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
|
+
// In a JavaScript file the checker binds `const x = require('x')` as an alias, which has
|
|
724
|
+
// declarations but no value declaration.
|
|
725
|
+
const symbol = this.checker.getSymbolAtLocation(node);
|
|
726
|
+
const valueDecl = symbol?.valueDeclaration ?? symbol?.declarations?.[0];
|
|
727
|
+
return valueDecl && ts.isVariableDeclaration(valueDecl) && valueDecl.initializer ?
|
|
728
|
+
this.requiredModuleSpecifier(valueDecl.initializer) : undefined;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* How an identifier is bound by an `import` declaration: the module specifier, whether it binds
|
|
733
|
+
* the whole module (a default or namespace import) or one named export, and that export's name.
|
|
734
|
+
* Read from the syntax, since an import of a package without type declarations resolves to
|
|
735
|
+
* nothing the checker can describe.
|
|
736
|
+
*/
|
|
737
|
+
private importBinding(node: ts.Expression): { module: string, namespace: boolean, exportName?: string } | undefined {
|
|
738
|
+
if (!ts.isIdentifier(node)) {
|
|
739
|
+
return undefined;
|
|
740
|
+
}
|
|
741
|
+
const declaration = this.checker.getSymbolAtLocation(node)?.declarations?.[0];
|
|
742
|
+
if (!declaration) {
|
|
743
|
+
return undefined;
|
|
744
|
+
}
|
|
745
|
+
let importDecl: ts.Node | undefined = declaration;
|
|
746
|
+
while (importDecl && !ts.isImportDeclaration(importDecl)) {
|
|
747
|
+
importDecl = importDecl.parent;
|
|
748
|
+
}
|
|
749
|
+
if (!importDecl || !ts.isStringLiteral((importDecl as ts.ImportDeclaration).moduleSpecifier)) {
|
|
750
|
+
return undefined;
|
|
751
|
+
}
|
|
752
|
+
const module = ((importDecl as ts.ImportDeclaration).moduleSpecifier as ts.StringLiteral).text;
|
|
753
|
+
if (ts.isNamespaceImport(declaration)) {
|
|
754
|
+
return {module, namespace: true};
|
|
755
|
+
}
|
|
756
|
+
if (ts.isImportClause(declaration)) {
|
|
757
|
+
return {module, namespace: false};
|
|
758
|
+
}
|
|
759
|
+
if (ts.isImportSpecifier(declaration)) {
|
|
760
|
+
return {module, namespace: false, exportName: (declaration.propertyName ?? declaration.name).text};
|
|
761
|
+
}
|
|
762
|
+
return undefined;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* The module an expression evaluates to where it binds a whole module: a `require()` result or
|
|
767
|
+
* a default or namespace import. Named imports bind an export, not the module.
|
|
768
|
+
*/
|
|
769
|
+
private moduleOfExpression(node: ts.Expression): string | undefined {
|
|
770
|
+
const required = this.requiredModuleOfExpression(node);
|
|
771
|
+
if (required) {
|
|
772
|
+
return required;
|
|
773
|
+
}
|
|
774
|
+
const binding = this.importBinding(node);
|
|
775
|
+
return binding && binding.exportName === undefined ? binding.module : undefined;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
private moduleType(module: string): Type.FullyQualified {
|
|
779
|
+
return {
|
|
780
|
+
kind: Type.Kind.Class,
|
|
781
|
+
flags: 0, // TODO - determine flags
|
|
782
|
+
fullyQualifiedName: module
|
|
783
|
+
} as Type.FullyQualified;
|
|
784
|
+
}
|
|
785
|
+
|
|
696
786
|
/**
|
|
697
787
|
* Extract the npm module name from a file path.
|
|
698
788
|
* Handles various package manager layouts:
|
|
@@ -869,6 +959,12 @@ export class JavaScriptTypeMapping {
|
|
|
869
959
|
return undefined;
|
|
870
960
|
}
|
|
871
961
|
|
|
962
|
+
// Calling a module itself, as in `require('m')()`, calls its default export.
|
|
963
|
+
const calledModule = ts.isCallExpression(node) ? this.requiredModuleSpecifier(node.expression) : undefined;
|
|
964
|
+
if (calledModule) {
|
|
965
|
+
return this.createMethodType(signature, node, this.moduleType(calledModule), '<default>');
|
|
966
|
+
}
|
|
967
|
+
|
|
872
968
|
let symbol = this.checker.getSymbolAtLocation(node.expression);
|
|
873
969
|
|
|
874
970
|
|
|
@@ -890,42 +986,15 @@ export class JavaScriptTypeMapping {
|
|
|
890
986
|
// to find the declaring type. This happens with CommonJS require() calls
|
|
891
987
|
// where the module is typed as 'any' but methods still have signatures
|
|
892
988
|
|
|
893
|
-
//
|
|
894
|
-
|
|
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
|
-
}
|
|
989
|
+
// Trace back through the AST to the require() call or import binding the receiver comes from
|
|
990
|
+
const moduleName = this.moduleOfExpression(node.expression.expression);
|
|
926
991
|
|
|
927
992
|
// Use the inferred type or fall back to unknown
|
|
928
|
-
declaringType =
|
|
993
|
+
declaringType = moduleName ? {
|
|
994
|
+
kind: Type.Kind.Class,
|
|
995
|
+
flags: 0, // TODO - determine flags
|
|
996
|
+
fullyQualifiedName: moduleName
|
|
997
|
+
} as Type.FullyQualified : Type.unknownType as Type.FullyQualified;
|
|
929
998
|
|
|
930
999
|
// Create the method type using the helper
|
|
931
1000
|
return this.createMethodType(signature, node, declaringType, methodName);
|
|
@@ -954,26 +1023,22 @@ export class JavaScriptTypeMapping {
|
|
|
954
1023
|
const exprType = this.checker.getTypeAtLocation(node.expression.expression);
|
|
955
1024
|
const mappedType = this.getType(exprType);
|
|
956
1025
|
|
|
1026
|
+
// A namespace import of an ES module is typed as its anonymous namespace object, so the
|
|
1027
|
+
// module is what declares its members, and its `default` is the default export. One
|
|
1028
|
+
// declared as a TypeScript namespace (`export = React`) keeps that namespace's name.
|
|
1029
|
+
const receiverBinding = isImport ? this.importBinding(node.expression.expression) : undefined;
|
|
1030
|
+
const anonymous = !mappedType || mappedType.kind !== Type.Kind.Class ||
|
|
1031
|
+
(mappedType as Type.Class).fullyQualifiedName.startsWith('{');
|
|
1032
|
+
|
|
957
1033
|
// Handle different types
|
|
958
|
-
if (
|
|
959
|
-
|
|
960
|
-
if (
|
|
961
|
-
|
|
962
|
-
const origFqn = (mappedType as Type.Class).fullyQualifiedName;
|
|
963
|
-
const lastDot = origFqn.lastIndexOf('.');
|
|
964
|
-
if (lastDot > 0) {
|
|
965
|
-
const typeName = origFqn.substring(lastDot + 1);
|
|
966
|
-
declaringType = {
|
|
967
|
-
kind: Type.Kind.Class,
|
|
968
|
-
flags: 0, // TODO - determine flags
|
|
969
|
-
fullyQualifiedName: `${importName}.${typeName}`
|
|
970
|
-
} as Type.FullyQualified;
|
|
971
|
-
} else {
|
|
972
|
-
declaringType = mappedType as Type.FullyQualified;
|
|
973
|
-
}
|
|
974
|
-
} else {
|
|
975
|
-
declaringType = mappedType as Type.FullyQualified;
|
|
1034
|
+
if (receiverBinding?.namespace && anonymous) {
|
|
1035
|
+
declaringType = this.moduleType(receiverBinding.module);
|
|
1036
|
+
if (methodName === 'default') {
|
|
1037
|
+
methodName = '<default>';
|
|
976
1038
|
}
|
|
1039
|
+
} else if (mappedType && mappedType.kind === Type.Kind.Class) {
|
|
1040
|
+
// An imported class keeps its own fully qualified name, as it does wherever else it's referenced.
|
|
1041
|
+
declaringType = mappedType as Type.FullyQualified;
|
|
977
1042
|
} else if (mappedType && mappedType.kind === Type.Kind.Parameterized) {
|
|
978
1043
|
// For parameterized types (e.g., Array<string>, number[]), use the base class type
|
|
979
1044
|
declaringType = (mappedType as Type.Parameterized).type;
|
|
@@ -985,6 +1050,19 @@ export class JavaScriptTypeMapping {
|
|
|
985
1050
|
declaringType = Type.unknownType as Type.FullyQualified;
|
|
986
1051
|
}
|
|
987
1052
|
|
|
1053
|
+
// A `require()` result, and an import of a package without type declarations, is typed
|
|
1054
|
+
// `any`, so nothing above identifies the module; the specifier in the call or import does.
|
|
1055
|
+
if (declaringType === Type.unknownType) {
|
|
1056
|
+
const moduleName = this.moduleOfExpression(node.expression.expression);
|
|
1057
|
+
if (moduleName) {
|
|
1058
|
+
declaringType = {
|
|
1059
|
+
kind: Type.Kind.Class,
|
|
1060
|
+
flags: 0, // TODO - determine flags
|
|
1061
|
+
fullyQualifiedName: moduleName
|
|
1062
|
+
} as Type.FullyQualified;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
988
1066
|
// For string methods like 'hello'.split(), ensure we have a proper declaring type for primitives
|
|
989
1067
|
if (!isImport && declaringType === Type.unknownType) {
|
|
990
1068
|
// If the expression type is a primitive string, use String as declaring type
|
|
@@ -1067,12 +1145,20 @@ export class JavaScriptTypeMapping {
|
|
|
1067
1145
|
// name of the default export (e.g. `e` for express), so represent it as `<default>`.
|
|
1068
1146
|
// Named imports (`ImportSpecifier`) keep the original exported name.
|
|
1069
1147
|
const isDefaultImport = exprSymbol?.declarations?.some(ts.isImportClause) ?? false;
|
|
1070
|
-
if (
|
|
1148
|
+
if (isDefaultImport) {
|
|
1149
|
+
methodName = '<default>';
|
|
1150
|
+
} else if (aliasedSymbol?.declarations?.length) {
|
|
1071
1151
|
methodName = aliasedSymbol.name;
|
|
1072
1152
|
} else {
|
|
1073
|
-
|
|
1153
|
+
// A package without type declarations resolves the import to the checker's
|
|
1154
|
+
// `unknown` symbol, so the exported name comes from the import itself.
|
|
1155
|
+
methodName = this.importBinding(node.expression)?.exportName ?? aliasedSymbol?.name ?? methodName;
|
|
1074
1156
|
}
|
|
1075
1157
|
}
|
|
1158
|
+
} else if (this.requiredModuleOfExpression(node.expression)) {
|
|
1159
|
+
// `const m = require('m'); m()` calls the module's default export.
|
|
1160
|
+
declaringType = this.moduleType(this.requiredModuleOfExpression(node.expression)!);
|
|
1161
|
+
methodName = '<default>';
|
|
1076
1162
|
} else {
|
|
1077
1163
|
// Fall back to the original logic for non-imported functions
|
|
1078
1164
|
const exprType = this.checker.getTypeAtLocation(node.expression);
|
|
@@ -1294,9 +1380,14 @@ export class JavaScriptTypeMapping {
|
|
|
1294
1380
|
// file's node_modules path) match the names used for directly imported types.
|
|
1295
1381
|
packageName = this.normalizePackageName(packageName);
|
|
1296
1382
|
|
|
1297
|
-
//
|
|
1383
|
+
// The symbol name is appended to the declaration file's path, so it is whatever
|
|
1384
|
+
// follows the last dot of the final path segment. Directory names carry dots of
|
|
1385
|
+
// their own — `.pnpm`, `.yarn`, a hidden parent such as `.claude`, a versioned
|
|
1386
|
+
// pnpm directory — so a search over the whole path would split there instead and
|
|
1387
|
+
// fold most of the path into the name.
|
|
1388
|
+
const lastSegmentIndex = cleanedName.lastIndexOf('/') + 1;
|
|
1298
1389
|
const lastDotIndex = cleanedName.lastIndexOf('.');
|
|
1299
|
-
if (lastDotIndex >
|
|
1390
|
+
if (lastDotIndex > lastSegmentIndex) {
|
|
1300
1391
|
const symbolName = cleanedName.substring(lastDotIndex + 1);
|
|
1301
1392
|
cleanedName = `${packageName}.${symbolName}`;
|
|
1302
1393
|
} else {
|