Skip to content
Merged
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c95daab
Add support for Optional Chaining
rbuckton Sep 6, 2019
c2f53fc
Add grammar error for invalid tagged template, more tests
rbuckton Sep 7, 2019
0f5d5d6
Prototype
andrewbranch Sep 23, 2019
24de747
Merge branch 'master' into optionalChainingStage3
rbuckton Sep 23, 2019
7be47ab
PR feedback
rbuckton Sep 24, 2019
e073c05
Add errors for invalid assignments and a trailing '?.'
rbuckton Sep 24, 2019
72f44d9
Add additional signature help test, fix lint warnings
rbuckton Sep 24, 2019
488c9e6
Merge branch 'master' into optionalChainingStage3
rbuckton Sep 24, 2019
2ffd8e1
Merge branch 'enhancement/auto-insert-question-dot' of github.com:and…
rbuckton Sep 24, 2019
096bb49
Fix to insert text for completions
rbuckton Sep 24, 2019
1bf2d56
Add initial control-flow analysis for optional chains
rbuckton Sep 25, 2019
1d7446f
PR Feedback and more tests
rbuckton Sep 26, 2019
b282b62
Update to control flow
rbuckton Sep 26, 2019
be3e21f
Merge branch 'master' into optionalChainingStage3
rbuckton Sep 26, 2019
fd8c0d4
Remove mangled smart quotes in comments
rbuckton Sep 26, 2019
7c9ef50
Fix lint, PR feedback
rbuckton Sep 27, 2019
ad7c33c
Updates to control flow
rbuckton Sep 28, 2019
6b49a03
Switch to FlowCondition for CFA of optional chains
rbuckton Sep 29, 2019
aaa30f4
Fix ?. insertion for completions on type variables
rbuckton Sep 29, 2019
5ea7cb5
Accept API baseline change
rbuckton Sep 29, 2019
7463860
Clean up types
rbuckton Sep 29, 2019
0828674
improve control-flow debug output
rbuckton Sep 30, 2019
d408e81
Merge branch 'master' into optionalChainingStage3
rbuckton Sep 30, 2019
c2070be
Revert Debug.formatControlFlowGraph helper
rbuckton Sep 30, 2019
dfc798f
Merge branch 'master' into optionalChainingStage3
rbuckton Sep 30, 2019
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
Prev Previous commit
Next Next commit
Prototype
  • Loading branch information
andrewbranch committed Sep 23, 2019
commit 0f5d5d69f15a5b4c902abf521693c9e5fb1477ab
87 changes: 62 additions & 25 deletions src/services/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,42 @@ 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,
SymbolMemberNoExport = 1 << 1,
SymbolMemberExport = 1 << 2,
Nullable = 1 << 3,
Export = 1 << 4,
Promise = 1 << 4
}

interface SymbolOriginInfo {
kind: SymbolOriginInfoKind;
}

interface SymbolOriginInfoExport extends SymbolOriginInfo {
kind: SymbolOriginInfoKind;
moduleSymbol: Symbol;
isDefaultExport: boolean;
}
function originIsSymbolMember(origin: SymbolOriginInfo): boolean {
return origin.kind === SymbolOriginInfoKind.SymbolMemberExport || origin.kind === SymbolOriginInfoKind.SymbolMemberNoExport;
return !!(origin.kind & SymbolOriginInfoKind.SymbolMemberExport || origin.kind & SymbolOriginInfoKind.SymbolMemberNoExport);
}
function originIsExport(origin: SymbolOriginInfo): origin is SymbolOriginInfoExport {
return origin.kind === SymbolOriginInfoKind.SymbolMemberExport || origin.kind === SymbolOriginInfoKind.Export;
return !!(origin.kind & SymbolOriginInfoKind.SymbolMemberExport || 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)[];

Expand Down Expand Up @@ -249,14 +263,23 @@ 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 && origin.kind & SymbolOriginInfoKind.ThisType) {
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) {
insertText = `?.${insertText}`;
}

