diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index e81cd4fbc221a..6cc4e1ea1bcbd 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3898,6 +3898,10 @@ "category": "Suggestion", "code": 80004 }, + "'require' call may be converted to an import.": { + "category": "Suggestion", + "code": 80005 + }, "Add missing 'super()' call": { "category": "Message", @@ -4174,5 +4178,13 @@ "Generate 'get' and 'set' accessors": { "category": "Message", "code": 95046 + }, + "Convert 'require' to 'import'": { + "category": "Message", + "code": 95047 + }, + "Convert all 'require' to 'import'": { + "category": "Message", + "code": 95048 } } diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 7dfff7dd18995..9fa6dad39b354 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -324,8 +324,10 @@ namespace FourSlash { this.languageServiceAdapterHost.addScript(fileName, file, /*isRootFile*/ true); } }); - this.languageServiceAdapterHost.addScript(Harness.Compiler.defaultLibFileName, - Harness.Compiler.getDefaultLibrarySourceFile().text, /*isRootFile*/ false); + if (!compilationOptions.noLib) { + this.languageServiceAdapterHost.addScript(Harness.Compiler.defaultLibFileName, + Harness.Compiler.getDefaultLibrarySourceFile().text, /*isRootFile*/ false); + } } for (const file of testData.files) { diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json index a4ad8cb37975f..faeff1d4bcf1c 100644 --- a/src/harness/tsconfig.json +++ b/src/harness/tsconfig.json @@ -112,6 +112,7 @@ "../services/codefixes/inferFromUsage.ts", "../services/codefixes/fixInvalidImportSyntax.ts", "../services/codefixes/fixStrictClassInitialization.ts", + "../services/codefixes/requireInTs.ts", "../services/codefixes/useDefaultImport.ts", "../services/refactors/extractSymbol.ts", "../services/refactors/generateGetAccessorAndSetAccessor.ts", diff --git a/src/server/tsconfig.json b/src/server/tsconfig.json index e6768f4edeed1..71ff00e6ac1ab 100644 --- a/src/server/tsconfig.json +++ b/src/server/tsconfig.json @@ -108,6 +108,7 @@ "../services/codefixes/inferFromUsage.ts", "../services/codefixes/fixInvalidImportSyntax.ts", "../services/codefixes/fixStrictClassInitialization.ts", + "../services/codefixes/requireInTs.ts", "../services/codefixes/useDefaultImport.ts", "../services/refactors/extractSymbol.ts", "../services/refactors/generateGetAccessorAndSetAccessor.ts", diff --git a/src/server/tsconfig.library.json b/src/server/tsconfig.library.json index a167d3b39a2ff..95489ff0ea357 100644 --- a/src/server/tsconfig.library.json +++ b/src/server/tsconfig.library.json @@ -114,6 +114,7 @@ "../services/codefixes/inferFromUsage.ts", "../services/codefixes/fixInvalidImportSyntax.ts", "../services/codefixes/fixStrictClassInitialization.ts", + "../services/codefixes/requireInTs.ts", "../services/codefixes/useDefaultImport.ts", "../services/refactors/extractSymbol.ts", "../services/refactors/generateGetAccessorAndSetAccessor.ts", diff --git a/src/services/codefixes/requireInTs.ts b/src/services/codefixes/requireInTs.ts new file mode 100644 index 0000000000000..7efd9bd7fa6fc --- /dev/null +++ b/src/services/codefixes/requireInTs.ts @@ -0,0 +1,29 @@ +/* @internal */ +namespace ts.codefix { + const fixId = "requireInTs"; + const errorCodes = [Diagnostics.require_call_may_be_converted_to_an_import.code]; + registerCodeFix({ + errorCodes, + getCodeActions(context) { + const changes = textChanges.ChangeTracker.with(context, t => doChange(t, context.sourceFile, context.span.start, context.program)); + return [createCodeFixAction(fixId, changes, Diagnostics.Convert_require_to_import, fixId, Diagnostics.Convert_all_require_to_import)]; + }, + fixIds: [fixId], + getAllCodeActions: context => codeFixAll(context, errorCodes, (changes, diag) => doChange(changes, diag.file, diag.start, context.program)), + }); + + function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, pos: number, program: Program) { + const { statement, name, required } = getInfo(sourceFile, pos); + changes.replaceNode(sourceFile, statement, getAllowSyntheticDefaultImports(program.getCompilerOptions()) + ? createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, createImportClause(name, /*namedBindings*/ undefined), required) + : createImportEqualsDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, name, createExternalModuleReference(required))); + } + + interface Info { readonly statement: VariableStatement; readonly name: Identifier; readonly required: StringLiteralLike; } + function getInfo(sourceFile: SourceFile, pos: number): Info { + const { parent } = getTokenAtPosition(sourceFile, pos, /*includeJsDocComment*/ false); + if (!isRequireCall(parent, /*checkArgumentIsStringLiteralLike*/ true)) throw Debug.failBadSyntaxKind(parent); + const decl = cast(parent.parent, isVariableDeclaration); + return { statement: cast(decl.parent.parent, isVariableStatement), name: cast(decl.name, isIdentifier), required: parent.arguments[0] }; + } +} diff --git a/src/services/suggestionDiagnostics.ts b/src/services/suggestionDiagnostics.ts index d04f3f15cf909..0c8ec58798c94 100644 --- a/src/services/suggestionDiagnostics.ts +++ b/src/services/suggestionDiagnostics.ts @@ -32,6 +32,19 @@ namespace ts { } check(sourceFile); + if (!isJsFile) { + for (const statement of sourceFile.statements) { + if (isVariableStatement(statement) && + statement.declarationList.flags & NodeFlags.Const && + statement.declarationList.declarations.length === 1) { + const init = statement.declarationList.declarations[0].initializer; + if (init && isRequireCall(init, /*checkArgumentIsStringLiteralLike*/ true)) { + diags.push(createDiagnosticForNode(init, Diagnostics.require_call_may_be_converted_to_an_import)); + } + } + } + } + if (getAllowSyntheticDefaultImports(program.getCompilerOptions())) { for (const moduleSpecifier of sourceFile.imports) { const importNode = importFromModuleSpecifier(moduleSpecifier); diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index 75b46a5c49bbe..be46c21e06b12 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -105,6 +105,7 @@ "codefixes/inferFromUsage.ts", "codefixes/fixInvalidImportSyntax.ts", "codefixes/fixStrictClassInitialization.ts", + "codefixes/requireInTs.ts", "codefixes/useDefaultImport.ts", "refactors/extractSymbol.ts", "refactors/generateGetAccessorAndSetAccessor.ts", diff --git a/tests/cases/fourslash/codeFixRequireInTs.ts b/tests/cases/fourslash/codeFixRequireInTs.ts new file mode 100644 index 0000000000000..345b15f69f250 --- /dev/null +++ b/tests/cases/fourslash/codeFixRequireInTs.ts @@ -0,0 +1,15 @@ +/// + +// @Filename: /a.ts +////const a = [|require("a")|]; + +verify.getSuggestionDiagnostics([{ + message: "'require' call may be converted to an import.", + code: 80005, +}]); + +verify.codeFix({ + description: "Convert 'require' to 'import'", + newFileContent: +`import a = require("a");`, +}); diff --git a/tests/cases/fourslash/codeFixRequireInTs_all.ts b/tests/cases/fourslash/codeFixRequireInTs_all.ts new file mode 100644 index 0000000000000..9f9fe4824a8f5 --- /dev/null +++ b/tests/cases/fourslash/codeFixRequireInTs_all.ts @@ -0,0 +1,13 @@ +/// + +// @Filename: /a.ts +////const a = [|require("a")|]; +////const b = [|require("b")|]; + +verify.codeFixAll({ + fixId: "requireInTs", + fixAllDescription: "Convert all 'require' to 'import'", + newFileContent: +`import a = require("a"); +import b = require("b");`, +}); diff --git a/tests/cases/fourslash/codeFixRequireInTs_allowSyntheticDefaultImports.ts b/tests/cases/fourslash/codeFixRequireInTs_allowSyntheticDefaultImports.ts new file mode 100644 index 0000000000000..949387af2e542 --- /dev/null +++ b/tests/cases/fourslash/codeFixRequireInTs_allowSyntheticDefaultImports.ts @@ -0,0 +1,11 @@ +/// + +// @allowSyntheticDefaultImports: true + +// @Filename: /a.ts +////const a = [|require("a")|]; + +verify.codeFix({ + description: "Convert 'require' to 'import'", + newFileContent: `import a from "a";`, +});