-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Code fix for missing imports #11768
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Code fix for missing imports #11768
Changes from 2 commits
0389207
eb46886
80d7f5c
a6807ab
1502f75
e72f0c8
d273308
35d668a
eb892d3
6d024dc
26cd24a
a282fc0
1f82ce7
d83c292
7568f6f
aa2891c
055d20f
11ea6b3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
///<reference path='superFixes.ts' /> | ||
///<reference path='importFixes.ts' /> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,291 @@ | ||
/* @internal */ | ||
namespace ts.codefix { | ||
const nodeModulesDir = "node_modules"; | ||
|
||
registerCodeFix({ | ||
errorCodes: [Diagnostics.Cannot_find_name_0.code], | ||
getCodeActions: (context: CodeFixContext) => { | ||
const sourceFile = context.sourceFile; | ||
const checker = context.program.getTypeChecker(); | ||
const allFiles = context.program.getSourceFiles(); | ||
const useCaseSensitiveFileNames = context.host.useCaseSensitiveFileNames ? context.host.useCaseSensitiveFileNames() : false; | ||
|
||
const token = getTokenAtPosition(sourceFile, context.span.start); | ||
const name = token.getText(); | ||
const allActions: CodeAction[] = []; | ||
|
||
// Get existing ImportDeclarations from the source file | ||
const imports: ImportDeclaration[] = []; | ||
if (sourceFile.statements) { | ||
for (const statement of sourceFile.statements) { | ||
if (statement.kind === SyntaxKind.ImportDeclaration) { | ||
imports.push(<ImportDeclaration>statement); | ||
} | ||
} | ||
} | ||
|
||
// Check if a matching symbol is exported by any ambient modules that has been declared | ||
const ambientModules = checker.getAmbientModules(); | ||
for (const moduleSymbol of ambientModules) { | ||
const exports = checker.getExportsOfModule(moduleSymbol) || []; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i would recommend either exposing the getExportsOfModule that returns a symbolTable or adding a new |
||
for (const exported of exports) { | ||
if (exported.name === name) { | ||
allActions.push(getCodeActionForImport(removeQuotes(moduleSymbol.getName()))); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand what the above few lines do. Can you add some explanatory comments? In what scenario can we detect that you need to import a default export? It has no name to match on, as the local name for the 'default' export is provided by the import statement. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (I think I see the scenario in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't have data, but a quick search in the DefinitelyTyped repo did give me some library files with named |
||
} | ||
} | ||
|
||
// Check if a matching symbol is exported by any files known to the compiler | ||
for (const file of allFiles) { | ||
const exports = file.symbol && file.symbol.exports; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. check cancellation toke for every file. |
||
if (exports) { | ||
for (const exported in exports) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why? just check There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. actually this is not accurate, you want to use checker.ts::getExportsOfModule here to make sure There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you mean instead of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes. |
||
if (exported === name) { | ||
let moduleSpecifier: string; | ||
const sourceDir = getDirectoryPath(sourceFile.fileName); | ||
if (file.fileName.indexOf(nodeModulesDir) !== -1) { | ||
moduleSpecifier = convertPathToModuleSpecifier(file.fileName); | ||
} | ||
else { | ||
// Try and convert the file path into one relative to the source file | ||
|
||
const pathName = getRelativePathToDirectoryOrUrl( | ||
sourceDir, | ||
file.fileName, | ||
sourceDir, | ||
createGetCanonicalFileName(useCaseSensitiveFileNames), | ||
/* isAbsolutePathAnUrl */ false | ||
); | ||
|
||
// Make sure we got back a path that can be a valid module specifier | ||
const isRootedOrRelative = isExternalModuleNameRelative(pathName) || isRootedDiskPath(pathName); | ||
moduleSpecifier = removeFileExtension(isRootedOrRelative ? pathName : combinePaths(".", pathName)); | ||
} | ||
|
||
allActions.push(getCodeActionForImport(moduleSpecifier, (a, b) => | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could get the same symbol from multiple imports, we need to filter them and only include the "best" for a symbol. where best is:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should split this into phases, first phase is to find all [exported symbol, module] that match the name we are looking for, then the next phase is to generate the action for each symbol. |
||
compareModuleSpecifiers(a, b, sourceDir, useCaseSensitiveFileNames) | ||
)); | ||
} | ||
} | ||
} | ||
} | ||
|
||
return allActions; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. worth investigating the order of the result. possibly by the shortest path. |
||
|
||
function convertPathToModuleSpecifier(modulePath: string): string { | ||
const i = modulePath.lastIndexOf(nodeModulesDir); | ||
|
||
// This is not a module from the node_module folder, | ||
// so just retun the path | ||
if (i === -1) { | ||
return modulePath; | ||
} | ||
|
||
// If this is a node module, check to see if the given file is the main export of the module or not. If so, | ||
// it can be referenced by just the module name. | ||
const moduleDir = getDirectoryPath(modulePath); | ||
let nodePackage: any; | ||
try { | ||
nodePackage = JSON.parse(context.host.readFile(combinePaths(moduleDir, "package.json"))); | ||
} | ||
catch (e) { } | ||
|
||
// If no main export is explicitly defined, check for the default (index.js) | ||
const mainExport = (nodePackage && nodePackage.main) || "index.js"; | ||
const mainExportPath = normalizePath(isRootedDiskPath(mainExport) ? mainExport : combinePaths(moduleDir, mainExport)); | ||
|
||
const moduleSpecifier = removeFileExtension(modulePath.substring(i + nodeModulesDir.length + 1)); | ||
|
||
if (compareModuleSpecifiers(modulePath, mainExportPath, getDirectoryPath(sourceFile.fileName), useCaseSensitiveFileNames) === Comparison.EqualTo) { | ||
return getDirectoryPath(moduleSpecifier); | ||
} | ||
else { | ||
return moduleSpecifier; | ||
} | ||
} | ||
|
||
function getCodeActionForImport(moduleName: string, isEqual: (a: string, b: string) => Comparison = compareStrings): CodeAction { | ||
// Check to see if there are already imports being made from this source in the current file | ||
const existing = forEach(imports, (importDeclaration) => { | ||
if (isEqual(removeQuotes(importDeclaration.moduleSpecifier.getText()), moduleName) === Comparison.EqualTo) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the fileName is not guaranteed to be the same as the module name. e.g. path mapping. use sourcefile.resolvedModules, build a reverse lookup map and use it here. |
||
return importDeclaration; | ||
} | ||
}); | ||
|
||
if (existing) { | ||
return getCodeActionForExistingImport(existing); | ||
} | ||
else { | ||
return getCodeActionForNewImport(); | ||
} | ||
|
||
function getCodeActionForExistingImport(declaration: ImportDeclaration): CodeAction { | ||
const moduleSpecifier = declaration.moduleSpecifier.getText(); | ||
|
||
// We have to handle all of the different import declaration forms | ||
if (declaration.importClause) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we need to handle the case where the name of the exported symbol is "default" but the local is a different name. options: import {default as Foo} from "mod"; or import default from "mod"; |
||
if (declaration.importClause.namedBindings) { | ||
const namedBindings = declaration.importClause.namedBindings; | ||
if (namedBindings.kind === SyntaxKind.NamespaceImport) { | ||
/** | ||
* Cases: | ||
* import * as ns from "mod" | ||
* import d, * as ns from "mod" | ||
* | ||
* Because there is no import list, we alter the reference to include the | ||
* namespace instead of altering the import declaration. For example, "foo" would | ||
* become "ns.foo" | ||
*/ | ||
const ns = (<NamespaceImport>namedBindings).name.getText(); | ||
return createCodeAction( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i would just add an import as in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we should also handle import equals declarations the same way. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After trying it I find it quite annoying if we insert a new line for every single imports. I tried webstorm, and they do consolidate imports from the same file together. |
||
Diagnostics.Change_0_to_1, | ||
[name, `${ns}.${name}`], | ||
`${ns}.`, | ||
{ start: token.getStart(), length: 0 }, | ||
sourceFile.fileName | ||
); | ||
} | ||
else if (namedBindings.kind === SyntaxKind.NamedImports) { | ||
/** | ||
* Cases: | ||
* import { a, b as x } from "mod" | ||
* import d, { a, b as x } from "mod" | ||
* | ||
* Because there is already an import list, just insert the identifier into it | ||
*/ | ||
const textChange = getTextChangeForImportList(<NamedImports>namedBindings); | ||
return createCodeAction( | ||
Diagnostics.Add_0_to_existing_import_declaration_from_1, | ||
[name, moduleSpecifier], | ||
textChange.newText, | ||
textChange.span, | ||
sourceFile.fileName | ||
); | ||
} | ||
} | ||
else if (declaration.importClause.name) { | ||
/** | ||
* Case: import d from "mod" | ||
* | ||
* Add a list of imports after the default import | ||
*/ | ||
return createCodeAction( | ||
Diagnostics.Add_0_to_existing_import_declaration_from_1, | ||
[name, moduleSpecifier], | ||
`, { ${name} }`, | ||
{ start: declaration.importClause.name.getEnd(), length: 0 }, | ||
sourceFile.fileName | ||
); | ||
} | ||
|
||
function getTextChangeForImportList(importList: NamedImports): TextChange { | ||
if (importList.elements.length === 0) { | ||
const start = importList.getStart(); | ||
return { | ||
newText: `{ ${name} }`, | ||
span: { start, length: importList.getEnd() - start } | ||
}; | ||
} | ||
|
||
// Insert after the last element | ||
const insertPoint = importList.elements[importList.elements.length - 1].getEnd(); | ||
|
||
// If the import list has one import per line, preserve that. Otherwise, insert on same line as last element | ||
let oneImportPerLine: boolean; | ||
|
||
if (importList.elements.length === 1) { | ||
/** | ||
* If there is only one symbol being imported, still check to see if it's set up for multi-line imports like this: | ||
* import { | ||
* foo | ||
* } from "./module"; | ||
*/ | ||
const startLine = getLineOfLocalPosition(sourceFile, importList.getStart()); | ||
const endLine = getLineOfLocalPosition(sourceFile, importList.getEnd()); | ||
|
||
oneImportPerLine = endLine - startLine >= 2; | ||
} | ||
else { | ||
const startLine = getLineOfLocalPosition(sourceFile, importList.elements[0].getStart()); | ||
const endLine = getLineOfLocalPosition(sourceFile, insertPoint); | ||
|
||
oneImportPerLine = endLine - startLine >= importList.elements.length - 1; | ||
} | ||
|
||
return { | ||
newText: oneImportPerLine ? `, ${context.newLineCharacter}${name}` : `,${name}`, | ||
span: { start: insertPoint, length: 0 } | ||
}; | ||
} | ||
|
||
} | ||
|
||
return createCodeAction( | ||
Diagnostics.Add_0_to_existing_import_declaration_from_1, | ||
[name, moduleSpecifier], | ||
`{ ${name} } from `, | ||
{ start: declaration.moduleSpecifier.getStart(), length: 0 }, | ||
sourceFile.fileName | ||
); | ||
} | ||
|
||
function getCodeActionForNewImport(): CodeAction { | ||
// Try to insert after any existing imports | ||
let lastDeclaration: ImportDeclaration; | ||
let lastEnd: number; | ||
for (const declaration of imports) { | ||
const end = declaration.getEnd(); | ||
if (!lastDeclaration || end > lastEnd) { | ||
lastDeclaration = declaration; | ||
lastEnd = end; | ||
} | ||
} | ||
|
||
let newText = `import { ${name} } from "${moduleName}";`; | ||
newText = lastDeclaration ? context.newLineCharacter + newText : newText + context.newLineCharacter; | ||
|
||
return createCodeAction( | ||
Diagnostics.Import_0_from_1, | ||
[name, `"${moduleName}"`], | ||
newText, | ||
{ | ||
start: lastDeclaration ? lastEnd : sourceFile.getStart(), | ||
length: 0 | ||
}, | ||
sourceFile.fileName | ||
); | ||
} | ||
} | ||
|
||
function removeQuotes(name: string): string { | ||
if ((startsWith(name, "\"") && endsWith(name, "\"")) || (startsWith(name, "'") && endsWith(name, "'"))) { | ||
return name.substr(1, name.length - 2); | ||
} | ||
else { | ||
return name; | ||
} | ||
} | ||
|
||
function compareModuleSpecifiers(a: string, b: string, basePath: string, useCaseSensitiveFileNames: boolean): Comparison { | ||
// Paths to modules can be relative or absolute and may optionally include the file | ||
// extension of the module | ||
a = removeFileExtension(a); | ||
b = removeFileExtension(b); | ||
return comparePaths(a, b, basePath, !useCaseSensitiveFileNames); | ||
} | ||
|
||
function createCodeAction(description: DiagnosticMessage, diagnosticArgs: string[], newText: string, span: TextSpan, fileName: string): CodeAction { | ||
return { | ||
description: formatMessage.apply(undefined, [undefined, description].concat(<any[]>diagnosticArgs)), | ||
changes: [{ | ||
fileName, | ||
textChanges: [{ | ||
newText, | ||
span | ||
}] | ||
}] | ||
}; | ||
} | ||
} | ||
}); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
/// <reference path="fourslash.ts" /> | ||
|
||
//// import [|{ v1 }|] from "./module"; | ||
//// f1/*0*/(); | ||
|
||
// @Filename: module.ts | ||
//// export function f1() {} | ||
//// export var v1 = 5; | ||
|
||
verify.codeFixAtPosition(`{ v1, f1 }`); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
/// <reference path="fourslash.ts" /> | ||
|
||
//// import d, [|{ v1 }|] from "./module"; | ||
//// f1/*0*/(); | ||
|
||
// @Filename: module.ts | ||
//// export function f1() {} | ||
//// export var v1 = 5; | ||
//// export default var d1 = 6; | ||
|
||
verify.codeFixAtPosition(`{ v1, f1 }`); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
/// <reference path="fourslash.ts" /> | ||
|
||
//// import [|{ | ||
//// v1, | ||
//// v2 | ||
//// }|] from "./module"; | ||
//// f1/*0*/(); | ||
|
||
// @Filename: module.ts | ||
//// export function f1() {} | ||
//// export var v1 = 5; | ||
//// export var v2 = 5; | ||
//// export var v3 = 5; | ||
|
||
verify.codeFixAtPosition( | ||
`{ | ||
v1, | ||
v2, | ||
f1 | ||
}` | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use soruceFile.resolvedModules instead of rewalking the tree.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The closest thing I can find is the
sourceFIle.Imports
, however I don't know how to get the import declarations from the module names. Maybe I can callgetSymbolAtLocation
? Or do you know there are better ways.