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;`, pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy