For import completion of default import, convert module name to identifier (#19875)

* For import completion of default import, convert module name to identifier

* Suggestions from code review
This commit is contained in:
Andy
2017-11-09 13:13:23 -08:00
committed by GitHub
parent 90ae9ffe6e
commit 65a191fa2b
8 changed files with 108 additions and 18 deletions

View File

@@ -699,9 +699,10 @@ namespace ts.codefix {
const defaultExport = checker.tryGetMemberInModuleExports("default", moduleSymbol);
if (defaultExport) {
const localSymbol = getLocalSymbolForExportDefault(defaultExport);
if (localSymbol && localSymbol.escapedName === symbolName && checkSymbolHasMeaning(localSymbol, currentTokenMeaning)) {
if ((localSymbol && localSymbol.escapedName === symbolName || moduleSymbolToValidIdentifier(moduleSymbol, context.compilerOptions.target) === symbolName)
&& checkSymbolHasMeaning(localSymbol || defaultExport, currentTokenMeaning)) {
// check if this symbol is already used
const symbolId = getUniqueSymbolId(localSymbol, checker);
const symbolId = getUniqueSymbolId(localSymbol || defaultExport, checker);
symbolIdActionMap.addActions(symbolId, getCodeActionForImport(moduleSymbol, { ...context, kind: ImportKind.Default }));
}
}
@@ -731,4 +732,35 @@ namespace ts.codefix {
}
}
}
export function moduleSymbolToValidIdentifier(moduleSymbol: Symbol, target: ScriptTarget): string {
return moduleSpecifierToValidIdentifier(removeFileExtension(getBaseFileName(moduleSymbol.name)), target);
}
function moduleSpecifierToValidIdentifier(moduleSpecifier: string, target: ScriptTarget): string {
let res = "";
let lastCharWasValid = true;
const firstCharCode = moduleSpecifier.charCodeAt(0);
if (isIdentifierStart(firstCharCode, target)) {
res += String.fromCharCode(firstCharCode);
}
else {
lastCharWasValid = false;
}
for (let i = 1; i < moduleSpecifier.length; i++) {
const ch = moduleSpecifier.charCodeAt(i);
const isValid = isIdentifierPart(ch, target);
if (isValid) {
let char = String.fromCharCode(ch);
if (!lastCharWasValid) {
char = char.toUpperCase();
}
res += char;
}
lastCharWasValid = isValid;
}
// Need `|| "_"` to ensure result isn't empty.
const token = stringToToken(res);
return token === undefined || !isNonContextualKeyword(token) ? res || "_" : `_${res}`;
}
}

View File

