Skip to content

Block completions in invalid positions #903

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

Merged
merged 12 commits into from
Jun 4, 2025
4 changes: 4 additions & 0 deletions internal/ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -5521,6 +5521,10 @@ func (node *RegularExpressionLiteral) Clone(f NodeFactoryCoercible) *Node {
return cloneNode(f.AsNodeFactory().NewRegularExpressionLiteral(node.Text), node.AsNode(), f.AsNodeFactory().hooks)
}

func IsRegularExpressionLiteral(node *Node) bool {
return node.Kind == KindRegularExpressionLiteral
}

// NoSubstitutionTemplateLiteral

type NoSubstitutionTemplateLiteral struct {
Expand Down
14 changes: 14 additions & 0 deletions internal/ast/utilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -3303,3 +3303,17 @@ func IsTypeDeclarationName(name *Node) bool {
IsTypeDeclaration(name.Parent) &&
GetNameOfDeclaration(name.Parent) == name
}

func IsUnterminatedNode(node *Node) bool {
return IsLiteralKind(node.Kind) && IsUnterminatedLiteral(node)
}

// Gets a value indicating whether a class element is either a static or an instance property declaration with an initializer.
func IsInitializedProperty(member *ClassElement) bool {
return member.Kind == KindPropertyDeclaration &&
member.Initializer() != nil
}

func IsJsxOpeningLikeElement(node *Node) bool {
return IsJsxOpeningElement(node) || IsJsxSelfClosingElement(node)
}
16 changes: 8 additions & 8 deletions internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -8420,7 +8420,7 @@ type CallState struct {
func (c *Checker) resolveCall(node *ast.Node, signatures []*Signature, candidatesOutArray *[]*Signature, checkMode CheckMode, callChainFlags SignatureFlags, headMessage *diagnostics.Message) *Signature {
isTaggedTemplate := node.Kind == ast.KindTaggedTemplateExpression
isDecorator := node.Kind == ast.KindDecorator
isJsxOpeningOrSelfClosingElement := isJsxOpeningLikeElement(node)
isJsxOpeningOrSelfClosingElement := ast.IsJsxOpeningLikeElement(node)
isInstanceof := node.Kind == ast.KindBinaryExpression
reportErrors := !c.isInferencePartiallyBlocked && candidatesOutArray == nil
var s CallState
Expand Down Expand Up @@ -8726,7 +8726,7 @@ func (c *Checker) hasCorrectArity(node *ast.Node, args []*ast.Node, signature *S
argCount = c.getDecoratorArgumentCount(node, signature)
case ast.IsBinaryExpression(node):
argCount = 1
case isJsxOpeningLikeElement(node):
case ast.IsJsxOpeningLikeElement(node):
callIsIncomplete = node.Attributes().End() == node.End()
if callIsIncomplete {
return true
Expand Down Expand Up @@ -8846,7 +8846,7 @@ func (c *Checker) checkTypeArguments(signature *Signature, typeArgumentNodes []*
}

func (c *Checker) isSignatureApplicable(node *ast.Node, args []*ast.Node, signature *Signature, relation *Relation, checkMode CheckMode, reportErrors bool, inferenceContext *InferenceContext, diagnosticOutput *[]*ast.Diagnostic) bool {
if isJsxOpeningLikeElement(node) {
if ast.IsJsxOpeningLikeElement(node) {
return c.checkApplicableSignatureForJsxOpeningLikeElement(node, signature, relation, checkMode, reportErrors, diagnosticOutput)
}
thisType := c.getThisTypeOfSignature(signature)
Expand Down Expand Up @@ -8987,7 +8987,7 @@ func (c *Checker) getEffectiveCheckNode(argument *ast.Node) *ast.Node {
}

func (c *Checker) inferTypeArguments(node *ast.Node, signature *Signature, args []*ast.Node, checkMode CheckMode, context *InferenceContext) []*Type {
if isJsxOpeningLikeElement(node) {
if ast.IsJsxOpeningLikeElement(node) {
return c.inferJsxTypeArguments(node, signature, checkMode, context)
}
// If a contextual type is available, infer from that type to the return type of the call expression. For
Expand Down Expand Up @@ -26512,7 +26512,7 @@ func (c *Checker) markLinkedReferences(location *ast.Node, hint ReferenceHint, p
c.markExportAssignmentAliasReferenced(location)
return
}
if isJsxOpeningLikeElement(location) || ast.IsJsxOpeningFragment(location) {
if ast.IsJsxOpeningLikeElement(location) || ast.IsJsxOpeningFragment(location) {
c.markJsxAliasReferenced(location)
return
}
Expand Down Expand Up @@ -26677,7 +26677,7 @@ func (c *Checker) markJsxAliasReferenced(node *ast.Node /*JsxOpeningLikeElement
jsxFactoryRefErr := core.IfElse(c.compilerOptions.Jsx == core.JsxEmitReact, diagnostics.This_JSX_tag_requires_0_to_be_in_scope_but_it_could_not_be_found, nil)
jsxFactoryNamespace := c.getJsxNamespace(node)
jsxFactoryLocation := node
if isJsxOpeningLikeElement(node) {
if ast.IsJsxOpeningLikeElement(node) {
jsxFactoryLocation = node.TagName()
}
// allow null as jsxFragmentFactory
Expand Down Expand Up @@ -27722,7 +27722,7 @@ func (c *Checker) getContextualTypeForArgumentAtIndex(callTarget *ast.Node, argI
} else {
signature = c.getResolvedSignature(callTarget, nil, CheckModeNormal)
}
if isJsxOpeningLikeElement(callTarget) && argIndex == 0 {
if ast.IsJsxOpeningLikeElement(callTarget) && argIndex == 0 {
return c.getEffectiveFirstArgumentForJsxSignature(signature, callTarget)
}
restIndex := len(signature.parameters) - 1
Expand Down Expand Up @@ -27976,7 +27976,7 @@ func (c *Checker) getEffectiveCallArguments(node *ast.Node) []*ast.Node {
case ast.IsBinaryExpression(node):
// Handles instanceof operator
return []*ast.Node{node.AsBinaryExpression().Left}
case isJsxOpeningLikeElement(node):
case ast.IsJsxOpeningLikeElement(node):
if len(node.Attributes().AsJsxAttributes().Properties.Nodes) != 0 || (ast.IsJsxOpeningElement(node) && len(node.Parent.Children().Nodes) != 0) {
return []*ast.Node{node.Attributes()}
}
Expand Down
2 changes: 1 addition & 1 deletion internal/checker/jsx.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ func (c *Checker) checkJsxAttributes(node *ast.Node, checkMode CheckMode) *Type
}

func (c *Checker) checkJsxOpeningLikeElementOrOpeningFragment(node *ast.Node) {
isNodeOpeningLikeElement := isJsxOpeningLikeElement(node)
isNodeOpeningLikeElement := ast.IsJsxOpeningLikeElement(node)
if isNodeOpeningLikeElement {
c.checkGrammarJsxElement(node)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/checker/relater.go
Original file line number Diff line number Diff line change
Expand Up @@ -2693,7 +2693,7 @@ func (r *Relater) hasExcessProperties(source *Type, target *Type, reportErrors b
if r.errorNode == nil {
panic("No errorNode in hasExcessProperties")
}
if ast.IsJsxAttributes(r.errorNode) || isJsxOpeningLikeElement(r.errorNode) || isJsxOpeningLikeElement(r.errorNode.Parent) {
if ast.IsJsxAttributes(r.errorNode) || ast.IsJsxOpeningLikeElement(r.errorNode) || ast.IsJsxOpeningLikeElement(r.errorNode.Parent) {
// JsxAttributes has an object-literal flag and undergo same type-assignablity check as normal object-literal.
// However, using an object-literal error message will be very confusing to the users so we give different a message.
if prop.ValueDeclaration != nil && ast.IsJsxAttribute(prop.ValueDeclaration) && ast.GetSourceFileOfNode(r.errorNode) == ast.GetSourceFileOfNode(prop.ValueDeclaration.Name()) {
Expand Down
11 changes: 1 addition & 10 deletions internal/checker/utilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -1208,15 +1208,6 @@ func reverseAccessKind(a AccessKind) AccessKind {
panic("Unhandled case in reverseAccessKind")
}

func isJsxOpeningLikeElement(node *ast.Node) bool {
return ast.IsJsxOpeningElement(node) || ast.IsJsxSelfClosingElement(node)
}

// Deprecated in favor of `ast.IsObjectLiteralElement`
func isObjectLiteralElementLike(node *ast.Node) bool {
return ast.IsObjectLiteralElement(node)
}

func isInfinityOrNaNString(name string) bool {
return name == "Infinity" || name == "-Infinity" || name == "NaN"
}
Expand Down Expand Up @@ -1325,7 +1316,7 @@ func isCallChain(node *ast.Node) bool {
}

func (c *Checker) callLikeExpressionMayHaveTypeArguments(node *ast.Node) bool {
return isCallOrNewExpression(node) || ast.IsTaggedTemplateExpression(node) || isJsxOpeningLikeElement(node)
return isCallOrNewExpression(node) || ast.IsTaggedTemplateExpression(node) || ast.IsJsxOpeningLikeElement(node)
}

func isSuperCall(n *ast.Node) bool {
Expand Down
4 changes: 4 additions & 0 deletions internal/core/text.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ func (t TextRange) ContainsInclusive(pos int) bool {
return pos >= int(t.pos) && pos <= int(t.end)
}

func (t TextRange) ContainsExclusive(pos int) bool {
return int(t.pos) < pos && pos < int(t.end)
}

func (t TextRange) WithPos(pos int) TextRange {
return TextRange{pos: TextPos(pos), end: t.end}
}
Expand Down
234 changes: 234 additions & 0 deletions internal/ls/completions.go
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,15 @@ func getCompletionData(program *compiler.Program, typeChecker *checker.Checker,

if contextToken != nil {
// !!! import completions
// Bail out if this is a known invalid completion location.
// !!! if (!importStatementCompletionInfo.replacementSpan && ...)
if isCompletionListBlocker(contextToken, previousToken, location, file, position, typeChecker) {
if keywordFilters != KeywordCompletionFiltersNone {
isNewIdentifierLocation, _ := computeCommitCharactersAndIsNewIdentifier(contextToken, file, position)
return keywordCompletionData(keywordFilters, isJSOnlyLocation, isNewIdentifierLocation)
}
return nil
}

parent := contextToken.Parent
if contextToken.Kind == ast.KindDotToken || contextToken.Kind == ast.KindQuestionDotToken {
Expand Down Expand Up @@ -4200,3 +4209,228 @@ func (l *LanguageService) getLabelStatementCompletions(
}
return items
}

func isCompletionListBlocker(
contextToken *ast.Node,
previousToken *ast.Node,
location *ast.Node,
file *ast.SourceFile,
position int,
typeChecker *checker.Checker,
) bool {
return isInStringOrRegularExpressionOrTemplateLiteral(contextToken, position) ||
isSolelyIdentifierDefinitionLocation(contextToken, previousToken, file, position, typeChecker) ||
isDotOfNumericLiteral(contextToken, file) ||
isInJsxText(contextToken, location) ||
ast.IsBigIntLiteral(contextToken)
}

func isInStringOrRegularExpressionOrTemplateLiteral(contextToken *ast.Node, position int) bool {
// To be "in" one of these literals, the position has to be:
// 1. entirely within the token text.
// 2. at the end position of an unterminated token.
// 3. at the end of a regular expression (due to trailing flags like '/foo/g').
return (ast.IsRegularExpressionLiteral(contextToken) || ast.IsStringTextContainingNode(contextToken)) &&
(contextToken.Loc.ContainsExclusive(position)) ||
position == contextToken.End() &&
(ast.IsUnterminatedNode(contextToken) || ast.IsRegularExpressionLiteral(contextToken))
}

// true if we are certain that the currently edited location must define a new location; false otherwise.
func isSolelyIdentifierDefinitionLocation(
contextToken *ast.Node,
previousToken *ast.Node,
file *ast.SourceFile,
position int,
typeChecker *checker.Checker,
) bool {
parent := contextToken.Parent
containingNodeKind := parent.Kind
switch contextToken.Kind {
case ast.KindCommaToken:
return containingNodeKind == ast.KindVariableDeclaration ||
isVariableDeclarationListButNotTypeArgument(contextToken, file, typeChecker) ||
containingNodeKind == ast.KindVariableStatement ||
containingNodeKind == ast.KindEnumDeclaration || // enum a { foo, |
isFunctionLikeButNotConstructor(containingNodeKind) ||
containingNodeKind == ast.KindInterfaceDeclaration || // interface A<T, |
containingNodeKind == ast.KindArrayBindingPattern || // var [x, y|
containingNodeKind == ast.KindTypeAliasDeclaration || // type Map, K, |
// class A<T, |
// var C = class D<T, |
(ast.IsClassLike(parent) && parent.TypeParameterList() != nil && parent.TypeParameterList().End() >= contextToken.Pos())
case ast.KindDotToken:
return containingNodeKind == ast.KindArrayBindingPattern // var [.|
case ast.KindColonToken:
return containingNodeKind == ast.KindBindingElement // var {x :html|
case ast.KindOpenBracketToken:
return containingNodeKind == ast.KindArrayBindingPattern // var [x|
case ast.KindOpenParenToken:
return containingNodeKind == ast.KindCatchClause || isFunctionLikeButNotConstructor(containingNodeKind)
case ast.KindOpenBraceToken:
return containingNodeKind == ast.KindEnumDeclaration // enum a { |
case ast.KindLessThanToken:
return containingNodeKind == ast.KindClassDeclaration || // class A< |
containingNodeKind == ast.KindClassExpression || // var C = class D< |
containingNodeKind == ast.KindInterfaceDeclaration || // interface A< |
containingNodeKind == ast.KindTypeAliasDeclaration || // type List< |
ast.IsFunctionLikeKind(containingNodeKind)
case ast.KindStaticKeyword:
return containingNodeKind == ast.KindPropertyDeclaration &&
!ast.IsClassLike(parent.Parent)
case ast.KindDotDotDotToken:
return containingNodeKind == ast.KindParameter ||
(parent.Parent != nil && parent.Parent.Kind == ast.KindArrayBindingPattern) // var [...z|
case ast.KindPublicKeyword, ast.KindPrivateKeyword, ast.KindProtectedKeyword:
return containingNodeKind == ast.KindParameter && !ast.IsConstructorDeclaration(parent.Parent)
case ast.KindAsKeyword:
return containingNodeKind == ast.KindImportSpecifier ||
containingNodeKind == ast.KindExportSpecifier ||
containingNodeKind == ast.KindNamespaceImport
case ast.KindGetKeyword, ast.KindSetKeyword:
return !isFromObjectTypeDeclaration(contextToken)
case ast.KindIdentifier:
if (containingNodeKind == ast.KindImportSpecifier || containingNodeKind == ast.KindExportSpecifier) &&
contextToken == parent.Name() &&
contextToken.Text() == "type" {
// import { type | }
return false
}
ancestorVariableDeclaration := ast.FindAncestor(parent, ast.IsVariableDeclaration)
if ancestorVariableDeclaration != nil && getLineOfPosition(file, contextToken.End()) < position {
// let a
// |
return false
}
case ast.KindClassKeyword, ast.KindEnumKeyword, ast.KindInterfaceKeyword, ast.KindFunctionKeyword,
ast.KindVarKeyword, ast.KindImportKeyword, ast.KindLetKeyword, ast.KindConstKeyword, ast.KindInferKeyword:
return true
case ast.KindTypeKeyword:
// import { type foo| }
return containingNodeKind != ast.KindImportSpecifier
case ast.KindAsteriskToken:
return ast.IsFunctionLike(parent) && !ast.IsMethodDeclaration(parent)
}

// If the previous token is keyword corresponding to class member completion keyword
// there will be completion available here
if isClassMemberCompletionKeyword(keywordForNode(contextToken)) && isFromObjectTypeDeclaration(contextToken) {
return false
}

if isConstructorParameterCompletion(contextToken) {
// constructor parameter completion is available only if
// - its modifier of the constructor parameter or
// - its name of the parameter and not being edited
// eg. constructor(a |<- this shouldnt show completion
if !ast.IsIdentifier(contextToken) ||
ast.IsParameterPropertyModifier(keywordForNode(contextToken)) ||
isCurrentlyEditingNode(contextToken, file, position) {
return false
}
}

// Previous token may have been a keyword that was converted to an identifier.
switch keywordForNode(contextToken) {
case ast.KindAbstractKeyword, ast.KindClassKeyword, ast.KindConstKeyword, ast.KindDeclareKeyword,
ast.KindEnumKeyword, ast.KindFunctionKeyword, ast.KindInterfaceKeyword, ast.KindLetKeyword,
ast.KindPrivateKeyword, ast.KindProtectedKeyword, ast.KindPublicKeyword,
ast.KindStaticKeyword, ast.KindVarKeyword:
return true
case ast.KindAsyncKeyword:
return ast.IsPropertyDeclaration(contextToken.Parent)
}

// If we are inside a class declaration, and `constructor` is totally not present,
// but we request a completion manually at a whitespace...
ancestorClassLike := ast.FindAncestor(parent, ast.IsClassLike)
if ancestorClassLike != nil && contextToken == previousToken &&
isPreviousPropertyDeclarationTerminated(contextToken, file, position) {
// Don't block completions.
return false
}

ancestorPropertyDeclaration := ast.FindAncestor(parent, ast.IsPropertyDeclaration)
// If we are inside a class declaration and typing `constructor` after property declaration...
if ancestorPropertyDeclaration != nil && contextToken != previousToken &&
ast.IsClassLike(previousToken.Parent.Parent) &&
// And the cursor is at the token...
position <= previousToken.End() {
// If we are sure that the previous property declaration is terminated according to newline or semicolon...
if isPreviousPropertyDeclarationTerminated(contextToken, file, previousToken.End()) {
// Don't block completions.
return false
} else if contextToken.Kind != ast.KindEqualsToken &&
// Should not block: `class C { blah = c/**/ }`
// But should block: `class C { blah = somewhat c/**/ }` and `class C { blah: SomeType c/**/ }`
(ast.IsInitializedProperty(ancestorPropertyDeclaration) || ancestorPropertyDeclaration.Type() != nil) {
return true
}
}

return ast.IsDeclarationName(contextToken) &&
!ast.IsShorthandPropertyAssignment(parent) &&
!ast.IsJsxAttribute(parent) &&
// Don't block completions if we're in `class C /**/`, `interface I /**/` or `<T /**/>` ,
// because we're *past* the end of the identifier and might want to complete `extends`.
// If `contextToken !== previousToken`, this is `class C ex/**/`, `interface I ex/**/` or `<T ex/**/>`.
!((ast.IsClassLike(parent) || ast.IsInterfaceDeclaration(parent) || ast.IsTypeParameterDeclaration(parent)) &&
(contextToken != previousToken || position > previousToken.End()))
}

func isVariableDeclarationListButNotTypeArgument(node *ast.Node, file *ast.SourceFile, typeChecker *checker.Checker) bool {
return node.Parent.Kind == ast.KindVariableDeclarationList &&
!isPossiblyTypeArgumentPosition(node, file, typeChecker)
}

func isFunctionLikeButNotConstructor(kind ast.Kind) bool {
return ast.IsFunctionLikeKind(kind) && kind != ast.KindConstructor
}

func isPreviousPropertyDeclarationTerminated(contextToken *ast.Node, file *ast.SourceFile, position int) bool {
return contextToken.Kind != ast.KindEqualsToken &&
(contextToken.Kind == ast.KindSemicolonToken ||
getLineOfPosition(file, contextToken.End()) != getLineOfPosition(file, position))
}

func isDotOfNumericLiteral(contextToken *ast.Node, file *ast.SourceFile) bool {
if contextToken.Kind == ast.KindNumericLiteral {
text := file.Text()[contextToken.Pos():contextToken.End()]
r, _ := utf8.DecodeLastRuneInString(text)
return r == '.'
}

return false
}

func isInJsxText(contextToken *ast.Node, location *ast.Node) bool {
if contextToken.Kind == ast.KindJsxText {
return true
}

if contextToken.Kind == ast.KindGreaterThanToken && contextToken.Parent != nil {
// <Component<string> /**/ />
// <Component<string> /**/ ><Component>
// - contextToken: GreaterThanToken (before cursor)
// - location: JsxSelfClosingElement or JsxOpeningElement
// - contextToken.parent === location
if location == contextToken.Parent && ast.IsJsxOpeningLikeElement(location) {
return false
}

if contextToken.Parent.Kind == ast.KindJsxOpeningElement {
// <div>/**/
// - contextToken: GreaterThanToken (before cursor)
// - location: JSXElement
// - different parents (JSXOpeningElement, JSXElement)
return location.Parent.Kind != ast.KindJsxOpeningElement
}

if contextToken.Parent.Kind == ast.KindJsxClosingElement ||
contextToken.Parent.Kind == ast.KindJsxSelfClosingElement {
return contextToken.Parent.Parent != nil && contextToken.Parent.Parent.Kind == ast.KindJsxElement
}
}

return false
}
Loading