-
Notifications
You must be signed in to change notification settings - Fork 12.9k
Better completion for property access of computed properties #56220
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
b2b138f
00303d5
c7cb038
cf1bf06
e6eccb9
f59cd66
6ed62ed
825f71b
27dd761
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -240,6 +240,7 @@ import { | |
isStringLiteralOrTemplate, | ||
isStringTextContainingNode, | ||
isSyntaxList, | ||
isTransientSymbol, | ||
isTypeKeyword, | ||
isTypeKeywordTokenOrIdentifier, | ||
isTypeLiteralNode, | ||
|
@@ -3663,71 +3664,131 @@ function getCompletionData( | |
} | ||
|
||
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`. | ||
// For a computed property `x.y` in an exported namespace `n` that is imported | ||
// in another file as `m`, we can access `y` as `m.n.x.y`. To form this access chain, first | ||
// we follow `x` up it symbol parents until we find a symbol that is accessible from the completion | ||
// location. This gives us a property access chain (`m.n.x`) which we can then combine with the original | ||
// computed property expression (`x.y`) by substitution of `m.n.x` for `x` in `x.y` to get `m.n.x.y`. | ||
// If this fails, we will fall back to the literal value of `y`. | ||
|
||
const computedPropertyName = firstDefined(symbol.declarations, decl => tryCast(getNameOfDeclaration(decl), isComputedPropertyName)); | ||
if (computedPropertyName) { | ||
const leftMostName = getLeftMostName(computedPropertyName.expression); // The completion is for `Symbol`, not `iterator`. | ||
const nameSymbol = leftMostName && typeChecker.getSymbolAtLocation(leftMostName); | ||
// If this is nested like for `namespace N { export const sym = Symbol(); }`, we'll add the completion for `N`. | ||
const firstAccessibleSymbol = nameSymbol && getFirstSymbolInChain(nameSymbol, contextToken, typeChecker); | ||
const firstAccessibleSymbolId = firstAccessibleSymbol && getSymbolId(firstAccessibleSymbol); | ||
if (firstAccessibleSymbolId && addToSeen(seenPropertySymbols, firstAccessibleSymbolId)) { | ||
const index = symbols.length; | ||
symbols.push(firstAccessibleSymbol); | ||
const moduleSymbol = firstAccessibleSymbol.parent; | ||
if ( | ||
!moduleSymbol || | ||
!isExternalModuleSymbol(moduleSymbol) || | ||
typeChecker.tryGetMemberInModuleExportsAndProperties(firstAccessibleSymbol.name, moduleSymbol) !== firstAccessibleSymbol | ||
) { | ||
symbolToOriginInfoMap[index] = { kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.SymbolMemberNoExport) }; | ||
} | ||
else { | ||
const fileName = isExternalModuleNameRelative(stripQuotes(moduleSymbol.name)) ? getSourceFileOfModule(moduleSymbol)?.fileName : undefined; | ||
const { moduleSpecifier } = (importSpecifierResolver ||= codefix.createImportSpecifierResolver(sourceFile, program, host, preferences)).getModuleSpecifierForBestExportInfo( | ||
[{ | ||
exportKind: ExportKind.Named, | ||
moduleFileName: fileName, | ||
isFromPackageJson: false, | ||
moduleSymbol, | ||
symbol: firstAccessibleSymbol, | ||
targetFlags: skipAlias(firstAccessibleSymbol, typeChecker).flags, | ||
}], | ||
position, | ||
isValidTypeOnlyAliasUseSite(location), | ||
) || {}; | ||
|
||
if (moduleSpecifier) { | ||
const origin: SymbolOriginInfoResolvedExport = { | ||
kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.SymbolMemberExport), | ||
moduleSymbol, | ||
isDefaultExport: false, | ||
symbolName: firstAccessibleSymbol.name, | ||
exportName: firstAccessibleSymbol.name, | ||
fileName, | ||
moduleSpecifier, | ||
}; | ||
symbolToOriginInfoMap[index] = origin; | ||
} | ||
if (!computedPropertyName) { | ||
addLiteralSymbol(); | ||
return; | ||
} | ||
|
||
const computedPropertyNameExpression = computedPropertyName.expression; | ||
const name = isEntityName(computedPropertyNameExpression) | ||
? computedPropertyNameExpression | ||
: isPropertyAccessExpression(computedPropertyNameExpression) | ||
? computedPropertyNameExpression.name | ||
: undefined; | ||
const nameSymbol = name && typeChecker.getSymbolAtLocation(name); | ||
const nameSymbolId = nameSymbol && getSymbolId(nameSymbol); | ||
if (!nameSymbolId) { // Not a property access or entity name | ||
addLiteralSymbol(); | ||
return; | ||
} | ||
|
||
if (addToSeen(seenPropertySymbols, nameSymbolId)) { | ||
const leftMostName = getLeftMostName(computedPropertyNameExpression); | ||
const leftMostNameSymbol = leftMostName && typeChecker.getSymbolAtLocation(leftMostName); | ||
const firstAccessibleSymbol = leftMostNameSymbol && getFirstSymbolInChain(leftMostNameSymbol, contextToken, typeChecker); | ||
if (!firstAccessibleSymbol) { // Symbol is not accessible from completion location | ||
addLiteralSymbol(); | ||
return; | ||
} | ||
|
||
const index = symbols.length; | ||
symbols.push(nameSymbol); | ||
const moduleSymbol = firstAccessibleSymbol.parent; | ||
if ( | ||
!moduleSymbol || | ||
!isExternalModuleSymbol(moduleSymbol) || | ||
typeChecker.tryGetMemberInModuleExportsAndProperties(firstAccessibleSymbol.name, moduleSymbol) !== firstAccessibleSymbol | ||
) { | ||
// If preferences allow insert text, add completion for [<QualifiedSymbolName>] | ||
const node = preferences.includeCompletionsWithInsertText | ||
? createComputedPropertyAccess(nameSymbol, leftMostNameSymbol, computedPropertyNameExpression) | ||
: undefined; | ||
|
||
if (!node) { | ||
// Switch to literal symbol if user doesn't want insert text | ||
symbols[index] = symbol; | ||
} else { | ||
const printer = createPrinter({ | ||
removeComments: true, | ||
module: compilerOptions.module, | ||
target: compilerOptions.target, | ||
omitTrailingSemicolon: true, | ||
}); | ||
const origin: SymbolOriginInfoComputedPropertyName = { | ||
kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.SymbolMemberNoExport) | SymbolOriginInfoKind.ComputedPropertyName, | ||
symbolName: printer.printNode(EmitHint.Unspecified, node, contextToken.getSourceFile()) | ||
}; | ||
symbolToOriginInfoMap[index] = origin; | ||
} | ||
} | ||
else if (preferences.includeCompletionsWithInsertText) { | ||
if (firstAccessibleSymbolId && seenPropertySymbols.has(firstAccessibleSymbolId)) { | ||
return; | ||
else { | ||
const fileName = isExternalModuleNameRelative(stripQuotes(moduleSymbol.name)) ? getSourceFileOfModule(moduleSymbol)?.fileName : undefined; | ||
const { moduleSpecifier } = (importSpecifierResolver ||= codefix.createImportSpecifierResolver(sourceFile, program, host, preferences)).getModuleSpecifierForBestExportInfo( | ||
[{ | ||
exportKind: ExportKind.Named, | ||
moduleFileName: fileName, | ||
isFromPackageJson: false, | ||
moduleSymbol, | ||
symbol: firstAccessibleSymbol, | ||
targetFlags: skipAlias(firstAccessibleSymbol, typeChecker).flags, | ||
}], | ||
position, | ||
isValidTypeOnlyAliasUseSite(location), | ||
) || {}; | ||
|
||
if (moduleSpecifier) { | ||
const origin: SymbolOriginInfoResolvedExport = { | ||
kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.SymbolMemberExport), | ||
moduleSymbol, | ||
isDefaultExport: false, | ||
symbolName: firstAccessibleSymbol.name, | ||
exportName: firstAccessibleSymbol.name, | ||
fileName, | ||
moduleSpecifier, | ||
}; | ||
symbolToOriginInfoMap[index] = origin; | ||
} | ||
addSymbolOriginInfo(symbol); | ||
addSymbolSortInfo(symbol); | ||
symbols.push(symbol); | ||
} | ||
} | ||
else { | ||
|
||
function addLiteralSymbol() { | ||
PranavSenthilnathan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
addSymbolOriginInfo(symbol); | ||
addSymbolSortInfo(symbol); | ||
symbols.push(symbol); | ||
} | ||
|
||
/** Combines a property access expression (from the completion location to the left-most name in the computed property expression) | ||
* and the computed property expression itself to return an expression to access the computed property from the completion location. | ||
* Assumes that the left-most symbol is accessible so there should always be a way to access the computed property symbolically. */ | ||
PranavSenthilnathan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
function createComputedPropertyAccess(nameSymbol: Symbol, leftMostNameSymbol: Symbol, computedPropertyNameExpression: Expression) { | ||
let node: Node | undefined; | ||
if (!isTransientSymbol(nameSymbol)) { | ||
node = typeChecker.symbolToEntityName(nameSymbol, /*meaning*/ undefined!, contextToken, NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope); | ||
PranavSenthilnathan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
else { | ||
// Object literals assigned as const | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's this case for, exactly? I think I don't understand the comment here and how we know this from knowing that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This scenario goes into this branch: const x = { a: "foo" } as const;
const y = { [x.a]: 0 };
y.| It's because the symbol That being said, I don't know if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure if all transient symbols run into this problem with |
||
const leftMostNodeAccessExpression = typeChecker.symbolToNode(leftMostNameSymbol, /*meaning*/ undefined!, contextToken, NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope); | ||
PranavSenthilnathan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
type OnlyPropertyAccess = Identifier | (PropertyAccessExpression & { expression: OnlyPropertyAccess; }); | ||
node = createPropertyAccess(computedPropertyNameExpression as OnlyPropertyAccess); | ||
function createPropertyAccess(n: OnlyPropertyAccess): Expression { | ||
if (isIdentifier(n)) { | ||
return leftMostNodeAccessExpression! as Expression; //TODO ! and cast | ||
PranavSenthilnathan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
return factory.createPropertyAccessExpression(createPropertyAccess(n.expression), n.name); | ||
} | ||
} | ||
return node; | ||
} | ||
|
||
function addSymbolSortInfo(symbol: Symbol) { | ||
if (isStaticProperty(symbol)) { | ||
symbolToSortTextMap[getSymbolId(symbol)] = SortText.LocalDeclarationPriority; | ||
|
@@ -4535,7 +4596,7 @@ function getCompletionData( | |
if (declaration && isClassElement(declaration) && declaration.name && isComputedPropertyName(declaration.name)) { | ||
const origin: SymbolOriginInfoComputedPropertyName = { | ||
kind: SymbolOriginInfoKind.ComputedPropertyName, | ||
symbolName: typeChecker.symbolToString(symbol), | ||
symbolName: typeChecker.symbolToString(symbol) | ||
}; | ||
symbolToOriginInfoMap[index] = origin; | ||
} | ||
|
@@ -5175,7 +5236,7 @@ function getCompletionEntryDisplayNameForSymbol( | |
case CompletionKind.PropertyAccess: | ||
case CompletionKind.Global: // For a 'this.' completion it will be in a global context, but may have a non-identifier name. | ||
// Don't add a completion for a name starting with a space. See https://github.com/Microsoft/TypeScript/pull/20547 | ||
return name.charCodeAt(0) === CharacterCodes.space ? undefined : { name, needsConvertPropertyAccess: true }; | ||
return name.charCodeAt(0) === CharacterCodes.space ? undefined : { name, needsConvertPropertyAccess: !originIsComputedPropertyName(origin) }; | ||
case CompletionKind.None: | ||
case CompletionKind.String: | ||
return validNameResult; | ||
|
Uh oh!
There was an error while loading. Please reload this page.