@@ -39,7 +39,7 @@ namespace ts.Completions {
return getStringLiteralCompletionEntries(sourceFile, position, typeChecker, compilerOptions, host, log);
}
const completionData = getCompletionData(typeChecker, log, sourceFile, position, allSourceFiles, options);
const completionData = getCompletionData(typeChecker, log, sourceFile, position, allSourceFiles, options, compilerOptions.target);
if (!completionData) {
return undefined;
}
@@ -136,12 +136,12 @@ namespace ts.Completions {
typeChecker: TypeChecker,
target: ScriptTarget,
allowStringLiteral: boolean,
origin: SymbolOriginInfo,
origin: SymbolOriginInfo | undefined,
): CompletionEntry | undefined {
// Try to get a valid display name for this symbol, if we could not find one, then ignore it.
// We would like to only show things that can be added after a dot, so for instance numeric properties can
// not be accessed with a dot (a.1 <- invalid)
const displayName = getCompletionEntryDisplayNameForSymbol(symbol, target, performCharacterChecks, allowStringLiteral);
const displayName = getCompletionEntryDisplayNameForSymbol(symbol, target, performCharacterChecks, allowStringLiteral, origin);
if (!displayName) {
return undefined;
}
@@ -381,7 +381,7 @@ namespace ts.Completions {
{ name, source }: CompletionEntryIdentifier,
allSourceFiles: ReadonlyArray<SourceFile>,
): { type: "symbol", symbol: Symbol, location: Node, symbolToOriginInfoMap: SymbolOriginInfoMap } | { type: "request", request: Request } | { type: "none" } {
const completionData = getCompletionData(typeChecker, log, sourceFile, position, allSourceFiles, { includeExternalModuleExports: true });
const completionData = getCompletionData(typeChecker, log, sourceFile, position, allSourceFiles, { includeExternalModuleExports: true }, compilerOptions.target);
if (!completionData) {
return { type: "none" };
}
@@ -395,12 +395,18 @@ namespace ts.Completions {
// We don't need to perform character checks here because we're only comparing the
// name against 'entryName' (which is known to be good), not building a new
// completion entry.
const symbol = find(symbols, s =>
getCompletionEntryDisplayNameForSymbol(s, compilerOptions.target, /*performCharacterChecks*/ false, allowStringLiteral) === name
&& getSourceFromOrigin(symbolToOriginInfoMap[getSymbolId(s)]) === source);
const symbol = find(symbols, s => {
const origin = symbolToOriginInfoMap[getSymbolId(s)];
return getCompletionEntryDisplayNameForSymbol(s, compilerOptions.target, /*performCharacterChecks*/ false, allowStringLiteral, origin) === name
&& getSourceFromOrigin(origin) === source;
});
return symbol ? { type: "symbol", symbol, location, symbolToOriginInfoMap } : { type: "none" };
}
function getSymbolName(symbol: Symbol, origin: SymbolOriginInfo | undefined, target: ScriptTarget): string {
return origin && origin.isDefaultExport && symbol.name === "default" ? codefix.moduleSymbolToValidIdentifier(origin.moduleSymbol, target) : symbol.name;
}
export interface CompletionEntryIdentifier {
name: string;
source?: string;
@@ -482,7 +488,7 @@ namespace ts.Completions {
compilerOptions,
sourceFile,
formatContext,
symbolName: symbol.name,
symbolName: getSymbolName(symbol, symbolOriginInfo, compilerOptions.target),
getCanonicalFileName: createGetCanonicalFileName(host.useCaseSensitiveFileNames ? host.useCaseSensitiveFileNames() : false),
symbolToken: undefined,
kind: isDefaultExport ? codefix.ImportKind.Default : codefix.ImportKind.Named,
@@ -523,6 +529,7 @@ namespace ts.Completions {
position: number,
allSourceFiles: ReadonlyArray<SourceFile>,
options: GetCompletionsAtPositionOptions,
target: ScriptTarget,
): CompletionData | undefined {
const isJavaScriptFile = isSourceFileJavaScript(sourceFile);
@@ -921,7 +928,7 @@ namespace ts.Completions {
symbols = typeChecker.getSymbolsInScope(scopeNode, symbolMeanings);
if (options.includeExternalModuleExports) {
getSymbolsFromOtherSourceFileExports(symbols, previousToken && isIdentifier(previousToken) ? previousToken.text : "");
getSymbolsFromOtherSourceFileExports(symbols, previousToken && isIdentifier(previousToken) ? previousToken.text : "", target);
}
filterGlobalCompletion(symbols);
@@ -1003,7 +1010,7 @@ namespace ts.Completions {
}
}
function getSymbolsFromOtherSourceFileExports(symbols: Symbol[], tokenText: string): void {
function getSymbolsFromOtherSourceFileExports(symbols: Symbol[], tokenText: string, target: ScriptTarget): void {
const tokenTextLowerCase = tokenText.toLowerCase();
codefix.forEachExternalModule(typeChecker, allSourceFiles, moduleSymbol => {
@@ -1020,6 +1027,9 @@ namespace ts.Completions {
symbol = localSymbol;
name = localSymbol.name;
}
else {
name = codefix.moduleSymbolToValidIdentifier(moduleSymbol, target);
}
}
if (symbol.declarations && symbol.declarations.some(d => isExportSpecifier(d) && !!d.parent.parent.moduleSpecifier)) {
@@ -1847,8 +1857,8 @@ namespace ts.Completions {
*
* @return undefined if the name is of external module
*/
function getCompletionEntryDisplayNameForSymbol(symbol: Symbol, target: ScriptTarget, performCharacterChecks: boolean, allowStringLiteral: boolean): string | undefined {
const name = symbol.name;
function getCompletionEntryDisplayNameForSymbol(symbol: Symbol, target: ScriptTarget, performCharacterChecks: boolean, allowStringLiteral: boolean, origin: SymbolOriginInfo | undefined): string | undefined {
const name = getSymbolName(symbol, origin, target);
if (!name) return undefined;
// First check of the displayName is not external module; if it is an external module, it is not valid entry