@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.
@@ -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
- // Try to trace back through the AST to find the require() call
894
- let inferredDeclaringType: Type.FullyQualified | undefined;
895
- const objExpr = node.expression.expression;
896
-
897
- if (ts.isIdentifier(objExpr)) {
898
- // Look for the variable declaration that assigns the require() result
899
- const objSymbol = this.checker.getSymbolAtLocation(objExpr);
900
-
901
- if (objSymbol && objSymbol.valueDeclaration) {
902
- const valueDecl = objSymbol.valueDeclaration;
903
- if (ts.isVariableDeclaration(valueDecl) && valueDecl.initializer) {
904
- // Check if it's a require() call
905
- if (ts.isCallExpression(valueDecl.initializer)) {
906
- const callExpr = valueDecl.initializer;
907
- if (ts.isIdentifier(callExpr.expression) &&
908
- callExpr.expression.getText() === 'require' &&
909
- callExpr.arguments.length > 0) {
910
- // Extract the module name from require('module-name')
911
- const moduleArg = callExpr.arguments[0];
912
- if (ts.isStringLiteral(moduleArg)) {
913
- const moduleName = moduleArg.text;
914
-
915
- inferredDeclaringType = {
916
- kind: Type.Kind.Class,
917
- flags: 0, // TODO - determine flags
918
- fullyQualifiedName: moduleName
919
- } as Type.FullyQualified;
920
- }
921
- }
922
- }
923
- }
924
- }
925
- }
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 = inferredDeclaringType || Type.unknownType as Type.FullyQualified;
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 (mappedType && mappedType.kind === Type.Kind.Class) {
959
- // Update the declaring type with the corrected FQN
960
- if (isImport && objSymbol) {
961
- const importName = objSymbol.getName();
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 (!isDefaultImport && aliasedSymbol && aliasedSymbol.name) {
1148
+ if (isDefaultImport) {
1149
+ methodName = '<default>';
1150
+ } else if (aliasedSymbol?.declarations?.length) {
1071
1151
  methodName = aliasedSymbol.name;
1072
1152
  } else {
1073
- methodName = '<default>';
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
- // Find the symbol name (everything after the last dot in the original cleaned name)
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 > 0) {
1390
+ if (lastDotIndex > lastSegmentIndex) {
1300
1391
  const symbolName = cleanedName.substring(lastDotIndex + 1);
1301
1392
  cleanedName = `${packageName}.${symbolName}`;
1302
1393
  } else {