Skip to content

Commit 6f5e2b9

Browse files
authored
Block completions in invalid positions (microsoft#903)
1 parent fa9414a commit 6f5e2b9

File tree

7 files changed

+395
-6
lines changed

7 files changed

+395
-6
lines changed

internal/ast/ast.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5529,6 +5529,10 @@ func (node *RegularExpressionLiteral) Clone(f NodeFactoryCoercible) *Node {
55295529
return cloneNode(f.AsNodeFactory().NewRegularExpressionLiteral(node.Text), node.AsNode(), f.AsNodeFactory().hooks)
55305530
}
55315531

5532+
func IsRegularExpressionLiteral(node *Node) bool {
5533+
return node.Kind == KindRegularExpressionLiteral
5534+
}
5535+
55325536
// NoSubstitutionTemplateLiteral
55335537

55345538
type NoSubstitutionTemplateLiteral struct {

internal/ast/utilities.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3461,3 +3461,13 @@ func IndexOfNode(nodes []*Node, node *Node) int {
34613461
func compareNodePositions(n1, n2 *Node) int {
34623462
return n1.Pos() - n2.Pos()
34633463
}
3464+
3465+
func IsUnterminatedNode(node *Node) bool {
3466+
return IsLiteralKind(node.Kind) && IsUnterminatedLiteral(node)
3467+
}
3468+
3469+
// Gets a value indicating whether a class element is either a static or an instance property declaration with an initializer.
3470+
func IsInitializedProperty(member *ClassElement) bool {
3471+
return member.Kind == KindPropertyDeclaration &&
3472+
member.Initializer() != nil
3473+
}

internal/checker/utilities.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1174,11 +1174,6 @@ func reverseAccessKind(a AccessKind) AccessKind {
11741174
panic("Unhandled case in reverseAccessKind")
11751175
}
11761176

1177-
// Deprecated in favor of `ast.IsObjectLiteralElement`
1178-
func isObjectLiteralElementLike(node *ast.Node) bool {
1179-
return ast.IsObjectLiteralElement(node)
1180-
}
1181-
11821177
func isInfinityOrNaNString(name string) bool {
11831178
return name == "Infinity" || name == "-Infinity" || name == "NaN"
11841179
}

internal/core/text.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ func (t TextRange) ContainsInclusive(pos int) bool {
4343
return pos >= int(t.pos) && pos <= int(t.end)
4444
}
4545

46+
func (t TextRange) ContainsExclusive(pos int) bool {
47+
return int(t.pos) < pos && pos < int(t.end)
48+
}
49+
4650
func (t TextRange) WithPos(pos int) TextRange {
4751
return TextRange{pos: TextPos(pos), end: t.end}
4852
}

internal/ls/completions.go

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,15 @@ func getCompletionData(program *compiler.Program, typeChecker *checker.Checker,
385385

386386
if contextToken != nil {
387387
// !!! import completions
388+
// Bail out if this is a known invalid completion location.
389+
// !!! if (!importStatementCompletionInfo.replacementSpan && ...)
390+
if isCompletionListBlocker(contextToken, previousToken, location, file, position, typeChecker) {
391+
if keywordFilters != KeywordCompletionFiltersNone {
392+
isNewIdentifierLocation, _ := computeCommitCharactersAndIsNewIdentifier(contextToken, file, position)
393+
return keywordCompletionData(keywordFilters, isJSOnlyLocation, isNewIdentifierLocation)
394+
}
395+
return nil
396+
}
388397

389398
parent := contextToken.Parent
390399
if contextToken.Kind == ast.KindDotToken || contextToken.Kind == ast.KindQuestionDotToken {
@@ -4200,3 +4209,228 @@ func (l *LanguageService) getLabelStatementCompletions(
42004209
}
42014210
return items
42024211
}
4212+
4213+
func isCompletionListBlocker(
4214+
contextToken *ast.Node,
4215+
previousToken *ast.Node,
4216+
location *ast.Node,
4217+
file *ast.SourceFile,
4218+
position int,
4219+
typeChecker *checker.Checker,
4220+
) bool {
4221+
return isInStringOrRegularExpressionOrTemplateLiteral(contextToken, position) ||
4222+
isSolelyIdentifierDefinitionLocation(contextToken, previousToken, file, position, typeChecker) ||
4223+
isDotOfNumericLiteral(contextToken, file) ||
4224+
isInJsxText(contextToken, location) ||
4225+
ast.IsBigIntLiteral(contextToken)
4226+
}
4227+
4228+
func isInStringOrRegularExpressionOrTemplateLiteral(contextToken *ast.Node, position int) bool {
4229+
// To be "in" one of these literals, the position has to be:
4230+
// 1. entirely within the token text.
4231+
// 2. at the end position of an unterminated token.
4232+
// 3. at the end of a regular expression (due to trailing flags like '/foo/g').
4233+
return (ast.IsRegularExpressionLiteral(contextToken) || ast.IsStringTextContainingNode(contextToken)) &&
4234+
(contextToken.Loc.ContainsExclusive(position)) ||
4235+
position == contextToken.End() &&
4236+
(ast.IsUnterminatedNode(contextToken) || ast.IsRegularExpressionLiteral(contextToken))
4237+
}
4238+
4239+
// true if we are certain that the currently edited location must define a new location; false otherwise.
4240+
func isSolelyIdentifierDefinitionLocation(
4241+
contextToken *ast.Node,
4242+
previousToken *ast.Node,
4243+
file *ast.SourceFile,
4244+
position int,
4245+
typeChecker *checker.Checker,
4246+
) bool {
4247+
parent := contextToken.Parent
4248+
containingNodeKind := parent.Kind
4249+
switch contextToken.Kind {
4250+
case ast.KindCommaToken:
4251+
return containingNodeKind == ast.KindVariableDeclaration ||
4252+
isVariableDeclarationListButNotTypeArgument(contextToken, file, typeChecker) ||
4253+
containingNodeKind == ast.KindVariableStatement ||
4254+
containingNodeKind == ast.KindEnumDeclaration || // enum a { foo, |
4255+
isFunctionLikeButNotConstructor(containingNodeKind) ||
4256+
containingNodeKind == ast.KindInterfaceDeclaration || // interface A<T, |
4257+
containingNodeKind == ast.KindArrayBindingPattern || // var [x, y|
4258+
containingNodeKind == ast.KindTypeAliasDeclaration || // type Map, K, |
4259+
// class A<T, |
4260+
// var C = class D<T, |
4261+
(ast.IsClassLike(parent) && parent.TypeParameterList() != nil && parent.TypeParameterList().End() >= contextToken.Pos())
4262+
case ast.KindDotToken:
4263+
return containingNodeKind == ast.KindArrayBindingPattern // var [.|
4264+
case ast.KindColonToken:
4265+
return containingNodeKind == ast.KindBindingElement // var {x :html|
4266+
case ast.KindOpenBracketToken:
4267+
return containingNodeKind == ast.KindArrayBindingPattern // var [x|
4268+
case ast.KindOpenParenToken:
4269+
return containingNodeKind == ast.KindCatchClause || isFunctionLikeButNotConstructor(containingNodeKind)
4270+
case ast.KindOpenBraceToken:
4271+
return containingNodeKind == ast.KindEnumDeclaration // enum a { |
4272+
case ast.KindLessThanToken:
4273+
return containingNodeKind == ast.KindClassDeclaration || // class A< |
4274+
containingNodeKind == ast.KindClassExpression || // var C = class D< |
4275+
containingNodeKind == ast.KindInterfaceDeclaration || // interface A< |
4276+
containingNodeKind == ast.KindTypeAliasDeclaration || // type List< |
4277+
ast.IsFunctionLikeKind(containingNodeKind)
4278+
case ast.KindStaticKeyword:
4279+
return containingNodeKind == ast.KindPropertyDeclaration &&
4280+
!ast.IsClassLike(parent.Parent)
4281+
case ast.KindDotDotDotToken:
4282+
return containingNodeKind == ast.KindParameter ||
4283+
(parent.Parent != nil && parent.Parent.Kind == ast.KindArrayBindingPattern) // var [...z|
4284+
case ast.KindPublicKeyword, ast.KindPrivateKeyword, ast.KindProtectedKeyword:
4285+
return containingNodeKind == ast.KindParameter && !ast.IsConstructorDeclaration(parent.Parent)
4286+
case ast.KindAsKeyword:
4287+
return containingNodeKind == ast.KindImportSpecifier ||
4288+
containingNodeKind == ast.KindExportSpecifier ||
4289+
containingNodeKind == ast.KindNamespaceImport
4290+
case ast.KindGetKeyword, ast.KindSetKeyword:
4291+
return !isFromObjectTypeDeclaration(contextToken)
4292+
case ast.KindIdentifier:
4293+
if (containingNodeKind == ast.KindImportSpecifier || containingNodeKind == ast.KindExportSpecifier) &&
4294+
contextToken == parent.Name() &&
4295+
contextToken.Text() == "type" {
4296+
// import { type | }
4297+
return false
4298+
}
4299+
ancestorVariableDeclaration := ast.FindAncestor(parent, ast.IsVariableDeclaration)
4300+
if ancestorVariableDeclaration != nil && getLineOfPosition(file, contextToken.End()) < position {
4301+
// let a
4302+
// |
4303+
return false
4304+
}
4305+
case ast.KindClassKeyword, ast.KindEnumKeyword, ast.KindInterfaceKeyword, ast.KindFunctionKeyword,
4306+
ast.KindVarKeyword, ast.KindImportKeyword, ast.KindLetKeyword, ast.KindConstKeyword, ast.KindInferKeyword:
4307+
return true
4308+
case ast.KindTypeKeyword:
4309+
// import { type foo| }
4310+
return containingNodeKind != ast.KindImportSpecifier
4311+
case ast.KindAsteriskToken:
4312+
return ast.IsFunctionLike(parent) && !ast.IsMethodDeclaration(parent)
4313+
}
4314+
4315+
// If the previous token is keyword corresponding to class member completion keyword
4316+
// there will be completion available here
4317+
if isClassMemberCompletionKeyword(keywordForNode(contextToken)) && isFromObjectTypeDeclaration(contextToken) {
4318+
return false
4319+
}
4320+
4321+
if isConstructorParameterCompletion(contextToken) {
4322+
// constructor parameter completion is available only if
4323+
// - its modifier of the constructor parameter or
4324+
// - its name of the parameter and not being edited
4325+
// eg. constructor(a |<- this shouldnt show completion
4326+
if !ast.IsIdentifier(contextToken) ||
4327+
ast.IsParameterPropertyModifier(keywordForNode(contextToken)) ||
4328+
isCurrentlyEditingNode(contextToken, file, position) {
4329+
return false
4330+
}
4331+
}
4332+
4333+
// Previous token may have been a keyword that was converted to an identifier.
4334+
switch keywordForNode(contextToken) {
4335+
case ast.KindAbstractKeyword, ast.KindClassKeyword, ast.KindConstKeyword, ast.KindDeclareKeyword,
4336+
ast.KindEnumKeyword, ast.KindFunctionKeyword, ast.KindInterfaceKeyword, ast.KindLetKeyword,
4337+
ast.KindPrivateKeyword, ast.KindProtectedKeyword, ast.KindPublicKeyword,
4338+
ast.KindStaticKeyword, ast.KindVarKeyword:
4339+
return true
4340+
case ast.KindAsyncKeyword:
4341+
return ast.IsPropertyDeclaration(contextToken.Parent)
4342+
}
4343+
4344+
// If we are inside a class declaration, and `constructor` is totally not present,
4345+
// but we request a completion manually at a whitespace...
4346+
ancestorClassLike := ast.FindAncestor(parent, ast.IsClassLike)
4347+
if ancestorClassLike != nil && contextToken == previousToken &&
4348+
isPreviousPropertyDeclarationTerminated(contextToken, file, position) {
4349+
// Don't block completions.
4350+
return false
4351+
}
4352+
4353+
ancestorPropertyDeclaration := ast.FindAncestor(parent, ast.IsPropertyDeclaration)
4354+
// If we are inside a class declaration and typing `constructor` after property declaration...
4355+
if ancestorPropertyDeclaration != nil && contextToken != previousToken &&
4356+
ast.IsClassLike(previousToken.Parent.Parent) &&
4357+
// And the cursor is at the token...
4358+
position <= previousToken.End() {
4359+
// If we are sure that the previous property declaration is terminated according to newline or semicolon...
4360+
if isPreviousPropertyDeclarationTerminated(contextToken, file, previousToken.End()) {
4361+
// Don't block completions.
4362+
return false
4363+
} else if contextToken.Kind != ast.KindEqualsToken &&
4364+
// Should not block: `class C { blah = c/**/ }`
4365+
// But should block: `class C { blah = somewhat c/**/ }` and `class C { blah: SomeType c/**/ }`
4366+
(ast.IsInitializedProperty(ancestorPropertyDeclaration) || ancestorPropertyDeclaration.Type() != nil) {
4367+
return true
4368+
}
4369+
}
4370+
4371+
return ast.IsDeclarationName(contextToken) &&
4372+
!ast.IsShorthandPropertyAssignment(parent) &&
4373+
!ast.IsJsxAttribute(parent) &&
4374+
// Don't block completions if we're in `class C /**/`, `interface I /**/` or `<T /**/>` ,
4375+
// because we're *past* the end of the identifier and might want to complete `extends`.
4376+
// If `contextToken !== previousToken`, this is `class C ex/**/`, `interface I ex/**/` or `<T ex/**/>`.
4377+
!((ast.IsClassLike(parent) || ast.IsInterfaceDeclaration(parent) || ast.IsTypeParameterDeclaration(parent)) &&
4378+
(contextToken != previousToken || position > previousToken.End()))
4379+
}
4380+
4381+
func isVariableDeclarationListButNotTypeArgument(node *ast.Node, file *ast.SourceFile, typeChecker *checker.Checker) bool {
4382+
return node.Parent.Kind == ast.KindVariableDeclarationList &&
4383+
!isPossiblyTypeArgumentPosition(node, file, typeChecker)
4384+
}
4385+
4386+
func isFunctionLikeButNotConstructor(kind ast.Kind) bool {
4387+
return ast.IsFunctionLikeKind(kind) && kind != ast.KindConstructor
4388+
}
4389+
4390+
func isPreviousPropertyDeclarationTerminated(contextToken *ast.Node, file *ast.SourceFile, position int) bool {
4391+
return contextToken.Kind != ast.KindEqualsToken &&
4392+
(contextToken.Kind == ast.KindSemicolonToken ||
4393+
getLineOfPosition(file, contextToken.End()) != getLineOfPosition(file, position))
4394+
}
4395+
4396+
func isDotOfNumericLiteral(contextToken *ast.Node, file *ast.SourceFile) bool {
4397+
if contextToken.Kind == ast.KindNumericLiteral {
4398+
text := file.Text()[contextToken.Pos():contextToken.End()]
4399+
r, _ := utf8.DecodeLastRuneInString(text)
4400+
return r == '.'
4401+
}
4402+
4403+
return false
4404+
}
4405+
4406+
func isInJsxText(contextToken *ast.Node, location *ast.Node) bool {
4407+
if contextToken.Kind == ast.KindJsxText {
4408+
return true
4409+
}
4410+
4411+
if contextToken.Kind == ast.KindGreaterThanToken && contextToken.Parent != nil {
4412+
// <Component<string> /**/ />
4413+
// <Component<string> /**/ ><Component>
4414+
// - contextToken: GreaterThanToken (before cursor)
4415+
// - location: JsxSelfClosingElement or JsxOpeningElement
4416+
// - contextToken.parent === location
4417+
if location == contextToken.Parent && ast.IsJsxOpeningLikeElement(location) {
4418+
return false
4419+
}
4420+
4421+
if contextToken.Parent.Kind == ast.KindJsxOpeningElement {
4422+
// <div>/**/
4423+
// - contextToken: GreaterThanToken (before cursor)
4424+
// - location: JSXElement
4425+
// - different parents (JSXOpeningElement, JSXElement)
4426+
return location.Parent.Kind != ast.KindJsxOpeningElement
4427+
}
4428+
4429+
if contextToken.Parent.Kind == ast.KindJsxClosingElement ||
4430+
contextToken.Parent.Kind == ast.KindJsxSelfClosingElement {
4431+
return contextToken.Parent.Parent != nil && contextToken.Parent.Parent.Kind == ast.KindJsxElement
4432+
}
4433+
}
4434+
4435+
return false
4436+
}

0 commit comments

Comments
 (0)