const dot = findChildOfKind(propertyAccessToConvert, SyntaxKind.DotToken, sourceFile) ||
findChildOfKind(propertyAccessToConvert, SyntaxKind.QuestionDotToken, sourceFile)!;
// 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);
Expand All @@ -272,7 +295,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);
}

Expand Down Expand Up @@ -1003,7 +1026,10 @@ namespace ts.Completions {
if (!isTypeLocation &&
symbol.declarations &&
symbol.declarations.some(d => d.kind !== SyntaxKind.SourceFile && d.kind !== SyntaxKind.ModuleDeclaration && d.kind !== SyntaxKind.EnumDeclaration)) {
addTypeProperties(removeOptionality(typeChecker.getTypeOfSymbolAtLocation(symbol, node), isRightOfQuestionDot, isOptional), !!(node.flags & NodeFlags.AwaitContext));
const type = removeOptionality(typeChecker.getTypeOfSymbolAtLocation(symbol, node), isRightOfQuestionDot, isOptional);
const nonNullType = type.getNonNullableType();
const insertQuestionDot = isRightOfDot && !isRightOfQuestionDot && type !== nonNullType;
addTypeProperties(nonNullType, !!(node.flags & NodeFlags.AwaitContext), insertQuestionDot);
}

return;
Expand All @@ -1018,11 +1044,14 @@ namespace ts.Completions {
}

if (!isTypeLocation) {
addTypeProperties(removeOptionality(typeChecker.getTypeAtLocation(node), isRightOfQuestionDot, isOptional), !!(node.flags & NodeFlags.AwaitContext));
const type = removeOptionality(typeChecker.getTypeAtLocation(node), isRightOfQuestionDot, isOptional);
const nonNullType = type.getNonNullableType();
const insertQuestionDot = isRightOfDot && !isRightOfQuestionDot && type !== nonNullType;
addTypeProperties(nonNullType, !!(node.flags & NodeFlags.AwaitContext), insertQuestionDot);
}
}

function addTypeProperties(type: Type, insertAwait?: boolean): void {
function addTypeProperties(type: Type, insertAwait: boolean, insertQuestionDot: boolean): void {
isNewIdentifierLocation = !!type.getStringIndexType();

const propertyAccess = node.kind === SyntaxKind.ImportType ? <ImportTypeNode>node : <PropertyAccessExpression | QualifiedName>node.parent;
Expand All @@ -1037,7 +1066,7 @@ namespace ts.Completions {
else {
for (const symbol of type.getApparentProperties()) {
if (typeChecker.isValidPropertyAccessForCompletions(propertyAccess, type, symbol)) {
addPropertySymbol(symbol);
addPropertySymbol(symbol, /*insertAwait*/ false, insertQuestionDot);
}
}
}
Expand All @@ -1047,14 +1076,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`.
Expand All @@ -1068,22 +1097,30 @@ 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) {
function addSymbolOriginInfo(symbol: Symbol) {
if (insertAwait && preferences.includeCompletionsWithInsertText && !symbolToOriginInfoMap[getSymbolId(symbol)]) {
symbolToOriginInfoMap[getSymbolId(symbol)] = { kind: SymbolOriginInfoKind.Promise };
symbolToOriginInfoMap[getSymbolId(symbol)] = { kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.Promise) };
}
else if (insertQuestionDot) {
Comment thread
rbuckton marked this conversation as resolved.
Outdated
symbolToOriginInfoMap[getSymbolId(symbol)] = { kind: SymbolOriginInfoKind.Nullable };
}
}
function getNullableSymbolOriginInfoKind(kind: SymbolOriginInfoKind) {
return insertQuestionDot ? kind | SymbolOriginInfoKind.Nullable : kind;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need tests for these scenarios..

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added some tests for completions following ?. and following . when a ?. could be inserted. Are these sufficient, are there any other test cases you would recommend?

}
}

Expand Down