diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index a619238c90f..13523516bab 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -4677,14 +4677,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { if (ext === Extension.Ts || ext === Extension.Js || ext === Extension.Tsx || ext === Extension.Jsx) { diagnosticDetails = createModeMismatchDetails(currentSourceFile); } + + const message = overrideHost?.kind === SyntaxKind.ImportDeclaration && overrideHost.importClause?.isTypeOnly ? Diagnostics.Type_only_import_of_an_ECMAScript_module_from_a_CommonJS_module_must_have_a_resolution_mode_attribute : + overrideHost?.kind === SyntaxKind.ImportType ? Diagnostics.Type_import_of_an_ECMAScript_module_from_a_CommonJS_module_must_have_a_resolution_mode_attribute : + Diagnostics.The_current_file_is_a_CommonJS_module_whose_imports_will_produce_require_calls_however_the_referenced_file_is_an_ECMAScript_module_and_cannot_be_imported_with_require_Consider_writing_a_dynamic_import_0_call_instead; diagnostics.add(createDiagnosticForNodeFromMessageChain( getSourceFileOfNode(errorNode), errorNode, - chainDiagnosticMessages( - diagnosticDetails, - Diagnostics.The_current_file_is_a_CommonJS_module_whose_imports_will_produce_require_calls_however_the_referenced_file_is_an_ECMAScript_module_and_cannot_be_imported_with_require_Consider_writing_a_dynamic_import_0_call_instead, - moduleReference, - ), + chainDiagnosticMessages(diagnosticDetails, message, moduleReference), )); } } diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 01cb974bc12..6f5d7ada6c0 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -1818,6 +1818,14 @@ "code": 1540, "reportsDeprecated": true }, + "Type-only import of an ECMAScript module from a CommonJS module must have a 'resolution-mode' attribute.": { + "category": "Error", + "code": 1541 + }, + "Type import of an ECMAScript module from a CommonJS module must have a 'resolution-mode' attribute.": { + "category": "Error", + "code": 1542 + }, "The types of '{0}' are incompatible between these types.": { "category": "Error", @@ -8197,6 +8205,14 @@ "category": "Message", "code": 95195 }, + "Add 'resolution-mode' import attribute": { + "category": "Message", + "code": 95196 + }, + "Add 'resolution-mode' import attribute to all type-only imports that need it": { + "category": "Message", + "code": 95197 + }, "No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": { "category": "Error", diff --git a/src/harness/fourslashImpl.ts b/src/harness/fourslashImpl.ts index 1abdbe2f6bf..2e23f91a36e 100644 --- a/src/harness/fourslashImpl.ts +++ b/src/harness/fourslashImpl.ts @@ -3368,8 +3368,8 @@ export class TestState { this.verifyTextMatches(this.rangeText(this.getOnlyRange()), !!includeWhiteSpace, expectedText); } - private getOnlyRange() { - const ranges = this.getRanges(); + private getOnlyRange(fileName?: string) { + const ranges = fileName ? this.getRangesInFile(fileName) : this.getRanges(); if (ranges.length !== 1) { this.raiseError("Exactly one range should be specified in the testfile."); } @@ -3455,7 +3455,7 @@ export class TestState { const change = ts.first(changes); assert(change.fileName = this.activeFile.fileName); const newText = ts.textChanges.applyChanges(this.getFileContent(this.activeFile.fileName), change.textChanges); - const newRange = updateTextRangeForTextChanges(this.getOnlyRange(), change.textChanges); + const newRange = updateTextRangeForTextChanges(this.getOnlyRange(this.activeFile.fileName), change.textChanges); const actualText = newText.slice(newRange.pos, newRange.end); this.verifyTextMatches(actualText, /*includeWhitespace*/ true, newRangeContent); } diff --git a/src/services/_namespaces/ts.codefix.ts b/src/services/_namespaces/ts.codefix.ts index 988323bd6a8..3cf05630388 100644 --- a/src/services/_namespaces/ts.codefix.ts +++ b/src/services/_namespaces/ts.codefix.ts @@ -8,6 +8,7 @@ export * from "../codefixes/addMissingAwait.js"; export * from "../codefixes/addMissingConst.js"; export * from "../codefixes/addMissingDeclareProperty.js"; export * from "../codefixes/addMissingInvocationForDecorator.js"; +export * from "../codefixes/addMissingResolutionModeImportAttribute.js"; export * from "../codefixes/addNameToNamelessParameter.js"; export * from "../codefixes/addOptionalPropertyUndefined.js"; export * from "../codefixes/annotateWithTypeFromJSDoc.js"; diff --git a/src/services/codefixes/addMissingResolutionModeImportAttribute.ts b/src/services/codefixes/addMissingResolutionModeImportAttribute.ts new file mode 100644 index 00000000000..1467ff96911 --- /dev/null +++ b/src/services/codefixes/addMissingResolutionModeImportAttribute.ts @@ -0,0 +1,109 @@ +import { + codeFixAll, + createCodeFixAction, + registerCodeFix, +} from "../_namespaces/ts.codefix.js"; +import { + Debug, + Diagnostics, + factory, + findAncestor, + getQuotePreference, + getTokenAtPosition, + isImportDeclaration, + isImportTypeNode, + LanguageServiceHost, + ModuleKind, + or, + Program, + QuotePreference, + resolveModuleName, + SourceFile, + SyntaxKind, + textChanges, + tryGetModuleSpecifierFromDeclaration, + UserPreferences, +} from "../_namespaces/ts.js"; + +const fixId = "addMissingResolutionModeImportAttribute"; +const errorCodes = [ + Diagnostics.Type_only_import_of_an_ECMAScript_module_from_a_CommonJS_module_must_have_a_resolution_mode_attribute.code, + Diagnostics.Type_import_of_an_ECMAScript_module_from_a_CommonJS_module_must_have_a_resolution_mode_attribute.code, +]; + +registerCodeFix({ + errorCodes, + getCodeActions: function getCodeActionsToAddMissingResolutionModeImportAttribute(context) { + const changes = textChanges.ChangeTracker.with(context, t => makeChange(t, context.sourceFile, context.span.start, context.program, context.host, context.preferences)); + return [createCodeFixAction(fixId, changes, Diagnostics.Add_resolution_mode_import_attribute, fixId, Diagnostics.Add_resolution_mode_import_attribute_to_all_type_only_imports_that_need_it)]; + }, + fixIds: [fixId], + getAllCodeActions: context => codeFixAll(context, errorCodes, (changes, diag) => makeChange(changes, diag.file, diag.start, context.program, context.host, context.preferences)), +}); + +function makeChange(changeTracker: textChanges.ChangeTracker, sourceFile: SourceFile, pos: number, program: Program, host: LanguageServiceHost, preferences: UserPreferences) { + const token = getTokenAtPosition(sourceFile, pos); + const importNode = findAncestor(token, or(isImportDeclaration, isImportTypeNode))!; + Debug.assert(!!importNode, "Expected position to be owned by an ImportDeclaration or ImportType."); + const useSingleQuotes = getQuotePreference(sourceFile, preferences) === QuotePreference.Single; + const moduleSpecifier = tryGetModuleSpecifierFromDeclaration(importNode); + const canUseImportMode = !moduleSpecifier || (resolveModuleName( + moduleSpecifier.text, + sourceFile.fileName, + program.getCompilerOptions(), + host, + program.getModuleResolutionCache(), + /*redirectedReference*/ undefined, + ModuleKind.ESNext, + ).resolvedModule?.resolvedFileName === program.getResolvedModuleFromModuleSpecifier( + moduleSpecifier, + sourceFile, + )?.resolvedModule?.resolvedFileName); + + const attributes = importNode.attributes + ? factory.updateImportAttributes( + importNode.attributes, + factory.createNodeArray([ + ...importNode.attributes.elements, + factory.createImportAttribute( + factory.createStringLiteral("resolution-mode", useSingleQuotes), + factory.createStringLiteral(canUseImportMode ? "import" : "require", useSingleQuotes), + ), + ], importNode.attributes.elements.hasTrailingComma), + importNode.attributes.multiLine, + ) + : factory.createImportAttributes( + factory.createNodeArray([ + factory.createImportAttribute( + factory.createStringLiteral("resolution-mode", useSingleQuotes), + factory.createStringLiteral(canUseImportMode ? "import" : "require", useSingleQuotes), + ), + ]), + ); + if (importNode.kind === SyntaxKind.ImportDeclaration) { + changeTracker.replaceNode( + sourceFile, + importNode, + factory.updateImportDeclaration( + importNode, + importNode.modifiers, + importNode.importClause, + importNode.moduleSpecifier, + attributes, + ), + ); + } + else { + changeTracker.replaceNode( + sourceFile, + importNode, + factory.updateImportTypeNode( + importNode, + importNode.argument, + attributes, + importNode.qualifier, + importNode.typeArguments, + ), + ); + } +} diff --git a/tests/baselines/reference/tsbuildWatch/moduleResolution/resolves-specifier-in-output-declaration-file-from-referenced-project-correctly-with-cts-and-mts-extensions.js b/tests/baselines/reference/tsbuildWatch/moduleResolution/resolves-specifier-in-output-declaration-file-from-referenced-project-correctly-with-cts-and-mts-extensions.js index 7c24c6bb94f..abb47d2f303 100644 --- a/tests/baselines/reference/tsbuildWatch/moduleResolution/resolves-specifier-in-output-declaration-file-from-referenced-project-correctly-with-cts-and-mts-extensions.js +++ b/tests/baselines/reference/tsbuildWatch/moduleResolution/resolves-specifier-in-output-declaration-file-from-referenced-project-correctly-with-cts-and-mts-extensions.js @@ -396,7 +396,7 @@ File '/user/username/projects/myproject/packages/pkg2/build/const.d.cts' exists File '/a/lib/package.json' does not exist. File '/a/package.json' does not exist. File '/package.json' does not exist. -packages/pkg1/index.ts:1:29 - error TS1479: The current file is a CommonJS module whose imports will produce 'require' calls; however, the referenced file is an ECMAScript module and cannot be imported with 'require'. Consider writing a dynamic 'import("pkg2")' call instead. +packages/pkg1/index.ts:1:29 - error TS1541: Type-only import of an ECMAScript module from a CommonJS module must have a 'resolution-mode' attribute. To convert this file to an ECMAScript module, change its file extension to '.mts' or create a local package.json file with `{ "type": "module" }`. 1 import type { TheNum } from 'pkg2' @@ -631,7 +631,7 @@ File '/user/username/projects/myproject/packages/pkg2/build/const.d.cts' exists File '/a/lib/package.json' does not exist. File '/a/package.json' does not exist. File '/package.json' does not exist. -packages/pkg1/index.ts:1:29 - error TS1479: The current file is a CommonJS module whose imports will produce 'require' calls; however, the referenced file is an ECMAScript module and cannot be imported with 'require'. Consider writing a dynamic 'import("pkg2")' call instead. +packages/pkg1/index.ts:1:29 - error TS1541: Type-only import of an ECMAScript module from a CommonJS module must have a 'resolution-mode' attribute. To convert this file to an ECMAScript module, change its file extension to '.mts' or create a local package.json file with `{ "type": "module" }`. 1 import type { TheNum } from 'pkg2' diff --git a/tests/baselines/reference/typeOnlyESMImportFromCJS.errors.txt b/tests/baselines/reference/typeOnlyESMImportFromCJS.errors.txt new file mode 100644 index 00000000000..dde6c0e433b --- /dev/null +++ b/tests/baselines/reference/typeOnlyESMImportFromCJS.errors.txt @@ -0,0 +1,19 @@ +common.cts(1,21): error TS1541: Type-only import of an ECMAScript module from a CommonJS module must have a 'resolution-mode' attribute. +common.cts(4,25): error TS1542: Type import of an ECMAScript module from a CommonJS module must have a 'resolution-mode' attribute. + + +==== module.mts (0 errors) ==== + export {}; + +==== common.cts (2 errors) ==== + import type {} from "./module.mts"; + ~~~~~~~~~~~~~~ +!!! error TS1541: Type-only import of an ECMAScript module from a CommonJS module must have a 'resolution-mode' attribute. + import type {} from "./module.mts" with { "resolution-mode": "import" }; + import type {} from "./module.mts" with { "resolution-mode": "require" }; + type _1 = typeof import("./module.mts"); + ~~~~~~~~~~~~~~ +!!! error TS1542: Type import of an ECMAScript module from a CommonJS module must have a 'resolution-mode' attribute. + type _2 = typeof import("./module.mts", { with: { "resolution-mode": "import" } }); + type _3 = typeof import("./module.mts", { with: { "resolution-mode": "require" } }); + \ No newline at end of file diff --git a/tests/baselines/reference/typeOnlyESMImportFromCJS.js b/tests/baselines/reference/typeOnlyESMImportFromCJS.js new file mode 100644 index 00000000000..01b406ded5d --- /dev/null +++ b/tests/baselines/reference/typeOnlyESMImportFromCJS.js @@ -0,0 +1,19 @@ +//// [tests/cases/conformance/externalModules/typeOnly/typeOnlyESMImportFromCJS.ts] //// + +//// [module.mts] +export {}; + +//// [common.cts] +import type {} from "./module.mts"; +import type {} from "./module.mts" with { "resolution-mode": "import" }; +import type {} from "./module.mts" with { "resolution-mode": "require" }; +type _1 = typeof import("./module.mts"); +type _2 = typeof import("./module.mts", { with: { "resolution-mode": "import" } }); +type _3 = typeof import("./module.mts", { with: { "resolution-mode": "require" } }); + + +//// [module.mjs] +export {}; +//// [common.cjs] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/tests/baselines/reference/typeOnlyESMImportFromCJS.symbols b/tests/baselines/reference/typeOnlyESMImportFromCJS.symbols new file mode 100644 index 00000000000..b47541e6f61 --- /dev/null +++ b/tests/baselines/reference/typeOnlyESMImportFromCJS.symbols @@ -0,0 +1,19 @@ +//// [tests/cases/conformance/externalModules/typeOnly/typeOnlyESMImportFromCJS.ts] //// + +=== module.mts === + +export {}; + +=== common.cts === +import type {} from "./module.mts"; +import type {} from "./module.mts" with { "resolution-mode": "import" }; +import type {} from "./module.mts" with { "resolution-mode": "require" }; +type _1 = typeof import("./module.mts"); +>_1 : Symbol(_1, Decl(common.cts, 2, 73)) + +type _2 = typeof import("./module.mts", { with: { "resolution-mode": "import" } }); +>_2 : Symbol(_2, Decl(common.cts, 3, 40)) + +type _3 = typeof import("./module.mts", { with: { "resolution-mode": "require" } }); +>_3 : Symbol(_3, Decl(common.cts, 4, 83)) + diff --git a/tests/baselines/reference/typeOnlyESMImportFromCJS.types b/tests/baselines/reference/typeOnlyESMImportFromCJS.types new file mode 100644 index 00000000000..1b0e5224019 --- /dev/null +++ b/tests/baselines/reference/typeOnlyESMImportFromCJS.types @@ -0,0 +1,22 @@ +//// [tests/cases/conformance/externalModules/typeOnly/typeOnlyESMImportFromCJS.ts] //// + +=== module.mts === + +export {}; + +=== common.cts === +import type {} from "./module.mts"; +import type {} from "./module.mts" with { "resolution-mode": "import" }; +import type {} from "./module.mts" with { "resolution-mode": "require" }; +type _1 = typeof import("./module.mts"); +>_1 : typeof import("module", { with: { "resolution-mode": "import" } }) +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +type _2 = typeof import("./module.mts", { with: { "resolution-mode": "import" } }); +>_2 : typeof import("module", { with: { "resolution-mode": "import" } }) +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +type _3 = typeof import("./module.mts", { with: { "resolution-mode": "require" } }); +>_3 : typeof import("module", { with: { "resolution-mode": "import" } }) +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + diff --git a/tests/cases/conformance/externalModules/typeOnly/typeOnlyESMImportFromCJS.ts b/tests/cases/conformance/externalModules/typeOnly/typeOnlyESMImportFromCJS.ts new file mode 100644 index 00000000000..ce350f09b93 --- /dev/null +++ b/tests/cases/conformance/externalModules/typeOnly/typeOnlyESMImportFromCJS.ts @@ -0,0 +1,12 @@ +// @module: nodenext + +// @Filename: module.mts +export {}; + +// @Filename: common.cts +import type {} from "./module.mts"; +import type {} from "./module.mts" with { "resolution-mode": "import" }; +import type {} from "./module.mts" with { "resolution-mode": "require" }; +type _1 = typeof import("./module.mts"); +type _2 = typeof import("./module.mts", { with: { "resolution-mode": "import" } }); +type _3 = typeof import("./module.mts", { with: { "resolution-mode": "require" } }); diff --git a/tests/cases/fourslash/codeFixMissingResolutionModeImportAttribute.ts b/tests/cases/fourslash/codeFixMissingResolutionModeImportAttribute.ts new file mode 100644 index 00000000000..29ec01f4f32 --- /dev/null +++ b/tests/cases/fourslash/codeFixMissingResolutionModeImportAttribute.ts @@ -0,0 +1,57 @@ +/// + +// @module: nodenext + +// @Filename: /package.json +//// { "type": "module" } + +// @Filename: /module.ts +//// export {}; + +// @Filename: /common1.cts +//// [|import type { } from "./module.ts"/*1*/;|] + +// @Filename: /common2.cts +//// [|import type { } from "./module.ts"/*2*/ with { "type": "typescript" };|] + +// @Filename: /common3.cts +//// [|import type { } from "./module"/*3*/;|] + +// @Filename: /common4.cts +//// [|type _1 = typeof import("./module.ts"/*4*/);|] + +goTo.marker("1"); +verify.codeFix({ + index: 0, + errorCode: ts.Diagnostics.Type_only_import_of_an_ECMAScript_module_from_a_CommonJS_module_must_have_a_resolution_mode_attribute.code, + applyChanges: false, + description: "Add 'resolution-mode' import attribute", + newRangeContent: `import type { } from "./module.ts" with { "resolution-mode": "import" };`, +}); + +goTo.marker("2"); +verify.codeFix({ + index: 0, + errorCode: ts.Diagnostics.Type_only_import_of_an_ECMAScript_module_from_a_CommonJS_module_must_have_a_resolution_mode_attribute.code, + applyChanges: false, + description: "Add 'resolution-mode' import attribute", + newRangeContent: `import type { } from "./module.ts" with { "type": "typescript", "resolution-mode": "import" };`, +}); + +goTo.marker("3"); +verify.codeFix({ + index: 0, + errorCode: ts.Diagnostics.Type_only_import_of_an_ECMAScript_module_from_a_CommonJS_module_must_have_a_resolution_mode_attribute.code, + applyChanges: false, + description: "Add 'resolution-mode' import attribute", + newRangeContent: `import type { } from "./module" with { "resolution-mode": "require" };`, +}); + +goTo.marker("4"); +verify.codeFix({ + index: 0, + errorCode: ts.Diagnostics.Type_import_of_an_ECMAScript_module_from_a_CommonJS_module_must_have_a_resolution_mode_attribute.code, + applyChanges: false, + description: "Add 'resolution-mode' import attribute", + newRangeContent: `type _1 = typeof import("./module.ts", { with: { "resolution-mode": "import" } });`, +}); \ No newline at end of file