Add support for Optional Chaining (#33294)

* Add support for Optional Chaining

* Add grammar error for invalid tagged template, more tests

* Prototype

* PR feedback

* Add errors for invalid assignments and a trailing '?.'

* Add additional signature help test, fix lint warnings

* Fix to insert text for completions

* Add initial control-flow analysis for optional chains

* PR Feedback and more tests

* Update to control flow

* Remove mangled smart quotes in comments

* Fix lint, PR feedback

* Updates to control flow

* Switch to FlowCondition for CFA of optional chains

* Fix ?. insertion for completions on type variables

* Accept API baseline change

* Clean up types

* improve control-flow debug output

* Revert Debug.formatControlFlowGraph helper
This commit is contained in:
Ron Buckton
2019-09-30 12:33:28 -07:00
committed by GitHub
parent 7ce793c5b8
commit fcd9334f57
76 changed files with 6422 additions and 885 deletions

View File

@@ -11,28 +11,52 @@ namespace ts.Completions {
}
export type Log = (message: string) => void;
const enum SymbolOriginInfoKind { ThisType, SymbolMemberNoExport, SymbolMemberExport, Export, Promise }
type SymbolOriginInfo = { kind: SymbolOriginInfoKind.ThisType } | { kind: SymbolOriginInfoKind.Promise } | { kind: SymbolOriginInfoKind.SymbolMemberNoExport } | SymbolOriginInfoExport;
interface SymbolOriginInfoExport {
kind: SymbolOriginInfoKind.SymbolMemberExport | SymbolOriginInfoKind.Export;
const enum SymbolOriginInfoKind {
ThisType = 1 << 0,
SymbolMember = 1 << 1,
Export = 1 << 2,
Promise = 1 << 3,
Nullable = 1 << 4,
SymbolMemberNoExport = SymbolMember,
SymbolMemberExport = SymbolMember | Export,
}
interface SymbolOriginInfo {
kind: SymbolOriginInfoKind;
}
interface SymbolOriginInfoExport extends SymbolOriginInfo {
kind: SymbolOriginInfoKind;
moduleSymbol: Symbol;
isDefaultExport: boolean;
}
function originIsThisType(origin: SymbolOriginInfo): boolean {
return !!(origin.kind & SymbolOriginInfoKind.ThisType);
}
function originIsSymbolMember(origin: SymbolOriginInfo): boolean {
return origin.kind === SymbolOriginInfoKind.SymbolMemberExport || origin.kind === SymbolOriginInfoKind.SymbolMemberNoExport;
return !!(origin.kind & SymbolOriginInfoKind.SymbolMember);
}
function originIsExport(origin: SymbolOriginInfo): origin is SymbolOriginInfoExport {
return origin.kind === SymbolOriginInfoKind.SymbolMemberExport || origin.kind === SymbolOriginInfoKind.Export;
return !!(origin.kind & SymbolOriginInfoKind.Export);
}
function originIsPromise(origin: SymbolOriginInfo): boolean {
return origin.kind === SymbolOriginInfoKind.Promise;
return !!(origin.kind & SymbolOriginInfoKind.Promise);
}
function originIsNullableMember(origin: SymbolOriginInfo): boolean {
return !!(origin.kind & SymbolOriginInfoKind.Nullable);
}
/**
* Map from symbol id -> SymbolOriginInfo.
* Only populated for symbols that come from other modules.
*/
type SymbolOriginInfoMap = (SymbolOriginInfo | undefined)[];
type SymbolOriginInfoMap = (SymbolOriginInfo | SymbolOriginInfoExport | undefined)[];
type SymbolSortTextMap = (SortText | undefined)[];
@@ -314,14 +338,26 @@ namespace ts.Completions {
): CompletionEntry | undefined {
let insertText: string | undefined;
let replacementSpan: TextSpan | undefined;
if (origin && origin.kind === SymbolOriginInfoKind.ThisType) {
insertText = needsConvertPropertyAccess ? `this[${quote(name, preferences)}]` : `this.${name}`;
const insertQuestionDot = origin && originIsNullableMember(origin);
const useBraces = origin && originIsSymbolMember(origin) || needsConvertPropertyAccess;
if (origin && originIsThisType(origin)) {
insertText = needsConvertPropertyAccess
? `this${insertQuestionDot ? "?." : ""}[${quote(name, preferences)}]`
: `this${insertQuestionDot ? "?." : "."}${name}`;
}
// We should only have needsConvertPropertyAccess if there's a property access to convert. But see #21790.
// Somehow there was a global with a non-identifier name. Hopefully someone will complain about getting a "foo bar" global completion and provide a repro.
else if ((origin && originIsSymbolMember(origin) || needsConvertPropertyAccess) && propertyAccessToConvert) {
insertText = needsConvertPropertyAccess ? `[${quote(name, preferences)}]` : `[${name}]`;
const dot = findChildOfKind(propertyAccessToConvert, SyntaxKind.DotToken, sourceFile)!;
else if ((useBraces || insertQuestionDot) && propertyAccessToConvert) {
insertText = useBraces ? needsConvertPropertyAccess ? `[${quote(name, preferences)}]` : `[${name}]` : name;
if (insertQuestionDot || propertyAccessToConvert.questionDotToken) {
insertText = `?.${insertText}`;
}
const dot = findChildOfKind(propertyAccessToConvert, SyntaxKind.DotToken, sourceFile) ||
findChildOfKind(propertyAccessToConvert, SyntaxKind.QuestionDotToken, sourceFile);
if (!dot) {
return undefined;
}
// If the text after the '.' starts with this name, write over it. Else, add new text.
const end = startsWith(name, propertyAccessToConvert.name.text) ? propertyAccessToConvert.name.end : dot.end;
replacementSpan = createTextSpanFromBounds(dot.getStart(sourceFile), end);
@@ -337,7 +373,7 @@ namespace ts.Completions {
if (origin && originIsPromise(origin) && propertyAccessToConvert) {
if (insertText === undefined) insertText = name;
const awaitText = `(await ${propertyAccessToConvert.expression.getText()})`;
insertText = needsConvertPropertyAccess ? `${awaitText}${insertText}` : `${awaitText}.${insertText}`;
insertText = needsConvertPropertyAccess ? `${awaitText}${insertText}` : `${awaitText}${insertQuestionDot ? "?." : "."}${insertText}`;
replacementSpan = createTextSpanFromBounds(propertyAccessToConvert.getStart(sourceFile), propertyAccessToConvert.end);
}
@@ -846,6 +882,7 @@ namespace ts.Completions {
let node = currentToken;
let propertyAccessToConvert: PropertyAccessExpression | undefined;
let isRightOfDot = false;
let isRightOfQuestionDot = false;
let isRightOfOpenTag = false;
let isStartingCloseTag = false;
let isJsxInitializer: IsJsxInitializer = false;
@@ -859,8 +896,9 @@ namespace ts.Completions {
}
let parent = contextToken.parent;
if (contextToken.kind === SyntaxKind.DotToken) {
isRightOfDot = true;
if (contextToken.kind === SyntaxKind.DotToken || contextToken.kind === SyntaxKind.QuestionDotToken) {
isRightOfDot = contextToken.kind === SyntaxKind.DotToken;
isRightOfQuestionDot = contextToken.kind === SyntaxKind.QuestionDotToken;
switch (parent.kind) {
case SyntaxKind.PropertyAccessExpression:
propertyAccessToConvert = parent as PropertyAccessExpression;
@@ -967,7 +1005,7 @@ namespace ts.Completions {
const symbolToSortTextMap: SymbolSortTextMap = [];
const importSuggestionsCache = host.getImportSuggestionsCache && host.getImportSuggestionsCache();
if (isRightOfDot) {
if (isRightOfDot || isRightOfQuestionDot) {
getTypeScriptMemberSymbols();
}
else if (isRightOfOpenTag) {
@@ -1074,7 +1112,13 @@ namespace ts.Completions {
if (!isTypeLocation &&
symbol.declarations &&
symbol.declarations.some(d => d.kind !== SyntaxKind.SourceFile && d.kind !== SyntaxKind.ModuleDeclaration && d.kind !== SyntaxKind.EnumDeclaration)) {
addTypeProperties(typeChecker.getTypeOfSymbolAtLocation(symbol, node), !!(node.flags & NodeFlags.AwaitContext));
let type = typeChecker.getTypeOfSymbolAtLocation(symbol, node).getNonOptionalType();
let insertQuestionDot = false;
if (type.isNullableType()) {
insertQuestionDot = isRightOfDot && !isRightOfQuestionDot;
type = type.getNonNullableType();
}
addTypeProperties(type, !!(node.flags & NodeFlags.AwaitContext), insertQuestionDot);
}
return;
@@ -1089,12 +1133,21 @@ namespace ts.Completions {
}
if (!isTypeLocation) {
addTypeProperties(typeChecker.getTypeAtLocation(node), !!(node.flags & NodeFlags.AwaitContext));
let type = typeChecker.getTypeAtLocation(node).getNonOptionalType();
let insertQuestionDot = false;
if (type.isNullableType()) {
insertQuestionDot = isRightOfDot && !isRightOfQuestionDot;
type = type.getNonNullableType();
}
addTypeProperties(type, !!(node.flags & NodeFlags.AwaitContext), insertQuestionDot);
}
}
function addTypeProperties(type: Type, insertAwait?: boolean): void {
function addTypeProperties(type: Type, insertAwait: boolean, insertQuestionDot: boolean): void {
isNewIdentifierLocation = !!type.getStringIndexType();
if (isRightOfQuestionDot && some(type.getCallSignatures())) {
isNewIdentifierLocation = true;
}
const propertyAccess = node.kind === SyntaxKind.ImportType ? <ImportTypeNode>node : <PropertyAccessExpression | QualifiedName>node.parent;
if (isUncheckedFile) {
@@ -1108,7 +1161,7 @@ namespace ts.Completions {
else {
for (const symbol of type.getApparentProperties()) {
if (typeChecker.isValidPropertyAccessForCompletions(propertyAccess, type, symbol)) {
addPropertySymbol(symbol);
addPropertySymbol(symbol, /*insertAwait*/ false, insertQuestionDot);
}
}
}
@@ -1118,14 +1171,14 @@ namespace ts.Completions {
if (promiseType) {
for (const symbol of promiseType.getApparentProperties()) {
if (typeChecker.isValidPropertyAccessForCompletions(propertyAccess, promiseType, symbol)) {
addPropertySymbol(symbol, /* insertAwait */ true);
addPropertySymbol(symbol, /* insertAwait */ true, insertQuestionDot);
}
}
}
}
}
function addPropertySymbol(symbol: Symbol, insertAwait?: boolean) {
function addPropertySymbol(symbol: Symbol, insertAwait: boolean, insertQuestionDot: boolean) {
// For a computed property with an accessible name like `Symbol.iterator`,
// we'll add a completion for the *name* `Symbol` instead of for the property.
// If this is e.g. [Symbol.iterator], add a completion for `Symbol`.
@@ -1139,23 +1192,34 @@ namespace ts.Completions {
symbols.push(firstAccessibleSymbol);
const moduleSymbol = firstAccessibleSymbol.parent;
symbolToOriginInfoMap[getSymbolId(firstAccessibleSymbol)] =
!moduleSymbol || !isExternalModuleSymbol(moduleSymbol) ? { kind: SymbolOriginInfoKind.SymbolMemberNoExport } : { kind: SymbolOriginInfoKind.SymbolMemberExport, moduleSymbol, isDefaultExport: false };
!moduleSymbol || !isExternalModuleSymbol(moduleSymbol)
? { kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.SymbolMemberNoExport) }
: { kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.SymbolMemberExport), moduleSymbol, isDefaultExport: false };
}
else if (preferences.includeCompletionsWithInsertText) {
addPromiseSymbolOriginInfo(symbol);
addSymbolOriginInfo(symbol);
symbols.push(symbol);
}
}
else {
addPromiseSymbolOriginInfo(symbol);
addSymbolOriginInfo(symbol);
symbols.push(symbol);
}
function addPromiseSymbolOriginInfo (symbol: Symbol) {
if (insertAwait && preferences.includeCompletionsWithInsertText && !symbolToOriginInfoMap[getSymbolId(symbol)]) {
symbolToOriginInfoMap[getSymbolId(symbol)] = { kind: SymbolOriginInfoKind.Promise };
function addSymbolOriginInfo(symbol: Symbol) {
if (preferences.includeCompletionsWithInsertText) {
if (insertAwait && !symbolToOriginInfoMap[getSymbolId(symbol)]) {
symbolToOriginInfoMap[getSymbolId(symbol)] = { kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.Promise) };
}
else if (insertQuestionDot) {
symbolToOriginInfoMap[getSymbolId(symbol)] = { kind: SymbolOriginInfoKind.Nullable };
}
}
}
function getNullableSymbolOriginInfoKind(kind: SymbolOriginInfoKind) {
return insertQuestionDot ? kind | SymbolOriginInfoKind.Nullable : kind;
}
}
/** Given 'a.b.c', returns 'a'. */

View File

@@ -409,9 +409,15 @@ namespace ts {
getBaseTypes(): BaseType[] | undefined {
return this.isClassOrInterface() ? this.checker.getBaseTypes(this) : undefined;
}
isNullableType(): boolean {
return this.checker.isNullableType(this);
}
getNonNullableType(): Type {
return this.checker.getNonNullableType(this);
}
getNonOptionalType(): Type {
return this.checker.getNonOptionalType(this);
}
getConstraint(): Type | undefined {
return this.checker.getBaseConstraintOfType(this);
}

View File

@@ -1061,6 +1061,8 @@ namespace ts.textChanges {
getColumn,
getIndent,
isAtStartOfLine,
hasTrailingComment: () => writer.hasTrailingComment(),
hasTrailingWhitespace: () => writer.hasTrailingWhitespace(),
clear
};
}

View File

@@ -52,6 +52,8 @@ namespace ts {
getNumberIndexType(): Type | undefined;
getBaseTypes(): BaseType[] | undefined;
getNonNullableType(): Type;
/*@internal*/ getNonOptionalType(): Type;
/*@internal*/ isNullableType(): boolean;
getConstraint(): Type | undefined;
getDefault(): Type | undefined;

View File

@@ -956,6 +956,12 @@ namespace ts {
}
}
export function removeOptionality(type: Type, isOptionalExpression: boolean, isOptionalChain: boolean) {
return isOptionalExpression ? type.getNonNullableType() :
isOptionalChain ? type.getNonOptionalType() :
type;
}
export function isPossiblyTypeArgumentPosition(token: Node, sourceFile: SourceFile, checker: TypeChecker): boolean {
const info = getPossibleTypeArgumentsInfo(token, sourceFile);
return info !== undefined && (isPartOfTypeNode(info.called) ||
@@ -964,7 +970,11 @@ namespace ts {
}
export function getPossibleGenericSignatures(called: Expression, typeArgumentCount: number, checker: TypeChecker): readonly Signature[] {
const type = checker.getTypeAtLocation(called);
let type = checker.getTypeAtLocation(called);
if (isOptionalChain(called.parent)) {
type = removeOptionality(type, !!called.parent.questionDotToken, /*isOptionalChain*/ true);
}
const signatures = isNewExpression(called.parent) ? type.getConstructSignatures() : type.getCallSignatures();
return signatures.filter(candidate => !!candidate.typeParameters && candidate.typeParameters.length >= typeArgumentCount);
}
@@ -993,6 +1003,9 @@ namespace ts {
case SyntaxKind.LessThanToken:
// Found the beginning of the generic argument expression
token = findPrecedingToken(token.getFullStart(), sourceFile);
if (token && token.kind === SyntaxKind.QuestionDotToken) {
token = findPrecedingToken(token.getFullStart(), sourceFile);
}
if (!token || !isIdentifier(token)) return undefined;
if (!remainingLessThanTokens) {
return isDeclarationName(token) ? undefined : { called: token, nTypeArguments };
@@ -1493,6 +1506,8 @@ namespace ts {
getColumn: () => 0,
getLine: () => 0,
isAtStartOfLine: () => false,
hasTrailingWhitespace: () => false,
hasTrailingComment: () => false,
rawWrite: notImplemented,
getIndent: () => indent,
increaseIndent: () => { indent++; },