Add completions from literal contextual types (#24674)

* Add completions from literal contextual types

* Remove getTypesOfUnion

* undo baseline changes
This commit is contained in:
Andy 2018-06-07 15:03:19 -07:00 committed by GitHub
parent 604bebab86
commit 33d0893259
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 60 additions and 23 deletions

View File

@ -101,7 +101,7 @@ namespace ts.Completions {
}
function completionInfoFromData(sourceFile: SourceFile, typeChecker: TypeChecker, compilerOptions: CompilerOptions, log: Log, completionData: CompletionData, preferences: UserPreferences): CompletionInfo | undefined {
const { symbols, completionKind, isInSnippetScope, isNewIdentifierLocation, location, propertyAccessToConvert, keywordFilters, symbolToOriginInfoMap, recommendedCompletion, isJsxInitializer } = completionData;
const { symbols, completionKind, isInSnippetScope, isNewIdentifierLocation, location, propertyAccessToConvert, keywordFilters, literals, symbolToOriginInfoMap, recommendedCompletion, isJsxInitializer } = completionData;
if (sourceFile.languageVariant === LanguageVariant.JSX && location && location.parent && isJsxClosingElement(location.parent)) {
// In the TypeScript JSX element, if such element is not defined. When users query for completion at closing tag,
@ -143,6 +143,10 @@ namespace ts.Completions {
addRange(entries, getKeywordCompletions(keywordFilters));
}
for (const literal of literals) {
entries.push(createCompletionEntryForLiteral(literal));
}
return { isGlobalCompletion: isInSnippetScope, isMemberCompletion, isNewIdentifierLocation, entries };
}
@ -184,6 +188,11 @@ namespace ts.Completions {
});
}
const completionNameForLiteral = JSON.stringify;
function createCompletionEntryForLiteral(literal: string | number): CompletionEntry {
return { name: completionNameForLiteral(literal), kind: ScriptElementKind.string, kindModifiers: ScriptElementKindModifier.none, sortText: "0" };
}
function createCompletionEntry(
symbol: Symbol,
location: Node | undefined,
@ -372,7 +381,7 @@ namespace ts.Completions {
case SyntaxKind.LiteralType:
switch (node.parent.parent.kind) {
case SyntaxKind.TypeReference:
return { kind: StringLiteralCompletionKind.Types, types: getStringLiteralTypes(typeChecker.getTypeArgumentConstraint(node.parent as LiteralTypeNode), typeChecker), isNewIdentifier: false };
return { kind: StringLiteralCompletionKind.Types, types: getStringLiteralTypes(typeChecker.getTypeArgumentConstraint(node.parent as LiteralTypeNode)), isNewIdentifier: false };
case SyntaxKind.IndexedAccessType:
// Get all apparent property names
// i.e. interface Foo {
@ -448,7 +457,7 @@ namespace ts.Completions {
function fromContextualType(): StringLiteralCompletion {
// Get completion for string literal from string literal type
// i.e. var x: "hi" | "hello" = "/*completion position*/"
return { kind: StringLiteralCompletionKind.Types, types: getStringLiteralTypes(getContextualTypeFromParent(node, typeChecker), typeChecker), isNewIdentifier: false };
return { kind: StringLiteralCompletionKind.Types, types: getStringLiteralTypes(getContextualTypeFromParent(node, typeChecker)), isNewIdentifier: false };
}
}
@ -462,7 +471,7 @@ namespace ts.Completions {
if (!candidate.hasRestParameter && argumentInfo.argumentCount > candidate.parameters.length) return;
const type = checker.getParameterType(candidate, argumentInfo.argumentIndex);
isNewIdentifier = isNewIdentifier || !!(type.flags & TypeFlags.String);
return getStringLiteralTypes(type, checker, uniques);
return getStringLiteralTypes(type, uniques);
});
return { kind: StringLiteralCompletionKind.Types, types, isNewIdentifier };
@ -472,11 +481,11 @@ namespace ts.Completions {
return type && { kind: StringLiteralCompletionKind.Properties, symbols: type.getApparentProperties(), hasIndexSignature: hasIndexSignature(type) };
}
function getStringLiteralTypes(type: Type | undefined, typeChecker: TypeChecker, uniques = createMap<true>()): ReadonlyArray<StringLiteralType> {
function getStringLiteralTypes(type: Type | undefined, uniques = createMap<true>()): ReadonlyArray<StringLiteralType> {
if (!type) return emptyArray;
type = skipConstraint(type);
return type.isUnion()
? flatMap(type.types, t => getStringLiteralTypes(t, typeChecker, uniques))
? flatMap(type.types, t => getStringLiteralTypes(t, uniques))
: type.isStringLiteral() && !(type.flags & TypeFlags.EnumLiteral) && addToSeen(uniques, type.value)
? [type]
: emptyArray;
@ -491,7 +500,7 @@ namespace ts.Completions {
readonly isJsxInitializer: IsJsxInitializer;
}
function getSymbolCompletionFromEntryId(program: Program, log: Log, sourceFile: SourceFile, position: number, entryId: CompletionEntryIdentifier,
): SymbolCompletion | { type: "request", request: Request } | { type: "none" } {
): SymbolCompletion | { type: "request", request: Request } | { type: "literal", literal: string | number } | { type: "none" } {
const compilerOptions = program.getCompilerOptions();
const completionData = getCompletionData(program, log, sourceFile, isUncheckedFile(sourceFile, compilerOptions), position, { includeCompletionsForModuleExports: true, includeCompletionsWithInsertText: true }, entryId);
if (!completionData) {
@ -501,7 +510,10 @@ namespace ts.Completions {
return { type: "request", request: completionData };
}
const { symbols, location, completionKind, symbolToOriginInfoMap, previousToken, isJsxInitializer } = completionData;
const { symbols, literals, location, completionKind, symbolToOriginInfoMap, previousToken, isJsxInitializer } = completionData;
const literal = find(literals, l => completionNameForLiteral(l) === entryId.name);
if (literal !== undefined) return { type: "literal", literal };
// Find the symbol with the matching entry name.
// We don't need to perform character checks here because we're only comparing the
@ -574,12 +586,22 @@ namespace ts.Completions {
const { codeActions, sourceDisplay } = getCompletionEntryCodeActionsAndSourceDisplay(symbolToOriginInfoMap, symbol, program, typeChecker, host, compilerOptions, sourceFile, previousToken, formatContext, getCanonicalFileName, program.getSourceFiles(), preferences);
return createCompletionDetailsForSymbol(symbol, typeChecker, sourceFile, location!, cancellationToken, codeActions, sourceDisplay); // TODO: GH#18217
}
case "literal": {
const { literal } = symbolCompletion;
return createSimpleDetails(completionNameForLiteral(literal), ScriptElementKind.string, typeof literal === "string" ? SymbolDisplayPartKind.stringLiteral : SymbolDisplayPartKind.numericLiteral);
}
case "none":
// Didn't find a symbol with this name. See if we can find a keyword instead.
return allKeywordsCompletions().some(c => c.name === name) ? createCompletionDetails(name, ScriptElementKindModifier.none, ScriptElementKind.keyword, [displayPart(name, SymbolDisplayPartKind.keyword)]) : undefined;
return allKeywordsCompletions().some(c => c.name === name) ? createSimpleDetails(name, ScriptElementKind.keyword, SymbolDisplayPartKind.keyword) : undefined;
default:
Debug.assertNever(symbolCompletion);
}
}
function createSimpleDetails(name: string, kind: ScriptElementKind, kind2: SymbolDisplayPartKind): CompletionEntryDetails {
return createCompletionDetails(name, ScriptElementKindModifier.none, kind, [displayPart(name, kind2)]);
}
function createCompletionDetailsForSymbol(symbol: Symbol, checker: TypeChecker, sourceFile: SourceFile, location: Node, cancellationToken: CancellationToken, codeActions?: CodeAction[], sourceDisplay?: SymbolDisplayPart[]): CompletionEntryDetails {
const { displayParts, documentation, symbolKind, tags } =
checker.runWithCancellationToken(cancellationToken, checker =>
@ -669,6 +691,7 @@ namespace ts.Completions {
readonly isNewIdentifierLocation: boolean;
readonly location: Node | undefined;
readonly keywordFilters: KeywordCompletionFilters;
readonly literals: ReadonlyArray<string | number>;
readonly symbolToOriginInfoMap: SymbolOriginInfoMap;
readonly recommendedCompletion: Symbol | undefined;
readonly previousToken: Node | undefined;
@ -685,23 +708,22 @@ namespace ts.Completions {
None,
}
function getRecommendedCompletion(currentToken: Node, position: number, sourceFile: SourceFile, checker: TypeChecker): Symbol | undefined {
const contextualType = getContextualType(currentToken, position, sourceFile, checker);
function getRecommendedCompletion(previousToken: Node, contextualType: Type, checker: TypeChecker): Symbol | undefined {
// For a union, return the first one with a recommended completion.
return firstDefined(contextualType && (contextualType.isUnion() ? contextualType.types : [contextualType]), type => {
const symbol = type && type.symbol;
// Don't include make a recommended completion for an abstract class
return symbol && (symbol.flags & (SymbolFlags.EnumMember | SymbolFlags.Enum | SymbolFlags.Class) && !isAbstractConstructorSymbol(symbol))
? getFirstSymbolInChain(symbol, currentToken, checker)
? getFirstSymbolInChain(symbol, previousToken, checker)
: undefined;
});
}
function getContextualType(currentToken: Node, position: number, sourceFile: SourceFile, checker: TypeChecker): Type | undefined {
const { parent } = currentToken;
switch (currentToken.kind) {
function getContextualType(previousToken: Node, position: number, sourceFile: SourceFile, checker: TypeChecker): Type | undefined {
const { parent } = previousToken;
switch (previousToken.kind) {
case SyntaxKind.Identifier:
return getContextualTypeFromParent(currentToken as Identifier, checker);
return getContextualTypeFromParent(previousToken as Identifier, checker);
case SyntaxKind.EqualsToken:
switch (parent.kind) {
case SyntaxKind.VariableDeclaration:
@ -720,14 +742,14 @@ namespace ts.Completions {
case SyntaxKind.OpenBraceToken:
return isJsxExpression(parent) && parent.parent.kind !== SyntaxKind.JsxElement ? checker.getContextualTypeForJsxAttribute(parent.parent) : undefined;
default:
const argInfo = SignatureHelp.getArgumentInfoForCompletions(currentToken, position, sourceFile);
const argInfo = SignatureHelp.getArgumentInfoForCompletions(previousToken, position, sourceFile);
return argInfo
// At `,`, treat this as the next argument after the comma.
? checker.getContextualTypeForArgumentAtIndex(argInfo.invocation, argInfo.argumentIndex + (currentToken.kind === SyntaxKind.CommaToken ? 1 : 0))
: isEqualityOperatorKind(currentToken.kind) && isBinaryExpression(parent) && isEqualityOperatorKind(parent.operatorToken.kind)
? checker.getContextualTypeForArgumentAtIndex(argInfo.invocation, argInfo.argumentIndex + (previousToken.kind === SyntaxKind.CommaToken ? 1 : 0))
: isEqualityOperatorKind(previousToken.kind) && isBinaryExpression(parent) && isEqualityOperatorKind(parent.operatorToken.kind)
// completion at `x ===/**/` should be for the right side
? checker.getTypeAtLocation(parent.left)
: checker.getContextualType(currentToken as Expression);
: checker.getContextualType(previousToken as Expression);
}
}
@ -1005,8 +1027,11 @@ namespace ts.Completions {
log("getCompletionData: Semantic work: " + (timestamp() - semanticStart));
const recommendedCompletion = previousToken && getRecommendedCompletion(previousToken, position, sourceFile, typeChecker);
return { kind: CompletionDataKind.Data, symbols, completionKind, isInSnippetScope, propertyAccessToConvert, isNewIdentifierLocation, location, keywordFilters, symbolToOriginInfoMap, recommendedCompletion, previousToken, isJsxInitializer };
const contextualType = previousToken && getContextualType(previousToken, position, sourceFile, typeChecker);
const literals = mapDefined(contextualType && (contextualType.isUnion() ? contextualType.types : [contextualType]), t => t.isLiteral() ? t.value : undefined);
const recommendedCompletion = previousToken && contextualType && getRecommendedCompletion(previousToken, contextualType, typeChecker);
return { kind: CompletionDataKind.Data, symbols, completionKind, isInSnippetScope, propertyAccessToConvert, isNewIdentifierLocation, location, keywordFilters, literals, symbolToOriginInfoMap, recommendedCompletion, previousToken, isJsxInitializer };
type JSDocTagWithTypeExpression = JSDocParameterTag | JSDocPropertyTag | JSDocReturnTag | JSDocTypeTag | JSDocTypedefTag;

View File

@ -426,7 +426,7 @@ namespace ts {
return !!(this.flags & TypeFlags.UnionOrIntersection);
}
isLiteral(): this is LiteralType {
return !!(this.flags & TypeFlags.Literal);
return !!(this.flags & TypeFlags.StringOrNumberLiteral);
}
isStringLiteral(): this is StringLiteralType {
return !!(this.flags & TypeFlags.StringLiteral);

View File

@ -0,0 +1,12 @@
/// <reference path="fourslash.ts" />
////const x: 0 | "one" = /**/;
verify.completions({
marker: "",
includes: [
{ name: "0", kind: "string", text: "0" },
{ name: '"one"', kind: "string", text: '"one"' },
],
isNewIdentifierLocation: true,
});