Full support for CommonJS auto-imports in JS (#37027)

* Support add new requires

* Always use destructuring for requiring default exports

* Add more tests

* Update existing fourslash tests

* Use `getExportsAndPropertiesOfModule`

* Add UMD test

* Apply suggestions from code review

Fix typos

Co-Authored-By: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com>

Co-authored-by: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com>
This commit is contained in:
Andrew Branch
2020-02-28 14:34:20 -08:00
committed by GitHub
co-authored by Nathan Shively-Sanders
parent 6a1e474cac
commit f9945f5acf
22 changed files with 558 additions and 119 deletions
+4 -1
View File
@@ -609,6 +609,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);
@@ -2576,7 +2579,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;
+12 -1
View File
@@ -3596,6 +3596,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.
@@ -3830,6 +3831,10 @@ namespace ts {
/* @internal */
export type AnyImportSyntax = ImportDeclaration | ImportEqualsDeclaration;
/* @internal */
export type AnyImportOrRequire = AnyImportSyntax | RequireVariableDeclaration;
/* @internal */
export type AnyImportOrReExport = AnyImportSyntax | ExportDeclaration;
@@ -3846,7 +3851,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 =
+14
View File
@@ -1856,6 +1856,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;
}
+1 -1
View File
@@ -2717,7 +2717,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);
+188 -95
View File
@@ -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<Mutable<ImportsCollection>>();
const addToExisting = createMap<{ readonly importClauseOrBindingPattern: ImportClause | ObjectBindingPattern, defaultImport: string | undefined; readonly namedImports: string[], canUseTypeOnlyImport: boolean }>();
const newImports = createMap<Mutable<ImportsCollection & { useRequire: boolean }>>();
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<StringLiteralLike, FixAddToExistingImportInfo>(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);
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 createConstEqualsRequireDeclaration(name: string, quotedModuleSpecifier: StringLiteral): VariableStatement {
return createVariableStatement(/*modifiers*/ undefined, createVariableDeclarationList([
createVariableDeclaration(
createIdentifier(name),
/*type*/ undefined,
createCall(createIdentifier("require"), /*typeArguments*/ undefined, [quotedModuleSpecifier])
)
], NodeFlags.Const));
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 {
+21 -9
View File
@@ -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<true>();
const seenExports = createMap<true>();
/** Bucket B */
const aliasesToAlreadyIncludedSymbols = createMap<true>();
/** 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);
}
}
+5 -1
View File
@@ -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);
}
@@ -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",
@@ -0,0 +1,31 @@
/// <reference path="fourslash.ts" />
// @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`,
});
@@ -0,0 +1,34 @@
/// <reference path="fourslash.ts" />
// @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`,
});
@@ -1,5 +1,6 @@
/// <reference path="fourslash.ts" />
// @module: esnext
// @allowJs: true
// @checkJs: true
@@ -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`]);
@@ -1,5 +1,6 @@
/// <reference path='fourslash.ts' />
// @module: esnext
// @allowJs: true
// @checkJs: true
@@ -1,5 +1,6 @@
/// <reference path="fourslash.ts" />
// @module: esnext
// @allowJs: true
// @checkJs: true
@@ -0,0 +1,36 @@
/// <reference path="fourslash.ts" />
// @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;`
});
@@ -0,0 +1,24 @@
/// <reference path="fourslash.ts" />
// @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 = {};`
});
@@ -0,0 +1,29 @@
/// <reference path="fourslash.ts" />
// @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`
});
@@ -0,0 +1,36 @@
/// <reference path="fourslash.ts" />
// 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`
});
@@ -0,0 +1,51 @@
/// <reference path="fourslash.ts" />
// @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`
});
@@ -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`
});
@@ -0,0 +1,24 @@
/// <reference path="fourslash.ts" />
// @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;`
});
+3 -4
View File
@@ -1,5 +1,6 @@
/// <reference path='fourslash.ts' />
// @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;`,