Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/compiler/scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ namespace ts {
}

/* @internal */
export function stringToToken(s: string): SyntaxKind {
export function stringToToken(s: string): SyntaxKind | undefined {
return textToToken.get(s);
}

Expand Down
6 changes: 4 additions & 2 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ namespace ts {
UndefinedKeyword,
FromKeyword,
GlobalKeyword,
OfKeyword, // LastKeyword and LastToken
OfKeyword, // LastKeyword and LastToken and LastContextualKeyword

// Parse tree nodes

Expand Down Expand Up @@ -415,7 +415,9 @@ namespace ts {
FirstJSDocNode = JSDocTypeExpression,
LastJSDocNode = JSDocTypeLiteral,
FirstJSDocTagNode = JSDocTag,
LastJSDocTagNode = JSDocTypeLiteral
LastJSDocTagNode = JSDocTypeLiteral,
/* @internal */ FirstContextualKeyword = AbstractKeyword,
/* @internal */ LastContextualKeyword = OfKeyword,
}

export const enum NodeFlags {
Expand Down
8 changes: 8 additions & 0 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1903,6 +1903,14 @@ namespace ts {
return SyntaxKind.FirstKeyword <= token && token <= SyntaxKind.LastKeyword;
}

export function isContextualKeyword(token: SyntaxKind): boolean {
return SyntaxKind.FirstContextualKeyword <= token && token <= SyntaxKind.LastContextualKeyword;
}

export function isNonContextualKeyword(token: SyntaxKind): boolean {
return isKeyword(token) && !isContextualKeyword(token);
}

export function isTrivia(token: SyntaxKind) {
return SyntaxKind.FirstTriviaToken <= token && token <= SyntaxKind.LastTriviaToken;
}
Expand Down
2 changes: 1 addition & 1 deletion src/harness/fourslash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3021,7 +3021,7 @@ Actual: ${stringify(fullActual)}`);
}
}

const itemsString = items.map(item => stringify({ name: item.name, kind: item.kind })).join(",\n");
const itemsString = items.map(item => stringify({ name: item.name, source: item.source, kind: item.kind })).join(",\n");

this.raiseError(`Expected "${stringify({ entryId, text, documentation, kind })}" to be in list [${itemsString}]`);
}
Expand Down
36 changes: 34 additions & 2 deletions src/services/codefixes/importFixes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -666,9 +666,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 }));
}
}
Expand Down Expand Up @@ -698,4 +699,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}`;
}
}
34 changes: 22 additions & 12 deletions src/services/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,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;
}
Expand Down Expand Up @@ -135,12 +135,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;
}
Expand Down Expand Up @@ -363,7 +363,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" };
}
Expand All @@ -377,12 +377,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;
Expand Down Expand Up @@ -464,7 +470,7 @@ namespace ts.Completions {
compilerOptions,
sourceFile,
rulesProvider,
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,
Expand Down Expand Up @@ -505,6 +511,7 @@ namespace ts.Completions {
position: number,
allSourceFiles: ReadonlyArray<SourceFile>,
options: GetCompletionsAtPositionOptions,
target: ScriptTarget,
): CompletionData | undefined {
const isJavaScriptFile = isSourceFileJavaScript(sourceFile);

Expand Down Expand Up @@ -903,7 +910,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);

Expand Down Expand Up @@ -985,7 +992,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 => {
Expand All @@ -1002,6 +1009,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)) {
Expand Down Expand Up @@ -1829,8 +1839,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
Expand Down
26 changes: 26 additions & 0 deletions tests/cases/fourslash/completionsImport_default_anonymous.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/// <reference path="fourslash.ts" />

// Use `/src` to test that directory names are not included in conversion from module path to identifier.

// @Filename: /src/foo-bar.ts
////export default 0;

// @Filename: /src/b.ts
////def/*0*/
////fooB/*1*/

goTo.marker("0");
verify.not.completionListContains({ name: "default", source: "/src/foo-bar" }, undefined, undefined, undefined, undefined, undefined, { includeExternalModuleExports: true });

goTo.marker("1");
verify.completionListContains({ name: "fooBar", source: "/src/foo-bar" }, "(property) default: 0", "", "property", /*spanIndex*/ undefined, /*hasAction*/ true, { includeExternalModuleExports: true });
verify.applyCodeActionFromCompletion("1", {
name: "fooBar",
source: "/src/foo-bar",
description: `Import 'fooBar' from "./foo-bar".`,
// TODO: GH#18445
newFileContent: `import fooBar from "./foo-bar";\r
\r
def
fooB`,
});
12 changes: 12 additions & 0 deletions tests/cases/fourslash/importNameCodeFixDefaultExport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/// <reference path="fourslash.ts" />

// @Filename: /foo-bar.ts
////export default 0;

// @Filename: /b.ts
////[|foo/**/Bar|]

goTo.file("/b.ts");
verify.importFixAtPosition([`import fooBar from "./foo-bar";

fooBar`]);