mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-06-15 22:15:05 -05:00
Merge pull request #16529 from Microsoft/completionWithMeaningReleaseBranch
[release-2.4] Filter completion list according to meaning of the location
This commit is contained in:
@@ -14590,12 +14590,31 @@ namespace ts {
|
||||
? (<PropertyAccessExpression>node).expression
|
||||
: (<QualifiedName>node).left;
|
||||
|
||||
const type = checkExpression(left);
|
||||
return isValidPropertyAccessWithType(node, left, propertyName, getWidenedType(checkExpression(left)));
|
||||
}
|
||||
|
||||
function isValidPropertyAccessWithType(
|
||||
node: PropertyAccessExpression | QualifiedName,
|
||||
left: LeftHandSideExpression | QualifiedName,
|
||||
propertyName: string,
|
||||
type: Type): boolean {
|
||||
|
||||
if (type !== unknownType && !isTypeAny(type)) {
|
||||
const prop = getPropertyOfType(getWidenedType(type), propertyName);
|
||||
const prop = getPropertyOfType(type, propertyName);
|
||||
if (prop) {
|
||||
return checkPropertyAccessibility(node, left, type, prop);
|
||||
}
|
||||
|
||||
// In js files properties of unions are allowed in completion
|
||||
if (isInJavaScriptFile(left) && (type.flags & TypeFlags.Union)) {
|
||||
for (const elementType of (<UnionType>type).types) {
|
||||
if (isValidPropertyAccessWithType(node, left, propertyName, elementType)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -814,8 +814,8 @@ namespace FourSlash {
|
||||
|
||||
function filterByTextOrDocumentation(entry: ts.CompletionEntry) {
|
||||
const details = that.getCompletionEntryDetails(entry.name);
|
||||
const documentation = ts.displayPartsToString(details.documentation);
|
||||
const text = ts.displayPartsToString(details.displayParts);
|
||||
const documentation = details && ts.displayPartsToString(details.documentation);
|
||||
const text = details && ts.displayPartsToString(details.displayParts);
|
||||
|
||||
// If any of the expected values are undefined, assume that users don't
|
||||
// care about them.
|
||||
@@ -852,6 +852,9 @@ namespace FourSlash {
|
||||
if (expectedKind) {
|
||||
error += "Expected kind: " + expectedKind + " to equal: " + filterCompletions[0].kind + ".";
|
||||
}
|
||||
else {
|
||||
error += "kind: " + filterCompletions[0].kind + ".";
|
||||
}
|
||||
if (replacementSpan) {
|
||||
const spanText = filterCompletions[0].replacementSpan ? stringify(filterCompletions[0].replacementSpan) : undefined;
|
||||
error += "Expected replacement span: " + stringify(replacementSpan) + " to equal: " + spanText + ".";
|
||||
|
||||
@@ -366,9 +366,8 @@ namespace ts.Completions {
|
||||
let request: Request | undefined;
|
||||
|
||||
let start = timestamp();
|
||||
const currentToken = getTokenAtPosition(sourceFile, position, /*includeJsDocComment*/ false);
|
||||
let currentToken = getTokenAtPosition(sourceFile, position, /*includeJsDocComment*/ false);
|
||||
// We will check for jsdoc comments with insideComment and getJsDocTagAtPosition. (TODO: that seems rather inefficient to check the same thing so many times.)
|
||||
|
||||
log("getCompletionData: Get current token: " + (timestamp() - start));
|
||||
|
||||
start = timestamp();
|
||||
@@ -376,6 +375,7 @@ namespace ts.Completions {
|
||||
const insideComment = isInComment(sourceFile, position, currentToken);
|
||||
log("getCompletionData: Is inside comment: " + (timestamp() - start));
|
||||
|
||||
let insideJsDocTagTypeExpression = false;
|
||||
if (insideComment) {
|
||||
if (hasDocComment(sourceFile, position)) {
|
||||
if (sourceFile.text.charCodeAt(position - 1) === CharacterCodes.at) {
|
||||
@@ -410,25 +410,23 @@ namespace ts.Completions {
|
||||
// Completion should work inside certain JsDoc tags. For example:
|
||||
// /** @type {number | string} */
|
||||
// Completion should work in the brackets
|
||||
let insideJsDocTagExpression = false;
|
||||
const tag = getJsDocTagAtPosition(currentToken, position);
|
||||
if (tag) {
|
||||
if (tag.tagName.pos <= position && position <= tag.tagName.end) {
|
||||
request = { kind: "JsDocTagName" };
|
||||
}
|
||||
|
||||
switch (tag.kind) {
|
||||
case SyntaxKind.JSDocTypeTag:
|
||||
case SyntaxKind.JSDocParameterTag:
|
||||
case SyntaxKind.JSDocReturnTag:
|
||||
const tagWithExpression = <JSDocTypeTag | JSDocParameterTag | JSDocReturnTag>tag;
|
||||
if (tagWithExpression.typeExpression && tagWithExpression.typeExpression.pos < position && position < tagWithExpression.typeExpression.end) {
|
||||
insideJsDocTagExpression = true;
|
||||
}
|
||||
else if (isJSDocParameterTag(tag) && (nodeIsMissing(tag.name) || tag.name.pos <= position && position <= tag.name.end)) {
|
||||
request = { kind: "JsDocParameterName", tag };
|
||||
}
|
||||
break;
|
||||
if (isTagWithTypeExpression(tag) && tag.typeExpression) {
|
||||
currentToken = getTokenAtPosition(sourceFile, position, /*includeJsDocComment*/ true);
|
||||
if (!currentToken ||
|
||||
(!isDeclarationName(currentToken) &&
|
||||
(currentToken.parent.kind !== SyntaxKind.JSDocPropertyTag ||
|
||||
(<JSDocPropertyTag>currentToken.parent).name !== currentToken))) {
|
||||
// Use as type location if inside tag's type expression
|
||||
insideJsDocTagTypeExpression = isCurrentlyEditingNode(tag.typeExpression);
|
||||
}
|
||||
}
|
||||
if (isJSDocParameterTag(tag) && (nodeIsMissing(tag.name) || tag.name.pos <= position && position <= tag.name.end)) {
|
||||
request = { kind: "JsDocParameterName", tag };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,7 +434,7 @@ namespace ts.Completions {
|
||||
return { symbols: undefined, isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, location: undefined, isRightOfDot: false, request, hasFilteredClassMemberKeywords: false };
|
||||
}
|
||||
|
||||
if (!insideJsDocTagExpression) {
|
||||
if (!insideJsDocTagTypeExpression) {
|
||||
// Proceed if the current position is in jsDoc tag expression; otherwise it is a normal
|
||||
// comment or the plain text part of a jsDoc comment, so no completion should be available
|
||||
log("Returning an empty list because completion was inside a regular comment or plain text part of a JsDoc comment.");
|
||||
@@ -445,7 +443,7 @@ namespace ts.Completions {
|
||||
}
|
||||
|
||||
start = timestamp();
|
||||
const previousToken = findPrecedingToken(position, sourceFile, /*startNode*/ undefined, /*includeJsDoc*/ true);
|
||||
const previousToken = findPrecedingToken(position, sourceFile, /*startNode*/ undefined, insideJsDocTagTypeExpression);
|
||||
log("getCompletionData: Get previous token 1: " + (timestamp() - start));
|
||||
|
||||
// The decision to provide completion depends on the contextToken, which is determined through the previousToken.
|
||||
@@ -456,7 +454,7 @@ namespace ts.Completions {
|
||||
// Skip this partial identifier and adjust the contextToken to the token that precedes it.
|
||||
if (contextToken && position <= contextToken.end && isWord(contextToken.kind)) {
|
||||
const start = timestamp();
|
||||
contextToken = findPrecedingToken(contextToken.getFullStart(), sourceFile, /*startNode*/ undefined, /*includeJsDoc*/ true);
|
||||
contextToken = findPrecedingToken(contextToken.getFullStart(), sourceFile, /*startNode*/ undefined, insideJsDocTagTypeExpression);
|
||||
log("getCompletionData: Get previous token 2: " + (timestamp() - start));
|
||||
}
|
||||
|
||||
@@ -468,7 +466,7 @@ namespace ts.Completions {
|
||||
let isRightOfOpenTag = false;
|
||||
let isStartingCloseTag = false;
|
||||
|
||||
let location = getTouchingPropertyName(sourceFile, position, /*includeJsDocComment*/ false); // TODO: GH#15853
|
||||
let location = getTouchingPropertyName(sourceFile, position, insideJsDocTagTypeExpression); // TODO: GH#15853
|
||||
if (contextToken) {
|
||||
// Bail out if this is a known invalid completion location
|
||||
if (isCompletionListBlocker(contextToken)) {
|
||||
@@ -572,12 +570,29 @@ namespace ts.Completions {
|
||||
|
||||
return { symbols, isGlobalCompletion, isMemberCompletion, isNewIdentifierLocation, location, isRightOfDot: (isRightOfDot || isRightOfOpenTag), request, hasFilteredClassMemberKeywords };
|
||||
|
||||
type JSDocTagWithTypeExpression = JSDocAugmentsTag | JSDocParameterTag | JSDocPropertyTag | JSDocReturnTag | JSDocTypeTag | JSDocTypedefTag;
|
||||
|
||||
function isTagWithTypeExpression(tag: JSDocTag): tag is JSDocTagWithTypeExpression {
|
||||
switch (tag.kind) {
|
||||
case SyntaxKind.JSDocAugmentsTag:
|
||||
case SyntaxKind.JSDocParameterTag:
|
||||
case SyntaxKind.JSDocPropertyTag:
|
||||
case SyntaxKind.JSDocReturnTag:
|
||||
case SyntaxKind.JSDocTypeTag:
|
||||
case SyntaxKind.JSDocTypedefTag:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function getTypeScriptMemberSymbols(): void {
|
||||
// Right of dot member completion list
|
||||
isGlobalCompletion = false;
|
||||
isMemberCompletion = true;
|
||||
isNewIdentifierLocation = false;
|
||||
|
||||
// Since this is qualified name check its a type node location
|
||||
const isTypeLocation = isPartOfTypeNode(node.parent) || insideJsDocTagTypeExpression;
|
||||
const isRhsOfImportDeclaration = isInRightSideOfInternalImportEqualsDeclaration(node);
|
||||
if (node.kind === SyntaxKind.Identifier || node.kind === SyntaxKind.QualifiedName || node.kind === SyntaxKind.PropertyAccessExpression) {
|
||||
let symbol = typeChecker.getSymbolAtLocation(node);
|
||||
|
||||
@@ -589,16 +604,24 @@ namespace ts.Completions {
|
||||
if (symbol && symbol.flags & SymbolFlags.HasExports) {
|
||||
// Extract module or enum members
|
||||
const exportedSymbols = typeChecker.getExportsOfModule(symbol);
|
||||
const isValidValueAccess = (symbol: Symbol) => typeChecker.isValidPropertyAccess(<PropertyAccessExpression>(node.parent), symbol.name);
|
||||
const isValidTypeAccess = (symbol: Symbol) => symbolCanbeReferencedAtTypeLocation(symbol);
|
||||
const isValidAccess = isRhsOfImportDeclaration ?
|
||||
// Any kind is allowed when dotting off namespace in internal import equals declaration
|
||||
(symbol: Symbol) => isValidTypeAccess(symbol) || isValidValueAccess(symbol) :
|
||||
isTypeLocation ? isValidTypeAccess : isValidValueAccess;
|
||||
forEach(exportedSymbols, symbol => {
|
||||
if (typeChecker.isValidPropertyAccess(<PropertyAccessExpression>(node.parent), symbol.name)) {
|
||||
if (isValidAccess(symbol)) {
|
||||
symbols.push(symbol);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const type = typeChecker.getTypeAtLocation(node);
|
||||
addTypeProperties(type);
|
||||
if (!isTypeLocation) {
|
||||
const type = typeChecker.getTypeAtLocation(node);
|
||||
addTypeProperties(type);
|
||||
}
|
||||
}
|
||||
|
||||
function addTypeProperties(type: Type) {
|
||||
@@ -706,13 +729,87 @@ namespace ts.Completions {
|
||||
isStatement(scopeNode);
|
||||
}
|
||||
|
||||
/// TODO filter meaning based on the current context
|
||||
const symbolMeanings = SymbolFlags.Type | SymbolFlags.Value | SymbolFlags.Namespace | SymbolFlags.Alias;
|
||||
symbols = typeChecker.getSymbolsInScope(scopeNode, symbolMeanings);
|
||||
symbols = filterGlobalCompletion(typeChecker.getSymbolsInScope(scopeNode, symbolMeanings));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function filterGlobalCompletion(symbols: Symbol[]) {
|
||||
return filter(symbols, symbol => {
|
||||
if (!isSourceFile(location)) {
|
||||
// export = /**/ here we want to get all meanings, so any symbol is ok
|
||||
if (isExportAssignment(location.parent)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// This is an alias, follow what it aliases
|
||||
if (symbol && symbol.flags & SymbolFlags.Alias) {
|
||||
symbol = typeChecker.getAliasedSymbol(symbol);
|
||||
}
|
||||
|
||||
// import m = /**/ <-- It can only access namespace (if typing import = x. this would get member symbols and not namespace)
|
||||
if (isInRightSideOfInternalImportEqualsDeclaration(location)) {
|
||||
return !!(symbol.flags & SymbolFlags.Namespace);
|
||||
}
|
||||
|
||||
if (insideJsDocTagTypeExpression ||
|
||||
(!isContextTokenValueLocation(contextToken) &&
|
||||
(isPartOfTypeNode(location) || isContextTokenTypeLocation(contextToken)))) {
|
||||
// Its a type, but you can reach it by namespace.type as well
|
||||
return symbolCanbeReferencedAtTypeLocation(symbol);
|
||||
}
|
||||
}
|
||||
|
||||
// expressions are value space (which includes the value namespaces)
|
||||
return !!(symbol.flags & SymbolFlags.Value);
|
||||
});
|
||||
}
|
||||
|
||||
function isContextTokenValueLocation(contextToken: Node) {
|
||||
return contextToken &&
|
||||
contextToken.kind === SyntaxKind.TypeOfKeyword &&
|
||||
contextToken.parent.kind === SyntaxKind.TypeQuery;
|
||||
}
|
||||
|
||||
function isContextTokenTypeLocation(contextToken: Node) {
|
||||
if (contextToken) {
|
||||
const parentKind = contextToken.parent.kind;
|
||||
switch (contextToken.kind) {
|
||||
case SyntaxKind.ColonToken:
|
||||
return parentKind === SyntaxKind.PropertyDeclaration ||
|
||||
parentKind === SyntaxKind.PropertySignature ||
|
||||
parentKind === SyntaxKind.Parameter ||
|
||||
parentKind === SyntaxKind.VariableDeclaration ||
|
||||
isFunctionLikeKind(parentKind);
|
||||
|
||||
case SyntaxKind.EqualsToken:
|
||||
return parentKind === SyntaxKind.TypeAliasDeclaration;
|
||||
|
||||
case SyntaxKind.AsKeyword:
|
||||
return parentKind === SyntaxKind.AsExpression;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function symbolCanbeReferencedAtTypeLocation(symbol: Symbol): boolean {
|
||||
// This is an alias, follow what it aliases
|
||||
if (symbol && symbol.flags & SymbolFlags.Alias) {
|
||||
symbol = typeChecker.getAliasedSymbol(symbol);
|
||||
}
|
||||
|
||||
if (symbol.flags & SymbolFlags.Type) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (symbol.flags & (SymbolFlags.ValueModule | SymbolFlags.NamespaceModule)) {
|
||||
const exportedSymbols = typeChecker.getExportsOfModule(symbol);
|
||||
// If the exported symbols contains type,
|
||||
// symbol can be referenced at locations where type is allowed
|
||||
return forEach(exportedSymbols, symbolCanbeReferencedAtTypeLocation);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the first node that "embraces" the position, so that one may
|
||||
* accurately aggregate locals from the closest containing scope.
|
||||
@@ -1140,21 +1237,6 @@ namespace ts.Completions {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isFunction(kind: SyntaxKind): boolean {
|
||||
if (!isFunctionLikeKind(kind)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (kind) {
|
||||
case SyntaxKind.Constructor:
|
||||
case SyntaxKind.ConstructorType:
|
||||
case SyntaxKind.FunctionType:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if we are certain that the currently edited location must define a new location; false otherwise.
|
||||
*/
|
||||
@@ -1166,7 +1248,7 @@ namespace ts.Completions {
|
||||
containingNodeKind === SyntaxKind.VariableDeclarationList ||
|
||||
containingNodeKind === SyntaxKind.VariableStatement ||
|
||||
containingNodeKind === SyntaxKind.EnumDeclaration || // enum a { foo, |
|
||||
isFunction(containingNodeKind) ||
|
||||
isFunctionLikeButNotConstructor(containingNodeKind) ||
|
||||
containingNodeKind === SyntaxKind.ClassDeclaration || // class A<T, |
|
||||
containingNodeKind === SyntaxKind.ClassExpression || // var C = class D<T, |
|
||||
containingNodeKind === SyntaxKind.InterfaceDeclaration || // interface A<T, |
|
||||
@@ -1184,7 +1266,7 @@ namespace ts.Completions {
|
||||
|
||||
case SyntaxKind.OpenParenToken:
|
||||
return containingNodeKind === SyntaxKind.CatchClause ||
|
||||
isFunction(containingNodeKind);
|
||||
isFunctionLikeButNotConstructor(containingNodeKind);
|
||||
|
||||
case SyntaxKind.OpenBraceToken:
|
||||
return containingNodeKind === SyntaxKind.EnumDeclaration || // enum a { |
|
||||
@@ -1202,7 +1284,7 @@ namespace ts.Completions {
|
||||
containingNodeKind === SyntaxKind.ClassExpression || // var C = class D< |
|
||||
containingNodeKind === SyntaxKind.InterfaceDeclaration || // interface A< |
|
||||
containingNodeKind === SyntaxKind.TypeAliasDeclaration || // type List< |
|
||||
isFunction(containingNodeKind);
|
||||
isFunctionLikeKind(containingNodeKind);
|
||||
|
||||
case SyntaxKind.StaticKeyword:
|
||||
return containingNodeKind === SyntaxKind.PropertyDeclaration && !isClassLike(contextToken.parent.parent);
|
||||
@@ -1271,6 +1353,10 @@ namespace ts.Completions {
|
||||
return false;
|
||||
}
|
||||
|
||||
function isFunctionLikeButNotConstructor(kind: SyntaxKind) {
|
||||
return isFunctionLikeKind(kind) && kind !== SyntaxKind.Constructor;
|
||||
}
|
||||
|
||||
function isDotOfNumericLiteral(contextToken: Node): boolean {
|
||||
if (contextToken.kind === SyntaxKind.NumericLiteral) {
|
||||
const text = contextToken.getFullText();
|
||||
|
||||
@@ -82,7 +82,7 @@ namespace ts {
|
||||
else if (node.parent.kind === SyntaxKind.ExportAssignment) {
|
||||
return SemanticMeaning.All;
|
||||
}
|
||||
else if (isInRightSideOfImport(node)) {
|
||||
else if (isInRightSideOfInternalImportEqualsDeclaration(node)) {
|
||||
return getMeaningFromRightHandSideOfImportEquals(node);
|
||||
}
|
||||
else if (isDeclarationName(node)) {
|
||||
@@ -118,7 +118,7 @@ namespace ts {
|
||||
return SemanticMeaning.Namespace;
|
||||
}
|
||||
|
||||
function isInRightSideOfImport(node: Node) {
|
||||
export function isInRightSideOfInternalImportEqualsDeclaration(node: Node) {
|
||||
while (node.parent.kind === SyntaxKind.QualifiedName) {
|
||||
node = node.parent;
|
||||
}
|
||||
@@ -745,7 +745,7 @@ namespace ts {
|
||||
// NOTE: JsxText is a weird kind of node that can contain only whitespaces (since they are not counted as trivia).
|
||||
// if this is the case - then we should assume that token in question is located in previous child.
|
||||
if (position < child.end && (nodeHasTokens(child) || child.kind === SyntaxKind.JsxText)) {
|
||||
const start = (includeJsDoc && child.jsDoc ? child.jsDoc[0] : child).getStart(sourceFile);
|
||||
const start = child.getStart(sourceFile, includeJsDoc);
|
||||
const lookInPreviousChild =
|
||||
(start >= position) || // cursor in the leading trivia
|
||||
(child.kind === SyntaxKind.JsxText && start === child.end); // whitespace only JsxText
|
||||
|
||||
Reference in New Issue
Block a user