diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index d2a30c0c875a7..2f6ff1c079ff7 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -607,6 +607,9 @@ namespace ts { getJsxNamespace: n => unescapeLeadingUnderscores(getJsxNamespace(n)), getAccessibleSymbolChain, getTypePredicateOfSignature, + resolveExternalModuleName: moduleSpecifier => { + return resolveExternalModuleName(moduleSpecifier, moduleSpecifier, /*ignoreErrors*/ true); + }, resolveExternalModuleSymbol, tryGetThisTypeAt: (node, includeGlobalThis) => { node = getParseTreeNode(node); @@ -2560,7 +2563,7 @@ namespace ts { } function getTargetOfExportAssignment(node: ExportAssignment | BinaryExpression, dontResolveAlias: boolean): Symbol | undefined { - const expression = (isExportAssignment(node) ? node.expression : node.right) as EntityNameExpression | ClassExpression; + const expression = isExportAssignment(node) ? node.expression : node.right; const resolved = getTargetOfAliasLikeExpression(expression, dontResolveAlias); markSymbolOfAliasDeclarationIfTypeOnly(node, /*immediateTarget*/ undefined, resolved, /*overwriteEmpty*/ false); return resolved; diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 26d5693391c65..24dc2daf76c62 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3583,6 +3583,7 @@ namespace ts { */ /* @internal */ getAccessibleSymbolChain(symbol: Symbol, enclosingDeclaration: Node | undefined, meaning: SymbolFlags, useOnlyExternalAliasing: boolean): Symbol[] | undefined; /* @internal */ getTypePredicateOfSignature(signature: Signature): TypePredicate | undefined; + /* @internal */ resolveExternalModuleName(moduleSpecifier: Expression): Symbol | undefined; /** * An external module with an 'export =' declaration resolves to the target of the 'export =' declaration, * and an external module with no 'export =' declaration resolves to the module itself. @@ -3817,6 +3818,10 @@ namespace ts { /* @internal */ export type AnyImportSyntax = ImportDeclaration | ImportEqualsDeclaration; + /* @internal */ + export type AnyImportOrRequire = AnyImportSyntax | RequireVariableDeclaration; + + /* @internal */ export type AnyImportOrReExport = AnyImportSyntax | ExportDeclaration; @@ -3833,7 +3838,13 @@ namespace ts { | ValidImportTypeNode; /* @internal */ - export type RequireOrImportCall = CallExpression & { arguments: [StringLiteralLike] }; + export type RequireOrImportCall = CallExpression & { expression: Identifier, arguments: [StringLiteralLike] }; + + /* @internal */ + export interface RequireVariableDeclaration extends VariableDeclaration { + + initializer: RequireOrImportCall; + } /* @internal */ export type LateVisibilityPaintedStatement = diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 55ceab79410af..ea1381f7e9642 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -1853,6 +1853,20 @@ namespace ts { return !requireStringLiteralLikeArgument || isStringLiteralLike(arg); } + /** + * Returns true if the node is a VariableDeclaration initialized to a require call (see `isRequireCall`). + * This function does not test if the node is in a JavaScript file or not. + */ + export function isRequireVariableDeclaration(node: Node, requireStringLiteralLikeArgument: true): node is RequireVariableDeclaration; + export function isRequireVariableDeclaration(node: Node, requireStringLiteralLikeArgument: boolean): node is VariableDeclaration; + export function isRequireVariableDeclaration(node: Node, requireStringLiteralLikeArgument: boolean): node is VariableDeclaration { + return isVariableDeclaration(node) && !!node.initializer && isRequireCall(node.initializer, requireStringLiteralLikeArgument); + } + + export function isRequireVariableDeclarationStatement(node: Node, requireStringLiteralLikeArgument = true): node is VariableStatement { + return isVariableStatement(node) && every(node.declarationList.declarations, decl => isRequireVariableDeclaration(decl, requireStringLiteralLikeArgument)); + } + export function isSingleOrDoubleQuote(charCode: number) { return charCode === CharacterCodes.singleQuote || charCode === CharacterCodes.doubleQuote; } diff --git a/src/harness/fourslashImpl.ts b/src/harness/fourslashImpl.ts index d85ff7ecd7718..5eef969eafd88 100644 --- a/src/harness/fourslashImpl.ts +++ b/src/harness/fourslashImpl.ts @@ -2700,7 +2700,7 @@ namespace FourSlash { const oldText = this.tryGetFileContent(change.fileName); ts.Debug.assert(!!change.isNewFile === (oldText === undefined)); const newContent = change.isNewFile ? ts.first(change.textChanges).newText : ts.textChanges.applyChanges(oldText!, change.textChanges); - assert.equal(newContent, expectedNewContent, `String mis-matched in file ${change.fileName}`); + this.verifyTextMatches(newContent, /*includeWhitespace*/ true, expectedNewContent); } for (const newFileName in newFileContent) { ts.Debug.assert(changes.some(c => c.fileName === newFileName), "No change in file", () => newFileName); diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 7cb59708500ac..1e53770bcd4ef 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -43,8 +43,8 @@ namespace ts.codefix { const addToNamespace: FixUseNamespaceImport[] = []; const importType: FixUseImportType[] = []; // Keys are import clause node IDs. - const addToExisting = createMap<{ readonly importClause: ImportClause, defaultImport: string | undefined; readonly namedImports: string[], canUseTypeOnlyImport: boolean }>(); - const newImports = createMap>(); + const addToExisting = createMap<{ readonly importClauseOrBindingPattern: ImportClause | ObjectBindingPattern, defaultImport: string | undefined; readonly namedImports: string[], canUseTypeOnlyImport: boolean }>(); + const newImports = createMap>(); let lastModuleSpecifier: string | undefined; return { addImportFromDiagnostic, addImportFromExportedSymbol, writeFixes }; @@ -61,7 +61,8 @@ namespace ts.codefix { const symbol = checker.getMergedSymbol(skipAlias(exportedSymbol, checker)); const exportInfos = getAllReExportingModules(sourceFile, symbol, moduleSymbol, symbolName, sourceFile, compilerOptions, checker, program.getSourceFiles()); const preferTypeOnlyImport = !!usageIsTypeOnly && compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Error; - const fix = getImportFixForSymbol(sourceFile, exportInfos, moduleSymbol, symbolName, program, /*position*/ undefined, preferTypeOnlyImport, host, preferences); + const useRequire = shouldUseRequire(sourceFile, compilerOptions); + const fix = getImportFixForSymbol(sourceFile, exportInfos, moduleSymbol, symbolName, program, /*position*/ undefined, preferTypeOnlyImport, useRequire, host, preferences); addImport({ fixes: [fix], symbolName }); } @@ -76,11 +77,11 @@ namespace ts.codefix { importType.push(fix); break; case ImportFixKind.AddToExisting: { - const { importClause, importKind, canUseTypeOnlyImport } = fix; - const key = String(getNodeId(importClause)); + const { importClauseOrBindingPattern, importKind, canUseTypeOnlyImport } = fix; + const key = String(getNodeId(importClauseOrBindingPattern)); let entry = addToExisting.get(key); if (!entry) { - addToExisting.set(key, entry = { importClause, defaultImport: undefined, namedImports: [], canUseTypeOnlyImport }); + addToExisting.set(key, entry = { importClauseOrBindingPattern, defaultImport: undefined, namedImports: [], canUseTypeOnlyImport }); } if (importKind === ImportKind.Named) { pushIfUnique(entry.namedImports, symbolName); @@ -92,10 +93,10 @@ namespace ts.codefix { break; } case ImportFixKind.AddNew: { - const { moduleSpecifier, importKind, typeOnly } = fix; + const { moduleSpecifier, importKind, useRequire, typeOnly } = fix; let entry = newImports.get(moduleSpecifier); if (!entry) { - newImports.set(moduleSpecifier, entry = { defaultImport: undefined, namedImports: [], namespaceLikeImport: undefined, typeOnly }); + newImports.set(moduleSpecifier, entry = { namedImports: [], namespaceLikeImport: undefined, typeOnly, useRequire }); lastModuleSpecifier = moduleSpecifier; } else { @@ -108,9 +109,9 @@ namespace ts.codefix { entry.defaultImport = symbolName; break; case ImportKind.Named: - pushIfUnique(entry.namedImports, symbolName); + pushIfUnique(entry.namedImports || (entry.namedImports = []), symbolName); break; - case ImportKind.Equals: + case ImportKind.CommonJS: case ImportKind.Namespace: Debug.assert(entry.namespaceLikeImport === undefined || entry.namespaceLikeImport.name === symbolName, "Namespacelike import shoudl be missing or match symbolName"); entry.namespaceLikeImport = { importKind, name: symbolName }; @@ -131,11 +132,12 @@ namespace ts.codefix { for (const fix of importType) { addImportType(changeTracker, sourceFile, fix, quotePreference); } - addToExisting.forEach(({ importClause, defaultImport, namedImports, canUseTypeOnlyImport }) => { - doAddExistingFix(changeTracker, sourceFile, importClause, defaultImport, namedImports, canUseTypeOnlyImport); + addToExisting.forEach(({ importClauseOrBindingPattern, defaultImport, namedImports, canUseTypeOnlyImport }) => { + doAddExistingFix(changeTracker, sourceFile, importClauseOrBindingPattern, defaultImport, namedImports, canUseTypeOnlyImport); }); - newImports.forEach((imports, moduleSpecifier) => { - addNewImports(changeTracker, sourceFile, moduleSpecifier, quotePreference, imports, /*blankLineBetween*/ lastModuleSpecifier === moduleSpecifier); + newImports.forEach(({ useRequire, ...imports }, moduleSpecifier) => { + const addDeclarations = useRequire ? addNewRequires : addNewImports; + addDeclarations(changeTracker, sourceFile, moduleSpecifier, quotePreference, imports, /*blankLineBetween*/ lastModuleSpecifier === moduleSpecifier); }); } } @@ -155,7 +157,8 @@ namespace ts.codefix { } interface FixAddToExistingImport { readonly kind: ImportFixKind.AddToExisting; - readonly importClause: ImportClause; + readonly importClauseOrBindingPattern: ImportClause | ObjectBindingPattern; + readonly moduleSpecifier: string; readonly importKind: ImportKind.Default | ImportKind.Named; readonly canUseTypeOnlyImport: boolean; } @@ -164,14 +167,14 @@ namespace ts.codefix { readonly moduleSpecifier: string; readonly importKind: ImportKind; readonly typeOnly: boolean; + readonly useRequire: boolean; } const enum ImportKind { Named, Default, Namespace, - Equals, - ConstEquals + CommonJS, } /** Information about how a symbol is exported from a module. (We don't need to store the exported symbol, just its module.) */ @@ -184,7 +187,7 @@ namespace ts.codefix { /** Information needed to augment an existing import declaration. */ interface FixAddToExistingImportInfo { - readonly declaration: AnyImportSyntax; + readonly declaration: AnyImportOrRequire; readonly importKind: ImportKind; } @@ -201,16 +204,17 @@ namespace ts.codefix { ): { readonly moduleSpecifier: string, readonly codeAction: CodeAction } { const compilerOptions = program.getCompilerOptions(); const exportInfos = getAllReExportingModules(sourceFile, exportedSymbol, moduleSymbol, symbolName, sourceFile, compilerOptions, program.getTypeChecker(), program.getSourceFiles()); - const preferTypeOnlyImport = compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Error && isValidTypeOnlyAliasUseSite(getTokenAtPosition(sourceFile, position)); - const moduleSpecifier = first(getNewImportInfos(program, sourceFile, position, preferTypeOnlyImport, exportInfos, host, preferences)).moduleSpecifier; - const fix = getImportFixForSymbol(sourceFile, exportInfos, moduleSymbol, symbolName, program, position, preferTypeOnlyImport, host, preferences); + const useRequire = shouldUseRequire(sourceFile, compilerOptions); + const preferTypeOnlyImport = compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Error && !isSourceFileJS(sourceFile) && isValidTypeOnlyAliasUseSite(getTokenAtPosition(sourceFile, position)); + const moduleSpecifier = first(getNewImportInfos(program, sourceFile, position, preferTypeOnlyImport, useRequire, exportInfos, host, preferences)).moduleSpecifier; + const fix = getImportFixForSymbol(sourceFile, exportInfos, moduleSymbol, symbolName, program, position, preferTypeOnlyImport, useRequire, host, preferences); return { moduleSpecifier, codeAction: codeFixActionToCodeAction(codeActionForFix({ host, formatContext, preferences }, sourceFile, symbolName, fix, getQuotePreference(sourceFile, preferences))) }; } - function getImportFixForSymbol(sourceFile: SourceFile, exportInfos: readonly SymbolExportInfo[], moduleSymbol: Symbol, symbolName: string, program: Program, position: number | undefined, preferTypeOnlyImport: boolean, host: LanguageServiceHost, preferences: UserPreferences) { + function getImportFixForSymbol(sourceFile: SourceFile, exportInfos: readonly SymbolExportInfo[], moduleSymbol: Symbol, symbolName: string, program: Program, position: number | undefined, preferTypeOnlyImport: boolean, useRequire: boolean, host: LanguageServiceHost, preferences: UserPreferences) { Debug.assert(exportInfos.some(info => info.moduleSymbol === moduleSymbol), "Some exportInfo should match the specified moduleSymbol"); // We sort the best codefixes first, so taking `first` is best. - return first(getFixForImport(exportInfos, symbolName, position, preferTypeOnlyImport, program, sourceFile, host, preferences)); + return first(getFixForImport(exportInfos, symbolName, position, preferTypeOnlyImport, useRequire, program, sourceFile, host, preferences)); } function codeFixActionToCodeAction({ description, changes, commands }: CodeFixAction): CodeAction { @@ -230,7 +234,7 @@ namespace ts.codefix { result.push({ moduleSymbol, importKind: defaultInfo.kind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(defaultInfo.symbol, checker) }); } - for (const exported of checker.getExportsOfModule(moduleSymbol)) { + for (const exported of checker.getExportsAndPropertiesOfModule(moduleSymbol)) { if (exported.name === symbolName && skipAlias(exported, checker) === exportedSymbol) { result.push({ moduleSymbol, importKind: ImportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exported, checker) }); } @@ -253,6 +257,7 @@ namespace ts.codefix { /** undefined only for missing JSX namespace */ position: number | undefined, preferTypeOnlyImport: boolean, + useRequire: boolean, program: Program, sourceFile: SourceFile, host: LanguageServiceHost, @@ -263,7 +268,7 @@ namespace ts.codefix { const useNamespace = position === undefined ? undefined : tryUseExistingNamespaceImport(existingImports, symbolName, position, checker); const addToExisting = tryAddToExistingImport(existingImports, position !== undefined && isTypeOnlyPosition(sourceFile, position)); // Don't bother providing an action to add a new import if we can add to an existing one. - const addImport = addToExisting ? [addToExisting] : getFixesForAddImport(exportInfos, existingImports, program, sourceFile, position, preferTypeOnlyImport, host, preferences); + const addImport = addToExisting ? [addToExisting] : getFixesForAddImport(exportInfos, existingImports, program, sourceFile, position, preferTypeOnlyImport, useRequire, host, preferences); return [...(useNamespace ? [useNamespace] : emptyArray), ...addImport]; } @@ -281,66 +286,100 @@ namespace ts.codefix { // 2. add "member3" to the second import statement's import list // and it is up to the user to decide which one fits best. return firstDefined(existingImports, ({ declaration }): FixUseNamespaceImport | undefined => { - const namespace = getNamespaceImportName(declaration); - if (namespace) { - const moduleSymbol = checker.getAliasedSymbol(checker.getSymbolAtLocation(namespace)!); + const namespacePrefix = getNamespaceLikeImportText(declaration); + if (namespacePrefix) { + const moduleSymbol = getTargetModuleFromNamespaceLikeImport(declaration, checker); if (moduleSymbol && moduleSymbol.exports!.has(escapeLeadingUnderscores(symbolName))) { - return { kind: ImportFixKind.UseNamespace, namespacePrefix: namespace.text, position }; + return { kind: ImportFixKind.UseNamespace, namespacePrefix, position }; } } }); } + function getTargetModuleFromNamespaceLikeImport(declaration: AnyImportOrRequire, checker: TypeChecker) { + switch (declaration.kind) { + case SyntaxKind.VariableDeclaration: + return checker.resolveExternalModuleName(declaration.initializer.arguments[0]); + case SyntaxKind.ImportEqualsDeclaration: + return checker.getAliasedSymbol(declaration.symbol); + case SyntaxKind.ImportDeclaration: + const namespaceImport = tryCast(declaration.importClause?.namedBindings, isNamespaceImport); + return namespaceImport && checker.getAliasedSymbol(namespaceImport.symbol); + default: + return Debug.assertNever(declaration); + } + } + + function getNamespaceLikeImportText(declaration: AnyImportOrRequire) { + switch (declaration.kind) { + case SyntaxKind.VariableDeclaration: + return tryCast(declaration.name, isIdentifier)?.text; + case SyntaxKind.ImportEqualsDeclaration: + return declaration.name.text; + case SyntaxKind.ImportDeclaration: + return tryCast(declaration.importClause?.namedBindings, isNamespaceImport)?.name.text; + default: + return Debug.assertNever(declaration); + } + } + function tryAddToExistingImport(existingImports: readonly FixAddToExistingImportInfo[], canUseTypeOnlyImport: boolean): FixAddToExistingImport | undefined { return firstDefined(existingImports, ({ declaration, importKind }): FixAddToExistingImport | undefined => { - if (declaration.kind !== SyntaxKind.ImportDeclaration) return undefined; + if (declaration.kind === SyntaxKind.ImportEqualsDeclaration) return undefined; + if (declaration.kind === SyntaxKind.VariableDeclaration) { + return (importKind === ImportKind.Named || importKind === ImportKind.Default) && declaration.name.kind === SyntaxKind.ObjectBindingPattern + ? { kind: ImportFixKind.AddToExisting, importClauseOrBindingPattern: declaration.name, importKind, moduleSpecifier: declaration.initializer.arguments[0].text, canUseTypeOnlyImport: false } + : undefined; + } const { importClause } = declaration; if (!importClause) return undefined; const { name, namedBindings } = importClause; return importKind === ImportKind.Default && !name || importKind === ImportKind.Named && (!namedBindings || namedBindings.kind === SyntaxKind.NamedImports) - ? { kind: ImportFixKind.AddToExisting, importClause, importKind, canUseTypeOnlyImport } + ? { kind: ImportFixKind.AddToExisting, importClauseOrBindingPattern: importClause, importKind, moduleSpecifier: declaration.moduleSpecifier.getText(), canUseTypeOnlyImport } : undefined; }); } - function getNamespaceImportName(declaration: AnyImportSyntax): Identifier | undefined { - if (declaration.kind === SyntaxKind.ImportDeclaration) { - const namedBindings = declaration.importClause && isImportClause(declaration.importClause) && declaration.importClause.namedBindings; - return namedBindings && namedBindings.kind === SyntaxKind.NamespaceImport ? namedBindings.name : undefined; - } - else { - return declaration.name; - } - } - function getExistingImportDeclarations({ moduleSymbol, importKind, exportedSymbolIsTypeOnly }: SymbolExportInfo, checker: TypeChecker, sourceFile: SourceFile): readonly FixAddToExistingImportInfo[] { // Can't use an es6 import for a type in JS. - return exportedSymbolIsTypeOnly && isSourceFileJS(sourceFile) ? emptyArray : mapDefined(sourceFile.imports, moduleSpecifier => { + return exportedSymbolIsTypeOnly && isSourceFileJS(sourceFile) ? emptyArray : mapDefined(sourceFile.imports, (moduleSpecifier): FixAddToExistingImportInfo | undefined => { const i = importFromModuleSpecifier(moduleSpecifier); - return (i.kind === SyntaxKind.ImportDeclaration || i.kind === SyntaxKind.ImportEqualsDeclaration) - && checker.getSymbolAtLocation(moduleSpecifier) === moduleSymbol ? { declaration: i, importKind, exportedSymbolIsTypeOnly } : undefined; + if (isRequireVariableDeclaration(i.parent, /*requireStringLiteralLikeArgument*/ true)) { + return checker.resolveExternalModuleName(moduleSpecifier) === moduleSymbol ? { declaration: i.parent, importKind } : undefined; + } + if (i.kind === SyntaxKind.ImportDeclaration || i.kind === SyntaxKind.ImportEqualsDeclaration) { + return checker.getSymbolAtLocation(moduleSpecifier) === moduleSymbol ? { declaration: i, importKind } : undefined; + } }); } + function shouldUseRequire(sourceFile: SourceFile, compilerOptions: CompilerOptions): boolean { + return isSourceFileJS(sourceFile) + && !sourceFile.externalModuleIndicator + && (!!sourceFile.commonJsModuleIndicator || getEmitModuleKind(compilerOptions) < ModuleKind.ES2015); + } + function getNewImportInfos( program: Program, sourceFile: SourceFile, position: number | undefined, preferTypeOnlyImport: boolean, + useRequire: boolean, moduleSymbols: readonly SymbolExportInfo[], host: LanguageServiceHost, preferences: UserPreferences, ): readonly (FixAddNewImport | FixUseImportType)[] { const isJs = isSourceFileJS(sourceFile); + const compilerOptions = program.getCompilerOptions(); const { allowsImportingSpecifier } = createAutoImportFilter(sourceFile, program, host); const choicesForEachExportingModule = flatMap(moduleSymbols, ({ moduleSymbol, importKind, exportedSymbolIsTypeOnly }) => - moduleSpecifiers.getModuleSpecifiers(moduleSymbol, program.getCompilerOptions(), sourceFile, host, program.getSourceFiles(), preferences, program.redirectTargetsMap) + moduleSpecifiers.getModuleSpecifiers(moduleSymbol, compilerOptions, sourceFile, host, program.getSourceFiles(), preferences, program.redirectTargetsMap) .map((moduleSpecifier): FixAddNewImport | FixUseImportType => // `position` should only be undefined at a missing jsx namespace, in which case we shouldn't be looking for pure types. exportedSymbolIsTypeOnly && isJs ? { kind: ImportFixKind.ImportType, moduleSpecifier, position: Debug.checkDefined(position, "position should be defined") } - : { kind: ImportFixKind.AddNew, moduleSpecifier, importKind, typeOnly: preferTypeOnlyImport })); + : { kind: ImportFixKind.AddNew, moduleSpecifier, importKind, useRequire, typeOnly: preferTypeOnlyImport })); // Sort by presence in package.json, then shortest paths first return sort(choicesForEachExportingModule, (a, b) => { @@ -363,21 +402,21 @@ namespace ts.codefix { sourceFile: SourceFile, position: number | undefined, preferTypeOnlyImport: boolean, + useRequire: boolean, host: LanguageServiceHost, preferences: UserPreferences, ): readonly (FixAddNewImport | FixUseImportType)[] { - const existingDeclaration = firstDefined(existingImports, info => newImportInfoFromExistingSpecifier(info, preferTypeOnlyImport)); - return existingDeclaration ? [existingDeclaration] : getNewImportInfos(program, sourceFile, position, preferTypeOnlyImport, exportInfos, host, preferences); + const existingDeclaration = firstDefined(existingImports, info => newImportInfoFromExistingSpecifier(info, preferTypeOnlyImport, useRequire)); + return existingDeclaration ? [existingDeclaration] : getNewImportInfos(program, sourceFile, position, preferTypeOnlyImport, useRequire, exportInfos, host, preferences); } - function newImportInfoFromExistingSpecifier({ declaration, importKind }: FixAddToExistingImportInfo, preferTypeOnlyImport: boolean): FixAddNewImport | undefined { - const expression = declaration.kind === SyntaxKind.ImportDeclaration - ? declaration.moduleSpecifier - : declaration.moduleReference.kind === SyntaxKind.ExternalModuleReference - ? declaration.moduleReference.expression - : undefined; - return expression && isStringLiteral(expression) - ? { kind: ImportFixKind.AddNew, moduleSpecifier: expression.text, importKind, typeOnly: preferTypeOnlyImport } + function newImportInfoFromExistingSpecifier({ declaration, importKind }: FixAddToExistingImportInfo, preferTypeOnlyImport: boolean, useRequire: boolean): FixAddNewImport | undefined { + const moduleSpecifier = declaration.kind === SyntaxKind.ImportDeclaration ? declaration.moduleSpecifier : + declaration.kind === SyntaxKind.VariableDeclaration ? declaration.initializer.arguments[0] : + declaration.moduleReference.kind === SyntaxKind.ExternalModuleReference ? declaration.moduleReference.expression : + undefined; + return moduleSpecifier && isStringLiteral(moduleSpecifier) + ? { kind: ImportFixKind.AddNew, moduleSpecifier: moduleSpecifier.text, importKind, typeOnly: preferTypeOnlyImport, useRequire } : undefined; } @@ -397,7 +436,8 @@ namespace ts.codefix { const symbol = checker.getAliasedSymbol(umdSymbol); const symbolName = umdSymbol.name; const exportInfos: readonly SymbolExportInfo[] = [{ moduleSymbol: symbol, importKind: getUmdImportKind(sourceFile, program.getCompilerOptions()), exportedSymbolIsTypeOnly: false }]; - const fixes = getFixForImport(exportInfos, symbolName, isIdentifier(token) ? token.getStart(sourceFile) : undefined, /*preferTypeOnlyImport*/ false, program, sourceFile, host, preferences); + const useRequire = shouldUseRequire(sourceFile, program.getCompilerOptions()); + const fixes = getFixForImport(exportInfos, symbolName, isIdentifier(token) ? token.getStart(sourceFile) : undefined, /*preferTypeOnlyImport*/ false, useRequire, program, sourceFile, host, preferences); return { fixes, symbolName }; } function getUmdSymbol(token: Node, checker: TypeChecker): Symbol | undefined { @@ -425,9 +465,9 @@ namespace ts.codefix { case ModuleKind.CommonJS: case ModuleKind.UMD: if (isInJSFile(importingFile)) { - return isExternalModule(importingFile) ? ImportKind.Namespace : ImportKind.ConstEquals; + return isExternalModule(importingFile) ? ImportKind.Namespace : ImportKind.CommonJS; } - return ImportKind.Equals; + return ImportKind.CommonJS; case ModuleKind.System: case ModuleKind.ES2015: case ModuleKind.ES2020: @@ -451,10 +491,12 @@ namespace ts.codefix { // "default" is a keyword and not a legal identifier for the import, so we don't expect it here Debug.assert(symbolName !== InternalSymbolName.Default, "'default' isn't a legal identifier and couldn't occur here"); - const preferTypeOnlyImport = program.getCompilerOptions().importsNotUsedAsValues === ImportsNotUsedAsValues.Error && isValidTypeOnlyAliasUseSite(symbolToken); + const compilerOptions = program.getCompilerOptions(); + const preferTypeOnlyImport = compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Error && isValidTypeOnlyAliasUseSite(symbolToken); + const useRequire = shouldUseRequire(sourceFile, compilerOptions); const exportInfos = getExportInfos(symbolName, getMeaningFromLocation(symbolToken), cancellationToken, sourceFile, checker, program, host); const fixes = arrayFrom(flatMapIterator(exportInfos.entries(), ([_, exportInfos]) => - getFixForImport(exportInfos, symbolName, symbolToken.getStart(sourceFile), preferTypeOnlyImport, program, sourceFile, host, preferences))); + getFixForImport(exportInfos, symbolName, symbolToken.getStart(sourceFile), preferTypeOnlyImport, useRequire, program, sourceFile, host, preferences))); return { fixes, symbolName }; } @@ -518,19 +560,19 @@ namespace ts.codefix { // 2. 'import =' will not work in JavaScript, so the decision is between a default // and const/require. if (isInJSFile(importingFile)) { - return isExternalModule(importingFile) ? ImportKind.Default : ImportKind.ConstEquals; + return isExternalModule(importingFile) ? ImportKind.Default : ImportKind.CommonJS; } // 3. At this point the most correct choice is probably 'import =', but people // really hate that, so look to see if the importing file has any precedent // on how to handle it. for (const statement of importingFile.statements) { if (isImportEqualsDeclaration(statement)) { - return ImportKind.Equals; + return ImportKind.CommonJS; } } // 4. We have no precedent to go on, so just use a default import if // allowSyntheticDefaultImports/esModuleInterop is enabled. - return allowSyntheticDefaults ? ImportKind.Default : ImportKind.Equals; + return allowSyntheticDefaults ? ImportKind.Default : ImportKind.CommonJS; } function getDefaultExportInfoWorker(defaultExport: Symbol, moduleSymbol: Symbol, checker: TypeChecker, compilerOptions: CompilerOptions): { readonly symbolForMeaning: Symbol, readonly name: string } | undefined { @@ -582,16 +624,18 @@ namespace ts.codefix { addImportType(changes, sourceFile, fix, quotePreference); return [Diagnostics.Change_0_to_1, symbolName, getImportTypePrefix(fix.moduleSpecifier, quotePreference) + symbolName]; case ImportFixKind.AddToExisting: { - const { importClause, importKind, canUseTypeOnlyImport } = fix; - doAddExistingFix(changes, sourceFile, importClause, importKind === ImportKind.Default ? symbolName : undefined, importKind === ImportKind.Named ? [symbolName] : emptyArray, canUseTypeOnlyImport); - const moduleSpecifierWithoutQuotes = stripQuotes(importClause.parent.moduleSpecifier.getText()); + const { importClauseOrBindingPattern, importKind, canUseTypeOnlyImport, moduleSpecifier } = fix; + doAddExistingFix(changes, sourceFile, importClauseOrBindingPattern, importKind === ImportKind.Default ? symbolName : undefined, importKind === ImportKind.Named ? [symbolName] : emptyArray, canUseTypeOnlyImport); + const moduleSpecifierWithoutQuotes = stripQuotes(moduleSpecifier); return [importKind === ImportKind.Default ? Diagnostics.Add_default_import_0_to_existing_import_declaration_from_1 : Diagnostics.Add_0_to_existing_import_declaration_from_1, symbolName, moduleSpecifierWithoutQuotes]; // you too! } case ImportFixKind.AddNew: { - const { importKind, moduleSpecifier, typeOnly } = fix; - addNewImports(changes, sourceFile, moduleSpecifier, quotePreference, importKind === ImportKind.Default ? { defaultImport: symbolName, namedImports: emptyArray, namespaceLikeImport: undefined, typeOnly } - : importKind === ImportKind.Named ? { defaultImport: undefined, namedImports: [symbolName], namespaceLikeImport: undefined, typeOnly } - : { defaultImport: undefined, namedImports: emptyArray, namespaceLikeImport: { importKind, name: symbolName }, typeOnly }, /*blankLineBetween*/ true); + const { importKind, moduleSpecifier, typeOnly, useRequire } = fix; + const addDeclarations = useRequire ? addNewRequires : addNewImports; + const importsCollection = importKind === ImportKind.Default ? { defaultImport: symbolName, typeOnly } : + importKind === ImportKind.Named ? { namedImports: [symbolName], typeOnly } : + { namespaceLikeImport: { importKind, name: symbolName }, typeOnly }; + addDeclarations(changes, sourceFile, moduleSpecifier, quotePreference, importsCollection, /*blankLineBetween*/ true); return [importKind === ImportKind.Default ? Diagnostics.Import_default_0_from_module_1 : Diagnostics.Import_0_from_module_1, symbolName, moduleSpecifier]; } default: @@ -599,7 +643,17 @@ namespace ts.codefix { } } - function doAddExistingFix(changes: textChanges.ChangeTracker, sourceFile: SourceFile, clause: ImportClause, defaultImport: string | undefined, namedImports: readonly string[], canUseTypeOnlyImport: boolean): void { + function doAddExistingFix(changes: textChanges.ChangeTracker, sourceFile: SourceFile, clause: ImportClause | ObjectBindingPattern, defaultImport: string | undefined, namedImports: readonly string[], canUseTypeOnlyImport: boolean): void { + if (clause.kind === SyntaxKind.ObjectBindingPattern) { + if (defaultImport) { + addElementToBindingPattern(clause, defaultImport, "default"); + } + for (const specifier of namedImports) { + addElementToBindingPattern(clause, specifier, /*propertyName*/ undefined); + } + return; + } + const convertTypeOnlyToRegular = !canUseTypeOnlyImport && clause.isTypeOnly; if (defaultImport) { Debug.assert(!clause.name, "Cannot add a default import to an import clause that already has one"); @@ -629,6 +683,16 @@ namespace ts.codefix { if (convertTypeOnlyToRegular) { changes.delete(sourceFile, getTypeKeywordOfTypeOnlyImport(clause, sourceFile)); } + + function addElementToBindingPattern(bindingPattern: ObjectBindingPattern, name: string, propertyName: string | undefined) { + const element = createBindingElement(/*dotDotDotToken*/ undefined, propertyName, name); + if (bindingPattern.elements.length) { + changes.insertNodeInListAfter(sourceFile, last(bindingPattern.elements), element); + } + else { + changes.replaceNode(sourceFile, bindingPattern, createObjectBindingPattern([element])); + } + } } function addNamespaceQualifier(changes: textChanges.ChangeTracker, sourceFile: SourceFile, { namespacePrefix, position }: FixUseNamespaceImport): void { @@ -646,39 +710,68 @@ namespace ts.codefix { interface ImportsCollection { readonly typeOnly: boolean; - readonly defaultImport: string | undefined; - readonly namedImports: string[]; - readonly namespaceLikeImport: { - readonly importKind: ImportKind.Equals | ImportKind.Namespace | ImportKind.ConstEquals; + readonly defaultImport?: string; + readonly namedImports?: string[]; + readonly namespaceLikeImport?: { + readonly importKind: ImportKind.CommonJS | ImportKind.Namespace; readonly name: string; - } | undefined; + }; } - function addNewImports(changes: textChanges.ChangeTracker, sourceFile: SourceFile, moduleSpecifier: string, quotePreference: QuotePreference, { defaultImport, namedImports, namespaceLikeImport, typeOnly }: ImportsCollection, blankLineBetween: boolean): void { + function addNewImports(changes: textChanges.ChangeTracker, sourceFile: SourceFile, moduleSpecifier: string, quotePreference: QuotePreference, imports: ImportsCollection, blankLineBetween: boolean): void { const quotedModuleSpecifier = makeStringLiteral(moduleSpecifier, quotePreference); - if (defaultImport !== undefined || namedImports.length) { + if (imports.defaultImport !== undefined || imports.namedImports?.length) { insertImport(changes, sourceFile, makeImport( - defaultImport === undefined ? undefined : createIdentifier(defaultImport), - namedImports.map(n => createImportSpecifier(/*propertyName*/ undefined, createIdentifier(n))), moduleSpecifier, quotePreference, typeOnly), /*blankLineBetween*/ blankLineBetween); + imports.defaultImport === undefined ? undefined : createIdentifier(imports.defaultImport), + imports.namedImports?.map(n => createImportSpecifier(/*propertyName*/ undefined, createIdentifier(n))), moduleSpecifier, quotePreference, imports.typeOnly), /*blankLineBetween*/ blankLineBetween); } + const { namespaceLikeImport, typeOnly } = imports; if (namespaceLikeImport) { - insertImport( - changes, - sourceFile, - namespaceLikeImport.importKind === ImportKind.Equals ? createImportEqualsDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, createIdentifier(namespaceLikeImport.name), createExternalModuleReference(quotedModuleSpecifier)) : - namespaceLikeImport.importKind === ImportKind.ConstEquals ? createConstEqualsRequireDeclaration(namespaceLikeImport.name, quotedModuleSpecifier) : - createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, createImportClause(/*name*/ undefined, createNamespaceImport(createIdentifier(namespaceLikeImport.name)), typeOnly), quotedModuleSpecifier), /*blankLineBetween*/ blankLineBetween); - } - } - - function createConstEqualsRequireDeclaration(name: string, quotedModuleSpecifier: StringLiteral): VariableStatement { - return createVariableStatement(/*modifiers*/ undefined, createVariableDeclarationList([ - createVariableDeclaration( - createIdentifier(name), - /*type*/ undefined, - createCall(createIdentifier("require"), /*typeArguments*/ undefined, [quotedModuleSpecifier]) - ) - ], NodeFlags.Const)); + const declaration = namespaceLikeImport.importKind === ImportKind.CommonJS + ? createImportEqualsDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + createIdentifier(namespaceLikeImport.name), + createExternalModuleReference(quotedModuleSpecifier)) + : createImportDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + createImportClause( + /*name*/ undefined, + createNamespaceImport(createIdentifier(namespaceLikeImport.name)), + typeOnly), + quotedModuleSpecifier); + insertImport(changes, sourceFile, declaration, /*blankLineBetween*/ blankLineBetween); + } + } + + function addNewRequires(changes: textChanges.ChangeTracker, sourceFile: SourceFile, moduleSpecifier: string, quotePreference: QuotePreference, imports: ImportsCollection, blankLineBetween: boolean) { + const quotedModuleSpecifier = makeStringLiteral(moduleSpecifier, quotePreference); + // const { default: foo, bar, etc } = require('./mod'); + if (imports.defaultImport || imports.namedImports?.length) { + const bindingElements = imports.namedImports?.map(name => createBindingElement(/*dotDotDotToken*/ undefined, /*propertyName*/ undefined, name)) || []; + if (imports.defaultImport) { + bindingElements.unshift(createBindingElement(/*dotDotDotToken*/ undefined, "default", imports.defaultImport)); + } + const declaration = createConstEqualsRequireDeclaration(createObjectBindingPattern(bindingElements), quotedModuleSpecifier); + insertImport(changes, sourceFile, declaration, blankLineBetween); + } + // const foo = require('./mod'); + if (imports.namespaceLikeImport) { + const declaration = createConstEqualsRequireDeclaration(imports.namespaceLikeImport.name, quotedModuleSpecifier); + insertImport(changes, sourceFile, declaration, blankLineBetween); + } + } + + function createConstEqualsRequireDeclaration(name: string | ObjectBindingPattern, quotedModuleSpecifier: StringLiteral): VariableStatement { + return createVariableStatement( + /*modifiers*/ undefined, + createVariableDeclarationList([ + createVariableDeclaration( + typeof name === "string" ? createIdentifier(name) : name, + /*type*/ undefined, + createCall(createIdentifier("require"), /*typeArguments*/ undefined, [quotedModuleSpecifier]))], + NodeFlags.Const)); } function symbolHasMeaning({ declarations }: Symbol, meaning: SemanticMeaning): boolean { diff --git a/src/services/completions.ts b/src/services/completions.ts index 27f959aa24c37..11f16ccf8fe2f 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1384,16 +1384,14 @@ namespace ts.Completions { } function shouldOfferImportCompletions(): boolean { - // If not already a module, must have modules enabled and not currently be in a commonjs module. (TODO: import completions for commonjs) + // If not already a module, must have modules enabled. if (!preferences.includeCompletionsForModuleExports) return false; // If already using ES6 modules, OK to continue using them. - if (sourceFile.externalModuleIndicator) return true; - // If already using commonjs, don't introduce ES6. - if (sourceFile.commonJsModuleIndicator) return false; + if (sourceFile.externalModuleIndicator || sourceFile.commonJsModuleIndicator) return true; // If module transpilation is enabled or we're targeting es6 or above, or not emitting, OK. if (compilerOptionsIndicateEs6Modules(program.getCompilerOptions())) return true; // If some file is using ES6 modules, assume that it's OK to add more. - return programContainsEs6Modules(program); + return programContainsModules(program); } function isSnippetScope(scopeNode: Node): boolean { @@ -1557,6 +1555,7 @@ namespace ts.Completions { const startTime = timestamp(); log(`getSymbolsFromOtherSourceFileExports: Recomputing list${detailsEntryId ? " for details entry" : ""}`); const seenResolvedModules = createMap(); + const seenExports = createMap(); /** Bucket B */ const aliasesToAlreadyIncludedSymbols = createMap(); /** Bucket C */ @@ -1580,18 +1579,21 @@ namespace ts.Completions { // Don't add another completion for `export =` of a symbol that's already global. // So in `declare namespace foo {} declare module "foo" { export = foo; }`, there will just be the global completion for `foo`. - if (resolvedModuleSymbol !== moduleSymbol && - every(resolvedModuleSymbol.declarations, d => !!d.getSourceFile().externalModuleIndicator && !findAncestor(d, isGlobalScopeAugmentation))) { + if (resolvedModuleSymbol !== moduleSymbol && every(resolvedModuleSymbol.declarations, isNonGlobalDeclaration)) { pushSymbol(resolvedModuleSymbol, moduleSymbol, /*skipFilter*/ true); } - for (const symbol of typeChecker.getExportsOfModule(moduleSymbol)) { + for (const symbol of typeChecker.getExportsAndPropertiesOfModule(moduleSymbol)) { + const symbolId = getSymbolId(symbol).toString(); + // `getExportsAndPropertiesOfModule` can include duplicates + if (!addToSeen(seenExports, symbolId)) { + continue; + } // If this is `export { _break as break };` (a keyword) -- skip this and prefer the keyword completion. if (some(symbol.declarations, d => isExportSpecifier(d) && !!d.propertyName && isIdentifierANonContextualKeyword(d.name))) { continue; } - const symbolId = getSymbolId(symbol).toString(); // If `symbol.parent !== moduleSymbol`, this is an `export * from "foo"` re-export. Those don't create new symbols. const isExportStarFromReExport = typeChecker.getMergedSymbol(symbol.parent!) !== resolvedModuleSymbol; // If `!!d.parent.parent.moduleSpecifier`, this is `export { foo } from "foo"` re-export, which creates a new symbol (thus isn't caught by the first check). @@ -2683,4 +2685,14 @@ namespace ts.Completions { } } } + + function isNonGlobalDeclaration(declaration: Declaration) { + const sourceFile = declaration.getSourceFile(); + // If the file is not a module, the declaration is global + if (!sourceFile.externalModuleIndicator && !sourceFile.commonJsModuleIndicator) { + return false; + } + // If the file is a module written in TypeScript, it still might be in a `declare global` augmentation + return isInJSFile(declaration) || !findAncestor(declaration, isGlobalScopeAugmentation); + } } diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 2e1b8a5795d20..4ef38879ca5b5 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -1691,6 +1691,9 @@ namespace ts { : isPrivateIdentifier(name) ? idText(name) : getTextOfIdentifierOrLiteral(name); } + export function programContainsModules(program: Program): boolean { + return program.getSourceFiles().some(s => !s.isDeclarationFile && !program.isSourceFileFromExternalLibrary(s) && !!(s.externalModuleIndicator || s.commonJsModuleIndicator)); + } export function programContainsEs6Modules(program: Program): boolean { return program.getSourceFiles().some(s => !s.isDeclarationFile && !program.isSourceFileFromExternalLibrary(s) && !!s.externalModuleIndicator); } @@ -1832,7 +1835,8 @@ namespace ts { } export function insertImport(changes: textChanges.ChangeTracker, sourceFile: SourceFile, importDecl: Statement, blankLineBetween: boolean): void { - const lastImportDeclaration = findLast(sourceFile.statements, isAnyImportSyntax); + const importKindPredicate = importDecl.kind === SyntaxKind.VariableStatement ? isRequireVariableDeclarationStatement : isAnyImportSyntax; + const lastImportDeclaration = findLast(sourceFile.statements, statement => importKindPredicate(statement)); if (lastImportDeclaration) { changes.insertNodeAfter(sourceFile, lastImportDeclaration, importDecl); } diff --git a/tests/cases/fourslash/completionsImport_compilerOptionsModule.ts b/tests/cases/fourslash/completionsImport_compilerOptionsModule.ts index 56d4ca9e27dc3..074727bbda98a 100644 --- a/tests/cases/fourslash/completionsImport_compilerOptionsModule.ts +++ b/tests/cases/fourslash/completionsImport_compilerOptionsModule.ts @@ -34,12 +34,7 @@ ////fo/*dts*/ verify.completions({ - marker: ["b"], - excludes: "foo", - preferences: { includeCompletionsForModuleExports: true } -}); -verify.completions({ - marker: ["c", "ccheck", "cts", "d", "dcheck", "dts"], + marker: ["b", "c", "ccheck", "cts", "d", "dcheck", "dts"], includes: [{ name: "foo", source: "/node_modules/a/index", diff --git a/tests/cases/fourslash/completionsImport_require_addNew.ts b/tests/cases/fourslash/completionsImport_require_addNew.ts new file mode 100644 index 0000000000000..9e7fac5db2e81 --- /dev/null +++ b/tests/cases/fourslash/completionsImport_require_addNew.ts @@ -0,0 +1,31 @@ +/// + +// @allowJs: true + +// @Filename: /a.js +////const x = 0; +////module.exports = { x }; + +// @Filename: /b.js +////x/**/ + +verify.completions({ + marker: "", + includes: { + name: "x", + source: "/a", + sourceDisplay: "./a", + text: "(property) x: number", + hasAction: true, + sortText: completion.SortText.AutoImportSuggestions + }, + preferences: { includeCompletionsForModuleExports: true }, +}); +verify.applyCodeActionFromCompletion("", { + name: "x", + source: "/a", + description: `Import 'x' from module "./a"`, + newFileContent: `const { x } = require("./a"); + +x`, +}); diff --git a/tests/cases/fourslash/completionsImport_require_addToExisting.ts b/tests/cases/fourslash/completionsImport_require_addToExisting.ts new file mode 100644 index 0000000000000..84ab8d587214f --- /dev/null +++ b/tests/cases/fourslash/completionsImport_require_addToExisting.ts @@ -0,0 +1,34 @@ +/// + +// @allowJs: true + +// @Filename: /a.js +////const x = 0; +////function f() {} +////module.exports = { x, f }; + +// @Filename: /b.js +////const { f } = require("./a"); +//// +////x/**/ + +verify.completions({ + marker: "", + includes: { + name: "x", + source: "/a", + sourceDisplay: "./a", + text: "(property) x: number", + hasAction: true, + sortText: completion.SortText.AutoImportSuggestions + }, + preferences: { includeCompletionsForModuleExports: true }, +}); +verify.applyCodeActionFromCompletion("", { + name: "x", + source: "/a", + description: `Add 'x' to existing import declaration from "./a"`, + newFileContent: `const { f, x } = require("./a"); + +x`, +}); diff --git a/tests/cases/fourslash/importFixWithMultipleModuleExportAssignment.ts b/tests/cases/fourslash/importFixWithMultipleModuleExportAssignment.ts index 546a613947f2a..b5b9f808e6fd3 100644 --- a/tests/cases/fourslash/importFixWithMultipleModuleExportAssignment.ts +++ b/tests/cases/fourslash/importFixWithMultipleModuleExportAssignment.ts @@ -1,5 +1,6 @@ /// +// @module: esnext // @allowJs: true // @checkJs: true diff --git a/tests/cases/fourslash/importFixesGlobalTypingsCache.ts b/tests/cases/fourslash/importFixesGlobalTypingsCache.ts index 21c8ddf0ebe05..68eeae6f0fbcf 100644 --- a/tests/cases/fourslash/importFixesGlobalTypingsCache.ts +++ b/tests/cases/fourslash/importFixesGlobalTypingsCache.ts @@ -13,6 +13,6 @@ ////BrowserRouter/**/ goTo.file("/project/index.js"); -verify.importFixAtPosition([`import { BrowserRouter } from "react-router-dom"; +verify.importFixAtPosition([`const { BrowserRouter } = require("react-router-dom"); BrowserRouter`]); diff --git a/tests/cases/fourslash/importNameCodeFix_all_js.ts b/tests/cases/fourslash/importNameCodeFix_all_js.ts index cf4921c69115d..6116d3da3aaec 100644 --- a/tests/cases/fourslash/importNameCodeFix_all_js.ts +++ b/tests/cases/fourslash/importNameCodeFix_all_js.ts @@ -1,5 +1,6 @@ /// +// @module: esnext // @allowJs: true // @checkJs: true diff --git a/tests/cases/fourslash/importNameCodeFix_defaultExport.ts b/tests/cases/fourslash/importNameCodeFix_defaultExport.ts index fa8cef71c99b2..8afd76df1cd27 100644 --- a/tests/cases/fourslash/importNameCodeFix_defaultExport.ts +++ b/tests/cases/fourslash/importNameCodeFix_defaultExport.ts @@ -1,5 +1,6 @@ /// +// @module: esnext // @allowJs: true // @checkJs: true diff --git a/tests/cases/fourslash/importNameCodeFix_require.ts b/tests/cases/fourslash/importNameCodeFix_require.ts new file mode 100644 index 0000000000000..c682c469246d4 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFix_require.ts @@ -0,0 +1,36 @@ +/// + +// @allowJs: true +// @checkJs: true + +// @Filename: foo.js +////module.exports = function foo() {} + +// @Filename: utils.js +////function util1() {} +////function util2() {} +////module.exports = { util1, util2 }; + +// @Filename: blah.js +////export default class Blah {} + +// @Filename: index.js +////foo(); +////util1(); +////util2(); +////new Blah; + +goTo.file("index.js"); +verify.codeFixAll({ + fixId: "fixMissingImport", + fixAllDescription: "Add all missing imports", + newFileContent: +`const foo = require("./foo"); +const { util1, util2 } = require("./utils"); +const { default: Blah } = require("./blah"); + +foo(); +util1(); +util2(); +new Blah;` +}); diff --git a/tests/cases/fourslash/importNameCodeFix_require_UMD.ts b/tests/cases/fourslash/importNameCodeFix_require_UMD.ts new file mode 100644 index 0000000000000..7f8e30876ae54 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFix_require_UMD.ts @@ -0,0 +1,24 @@ +/// + +// @allowJs: true +// @checkJs: true + +// @Filename: umd.d.ts +////namespace Foo { function f() {} } +////export = Foo; +////export as namespace Foo; + +// @Filename: index.js +////Foo; +////module.exports = {}; + +goTo.file("index.js"); +verify.codeFix({ + index: 0, + description: `Import 'Foo' from module "./umd"`, + newFileContent: +`const Foo = require("./umd"); + +Foo; +module.exports = {};` +}); diff --git a/tests/cases/fourslash/importNameCodeFix_require_addToExisting.ts b/tests/cases/fourslash/importNameCodeFix_require_addToExisting.ts new file mode 100644 index 0000000000000..2d64cfac390b2 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFix_require_addToExisting.ts @@ -0,0 +1,29 @@ +/// + +// @allowJs: true +// @checkJs: true + +// @Filename: blah.js +////export default class Blah {} +////export const Named1 = 0; +////export const Named2 = 1; + +// @Filename: index.js +////var path = require('path') +//// , { promisify } = require('util') +//// , { Named1 } = require('./blah') +//// +////new Blah + +goTo.file("index.js"); +verify.codeFix({ + index: 0, + errorCode: ts.Diagnostics.Cannot_find_name_0.code, + description: `Add default import 'Blah' to existing import declaration from "./blah"`, + newFileContent: +`var path = require('path') + , { promisify } = require('util') + , { Named1, default: Blah } = require('./blah') + +new Blah` +}); diff --git a/tests/cases/fourslash/importNameCodeFix_require_importVsRequire_addToExistingWins.ts b/tests/cases/fourslash/importNameCodeFix_require_importVsRequire_addToExistingWins.ts new file mode 100644 index 0000000000000..cd0c54b87c165 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFix_require_importVsRequire_addToExistingWins.ts @@ -0,0 +1,36 @@ +/// + +// If a file has both `require` and `import` declarations, +// prefer whichever can be used for an "add to existing" action. + +// @allowJs: true +// @checkJs: true + +// @Filename: blah.js +////export default class Blah {} +////export const Named1 = 0; +////export const Named2 = 1; + +// @Filename: index.js +////var path = require('path') +//// , { promisify } = require('util') +//// , { Named1 } = require('./blah') +//// +////import fs from 'fs' +//// +////new Blah + +goTo.file("index.js"); +verify.codeFix({ + index: 0, + errorCode: ts.Diagnostics.Cannot_find_name_0.code, + description: `Add default import 'Blah' to existing import declaration from "./blah"`, + newFileContent: +`var path = require('path') + , { promisify } = require('util') + , { Named1, default: Blah } = require('./blah') + +import fs from 'fs' + +new Blah` +}); diff --git a/tests/cases/fourslash/importNameCodeFix_require_importVsRequire_importWins.ts b/tests/cases/fourslash/importNameCodeFix_require_importVsRequire_importWins.ts new file mode 100644 index 0000000000000..6396606a7b9e8 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFix_require_importVsRequire_importWins.ts @@ -0,0 +1,51 @@ +/// + +// @allowJs: true +// @checkJs: true + +// @Filename: blah.js +////export default class Blah {} +////export const Named1 = 0; +////export const Named2 = 1; + +// @Filename: addToExisting.js +////const { Named2 } = require('./blah') +////import { Named1 } from './blah' +//// +////new Blah + +// @Filename: newImport.js +////import fs from 'fs'; +////const path = require('path'); +//// +////new Blah + +// If an "add to existing" fix could be applied both to an `import` +// and to a `require` declaration, prefer the `import`. +goTo.file("addToExisting.js"); +verify.codeFix({ + index: 0, + errorCode: ts.Diagnostics.Cannot_find_name_0.code, + description: `Add default import 'Blah' to existing import declaration from "./blah"`, + newFileContent: +`const { Named2 } = require('./blah') +import Blah, { Named1 } from './blah' + +new Blah` +}); + +// If a file contains `import` and `require` declarations but none +// can be used for an "add to existing" fix, prefer `import` for the +// new declaration. +goTo.file("newImport.js"); +verify.codeFix({ + index: 0, + errorCode: ts.Diagnostics.Cannot_find_name_0.code, + description: `Import default 'Blah' from module "./blah"`, + newFileContent: +`import fs from 'fs'; +import Blah from './blah'; +const path = require('path'); + +new Blah` +}); diff --git a/tests/cases/fourslash/importNameCodeFix_require_importVsRequire_moduleTarget.ts b/tests/cases/fourslash/importNameCodeFix_require_importVsRequire_moduleTarget.ts new file mode 100644 index 0000000000000..9214ea9eb4aec --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFix_require_importVsRequire_moduleTarget.ts @@ -0,0 +1,40 @@ +// If the module target is es2015+ and the file has no existing CommonJS +// indicators, use `import` declarations. + +// @allowJs: true +// @checkJs: true +// @module: es2015 + +// @Filename: a.js +////export const x = 0; + +// @Filename: index.js +////x + +goTo.file("index.js"); +verify.codeFix({ + index: 0, + errorCode: ts.Diagnostics.Cannot_find_name_0.code, + description: `Import 'x' from module "./a"`, + applyChanges: false, + newFileContent: +`import { x } from "./a"; + +x` +}); + +// If the module target is es2015+ but the file already uses `require` +// (and not `import`), use `require`. +goTo.position(0); +edit.insertLine("const fs = require('fs');\n"); +verify.codeFix({ + index: 0, + errorCode: ts.Diagnostics.Cannot_find_name_0.code, + description: `Import 'x' from module "./a"`, + applyChanges: false, + newFileContent: +`const fs = require('fs'); +const { x } = require('./a'); + +x` +}); diff --git a/tests/cases/fourslash/importNameCodeFix_require_namedAndDefault.ts b/tests/cases/fourslash/importNameCodeFix_require_namedAndDefault.ts new file mode 100644 index 0000000000000..d4772d166ff9d --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFix_require_namedAndDefault.ts @@ -0,0 +1,24 @@ +/// + +// @allowJs: true +// @checkJs: true + +// @Filename: blah.js +////export default class Blah {} +////export const Named1 = 0; +////export const Named2 = 1; + +// @Filename: index.js +////Named1 + Named2; +////new Blah; + +goTo.file("index.js"); +verify.codeFixAll({ + fixId: "fixMissingImport", + fixAllDescription: "Add all missing imports", + newFileContent: +`const { default: Blah, Named1, Named2 } = require("./blah"); + +Named1 + Named2; +new Blah;` +}); diff --git a/tests/cases/fourslash/moveToNewFile_js.ts b/tests/cases/fourslash/moveToNewFile_js.ts index 5e0bb712b9e8f..1d78b382acf83 100644 --- a/tests/cases/fourslash/moveToNewFile_js.ts +++ b/tests/cases/fourslash/moveToNewFile_js.ts @@ -1,5 +1,6 @@ /// +// @module: esnext // @allowJs: true // @Filename: /a.js @@ -16,10 +17,8 @@ verify.moveToNewFile({ newFileContents: { "/a.js": -// TODO: GH#22330 -`const { y, z } = require("./y"); - -const { a, } = require("./other"); +`const { a, } = require("./other"); +const { y, z } = require("./y"); const p = 0; exports.p = p; a; y; z;`,