diff --git a/internal/ast/ast.go b/internal/ast/ast.go index 7fcdea6133..225a85010e 100644 --- a/internal/ast/ast.go +++ b/internal/ast/ast.go @@ -779,6 +779,57 @@ func (n *Node) ModuleSpecifier() *Expression { panic("Unhandled case in Node.ModuleSpecifier: " + n.Kind.String()) } +func (n *Node) Statement() *Statement { + switch n.Kind { + case KindDoStatement: + return n.AsDoStatement().Statement + case KindWhileStatement: + return n.AsWhileStatement().Statement + case KindForStatement: + return n.AsForStatement().Statement + case KindForInStatement, KindForOfStatement: + return n.AsForInOrOfStatement().Statement + } + panic("Unhandled case in Node.Statement: " + n.Kind.String()) +} + +func (n *Node) PropertyList() *NodeList { + switch n.Kind { + case KindObjectLiteralExpression: + return n.AsObjectLiteralExpression().Properties + case KindJsxAttributes: + return n.AsJsxAttributes().Properties + } + panic("Unhandled case in Node.PropertyList: " + n.Kind.String()) +} + +func (n *Node) Properties() []*Node { + list := n.PropertyList() + if list != nil { + return list.Nodes + } + return nil +} + +func (n *Node) ElementList() *NodeList { + switch n.Kind { + case KindNamedImports: + return n.AsNamedImports().Elements + case KindNamedExports: + return n.AsNamedExports().Elements + } + + panic("Unhandled case in Node.ElementList: " + n.Kind.String()) +} + +func (n *Node) Elements() []*Node { + list := n.ElementList() + if list != nil { + return list.Nodes + } + return nil +} + // Determines if `n` contains `descendant` by walking up the `Parent` pointers from `descendant`. This method panics if // `descendant` or one of its ancestors is not parented except when that node is a `SourceFile`. func (n *Node) Contains(descendant *Node) bool { @@ -1693,6 +1744,11 @@ type ( AnyValidImportOrReExport = Node // (ImportDeclaration | ExportDeclaration | JSDocImportTag) & { moduleSpecifier: StringLiteral } | ImportEqualsDeclaration & { moduleReference: ExternalModuleReference & { expression: StringLiteral }} | RequireOrImportCall | ValidImportTypeNode ValidImportTypeNode = Node // ImportTypeNode & { argument: LiteralTypeNode & { literal: StringLiteral } } NumericOrStringLikeLiteral = Node // StringLiteralLike | NumericLiteral + TypeOnlyImportDeclaration = Node // ImportClause | ImportEqualsDeclaration | ImportSpecifier | NamespaceImport with isTypeOnly: true + ObjectLiteralLike = Node // ObjectLiteralExpression | ObjectBindingPattern + ObjectTypeDeclaration = Node // ClassLikeDeclaration | InterfaceDeclaration | TypeLiteralNode + JsxOpeningLikeElement = Node // JsxOpeningElement | JsxSelfClosingElement + NamedImportsOrExports = Node // NamedImports | NamedExports ) // Aliases for node singletons @@ -1735,6 +1791,10 @@ type ( JsxClosingFragmentNode = Node SourceFileNode = Node PropertyAccessExpressionNode = Node + TypeLiteral = Node + ObjectLiteralExpressionNode = Node + ConstructorDeclarationNode = Node + NamedExportsNode = Node ) type ( @@ -6939,6 +6999,10 @@ func (node *IntersectionTypeNode) Clone(f NodeFactoryCoercible) *Node { return cloneNode(f.AsNodeFactory().NewIntersectionTypeNode(node.Types), node.AsNode(), f.AsNodeFactory().hooks) } +func IsIntersectionTypeNode(node *Node) bool { + return node.Kind == KindIntersectionType +} + // ConditionalTypeNode type ConditionalTypeNode struct { diff --git a/internal/ast/utilities.go b/internal/ast/utilities.go index 56e7d3bdae..36cd8b764c 100644 --- a/internal/ast/utilities.go +++ b/internal/ast/utilities.go @@ -1527,14 +1527,14 @@ func GetExtendsHeritageClauseElement(node *Node) *ExpressionWithTypeArgumentsNod } func GetExtendsHeritageClauseElements(node *Node) []*ExpressionWithTypeArgumentsNode { - return getHeritageElements(node, KindExtendsKeyword) + return GetHeritageElements(node, KindExtendsKeyword) } func GetImplementsHeritageClauseElements(node *Node) []*ExpressionWithTypeArgumentsNode { - return getHeritageElements(node, KindImplementsKeyword) + return GetHeritageElements(node, KindImplementsKeyword) } -func getHeritageElements(node *Node, kind Kind) []*Node { +func GetHeritageElements(node *Node, kind Kind) []*Node { clause := getHeritageClause(node, kind) if clause != nil { return clause.AsHeritageClause().Types.Nodes @@ -2793,3 +2793,135 @@ func ForEachChildAndJSDoc(node *Node, sourceFile *SourceFile, v Visitor) bool { } return node.ForEachChild(v) } + +func IsTypeReferenceType(node *Node) bool { + return node.Kind == KindTypeReference || node.Kind == KindExpressionWithTypeArguments +} + +func IsVariableLike(node *Node) bool { + switch node.Kind { + case KindBindingElement, KindEnumMember, KindParameter, KindPropertyAssignment, KindPropertyDeclaration, + KindPropertySignature, KindShorthandPropertyAssignment, KindVariableDeclaration: + return true + } + return false +} + +func HasInitializer(node *Node) bool { + switch node.Kind { + case KindVariableDeclaration, KindParameter, KindBindingElement, KindPropertyDeclaration, + KindPropertyAssignment, KindEnumMember, KindForStatement, KindForInStatement, KindForOfStatement, + KindJsxAttribute: + return node.Initializer() != nil + default: + return false + } +} + +func GetTypeAnnotationNode(node *Node) *TypeNode { + switch node.Kind { + case KindVariableDeclaration, KindParameter, KindPropertySignature, KindPropertyDeclaration, + KindTypePredicate, KindParenthesizedType, KindTypeOperator, KindMappedType, KindTypeAssertionExpression, + KindAsExpression, KindSatisfiesExpression, KindTypeAliasDeclaration, KindJSTypeAliasDeclaration, + KindNamedTupleMember, KindOptionalType, KindRestType, KindTemplateLiteralTypeSpan, KindJSDocTypeExpression, + KindJSDocPropertyTag, KindJSDocNullableType, KindJSDocNonNullableType, KindJSDocOptionalType: + return node.Type() + default: + funcLike := node.FunctionLikeData() + if funcLike != nil { + return funcLike.Type + } + return nil + } +} + +func IsObjectTypeDeclaration(node *Node) bool { + return IsClassLike(node) || IsInterfaceDeclaration(node) || IsTypeLiteralNode(node) +} + +func IsClassOrTypeElement(node *Node) bool { + return IsClassElement(node) || IsTypeElement(node) +} + +func GetClassExtendsHeritageElement(node *Node) *ExpressionWithTypeArgumentsNode { + heritageElements := GetHeritageElements(node, KindExtendsKeyword) + if len(heritageElements) > 0 { + return heritageElements[0] + } + return nil +} + +func GetImplementsTypeNodes(node *Node) []*ExpressionWithTypeArgumentsNode { + return GetHeritageElements(node, KindImplementsKeyword) +} + +func IsTypeKeywordToken(node *Node) bool { + return node.Kind == KindTypeKeyword +} + +// If node is a single comment JSDoc, we do not visit the comment node list. +func IsJSDocSingleCommentNodeList(parent *Node, nodeList *NodeList) bool { + return IsJSDocSingleCommentNode(parent) && nodeList == parent.AsJSDoc().Comment +} + +func IsJSDocSingleCommentNode(node *Node) bool { + return node.Kind == KindJSDoc && node.AsJSDoc().Comment != nil && len(node.AsJSDoc().Comment.Nodes) == 1 +} + +func IsValidTypeOnlyAliasUseSite(useSite *Node) bool { + return useSite.Flags&NodeFlagsAmbient != 0 || + IsPartOfTypeQuery(useSite) || + isIdentifierInNonEmittingHeritageClause(useSite) || + isPartOfPossiblyValidTypeOrAbstractComputedPropertyName(useSite) || + !(IsExpressionNode(useSite) || isShorthandPropertyNameUseSite(useSite)) +} + +func isIdentifierInNonEmittingHeritageClause(node *Node) bool { + if !IsIdentifier(node) { + return false + } + parent := node.Parent + for IsPropertyAccessExpression(parent) || IsExpressionWithTypeArguments(parent) { + parent = parent.Parent + } + return IsHeritageClause(parent) && (parent.AsHeritageClause().Token == KindImplementsKeyword || IsInterfaceDeclaration(parent.Parent)) +} + +func isPartOfPossiblyValidTypeOrAbstractComputedPropertyName(node *Node) bool { + for NodeKindIs(node, KindIdentifier, KindPropertyAccessExpression) { + node = node.Parent + } + if node.Kind != KindComputedPropertyName { + return false + } + if HasSyntacticModifier(node.Parent, ModifierFlagsAbstract) { + return true + } + return NodeKindIs(node.Parent.Parent, KindInterfaceDeclaration, KindTypeLiteral) +} + +func isShorthandPropertyNameUseSite(useSite *Node) bool { + return IsIdentifier(useSite) && IsShorthandPropertyAssignment(useSite.Parent) && useSite.Parent.AsShorthandPropertyAssignment().Name() == useSite +} + +func GetPropertyNameForPropertyNameNode(name *Node) string { + switch name.Kind { + case KindIdentifier, KindPrivateIdentifier, KindStringLiteral, KindNoSubstitutionTemplateLiteral, + KindNumericLiteral, KindBigIntLiteral, KindJsxNamespacedName: + return name.Text() + case KindComputedPropertyName: + nameExpression := name.AsComputedPropertyName().Expression + if IsStringOrNumericLiteralLike(nameExpression) { + return nameExpression.Text() + } + if IsSignedNumericLiteral(nameExpression) { + text := nameExpression.AsPrefixUnaryExpression().Operand.Text() + if nameExpression.AsPrefixUnaryExpression().Operator == KindMinusToken { + text = "-" + text + } + return text + } + return InternalSymbolNameMissing + } + panic("Unhandled case in getPropertyNameForPropertyNameNode") +} diff --git a/internal/astnav/tokens.go b/internal/astnav/tokens.go index b70f2de7f9..e332874f2c 100644 --- a/internal/astnav/tokens.go +++ b/internal/astnav/tokens.go @@ -121,20 +121,10 @@ func getTokenAtPosition( return nodeList } - nodeVisitor := ast.NewNodeVisitor(core.Identity, nil, ast.NodeVisitorHooks{ - VisitNode: visitNode, - VisitToken: visitNode, - VisitNodes: visitNodeList, - VisitModifiers: func(modifiers *ast.ModifierList, visitor *ast.NodeVisitor) *ast.ModifierList { - if modifiers != nil { - visitNodeList(&modifiers.NodeList, visitor) - } - return modifiers - }, - }) + nodeVisitor := getNodeVisitor(visitNode, visitNodeList) for { - visitEachChildAndJSDoc(current, sourceFile, nodeVisitor) + VisitEachChildAndJSDoc(current, sourceFile, nodeVisitor) // If prevSubtree was set on the last iteration, it ends at the target position. // Check if the rightmost token of prevSubtree should be returned based on the // `includePrecedingTokenAtEndPosition` callback. @@ -203,26 +193,15 @@ func findRightmostNode(node *ast.Node) *ast.Node { } return node } - visitor := ast.NewNodeVisitor(core.Identity, nil, ast.NodeVisitorHooks{ - VisitNode: visitNode, - VisitToken: visitNode, - VisitNodes: func(nodeList *ast.NodeList, visitor *ast.NodeVisitor) *ast.NodeList { - if nodeList != nil { - if rightmost := ast.FindLastVisibleNode(nodeList.Nodes); rightmost != nil { - next = rightmost - } - } - return nodeList - }, - VisitModifiers: func(modifiers *ast.ModifierList, visitor *ast.NodeVisitor) *ast.ModifierList { - if modifiers != nil { - if rightmost := ast.FindLastVisibleNode(modifiers.Nodes); rightmost != nil { - next = rightmost - } + visitNodes := func(nodeList *ast.NodeList, visitor *ast.NodeVisitor) *ast.NodeList { + if nodeList != nil { + if rightmost := ast.FindLastVisibleNode(nodeList.Nodes); rightmost != nil { + next = rightmost } - return modifiers - }, - }) + } + return nodeList + } + visitor := getNodeVisitor(visitNode, visitNodes) for { current.VisitEachChild(visitor) @@ -234,7 +213,7 @@ func findRightmostNode(node *ast.Node) *ast.Node { } } -func visitEachChildAndJSDoc(node *ast.Node, sourceFile *ast.SourceFile, visitor *ast.NodeVisitor) { +func VisitEachChildAndJSDoc(node *ast.Node, sourceFile *ast.SourceFile, visitor *ast.NodeVisitor) { if node.Flags&ast.NodeFlagsHasJSDoc != 0 { for _, jsdoc := range node.JSDoc(sourceFile) { if visitor.Hooks.VisitNode != nil { @@ -292,7 +271,7 @@ func FindPrecedingTokenEx(sourceFile *ast.SourceFile, position int, startNode *a } if nodeList != nil && len(nodeList.Nodes) > 0 { nodes := nodeList.Nodes - if isJSDocSingleCommentNodeList(n, nodeList) { + if ast.IsJSDocSingleCommentNodeList(n, nodeList) { return nodeList } index, match := core.BinarySearchUniqueFunc(nodes, func(middle int, _ *ast.Node) int { @@ -325,18 +304,8 @@ func FindPrecedingTokenEx(sourceFile *ast.SourceFile, position int, startNode *a } return nodeList } - nodeVisitor := ast.NewNodeVisitor(core.Identity, nil, ast.NodeVisitorHooks{ - VisitNode: visitNode, - VisitToken: visitNode, - VisitNodes: visitNodes, - VisitModifiers: func(modifiers *ast.ModifierList, visitor *ast.NodeVisitor) *ast.ModifierList { - if modifiers != nil { - visitNodes(&modifiers.NodeList, visitor) - } - return modifiers - }, - }) - visitEachChildAndJSDoc(n, sourceFile, nodeVisitor) + nodeVisitor := getNodeVisitor(visitNode, visitNodes) + VisitEachChildAndJSDoc(n, sourceFile, nodeVisitor) if foundChild != nil { // Note that the span of a node's tokens is [getStartOfNode(node, ...), node.end). @@ -407,11 +376,6 @@ func GetStartOfNode(node *ast.Node, file *ast.SourceFile, includeJSDoc bool) int return scanner.GetTokenPosOfNode(node, file, includeJSDoc) } -// If this is a single comment JSDoc, we do not visit the comment node. -func isJSDocSingleCommentNodeList(parent *ast.Node, nodeList *ast.NodeList) bool { - return parent.Kind == ast.KindJSDoc && nodeList == parent.AsJSDoc().Comment && nodeList != nil && len(nodeList.Nodes) == 1 -} - // Looks for rightmost valid token in the range [startPos, endPos). // If position is >= 0, looks for rightmost valid token that precedes or touches that position. func findRightmostValidToken(endPos int, sourceFile *ast.SourceFile, containingNode *ast.Node, position int, excludeJSDoc bool) *ast.Node { @@ -452,7 +416,7 @@ func findRightmostValidToken(endPos int, sourceFile *ast.SourceFile, containingN } visitNodes := func(nodeList *ast.NodeList, _ *ast.NodeVisitor) *ast.NodeList { if nodeList != nil && len(nodeList.Nodes) > 0 { - if isJSDocSingleCommentNodeList(n, nodeList) { + if ast.IsJSDocSingleCommentNodeList(n, nodeList) { return nodeList } hasChildren = true @@ -482,18 +446,8 @@ func findRightmostValidToken(endPos int, sourceFile *ast.SourceFile, containingN } return nodeList } - nodeVisitor := ast.NewNodeVisitor(core.Identity, nil, ast.NodeVisitorHooks{ - VisitNode: visitNode, - VisitToken: visitNode, - VisitNodes: visitNodes, - VisitModifiers: func(modifiers *ast.ModifierList, visitor *ast.NodeVisitor) *ast.ModifierList { - if modifiers != nil { - visitNodes(&modifiers.NodeList, visitor) - } - return modifiers - }, - }) - visitEachChildAndJSDoc(n, sourceFile, nodeVisitor) + nodeVisitor := getNodeVisitor(visitNode, visitNodes) + VisitEachChildAndJSDoc(n, sourceFile, nodeVisitor) // Three cases: // 1. The answer is a token of `rightmostValidNode`. @@ -526,6 +480,7 @@ func findRightmostValidToken(endPos int, sourceFile *ast.SourceFile, containingN } startPos = visitedNode.End() scanner.ResetPos(startPos) + scanner.Scan() } // Trailing tokens after last visited node. for startPos < min(endPos, position) { @@ -564,7 +519,86 @@ func findRightmostValidToken(endPos int, sourceFile *ast.SourceFile, containingN return find(containingNode, endPos) } -// !!! func FindNextToken(previousToken *ast.Node, parent *ast.Node, file *ast.SourceFile) *ast.Node { - return nil + var find func(n *ast.Node) *ast.Node + find = func(n *ast.Node) *ast.Node { + if ast.IsTokenKind(n.Kind) && n.Pos() == previousToken.End() { + // this is token that starts at the end of previous token - return it + return n + } + // Node that contains `previousToken` or occurs immediately after it. + var foundNode *ast.Node + visitNode := func(node *ast.Node, _ *ast.NodeVisitor) *ast.Node { + if node != nil && node.Flags&ast.NodeFlagsReparsed == 0 && + node.Pos() <= previousToken.End() && node.End() > previousToken.End() { + foundNode = node + } + return node + } + visitNodes := func(nodeList *ast.NodeList, _ *ast.NodeVisitor) *ast.NodeList { + if nodeList != nil && len(nodeList.Nodes) > 0 && foundNode == nil { + nodes := nodeList.Nodes + index, match := core.BinarySearchUniqueFunc(nodes, func(_ int, node *ast.Node) int { + if node.Flags&ast.NodeFlagsReparsed != 0 { + return comparisonLessThan + } + if node.Pos() > previousToken.End() { + return comparisonGreaterThan + } + if node.End() <= previousToken.Pos() { + return comparisonLessThan + } + return comparisonEqualTo + }) + if match { + foundNode = nodes[index] + } + } + return nodeList + } + nodeVisitor := getNodeVisitor(visitNode, visitNodes) + VisitEachChildAndJSDoc(n, file, nodeVisitor) + // Cases: + // 1. no answer exists + // 2. answer is an unvisited token + // 3. answer is in the visited found node + + // Case 3: look for the next token inside the found node. + if foundNode != nil { + return find(foundNode) + } + startPos := previousToken.End() + // Case 2: look for the next token directly. + if startPos >= n.Pos() && startPos < n.End() { + scanner := scanner.GetScannerForSourceFile(file, startPos) + token := scanner.Token() + tokenFullStart := scanner.TokenFullStart() + tokenStart := scanner.TokenStart() + tokenEnd := scanner.TokenEnd() + if tokenStart == previousToken.End() { + return file.GetOrCreateToken(token, tokenFullStart, tokenEnd, n) + } + panic(fmt.Sprintf("Expected to find next token at %d, got token %s at %d", previousToken.End(), token, tokenStart)) + } + // Case 3: no answer. + return nil + } + return find(parent) +} + +func getNodeVisitor( + visitNode func(*ast.Node, *ast.NodeVisitor) *ast.Node, + visitNodes func(*ast.NodeList, *ast.NodeVisitor) *ast.NodeList, +) *ast.NodeVisitor { + return ast.NewNodeVisitor(core.Identity, nil, ast.NodeVisitorHooks{ + VisitNode: visitNode, + VisitToken: visitNode, + VisitNodes: visitNodes, + VisitModifiers: func(modifiers *ast.ModifierList, visitor *ast.NodeVisitor) *ast.ModifierList { + if modifiers != nil { + visitNodes(&modifiers.NodeList, visitor) + } + return modifiers + }, + }) } diff --git a/internal/checker/checker.go b/internal/checker/checker.go index 4ce18ebc0c..993804de3d 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -1711,7 +1711,7 @@ func (c *Checker) onSuccessfullyResolvedSymbol(errorLocation *ast.Node, result * c.error(errorLocation, diagnostics.Parameter_0_cannot_reference_identifier_1_declared_after_it, scanner.DeclarationNameToString(associatedDeclarationForContainingInitializerOrBindingName.Name()), scanner.DeclarationNameToString(errorLocation)) } } - if errorLocation != nil && meaning&ast.SymbolFlagsValue != 0 && result.Flags&ast.SymbolFlagsAlias != 0 && result.Flags&ast.SymbolFlagsValue == 0 && !isValidTypeOnlyAliasUseSite(errorLocation) { + if errorLocation != nil && meaning&ast.SymbolFlagsValue != 0 && result.Flags&ast.SymbolFlagsAlias != 0 && result.Flags&ast.SymbolFlagsValue == 0 && !ast.IsValidTypeOnlyAliasUseSite(errorLocation) { typeOnlyDeclaration := c.getTypeOnlyAliasDeclarationEx(result, ast.SymbolFlagsValue) if typeOnlyDeclaration != nil { message := core.IfElse(ast.NodeKindIs(typeOnlyDeclaration, ast.KindExportSpecifier, ast.KindExportDeclaration, ast.KindNamespaceExport), @@ -5528,7 +5528,7 @@ func (c *Checker) checkVariableLikeDeclaration(node *ast.Node) { } if len(symbol.Declarations) > 1 { if core.Some(symbol.Declarations, func(d *ast.Declaration) bool { - return d != node && isVariableLike(d) && !c.areDeclarationFlagsIdentical(d, node) + return d != node && ast.IsVariableLike(d) && !c.areDeclarationFlagsIdentical(d, node) }) { c.error(name, diagnostics.All_declarations_of_0_must_have_identical_modifiers, scanner.DeclarationNameToString(name)) } @@ -17148,7 +17148,7 @@ func (c *Checker) isVarConstLike(node *ast.Node) bool { } func (c *Checker) getEffectivePropertyNameForPropertyNameNode(node *ast.PropertyName) (string, bool) { - name := getPropertyNameForPropertyNameNode(node) + name := ast.GetPropertyNameForPropertyNameNode(node) switch { case name != ast.InternalSymbolNameMissing: return name, true @@ -21861,7 +21861,7 @@ func (c *Checker) getTypeFromTypeAliasReference(node *ast.Node, symbol *ast.Symb var aliasTypeArguments []*Type if newAliasSymbol != nil { aliasTypeArguments = c.getTypeArgumentsForAliasSymbol(newAliasSymbol) - } else if isTypeReferenceType(node) { + } else if ast.IsTypeReferenceType(node) { aliasSymbol := c.resolveTypeReferenceName(node, ast.SymbolFlagsAlias, true /*ignoreErrors*/) // refers to an alias import/export/reexport - by making sure we use the target as an aliasSymbol, // we ensure the exported symbol is used to refer to the type when it is reserialized later @@ -24921,7 +24921,7 @@ func (c *Checker) getLiteralTypeFromPropertyName(name *ast.Node) *Type { if ast.IsComputedPropertyName(name) { return c.getRegularTypeOfLiteralType(c.checkComputedPropertyName(name)) } - propertyName := getPropertyNameForPropertyNameNode(name) + propertyName := ast.GetPropertyNameForPropertyNameNode(name) if propertyName != ast.InternalSymbolNameMissing { return c.getStringLiteralType(propertyName) } @@ -25508,7 +25508,7 @@ func (c *Checker) getPropertyNameFromIndex(indexType *Type, accessNode *ast.Node return getPropertyNameFromType(indexType) } if accessNode != nil && ast.IsPropertyName(accessNode) { - return getPropertyNameForPropertyNameNode(accessNode) + return ast.GetPropertyNameForPropertyNameNode(accessNode) } return ast.InternalSymbolNameMissing } diff --git a/internal/checker/exports.go b/internal/checker/exports.go new file mode 100644 index 0000000000..4bf67fbba0 --- /dev/null +++ b/internal/checker/exports.go @@ -0,0 +1,50 @@ +package checker + +import ( + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/diagnostics" +) + +func (c *Checker) GetUnionType(types []*Type) *Type { + return c.getUnionType(types) +} + +func (c *Checker) GetGlobalSymbol(name string, meaning ast.SymbolFlags, diagnostic *diagnostics.Message) *ast.Symbol { + return c.getGlobalSymbol(name, meaning, diagnostic) +} + +func (c *Checker) GetTypeFromTypeNode(node *ast.Node) *Type { + return c.getTypeFromTypeNode(node) +} + +func (c *Checker) IsArrayLikeType(t *Type) bool { + return c.isArrayLikeType(t) +} + +func (c *Checker) GetPropertiesOfType(t *Type) []*ast.Symbol { + return c.getPropertiesOfType(t) +} + +func (c *Checker) TypeHasCallOrConstructSignatures(t *Type) bool { + return c.typeHasCallOrConstructSignatures(t) +} + +// Checks if a property can be accessed in a location. +// The location is given by the `node` parameter. +// The node does not need to be a property access. +// @param node location where to check property accessibility +// @param isSuper whether to consider this a `super` property access, e.g. `super.foo`. +// @param isWrite whether this is a write access, e.g. `++foo.x`. +// @param containingType type where the property comes from. +// @param property property symbol. +func (c *Checker) IsPropertyAccessible(node *ast.Node, isSuper bool, isWrite bool, containingType *Type, property *ast.Symbol) bool { + return c.isPropertyAccessible(node, isSuper, isWrite, containingType, property) +} + +func (c *Checker) GetTypeOfPropertyOfContextualType(t *Type, name string) *Type { + return c.getTypeOfPropertyOfContextualType(t, name) +} + +func GetDeclarationModifierFlagsFromSymbol(s *ast.Symbol) ast.ModifierFlags { + return getDeclarationModifierFlagsFromSymbol(s) +} diff --git a/internal/checker/services.go b/internal/checker/services.go index 9b5863d2ae..ea4394b498 100644 --- a/internal/checker/services.go +++ b/internal/checker/services.go @@ -18,7 +18,7 @@ func (c *Checker) getSymbolsInScope(location *ast.Node, meaning ast.SymbolFlags) return nil } - symbols := createSymbolTable(nil) + symbols := make(ast.SymbolTable) isStaticSymbol := false // Copy the given symbol into symbol tables if the symbol has the given meaning @@ -398,3 +398,73 @@ func (c *Checker) tryGetTarget(symbol *ast.Symbol) *ast.Symbol { func (c *Checker) GetExportSymbolOfSymbol(symbol *ast.Symbol) *ast.Symbol { return c.getMergedSymbol(core.IfElse(symbol.ExportSymbol != nil, symbol.ExportSymbol, symbol)) } + +func (c *Checker) GetTypeArgumentConstraint(node *ast.Node) *Type { + if !ast.IsTypeNode(node) { + return nil + } + return c.getTypeArgumentConstraint(node) +} + +func (c *Checker) getTypeArgumentConstraint(node *ast.Node) *Type { + typeReferenceNode := core.IfElse(ast.IsTypeReferenceType(node.Parent), node.Parent, nil) + if typeReferenceNode == nil { + return nil + } + typeParameters := c.getTypeParametersForTypeReferenceOrImport(typeReferenceNode) + if len(typeParameters) == 0 { + return nil + } + + typeParamIndex := core.FindIndex(typeReferenceNode.TypeArguments(), func(n *ast.Node) bool { + return n == node + }) + constraint := c.getConstraintOfTypeParameter(typeParameters[typeParamIndex]) + if constraint != nil { + return c.instantiateType( + constraint, + newTypeMapper(typeParameters, c.getEffectiveTypeArguments(typeReferenceNode, typeParameters))) + } + return nil +} + +func (c *Checker) IsTypeInvalidDueToUnionDiscriminant(contextualType *Type, obj *ast.Node) bool { + properties := obj.Properties() + return core.Some(properties, func(property *ast.Node) bool { + var nameType *Type + propertyName := property.Name() + if propertyName != nil { + if ast.IsJsxNamespacedName(propertyName) { + nameType = c.getStringLiteralType(propertyName.Text()) + } else { + nameType = c.getLiteralTypeFromPropertyName(propertyName) + } + } + var name string + if nameType != nil && isTypeUsableAsPropertyName(nameType) { + name = getPropertyNameFromType(nameType) + } + var expected *Type + if name != "" { + expected = c.getTypeOfPropertyOfType(contextualType, name) + } + return expected != nil && isLiteralType(expected) && !c.isTypeAssignableTo(c.getTypeOfNode(property), expected) + }) +} + +// Unlike `getExportsOfModule`, this includes properties of an `export =` value. +func (c *Checker) GetExportsAndPropertiesOfModule(moduleSymbol *ast.Symbol) []*ast.Symbol { + exports := c.getExportsOfModuleAsArray(moduleSymbol) + exportEquals := c.resolveExternalModuleSymbol(moduleSymbol, false /*dontResolveAlias*/) + if exportEquals != moduleSymbol { + t := c.getTypeOfSymbol(exportEquals) + if c.shouldTreatPropertiesOfExternalModuleAsExports(t) { + exports = append(exports, c.getPropertiesOfType(t)...) + } + } + return exports +} + +func (c *Checker) getExportsOfModuleAsArray(moduleSymbol *ast.Symbol) []*ast.Symbol { + return symbolsToArray(c.getExportsOfModule(moduleSymbol)) +} diff --git a/internal/checker/types.go b/internal/checker/types.go index 767abf2438..3783c781a8 100644 --- a/internal/checker/types.go +++ b/internal/checker/types.go @@ -675,6 +675,10 @@ func (t *Type) IsStringLike() bool { return t.flags&TypeFlagsStringLike != 0 } +func (t *Type) IsClass() bool { + return t.objectFlags&ObjectFlagsClass != 0 +} + // TypeData type TypeData interface { @@ -760,6 +764,10 @@ func (t *StructuredType) ConstructSignatures() []*Signature { return slices.Clip(t.signatures[t.callSignatureCount:]) } +func (t *StructuredType) Properties() []*ast.Symbol { + return t.properties +} + // Except for tuple type references and reverse mapped types, all object types have an associated symbol. // Possible object type instances are listed in the following. diff --git a/internal/checker/utilities.go b/internal/checker/utilities.go index bff81e1ef7..ca8704fc60 100644 --- a/internal/checker/utilities.go +++ b/internal/checker/utilities.go @@ -219,38 +219,6 @@ func getNameFromImportDeclaration(node *ast.Node) *ast.Node { return nil } -func isValidTypeOnlyAliasUseSite(useSite *ast.Node) bool { - return useSite.Flags&ast.NodeFlagsAmbient != 0 || - ast.IsPartOfTypeQuery(useSite) || - isIdentifierInNonEmittingHeritageClause(useSite) || - isPartOfPossiblyValidTypeOrAbstractComputedPropertyName(useSite) || - !(ast.IsExpressionNode(useSite) || isShorthandPropertyNameUseSite(useSite)) -} - -func isIdentifierInNonEmittingHeritageClause(node *ast.Node) bool { - if !ast.IsIdentifier(node) { - return false - } - parent := node.Parent - for ast.IsPropertyAccessExpression(parent) || ast.IsExpressionWithTypeArguments(parent) { - parent = parent.Parent - } - return ast.IsHeritageClause(parent) && (parent.AsHeritageClause().Token == ast.KindImplementsKeyword || ast.IsInterfaceDeclaration(parent.Parent)) -} - -func isPartOfPossiblyValidTypeOrAbstractComputedPropertyName(node *ast.Node) bool { - for ast.NodeKindIs(node, ast.KindIdentifier, ast.KindPropertyAccessExpression) { - node = node.Parent - } - if node.Kind != ast.KindComputedPropertyName { - return false - } - if ast.HasSyntacticModifier(node.Parent, ast.ModifierFlagsAbstract) { - return true - } - return ast.NodeKindIs(node.Parent.Parent, ast.KindInterfaceDeclaration, ast.KindTypeLiteral) -} - func nodeCanBeDecorated(useLegacyDecorators bool, node *ast.Node, parent *ast.Node, grandparent *ast.Node) bool { // private names cannot be used with decorators yet if useLegacyDecorators && node.Name() != nil && ast.IsPrivateIdentifier(node.Name()) { @@ -285,10 +253,6 @@ func nodeCanBeDecorated(useLegacyDecorators bool, node *ast.Node, parent *ast.No return false } -func isShorthandPropertyNameUseSite(useSite *ast.Node) bool { - return ast.IsIdentifier(useSite) && ast.IsShorthandPropertyAssignment(useSite.Parent) && useSite.Parent.AsShorthandPropertyAssignment().Name() == useSite -} - func isTypeDeclaration(node *ast.Node) bool { switch node.Kind { case ast.KindTypeParameter, ast.KindClassDeclaration, ast.KindInterfaceDeclaration, ast.KindTypeAliasDeclaration, ast.KindJSTypeAliasDeclaration, ast.KindEnumDeclaration: @@ -1014,10 +978,6 @@ func getContainingFunctionOrClassStaticBlock(node *ast.Node) *ast.Node { return ast.FindAncestor(node.Parent, ast.IsFunctionLikeOrClassStaticBlockDeclaration) } -func isTypeReferenceType(node *ast.Node) bool { - return node.Kind == ast.KindTypeReference || node.Kind == ast.KindExpressionWithTypeArguments -} - func isNodeDescendantOf(node *ast.Node, ancestor *ast.Node) bool { for node != nil { if node == ancestor { @@ -1072,28 +1032,6 @@ func isNumericLiteralName(name string) bool { return jsnum.FromString(name).String() == name } -func getPropertyNameForPropertyNameNode(name *ast.Node) string { - switch name.Kind { - case ast.KindIdentifier, ast.KindPrivateIdentifier, ast.KindStringLiteral, ast.KindNoSubstitutionTemplateLiteral, - ast.KindNumericLiteral, ast.KindBigIntLiteral, ast.KindJsxNamespacedName: - return name.Text() - case ast.KindComputedPropertyName: - nameExpression := name.AsComputedPropertyName().Expression - if ast.IsStringOrNumericLiteralLike(nameExpression) { - return nameExpression.Text() - } - if ast.IsSignedNumericLiteral(nameExpression) { - text := nameExpression.AsPrefixUnaryExpression().Operand.Text() - if nameExpression.AsPrefixUnaryExpression().Operator == ast.KindMinusToken { - text = "-" + text - } - return text - } - return ast.InternalSymbolNameMissing - } - panic("Unhandled case in getPropertyNameForPropertyNameNode") -} - func isThisProperty(node *ast.Node) bool { return (ast.IsPropertyAccessExpression(node) || ast.IsElementAccessExpression(node)) && node.Expression().Kind == ast.KindThisKeyword } @@ -1332,15 +1270,6 @@ func isInAmbientOrTypeNode(node *ast.Node) bool { }) != nil } -func isVariableLike(node *ast.Node) bool { - switch node.Kind { - case ast.KindBindingElement, ast.KindEnumMember, ast.KindParameter, ast.KindPropertyAssignment, ast.KindPropertyDeclaration, - ast.KindPropertySignature, ast.KindShorthandPropertyAssignment, ast.KindVariableDeclaration: - return true - } - return false -} - func getAncestor(node *ast.Node, kind ast.Kind) *ast.Node { for node != nil && node.Kind != kind { node = node.Parent @@ -1962,7 +1891,7 @@ func tryGetPropertyAccessOrIdentifierToString(expr *ast.Node) string { case ast.IsElementAccessExpression(expr): baseStr := tryGetPropertyAccessOrIdentifierToString(expr.Expression()) if baseStr != "" && ast.IsPropertyName(expr.AsElementAccessExpression().ArgumentExpression) { - return baseStr + "." + getPropertyNameForPropertyNameNode(expr.AsElementAccessExpression().ArgumentExpression) + return baseStr + "." + ast.GetPropertyNameForPropertyNameNode(expr.AsElementAccessExpression().ArgumentExpression) } case ast.IsIdentifier(expr): return expr.Text() diff --git a/internal/ls/completions.go b/internal/ls/completions.go index b003ed90d3..3be923998d 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -32,9 +32,10 @@ func (l *LanguageService) ProvideCompletion( return l.getCompletionsAtPosition(program, file, position, context, preferences, clientOptions) } -// !!! figure out other kinds of completion data return -type completionData struct { - // !!! +// *completionDataData | *completionDataKeyword +type completionData = any + +type completionDataData struct { symbols []*ast.Symbol completionKind CompletionKind isInSnippetScope bool @@ -50,7 +51,7 @@ type completionData struct { previousToken *ast.Node contextToken *ast.Node jsxInitializer jsxInitializer - insideJsDocTagTypeExpression bool + insideJSDocTagTypeExpression bool isTypeOnlyLocation bool // In JSX tag name and attribute names, identifiers like "my-tag" or "aria-name" is valid identifier. isJsxIdentifierExpected bool @@ -62,6 +63,11 @@ type completionData struct { defaultCommitCharacters []string } +type completionDataKeyword struct { + keywordCompletions []*lsproto.CompletionItem + isNewIdentifierLocation bool +} + type importStatementCompletionInfo struct { // !!! } @@ -107,6 +113,8 @@ var allCommitCharacters = []string{".", ",", ";"} // Commit characters valid at expression positions where we could be inside a parameter list. var noCommaCommitCharacters = []string{".", ";"} +var emptyCommitCharacters = []string{} + type sortText string const ( @@ -200,6 +208,14 @@ func (s *symbolOriginInfo) asObjectLiteralMethod() *symbolOriginInfoObjectLitera return s.data.(*symbolOriginInfoObjectLiteralMethod) } +type symbolOriginInfoTypeOnlyAlias struct { + declaration *ast.TypeOnlyImportDeclaration +} + +type symbolOriginInfoComputedPropertyName struct { + symbolName string +} + // Special values for `CompletionInfo['source']` used to disambiguate // completion items with the same `name`. (Each completion item must // have a unique name/source combination, because those two fields @@ -232,6 +248,14 @@ type uniqueNamesMap = map[string]bool type literalValue any // string | jsnum.Number | PseudoBigInt +type globalsSearch int + +const ( + globalsSearchContinue globalsSearch = iota + globalsSearchSuccess + globalsSearchFail +) + func (l *LanguageService) getCompletionsAtPosition( program *compiler.Program, file *ast.SourceFile, @@ -240,7 +264,7 @@ func (l *LanguageService) getCompletionsAtPosition( preferences *UserPreferences, clientOptions *lsproto.CompletionClientCapabilities, ) *lsproto.CompletionList { - previousToken, _ := getRelevantTokens(position, file) + _, previousToken := getRelevantTokens(position, file) if context.TriggerCharacter != nil && !isInString(file, position, previousToken) && !isValidTrigger(file, *context.TriggerCharacter, previousToken, position) { return nil } @@ -248,12 +272,8 @@ func (l *LanguageService) getCompletionsAtPosition( if context.TriggerCharacter != nil && *context.TriggerCharacter == " " { // `isValidTrigger` ensures we are at `import |` if ptrIsTrue(preferences.IncludeCompletionsForImportStatements) { - // !!! isMemberCompletion return &lsproto.CompletionList{ IsIncomplete: true, - ItemDefaults: &lsproto.CompletionItemDefaults{ // !!! do we need this if no entries? also, check if client supports item defaults - CommitCharacters: ptrTo(getDefaultCommitCharacters(true /*isNewIdentifierLocation*/)), - }, } } return nil @@ -267,28 +287,33 @@ func (l *LanguageService) getCompletionsAtPosition( // !!! label completions - completionData := getCompletionData(program, file, position, preferences) - if completionData == nil { + data := getCompletionData(program, file, position, preferences) + if data == nil { return nil } - // switch completionData.Kind // !!! other data cases - // !!! transform data into completion list - - response := l.completionInfoFromData( - file, - program, - compilerOptions, - completionData, - preferences, - position, - clientOptions, - ) - // !!! check if response is incomplete - return response + switch data := data.(type) { + case *completionDataData: + response := l.completionInfoFromData( + file, + program, + compilerOptions, + data, + preferences, + position, + clientOptions, + ) + // !!! check if response is incomplete + return response + case *completionDataKeyword: + return specificKeywordCompletionInfo(clientOptions, data.keywordCompletions, data.isNewIdentifierLocation) + // !!! jsdoc completion data cases + default: + panic("getCompletionData() returned unexpected type: " + fmt.Sprintf("%T", data)) + } } -func getCompletionData(program *compiler.Program, file *ast.SourceFile, position int, preferences *UserPreferences) *completionData { +func getCompletionData(program *compiler.Program, file *ast.SourceFile, position int, preferences *UserPreferences) completionData { typeChecker := program.GetTypeChecker() inCheckedFile := isCheckedFile(file, program.GetCompilerOptions()) @@ -296,7 +321,7 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position insideComment := isInComment(file, position, currentToken) - insideJsDocTagTypeExpression := false + insideJSDocTagTypeExpression := false insideJsDocImportTag := false isInSnippetScope := false if insideComment != nil { @@ -305,8 +330,8 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position // The decision to provide completion depends on the contextToken, which is determined through the previousToken. // Note: 'previousToken' (and thus 'contextToken') can be undefined if we are the beginning of the file - // isJSOnlyLocation := !insideJsDocTagTypeExpression && !insideJsDocImportTag && ast.IsSourceFileJS(file) - previousToken, contextToken := getRelevantTokens(position, file) + isJSOnlyLocation := !insideJSDocTagTypeExpression && !insideJsDocImportTag && ast.IsSourceFileJS(file) + contextToken, previousToken := getRelevantTokens(position, file) // Find the node where completion is requested on. // Also determine whether we are trying to complete with members of that node @@ -445,7 +470,7 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position symbolToSortTextMap := map[ast.SymbolId]sortText{} // var importSpecifierResolver any // !!! import var seenPropertySymbols core.Set[ast.SymbolId] - isTypeOnlyLocation := insideJsDocTagTypeExpression || insideJsDocImportTag || + isTypeOnlyLocation := insideJSDocTagTypeExpression || insideJsDocImportTag || importStatementCompletion != nil && ast.IsTypeOnlyImportOrExportDeclaration(location.Parent) || !isContextTokenValueLocation(contextToken) && (isPossiblyTypeArgumentPosition(contextToken, file, typeChecker) || @@ -647,7 +672,7 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position } else if isRhsOfImportDeclaration { // Any kind is allowed when dotting off namespace in internal import equals declaration isValidAccess = isValidTypeAccess(exportedSymbol) || isValidValueAccess(exportedSymbol) - } else if isTypeLocation || insideJsDocTagTypeExpression { + } else if isTypeLocation || insideJSDocTagTypeExpression { isValidAccess = isValidTypeAccess(exportedSymbol) } else { isValidAccess = isValidValueAccess(exportedSymbol) @@ -658,7 +683,7 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position } // If the module is merged with a value, we must get the type of the class and add its properties (for inherited static methods). - if !isTypeLocation && !insideJsDocTagTypeExpression && + if !isTypeLocation && !insideJSDocTagTypeExpression && core.Some( symbol.Declarations, func(decl *ast.Declaration) bool { @@ -711,6 +736,615 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position } } + // Aggregates relevant symbols for completion in object literals in type argument positions. + tryGetObjectTypeLiteralInTypeArgumentCompletionSymbols := func() globalsSearch { + typeLiteralNode := tryGetTypeLiteralNode(contextToken) + if typeLiteralNode == nil { + return globalsSearchContinue + } + + intersectionTypeNode := core.IfElse( + ast.IsIntersectionTypeNode(typeLiteralNode.Parent), + typeLiteralNode.Parent, + nil) + containerTypeNode := core.IfElse( + intersectionTypeNode != nil, + intersectionTypeNode, + typeLiteralNode) + + containerExpectedType := getConstraintOfTypeArgumentProperty(containerTypeNode, typeChecker) + if containerExpectedType == nil { + return globalsSearchContinue + } + + containerActualType := typeChecker.GetTypeFromTypeNode(containerTypeNode) + + members := getPropertiesForCompletion(containerExpectedType, typeChecker) + existingMembers := getPropertiesForCompletion(containerActualType, typeChecker) + + existingMemberNames := core.Set[string]{} + for _, member := range existingMembers { + existingMemberNames.Add(member.Name) + } + + symbols = append( + symbols, + core.Filter(members, func(member *ast.Symbol) bool { return !existingMemberNames.Has(member.Name) })...) + + completionKind = CompletionKindObjectPropertyDeclaration + isNewIdentifierLocation = true + + return globalsSearchSuccess + } + + // Aggregates relevant symbols for completion in object literals and object binding patterns. + // Relevant symbols are stored in the captured 'symbols' variable. + tryGetObjectLikeCompletionSymbols := func() globalsSearch { + if contextToken != nil && contextToken.Kind == ast.KindDotDotDotToken { + return globalsSearchContinue + } + objectLikeContainer := tryGetObjectLikeCompletionContainer(contextToken, position, file) + if objectLikeContainer == nil { + return globalsSearchContinue + } + + // We're looking up possible property names from contextual/inferred/declared type. + completionKind = CompletionKindObjectPropertyDeclaration + + var typeMembers []*ast.Symbol + var existingMembers []*ast.Declaration + + if objectLikeContainer.Kind == ast.KindObjectLiteralExpression { + instantiatedType := tryGetObjectLiteralContextualType(objectLikeContainer, typeChecker) + + // Check completions for Object property value shorthand + if instantiatedType == nil { + if objectLikeContainer.Flags&ast.NodeFlagsInWithStatement != 0 { + return globalsSearchFail + } + return globalsSearchContinue + } + completionsType := typeChecker.GetContextualType(objectLikeContainer, checker.ContextFlagsCompletions) + t := core.IfElse(completionsType != nil, completionsType, instantiatedType) + stringIndexType := typeChecker.GetStringIndexType(t) + numberIndexType := typeChecker.GetNumberIndexType(t) + isNewIdentifierLocation = stringIndexType != nil || numberIndexType != nil + typeMembers = getPropertiesForObjectExpression(instantiatedType, completionsType, objectLikeContainer, typeChecker) + properties := objectLikeContainer.AsObjectLiteralExpression().Properties + if properties != nil { + existingMembers = properties.Nodes + } + + if len(typeMembers) == 0 { + // Edge case: If NumberIndexType exists + if numberIndexType == nil { + return globalsSearchContinue + } + } + } else { + if objectLikeContainer.Kind != ast.KindObjectBindingPattern { + panic("Expected 'objectLikeContainer' to be an object binding pattern.") + } + // We are *only* completing on properties from the type being destructured. + isNewIdentifierLocation = false + rootDeclaration := ast.GetRootDeclaration(objectLikeContainer.Parent) + if !ast.IsVariableLike(rootDeclaration) { + panic("Root declaration is not variable-like.") + } + + // We don't want to complete using the type acquired by the shape + // of the binding pattern; we are only interested in types acquired + // through type declaration or inference. + // Also proceed if rootDeclaration is a parameter and if its containing function expression/arrow function is contextually typed - + // type of parameter will flow in from the contextual type of the function. + canGetType := ast.HasInitializer(rootDeclaration) || + ast.GetTypeAnnotationNode(rootDeclaration) != nil || + rootDeclaration.Parent.Parent.Kind == ast.KindForOfStatement + if !canGetType && rootDeclaration.Kind == ast.KindParameter { + if ast.IsExpression(rootDeclaration.Parent) { + canGetType = typeChecker.GetContextualType(rootDeclaration.Parent, checker.ContextFlagsNone) != nil + } else if rootDeclaration.Parent.Kind == ast.KindMethodDeclaration || + rootDeclaration.Parent.Kind == ast.KindSetAccessor { + canGetType = ast.IsExpression(rootDeclaration.Parent.Parent) && + typeChecker.GetContextualType(rootDeclaration.Parent.Parent, checker.ContextFlagsNone) != nil + } + } + if canGetType { + typeForObject := typeChecker.GetTypeAtLocation(objectLikeContainer) + if typeForObject == nil { + return globalsSearchFail + } + typeMembers = core.Filter( + typeChecker.GetPropertiesOfType(typeForObject), + func(propertySymbol *ast.Symbol) bool { + return typeChecker.IsPropertyAccessible( + objectLikeContainer, + false, /*isSuper*/ + false, /*isWrite*/ + typeForObject, + propertySymbol, + ) + }, + ) + elements := objectLikeContainer.AsBindingPattern().Elements + if elements != nil { + existingMembers = elements.Nodes + } + } + } + + if len(typeMembers) > 0 { + // Add filtered items to the completion list. + filteredMembers, spreadMemberNames := filterObjectMembersList( + typeMembers, + core.CheckEachDefined(existingMembers, "object like properties or elements should all be defined"), + file, + position, + typeChecker, + ) + symbols = append(symbols, filteredMembers...) + + // Set sort texts. + transformObjectLiteralMembers := ptrIsTrue(preferences.IncludeCompletionsWithObjectLiteralMethodSnippets) && + objectLikeContainer.Kind == ast.KindObjectLiteralExpression + for _, member := range filteredMembers { + symbolId := ast.GetSymbolId(member) + if spreadMemberNames.Has(member.Name) { + symbolToSortTextMap[symbolId] = SortTextMemberDeclaredBySpreadAssignment + } + if member.Flags&ast.SymbolFlagsOptional != 0 { + _, ok := symbolToSortTextMap[symbolId] + if !ok { + symbolToSortTextMap[symbolId] = SortTextOptionalMember + } + } + if transformObjectLiteralMembers { + // !!! object literal member snippet completions + } + } + } + + return globalsSearchSuccess + } + + tryGetImportCompletionSymbols := func() globalsSearch { + if importStatementCompletion == nil { + return globalsSearchContinue + } + isNewIdentifierLocation = true + // !!! auto imports + // collectAutoImports() + return globalsSearchSuccess + } + + // Aggregates relevant symbols for completion in import clauses and export clauses + // whose declarations have a module specifier; for instance, symbols will be aggregated for + // + // import { | } from "moduleName"; + // export { a as foo, | } from "moduleName"; + // + // but not for + // + // export { | }; + // + // Relevant symbols are stored in the captured 'symbols' variable. + tryGetImportOrExportClauseCompletionSymbols := func() globalsSearch { + if contextToken == nil { + return globalsSearchContinue + } + + // `import { |` or `import { a as 0, | }` or `import { type | }` + var namedImportsOrExports *ast.NamedImportsOrExports + if contextToken.Kind == ast.KindOpenBraceToken || contextToken.Kind == ast.KindCommaToken { + namedImportsOrExports = core.IfElse(isNamedImportsOrExports(contextToken.Parent), contextToken.Parent, nil) + } else if isTypeKeywordTokenOrIdentifier(contextToken) { + namedImportsOrExports = core.IfElse( + isNamedImportsOrExports(contextToken.Parent.Parent), + contextToken.Parent.Parent, + nil, + ) + } + + if namedImportsOrExports == nil { + return globalsSearchContinue + } + + // We can at least offer `type` at `import { |` + if !isTypeKeywordTokenOrIdentifier(contextToken) { + keywordFilters = KeywordCompletionFiltersTypeKeyword + } + + // try to show exported member for imported/re-exported module + moduleSpecifier := core.IfElse( + namedImportsOrExports.Kind == ast.KindNamedImports, + namedImportsOrExports.Parent.Parent, + namedImportsOrExports.Parent).ModuleSpecifier() + if moduleSpecifier == nil { + isNewIdentifierLocation = true + if namedImportsOrExports.Kind == ast.KindNamedImports { + return globalsSearchFail + } + return globalsSearchContinue + } + + moduleSpecifierSymbol := typeChecker.GetSymbolAtLocation(moduleSpecifier) + if moduleSpecifierSymbol == nil { + isNewIdentifierLocation = true + return globalsSearchFail + } + + completionKind = CompletionKindMemberLike + isNewIdentifierLocation = false + exports := typeChecker.GetExportsAndPropertiesOfModule(moduleSpecifierSymbol) + + existing := core.Set[string]{} + for _, element := range namedImportsOrExports.Elements() { + if isCurrentlyEditingNode(element, file, position) { + continue + } + existing.Add(element.PropertyNameOrName().Text()) + } + uniques := core.Filter(exports, func(symbol *ast.Symbol) bool { + return symbol.Name != ast.InternalSymbolNameDefault && !existing.Has(symbol.Name) + }) + + symbols = append(symbols, uniques...) + if len(uniques) == 0 { + // If there's nothing else to import, don't offer `type` either. + keywordFilters = KeywordCompletionFiltersNone + } + return globalsSearchSuccess + } + + // import { x } from "foo" with { | } + tryGetImportAttributesCompletionSymbols := func() globalsSearch { + if contextToken == nil { + return globalsSearchContinue + } + + var importAttributes *ast.ImportAttributesNode + if contextToken.Kind == ast.KindOpenBraceToken || contextToken.Kind == ast.KindCommaToken { + importAttributes = core.IfElse(ast.IsImportAttributes(contextToken.Parent), contextToken.Parent, nil) + } else if contextToken.Kind == ast.KindColonToken { + importAttributes = core.IfElse(ast.IsImportAttributes(contextToken.Parent.Parent), contextToken.Parent.Parent, nil) + } + + if importAttributes == nil { + return globalsSearchContinue + } + + var elements []*ast.Node + if importAttributes.AsImportAttributes().Attributes != nil { + elements = importAttributes.AsImportAttributes().Attributes.Nodes + } + existing := core.NewSetFromItems(core.Map(elements, (*ast.Node).Text)...) + uniques := core.Filter( + typeChecker.GetApparentProperties(typeChecker.GetTypeAtLocation(importAttributes)), + func(symbol *ast.Symbol) bool { + return !existing.Has(symbol.Name) + }) + symbols = append(symbols, uniques...) + return globalsSearchSuccess + } + + // Adds local declarations for completions in named exports: + // export { | }; + // Does not check for the absence of a module specifier (`export {} from "./other"`) + // because `tryGetImportOrExportClauseCompletionSymbols` runs first and handles that, + // preventing this function from running. + tryGetLocalNamedExportCompletionSymbols := func() globalsSearch { + if contextToken == nil { + return globalsSearchContinue + } + var namedExports *ast.NamedExportsNode + if contextToken.Kind == ast.KindOpenBraceToken || contextToken.Kind == ast.KindCommaToken { + namedExports = core.IfElse(ast.IsNamedExports(contextToken.Parent), contextToken.Parent, nil) + } + + if namedExports == nil { + return globalsSearchContinue + } + + localsContainer := ast.FindAncestor(namedExports, func(node *ast.Node) bool { + return ast.IsSourceFile(node) || ast.IsModuleDeclaration(node) + }) + completionKind = CompletionKindNone + isNewIdentifierLocation = false + localSymbol := localsContainer.Symbol() + var localExports ast.SymbolTable + if localSymbol != nil { + localExports = localSymbol.Exports + } + for name, symbol := range localsContainer.Locals() { + symbols = append(symbols, symbol) + if _, ok := localExports[name]; ok { + symbolId := ast.GetSymbolId(symbol) + symbolToSortTextMap[symbolId] = SortTextOptionalMember + } + } + + return globalsSearchSuccess + } + + tryGetConstructorCompletion := func() globalsSearch { + if tryGetConstructorLikeCompletionContainer(contextToken) == nil { + return globalsSearchContinue + } + + // no members, only keywords + completionKind = CompletionKindNone + // Declaring new property/method/accessor + isNewIdentifierLocation = true + // Has keywords for constructor parameter + keywordFilters = KeywordCompletionFiltersConstructorParameterKeywords + return globalsSearchSuccess + } + + // Aggregates relevant symbols for completion in class declaration + // Relevant symbols are stored in the captured 'symbols' variable. + tryGetClassLikeCompletionSymbols := func() globalsSearch { + decl := tryGetObjectTypeDeclarationCompletionContainer(file, contextToken, location, position) + if decl == nil { + return globalsSearchContinue + } + + // We're looking up possible property names from parent type. + completionKind = CompletionKindMemberLike + // Declaring new property/method/accessor + isNewIdentifierLocation = true + if contextToken.Kind == ast.KindAsteriskToken { + keywordFilters = KeywordCompletionFiltersNone + } else if ast.IsClassLike(decl) { + keywordFilters = KeywordCompletionFiltersClassElementKeywords + } else { + keywordFilters = KeywordCompletionFiltersInterfaceElementKeywords + } + + // If you're in an interface you don't want to repeat things from super-interface. So just stop here. + if !ast.IsClassLike(decl) { + return globalsSearchSuccess + } + + var classElement *ast.Node + if contextToken.Kind == ast.KindSemicolonToken { + classElement = contextToken.Parent.Parent + } else { + classElement = contextToken.Parent + } + var classElementModifierFlags ast.ModifierFlags + if ast.IsClassElement(classElement) { + classElementModifierFlags = classElement.ModifierFlags() + } + // If this is context token is not something we are editing now, consider if this would lead to be modifier. + if contextToken.Kind == ast.KindIdentifier && !isCurrentlyEditingNode(contextToken, file, position) { + switch contextToken.Text() { + case "private": + classElementModifierFlags |= ast.ModifierFlagsPrivate + case "static": + classElementModifierFlags |= ast.ModifierFlagsStatic + case "override": + classElementModifierFlags |= ast.ModifierFlagsOverride + } + } + if ast.IsClassStaticBlockDeclaration(classElement) { + classElementModifierFlags |= ast.ModifierFlagsStatic + } + + // No member list for private methods + if classElementModifierFlags&ast.ModifierFlagsPrivate == 0 { + // List of property symbols of base type that are not private and already implemented + var baseTypeNodes []*ast.Node + if ast.IsClassLike(decl) && classElementModifierFlags&ast.ModifierFlagsOverride != 0 { + baseTypeNodes = []*ast.Node{ast.GetClassExtendsHeritageElement(decl)} + } else { + baseTypeNodes = getAllSuperTypeNodes(decl) + } + var baseSymbols []*ast.Symbol + for _, baseTypeNode := range baseTypeNodes { + t := typeChecker.GetTypeAtLocation(baseTypeNode) + if classElementModifierFlags&ast.ModifierFlagsStatic != 0 { + if t.Symbol() != nil { + baseSymbols = append( + baseSymbols, + typeChecker.GetPropertiesOfType(typeChecker.GetTypeOfSymbolAtLocation(t.Symbol(), decl))...) + } + } else if t != nil { + baseSymbols = append(baseSymbols, typeChecker.GetPropertiesOfType(t)...) + } + } + + symbols = append(symbols, + filterClassMembersList(baseSymbols, decl.Members(), classElementModifierFlags, file, position)...) + for _, symbol := range symbols { + declaration := symbol.ValueDeclaration + if declaration != nil && ast.IsClassElement(declaration) && + declaration.Name() != nil && + ast.IsComputedPropertyName(declaration.Name()) { + symbolId := ast.GetSymbolId(symbol) + origin := &symbolOriginInfo{ + kind: symbolOriginInfoKindComputedPropertyName, + data: &symbolOriginInfoComputedPropertyName{symbolName: typeChecker.SymbolToString(symbol)}, + } + symbolToOriginInfoMap[symbolId] = origin + } + } + } + + return globalsSearchSuccess + } + + tryGetJsxCompletionSymbols := func() globalsSearch { + jsxContainer := tryGetContainingJsxElement(contextToken, file) + if jsxContainer == nil { + return globalsSearchContinue + } + // Cursor is inside a JSX self-closing element or opening element. + attrsType := typeChecker.GetContextualType(jsxContainer.Attributes(), checker.ContextFlagsNone) + if attrsType == nil { + return globalsSearchContinue + } + completionsType := typeChecker.GetContextualType(jsxContainer.Attributes(), checker.ContextFlagsCompletions) + filteredSymbols, spreadMemberNames := filterJsxAttributes( + getPropertiesForObjectExpression(attrsType, completionsType, jsxContainer.Attributes(), typeChecker), + jsxContainer.Attributes().Properties(), + file, + position, + typeChecker, + ) + + symbols = append(symbols, filteredSymbols...) + // Set sort texts. + for _, symbol := range filteredSymbols { + symbolId := ast.GetSymbolId(symbol) + if spreadMemberNames.Has(symbol.Name) { + symbolToSortTextMap[symbolId] = SortTextMemberDeclaredBySpreadAssignment + } + if symbol.Flags&ast.SymbolFlagsOptional != 0 { + _, ok := symbolToSortTextMap[symbolId] + if !ok { + symbolToSortTextMap[symbolId] = SortTextOptionalMember + } + } + } + + completionKind = CompletionKindMemberLike + isNewIdentifierLocation = false + return globalsSearchSuccess + } + + getGlobalCompletions := func() globalsSearch { + if tryGetFunctionLikeBodyCompletionContainer(contextToken) != nil { + keywordFilters = KeywordCompletionFiltersFunctionLikeBodyKeywords + } else { + keywordFilters = KeywordCompletionFiltersAll + } + // Get all entities in the current scope. + completionKind = CompletionKindGlobal + isNewIdentifierLocation, defaultCommitCharacters = computeCommitCharactersAndIsNewIdentifier(contextToken, file, position) + + if previousToken != contextToken { + if previousToken == nil { + panic("Expected 'contextToken' to be defined when different from 'previousToken'.") + } + } + + // We need to find the node that will give us an appropriate scope to begin + // aggregating completion candidates. This is achieved in 'getScopeNode' + // by finding the first node that encompasses a position, accounting for whether a node + // is "complete" to decide whether a position belongs to the node. + // + // However, at the end of an identifier, we are interested in the scope of the identifier + // itself, but fall outside of the identifier. For instance: + // + // xyz => x$ + // + // the cursor is outside of both the 'x' and the arrow function 'xyz => x', + // so 'xyz' is not returned in our results. + // + // We define 'adjustedPosition' so that we may appropriately account for + // being at the end of an identifier. The intention is that if requesting completion + // at the end of an identifier, it should be effectively equivalent to requesting completion + // anywhere inside/at the beginning of the identifier. So in the previous case, the + // 'adjustedPosition' will work as if requesting completion in the following: + // + // xyz => $x + // + // If previousToken !== contextToken, then + // - 'contextToken' was adjusted to the token prior to 'previousToken' + // because we were at the end of an identifier. + // - 'previousToken' is defined. + var adjustedPosition int + if previousToken != contextToken { + adjustedPosition = astnav.GetStartOfNode(previousToken, file, false /*includeJSDoc*/) + } else { + adjustedPosition = position + } + + scopeNode := getScopeNode(contextToken, adjustedPosition, file) + if scopeNode == nil { + scopeNode = file.AsNode() + } + isInSnippetScope = isSnippetScope(scopeNode) + + symbolMeanings := core.IfElse(isTypeOnlyLocation, ast.SymbolFlagsNone, ast.SymbolFlagsValue) | + ast.SymbolFlagsType | ast.SymbolFlagsNamespace | ast.SymbolFlagsAlias + typeOnlyAliasNeedsPromotion := previousToken != nil && !ast.IsValidTypeOnlyAliasUseSite(previousToken) + + symbols = append(symbols, typeChecker.GetSymbolsInScope(scopeNode, symbolMeanings)...) + core.CheckEachDefined(symbols, "getSymbolsInScope() should all be defined") + for _, symbol := range symbols { + symbolId := ast.GetSymbolId(symbol) + if !typeChecker.IsArgumentsSymbol(symbol) && + !core.Some(symbol.Declarations, func(decl *ast.Declaration) bool { + return ast.GetSourceFileOfNode(decl) == file + }) { + symbolToSortTextMap[symbolId] = SortTextGlobalsOrKeywords + } + if typeOnlyAliasNeedsPromotion && symbol.Flags&ast.SymbolFlagsValue == 0 { + typeOnlyAliasDeclaration := core.Find(symbol.Declarations, ast.IsTypeOnlyImportDeclaration) + if typeOnlyAliasDeclaration != nil { + origin := &symbolOriginInfo{ + kind: symbolOriginInfoKindTypeOnlyAlias, + data: &symbolOriginInfoTypeOnlyAlias{declaration: typeOnlyAliasDeclaration}, + } + symbolToOriginInfoMap[symbolId] = origin + } + } + } + + // Need to insert 'this.' before properties of `this` type, so only do that if `includeInsertTextCompletions` + if scopeNode.Kind != ast.KindSourceFile { + thisType := typeChecker.TryGetThisTypeAtEx( + scopeNode, + false, /*includeGlobalThis*/ + core.IfElse(ast.IsClassLike(scopeNode.Parent), scopeNode, nil)) + if thisType != nil && !isProbablyGlobalType(thisType, file, typeChecker) { + for _, symbol := range getPropertiesForCompletion(thisType, typeChecker) { + symbolId := ast.GetSymbolId(symbol) + symbols = append(symbols, symbol) + symbolToOriginInfoMap[symbolId] = &symbolOriginInfo{kind: symbolOriginInfoKindThisType} + symbolToSortTextMap[symbolId] = SortTextSuggestedClassMembers + } + } + } + + // !!! auto imports + // collectAutoImports() + + if isTypeOnlyLocation { + if contextToken != nil && ast.IsAssertionExpression(contextToken.Parent) { + keywordFilters = KeywordCompletionFiltersTypeAssertionKeywords + } else { + keywordFilters = KeywordCompletionFiltersTypeKeywords + } + } + + return globalsSearchSuccess + } + + tryGetGlobalSymbols := func() bool { + var result globalsSearch + globalSearchFuncs := []func() globalsSearch{ + tryGetObjectTypeLiteralInTypeArgumentCompletionSymbols, + tryGetObjectLikeCompletionSymbols, + tryGetImportCompletionSymbols, + tryGetImportOrExportClauseCompletionSymbols, + tryGetImportAttributesCompletionSymbols, + tryGetLocalNamedExportCompletionSymbols, + tryGetConstructorCompletion, + tryGetClassLikeCompletionSymbols, + tryGetJsxCompletionSymbols, + getGlobalCompletions, + } + for _, globalSearchFunc := range globalSearchFuncs { + result = globalSearchFunc() + if result != globalsSearchContinue { + break + } + } + return result == globalsSearchSuccess + } + if isRightOfDot || isRightOfQuestionDot { getTypeScriptMemberSymbols() } else if isRightOfOpenTag { @@ -721,7 +1355,12 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position // For JavaScript or TypeScript, if we're not after a dot, then just try to get the // global symbols in scope. These results should be valid for either language as // the set of symbols that can be referenced from this location. - // !!! global completions + if !tryGetGlobalSymbols() { + if keywordFilters != KeywordCompletionFiltersNone { + return keywordCompletionData(keywordFilters, isJSOnlyLocation, isNewIdentifierLocation) + } + return nil + } } var contextualType *checker.Type @@ -757,7 +1396,7 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position defaultCommitCharacters = getDefaultCommitCharacters(isNewIdentifierLocation) } - return &completionData{ + return &completionDataData{ symbols: symbols, completionKind: completionKind, isInSnippetScope: isInSnippetScope, @@ -772,7 +1411,7 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position previousToken: previousToken, contextToken: contextToken, jsxInitializer: jsxInitializer, - insideJsDocTagTypeExpression: insideJsDocTagTypeExpression, + insideJSDocTagTypeExpression: insideJSDocTagTypeExpression, isTypeOnlyLocation: isTypeOnlyLocation, isJsxIdentifierExpected: isJsxIdentifierExpected, isRightOfOpenTag: isRightOfOpenTag, @@ -783,6 +1422,17 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position } } +func keywordCompletionData( + keywordFilters KeywordCompletionFilters, + filterOutTSOnlyKeywords bool, + isNewIdentifierLocation bool, +) *completionDataKeyword { + return &completionDataKeyword{ + keywordCompletions: getKeywordCompletions(keywordFilters, filterOutTSOnlyKeywords), + isNewIdentifierLocation: isNewIdentifierLocation, + } +} + func getDefaultCommitCharacters(isNewIdentifierLocation bool) []string { if isNewIdentifierLocation { return []string{} @@ -794,7 +1444,7 @@ func (l *LanguageService) completionInfoFromData( file *ast.SourceFile, program *compiler.Program, compilerOptions *core.CompilerOptions, - data *completionData, + data *completionDataData, preferences *UserPreferences, position int, clientOptions *lsproto.CompletionClientCapabilities, @@ -840,7 +1490,7 @@ func (l *LanguageService) completionInfoFromData( if data.keywordFilters != KeywordCompletionFiltersNone { keywordCompletions := getKeywordCompletions( data.keywordFilters, - !data.insideJsDocTagTypeExpression && ast.IsSourceFileJS(file)) + !data.insideJSDocTagTypeExpression && ast.IsSourceFileJS(file)) for _, keywordEntry := range keywordCompletions { if data.isTypeOnlyLocation && isTypeKeyword(scanner.StringToToken(keywordEntry.Label)) || !data.isTypeOnlyLocation && isContextualKeywordInAutoImportableExpressionSpace(keywordEntry.Label) || @@ -876,17 +1526,7 @@ func (l *LanguageService) completionInfoFromData( // !!! exhaustive case completions - var defaultCommitCharacters *[]string - if supportsDefaultCommitCharacters(clientOptions) && ptrIsTrue(clientOptions.CompletionItem.CommitCharactersSupport) { - defaultCommitCharacters = &data.defaultCommitCharacters - } - - var itemDefaults *lsproto.CompletionItemDefaults - if defaultCommitCharacters != nil { - itemDefaults = &lsproto.CompletionItemDefaults{ - CommitCharacters: defaultCommitCharacters, - } - } + itemDefaults := setCommitCharacters(clientOptions, sortedEntries, &data.defaultCommitCharacters) // !!! port behavior of other strada fields of CompletionInfo that are non-LSP return &lsproto.CompletionList{ @@ -897,7 +1537,7 @@ func (l *LanguageService) completionInfoFromData( } func (l *LanguageService) getCompletionEntriesFromSymbols( - data *completionData, + data *completionDataData, replacementToken *ast.Node, position int, file *ast.SourceFile, @@ -1019,7 +1659,7 @@ func (l *LanguageService) createCompletionItem( symbol *ast.Symbol, sortText sortText, replacementToken *ast.Node, - data *completionData, + data *completionDataData, position int, file *ast.SourceFile, program *compiler.Program, @@ -1414,7 +2054,7 @@ func getWordRange(sourceFile *ast.SourceFile, position int) (wordRange *core.Tex text := sourceFile.Text()[:position] totalSize := 0 var firstRune rune - for r, size := utf8.DecodeLastRuneInString(text); size != 0; r, size = utf8.DecodeLastRuneInString(text[:len(text)-size]) { + for r, size := utf8.DecodeLastRuneInString(text); size != 0; r, size = utf8.DecodeLastRuneInString(text[:len(text)-totalSize]) { if wordSeparators.Has(r) || unicode.IsSpace(r) { break } @@ -1493,7 +2133,7 @@ func getFilterText( func getDotAccessorContext(file *ast.SourceFile, position int) (acessorRange *core.TextRange, accessorText string) { text := file.Text()[:position] totalSize := 0 - for r, size := utf8.DecodeLastRuneInString(text); size != 0; r, size = utf8.DecodeLastRuneInString(text[:len(text)-size]) { + for r, size := utf8.DecodeLastRuneInString(text); size != 0; r, size = utf8.DecodeLastRuneInString(text[:len(text)-totalSize]) { if !unicode.IsSpace(r) { break } @@ -1566,7 +2206,7 @@ func symbolAppearsToBeTypeOnly(symbol *ast.Symbol, typeChecker *checker.Checker) func shouldIncludeSymbol( symbol *ast.Symbol, - data *completionData, + data *completionDataData, closestSymbolDeclaration *ast.Declaration, file *ast.SourceFile, typeChecker *checker.Checker, @@ -2339,14 +2979,23 @@ var ( }) ) +func cloneItems(items []*lsproto.CompletionItem) []*lsproto.CompletionItem { + result := make([]*lsproto.CompletionItem, len(items)) + for i, item := range items { + itemClone := *item + result[i] = &itemClone + } + return result +} + func getKeywordCompletions(keywordFilter KeywordCompletionFilters, filterOutTsOnlyKeywords bool) []*lsproto.CompletionItem { if !filterOutTsOnlyKeywords { - return getTypescriptKeywordCompletions(keywordFilter) + return cloneItems(getTypescriptKeywordCompletions(keywordFilter)) } index := keywordFilter + KeywordCompletionFiltersLast + 1 if cached, ok := keywordCompletionsCache.Load(index); ok { - return cached + return cloneItems(cached) } result := core.Filter( getTypescriptKeywordCompletions(keywordFilter), @@ -2354,7 +3003,7 @@ func getKeywordCompletions(keywordFilter KeywordCompletionFilters, filterOutTsOn return !isTypeScriptOnlyKeyword(scanner.StringToToken(ci.Label)) }) keywordCompletionsCache.Store(index, result) - return result + return cloneItems(result) } func getTypescriptKeywordCompletions(keywordFilter KeywordCompletionFilters) []*lsproto.CompletionItem { @@ -2398,7 +3047,38 @@ func getTypescriptKeywordCompletions(keywordFilter KeywordCompletionFilters) []* } func isTypeScriptOnlyKeyword(kind ast.Kind) bool { - return false // !!! here + switch kind { + case ast.KindAbstractKeyword, + ast.KindAnyKeyword, + ast.KindBigIntKeyword, + ast.KindBooleanKeyword, + ast.KindDeclareKeyword, + ast.KindEnumKeyword, + ast.KindGlobalKeyword, + ast.KindImplementsKeyword, + ast.KindInferKeyword, + ast.KindInterfaceKeyword, + ast.KindIsKeyword, + ast.KindKeyOfKeyword, + ast.KindModuleKeyword, + ast.KindNamespaceKeyword, + ast.KindNeverKeyword, + ast.KindNumberKeyword, + ast.KindObjectKeyword, + ast.KindOverrideKeyword, + ast.KindPrivateKeyword, + ast.KindProtectedKeyword, + ast.KindPublicKeyword, + ast.KindReadonlyKeyword, + ast.KindStringKeyword, + ast.KindSymbolKeyword, + ast.KindTypeKeyword, + ast.KindUniqueKeyword, + ast.KindUnknownKeyword: + return true + default: + return false + } } func isFunctionLikeBodyKeyword(kind ast.Kind) bool { @@ -2508,3 +3188,773 @@ func isMemberCompletionKind(kind CompletionKind) bool { kind == CompletionKindMemberLike || kind == CompletionKindPropertyAccess } + +func tryGetFunctionLikeBodyCompletionContainer(contextToken *ast.Node) *ast.Node { + if contextToken == nil { + return nil + } + + var prev *ast.Node + container := ast.FindAncestorOrQuit(contextToken, func(node *ast.Node) ast.FindAncestorResult { + if ast.IsClassLike(node) { + return ast.FindAncestorQuit + } + if ast.IsFunctionLikeDeclaration(node) && prev == node.Body() { + return ast.FindAncestorTrue + } + prev = node + return ast.FindAncestorFalse + }) + return container +} + +func computeCommitCharactersAndIsNewIdentifier( + contextToken *ast.Node, + file *ast.SourceFile, + position int, +) (isNewIdentifierLocation bool, defaultCommitCharacters []string) { + if contextToken == nil { + return false, allCommitCharacters + } + containingNodeKind := contextToken.Parent.Kind + tokenKind := keywordForNode(contextToken) + // Previous token may have been a keyword that was converted to an identifier. + switch tokenKind { + case ast.KindCommaToken: + switch containingNodeKind { + // func( a, | + // new C(a, | + case ast.KindCallExpression, ast.KindNewExpression: + expression := contextToken.Parent.Expression() + // func\n(a, | + if getLineOfPosition(file, expression.End()) != getLineOfPosition(file, position) { + return true, noCommaCommitCharacters + } + return true, allCommitCharacters + // const x = (a, | + case ast.KindBinaryExpression: + return true, noCommaCommitCharacters + // constructor( a, | /* public, protected, private keywords are allowed here, so show completion */ + // var x: (s: string, list| + // const obj = { x, | + case ast.KindConstructor, ast.KindFunctionType, ast.KindObjectLiteralExpression: + return true, emptyCommitCharacters + // [a, | + case ast.KindArrayLiteralExpression: + return true, allCommitCharacters + default: + return false, allCommitCharacters + } + case ast.KindOpenParenToken: + switch containingNodeKind { + // func( | + // new C(a| + case ast.KindCallExpression, ast.KindNewExpression: + expression := contextToken.Parent.Expression() + // func\n( | + if getLineOfPosition(file, expression.End()) != getLineOfPosition(file, position) { + return true, noCommaCommitCharacters + } + return true, allCommitCharacters + // const x = (a| + case ast.KindParenthesizedExpression: + return true, noCommaCommitCharacters + // constructor( | + // function F(pred: (a| /* this can become an arrow function, where 'a' is the argument */ + case ast.KindConstructor, ast.KindParenthesizedType: + return true, emptyCommitCharacters + default: + return false, allCommitCharacters + } + case ast.KindOpenBracketToken: + switch containingNodeKind { + // [ | + // [ | : string ] + // [ | : string ] + // [ | /* this can become an index signature */ + case ast.KindArrayLiteralExpression, ast.KindIndexSignature, ast.KindTupleType, ast.KindComputedPropertyName: + return true, allCommitCharacters + default: + return false, allCommitCharacters + } + // module | + // namespace | + // import | + case ast.KindModuleKeyword, ast.KindNamespaceKeyword, ast.KindImportKeyword: + return true, emptyCommitCharacters + case ast.KindDotToken: + switch containingNodeKind { + // module A.| + case ast.KindModuleDeclaration: + return true, emptyCommitCharacters + default: + return false, allCommitCharacters + } + case ast.KindOpenBraceToken: + switch containingNodeKind { + // class A { | + // const obj = { | + case ast.KindClassDeclaration, ast.KindObjectLiteralExpression: + return true, emptyCommitCharacters + default: + return false, allCommitCharacters + } + case ast.KindEqualsToken: + switch containingNodeKind { + // const x = a| + // x = a| + case ast.KindVariableDeclaration, ast.KindBinaryExpression: + return true, allCommitCharacters + default: + return false, allCommitCharacters + } + case ast.KindTemplateHead: + // `aa ${| + return containingNodeKind == ast.KindTemplateExpression, allCommitCharacters + case ast.KindTemplateMiddle: + // `aa ${10} dd ${| + return containingNodeKind == ast.KindTemplateSpan, allCommitCharacters + case ast.KindAsyncKeyword: + // const obj = { async c|() + // const obj = { async c| + if containingNodeKind == ast.KindMethodDeclaration || containingNodeKind == ast.KindShorthandPropertyAssignment { + return true, emptyCommitCharacters + } + return false, allCommitCharacters + case ast.KindAsteriskToken: + // const obj = { * c| + if containingNodeKind == ast.KindMethodDeclaration { + return true, emptyCommitCharacters + } + return false, allCommitCharacters + } + + if isClassMemberCompletionKeyword(tokenKind) { + return true, emptyCommitCharacters + } + + return false, allCommitCharacters +} + +func keywordForNode(node *ast.Node) ast.Kind { + if ast.IsIdentifier(node) { + return scanner.IdentifierToKeywordKind(node.AsIdentifier()) + } + return node.Kind +} + +// Finds the first node that "embraces" the position, so that one may +// accurately aggregate locals from the closest containing scope. +func getScopeNode(initialToken *ast.Node, position int, file *ast.SourceFile) *ast.Node { + scope := initialToken + for scope != nil && !positionBelongsToNode(scope, position, file) { + scope = scope.Parent + } + return scope +} + +func isSnippetScope(scopeNode *ast.Node) bool { + switch scopeNode.Kind { + case ast.KindSourceFile, + ast.KindTemplateExpression, + ast.KindJsxExpression, + ast.KindBlock: + return true + default: + return ast.IsStatement(scopeNode) + } +} + +// Determines if a type is exactly the same type resolved by the global 'self', 'global', or 'globalThis'. +func isProbablyGlobalType(t *checker.Type, file *ast.SourceFile, typeChecker *checker.Checker) bool { + // The type of `self` and `window` is the same in lib.dom.d.ts, but `window` does not exist in + // lib.webworker.d.ts, so checking against `self` is also a check against `window` when it exists. + selfSymbol := typeChecker.GetGlobalSymbol("self", ast.SymbolFlagsValue, nil /*diagnostic*/) + if selfSymbol != nil && typeChecker.GetTypeOfSymbolAtLocation(selfSymbol, file.AsNode()) == t { + return true + } + globalSymbol := typeChecker.GetGlobalSymbol("global", ast.SymbolFlagsValue, nil /*diagnostic*/) + if globalSymbol != nil && typeChecker.GetTypeOfSymbolAtLocation(globalSymbol, file.AsNode()) == t { + return true + } + globalThisSymbol := typeChecker.GetGlobalSymbol("globalThis", ast.SymbolFlagsValue, nil /*diagnostic*/) + if globalThisSymbol != nil && typeChecker.GetTypeOfSymbolAtLocation(globalThisSymbol, file.AsNode()) == t { + return true + } + return false +} + +func tryGetTypeLiteralNode(node *ast.Node) *ast.TypeLiteral { + if node == nil { + return nil + } + + parent := node.Parent + switch node.Kind { + case ast.KindOpenBraceToken: + if ast.IsTypeLiteralNode(parent) { + return parent + } + case ast.KindSemicolonToken, ast.KindCommaToken, ast.KindIdentifier: + if parent.Kind == ast.KindPropertySignature && ast.IsTypeLiteralNode(parent.Parent) { + return parent.Parent + } + } + + return nil +} + +func getConstraintOfTypeArgumentProperty(node *ast.Node, typeChecker *checker.Checker) *checker.Type { + if node == nil { + return nil + } + + if ast.IsTypeNode(node) && ast.IsTypeReferenceType(node.Parent) { + return typeChecker.GetTypeArgumentConstraint(node) + } + + t := getConstraintOfTypeArgumentProperty(node.Parent, typeChecker) + if t == nil { + return nil + } + + switch node.Kind { + case ast.KindPropertySignature: + return typeChecker.GetTypeOfPropertyOfContextualType(t, node.Symbol().Name) + case ast.KindIntersectionType, ast.KindTypeLiteral, ast.KindUnionType: + return t + } + + return nil +} + +func tryGetObjectLikeCompletionContainer(contextToken *ast.Node, position int, file *ast.SourceFile) *ast.ObjectLiteralLike { + if contextToken == nil { + return nil + } + + parent := contextToken.Parent + switch contextToken.Kind { + // const x = { | + // const x = { a: 0, | + case ast.KindOpenBraceToken, ast.KindCommaToken: + if ast.IsObjectLiteralExpression(parent) || ast.IsObjectBindingPattern(parent) { + return parent + } + case ast.KindAsteriskToken: + if ast.IsMethodDeclaration(parent) && ast.IsObjectLiteralExpression(parent.Parent) { + return parent.Parent + } + case ast.KindAsyncKeyword: + if ast.IsObjectLiteralExpression(parent.Parent) { + return parent.Parent + } + case ast.KindIdentifier: + if contextToken.Text() == "async" && ast.IsShorthandPropertyAssignment(parent) { + return parent.Parent + } else { + if ast.IsObjectLiteralExpression(parent.Parent) && + (ast.IsSpreadAssignment(parent) || + ast.IsShorthandPropertyAssignment(parent) && + getLineOfPosition(file, contextToken.End()) != getLineOfPosition(file, position)) { + return parent.Parent + } + ancestorNode := ast.FindAncestor(parent, ast.IsPropertyAssignment) + if ancestorNode != nil && getLastToken(ancestorNode, file) == contextToken && ast.IsObjectLiteralExpression(ancestorNode.Parent) { + return ancestorNode.Parent + } + } + default: + if parent.Parent != nil && parent.Parent.Parent != nil && + (ast.IsMethodDeclaration(parent.Parent) || + ast.IsGetAccessorDeclaration(parent.Parent) || + ast.IsSetAccessorDeclaration(parent.Parent)) && + ast.IsObjectLiteralExpression(parent.Parent.Parent) { + return parent.Parent.Parent + } + if ast.IsSpreadAssignment(parent) && ast.IsObjectLiteralExpression(parent.Parent) { + return parent.Parent + } + ancestorNode := ast.FindAncestor(parent, ast.IsPropertyAssignment) + if contextToken.Kind != ast.KindColonToken && + ancestorNode != nil && getLastToken(ancestorNode, file) == contextToken && + ast.IsObjectLiteralExpression(ancestorNode.Parent) { + return ancestorNode.Parent + } + } + + return nil +} + +func tryGetObjectLiteralContextualType(node *ast.ObjectLiteralExpressionNode, typeChecker *checker.Checker) *checker.Type { + t := typeChecker.GetContextualType(node, checker.ContextFlagsNone) + if t != nil { + return t + } + + parent := ast.WalkUpParenthesizedExpressions(node.Parent) + if ast.IsBinaryExpression(parent) && + parent.AsBinaryExpression().OperatorToken.Kind == ast.KindEqualsToken && + node == parent.AsBinaryExpression().Left { + // Object literal is assignment pattern: ({ | } = x) + return typeChecker.GetTypeAtLocation(parent) + } + if ast.IsExpression(parent) { + // f(() => (({ | }))); + return typeChecker.GetContextualType(parent, checker.ContextFlagsNone) + } + + return nil +} + +func getPropertiesForObjectExpression( + contextualType *checker.Type, + completionsType *checker.Type, + obj *ast.Node, + typeChecker *checker.Checker, +) []*ast.Symbol { + hasCompletionsType := completionsType != nil && completionsType != contextualType + var types []*checker.Type + if contextualType.IsUnion() { + types = contextualType.Types() + } else { + types = []*checker.Type{contextualType} + } + promiseFilteredContextualType := typeChecker.GetUnionType(core.Filter(types, func(t *checker.Type) bool { + return typeChecker.GetPromisedTypeOfPromise(t) == nil + })) + + var t *checker.Type + if hasCompletionsType && completionsType.Flags()&checker.TypeFlagsAnyOrUnknown == 0 { + t = typeChecker.GetUnionType([]*checker.Type{promiseFilteredContextualType, completionsType}) + } else { + t = promiseFilteredContextualType + } + + // Filter out members whose only declaration is the object literal itself to avoid + // self-fulfilling completions like: + // + // function f(x: T) {} + // f({ abc/**/: "" }) // `abc` is a member of `T` but only because it declares itself + hasDeclarationOtherThanSelf := func(member *ast.Symbol) bool { + if len(member.Declarations) == 0 { + return true + } + return core.Some(member.Declarations, func(decl *ast.Declaration) bool { return decl.Parent != obj }) + } + + properties := getApparentProperties(t, obj, typeChecker) + if t.IsClass() && containsNonPublicProperties(properties) { + return nil + } else if hasCompletionsType { + return core.Filter(properties, hasDeclarationOtherThanSelf) + } else { + return properties + } +} + +func getApparentProperties(t *checker.Type, node *ast.Node, typeChecker *checker.Checker) []*ast.Symbol { + if !t.IsUnion() { + return typeChecker.GetApparentProperties(t) + } + return typeChecker.GetAllPossiblePropertiesOfTypes(core.Filter(t.Types(), func(memberType *checker.Type) bool { + return !(memberType.Flags()&checker.TypeFlagsPrimitive != 0 || + typeChecker.IsArrayLikeType(memberType) || + typeChecker.IsTypeInvalidDueToUnionDiscriminant(memberType, node) || + typeChecker.TypeHasCallOrConstructSignatures(memberType) || + memberType.IsClass() && containsNonPublicProperties(typeChecker.GetApparentProperties(memberType))) + })) +} + +func containsNonPublicProperties(props []*ast.Symbol) bool { + return core.Some(props, func(p *ast.Symbol) bool { + return checker.GetDeclarationModifierFlagsFromSymbol(p)&ast.ModifierFlagsNonPublicAccessibilityModifier != 0 + }) +} + +// Filters out members that are already declared in the object literal or binding pattern. +// Also computes the set of existing members declared by spread assignment. +func filterObjectMembersList( + contextualMemberSymbols []*ast.Symbol, + existingMembers []*ast.Declaration, + file *ast.SourceFile, + position int, + typeChecker *checker.Checker, +) (filteredMembers []*ast.Symbol, spreadMemberNames *core.Set[string]) { + if len(existingMembers) == 0 { + return contextualMemberSymbols, &core.Set[string]{} + } + + membersDeclaredBySpreadAssignment := core.Set[string]{} + existingMemberNames := core.Set[string]{} + for _, member := range existingMembers { + // Ignore omitted expressions for missing members. + if member.Kind != ast.KindPropertyAssignment && + member.Kind != ast.KindShorthandPropertyAssignment && + member.Kind != ast.KindBindingElement && + member.Kind != ast.KindMethodDeclaration && + member.Kind != ast.KindGetAccessor && + member.Kind != ast.KindSetAccessor && + member.Kind != ast.KindSpreadAssignment { + continue + } + + // If this is the current item we are editing right now, do not filter it out. + if isCurrentlyEditingNode(member, file, position) { + continue + } + + var existingName string + + if ast.IsSpreadAssignment(member) { + setMemberDeclaredBySpreadAssignment(member, &membersDeclaredBySpreadAssignment, typeChecker) + } else if ast.IsBindingElement(member) && member.AsBindingElement().PropertyName != nil { + // include only identifiers in completion list + if member.AsBindingElement().PropertyName.Kind == ast.KindIdentifier { + existingName = member.AsBindingElement().PropertyName.Text() + } + } else { + // TODO: Account for computed property name + // NOTE: if one only performs this step when m.name is an identifier, + // things like '__proto__' are not filtered out. + name := ast.GetNameOfDeclaration(member) + if name != nil && ast.IsPropertyNameLiteral(name) { + existingName = name.Text() + } + } + + if existingName != "" { + existingMemberNames.Add(existingName) + } + } + + filteredSymbols := core.Filter(contextualMemberSymbols, func(m *ast.Symbol) bool { + return !existingMemberNames.Has(m.Name) + }) + + return filteredSymbols, &membersDeclaredBySpreadAssignment +} + +func isCurrentlyEditingNode(node *ast.Node, file *ast.SourceFile, position int) bool { + start := astnav.GetStartOfNode(node, file, false /*includeJSDoc*/) + return start <= position && position <= node.End() +} + +func setMemberDeclaredBySpreadAssignment(declaration *ast.Node, members *core.Set[string], typeChecker *checker.Checker) { + expression := declaration.Expression() + symbol := typeChecker.GetSymbolAtLocation(expression) + var t *checker.Type + if symbol != nil { + t = typeChecker.GetTypeOfSymbolAtLocation(symbol, expression) + } + properties := t.AsStructuredType().Properties() + for _, property := range properties { + members.Add(property.Name) + } +} + +// Returns the immediate owning class declaration of a context token, +// on the condition that one exists and that the context implies completion should be given. +func tryGetConstructorLikeCompletionContainer(contextToken *ast.Node) *ast.ConstructorDeclarationNode { + if contextToken == nil { + return nil + } + + parent := contextToken.Parent + switch contextToken.Kind { + case ast.KindOpenParenToken, ast.KindCommaToken: + if ast.IsConstructorDeclaration(parent) { + return parent + } + return nil + default: + if isConstructorParameterCompletion(contextToken) { + return parent.Parent + } + } + return nil +} + +func isConstructorParameterCompletion(node *ast.Node) bool { + return node.Parent != nil && ast.IsParameter(node.Parent) && ast.IsConstructorDeclaration(node.Parent.Parent) && + (ast.IsParameterPropertyModifier(node.Kind) || ast.IsDeclarationName(node)) +} + +// Returns the immediate owning class declaration of a context token, +// on the condition that one exists and that the context implies completion should be given. +func tryGetObjectTypeDeclarationCompletionContainer( + file *ast.SourceFile, + contextToken *ast.Node, + location *ast.Node, + position int, +) *ast.ObjectTypeDeclaration { + // class c { method() { } | method2() { } } + switch location.Kind { + case ast.KindSyntaxList: + if ast.IsObjectTypeDeclaration(location.Parent) { + return location.Parent + } + return nil + // !!! we don't include EOF token anymore, verify what we should do in this case. + case ast.KindEndOfFile: + stmtList := location.Parent.AsSourceFile().Statements + if stmtList != nil && len(stmtList.Nodes) > 0 && ast.IsObjectTypeDeclaration(stmtList.Nodes[len(stmtList.Nodes)-1]) { + cls := stmtList.Nodes[len(stmtList.Nodes)-1] + if findChildOfKind(cls, ast.KindCloseBraceToken, file) == nil { + return cls + } + } + case ast.KindPrivateIdentifier: + if ast.IsPropertyDeclaration(location.Parent) { + return ast.FindAncestor(location, ast.IsClassLike) + } + case ast.KindIdentifier: + originalKeywordKind := scanner.IdentifierToKeywordKind(location.AsIdentifier()) + if originalKeywordKind != ast.KindUnknown { + return nil + } + // class c { public prop = c| } + if ast.IsPropertyDeclaration(location.Parent) && location.Parent.Initializer() == location { + return nil + } + // class c extends React.Component { a: () => 1\n compon| } + if isFromObjectTypeDeclaration(location) { + return ast.FindAncestor(location, ast.IsObjectTypeDeclaration) + } + } + + if contextToken == nil { + return nil + } + + // class C { blah; constructor/**/ } + // or + // class C { blah \n constructor/**/ } + if location.Kind == ast.KindConstructorKeyword || + (ast.IsIdentifier(contextToken) && ast.IsPropertyDeclaration(contextToken.Parent) && ast.IsClassLike(location)) { + return ast.FindAncestor(contextToken, ast.IsClassLike) + } + + switch contextToken.Kind { + // class c { public prop = | /* global completions */ } + case ast.KindEqualsToken: + return nil + // class c {getValue(): number; | } + // class c { method() { } | } + case ast.KindSemicolonToken, ast.KindCloseBraceToken: + // class c { method() { } b| } + if isFromObjectTypeDeclaration(location) && location.Parent.Name() == location { + return location.Parent.Parent + } + if ast.IsObjectTypeDeclaration(location) { + return location + } + return nil + // class c { | + // class c {getValue(): number, | } + case ast.KindOpenBraceToken, ast.KindCommaToken: + if ast.IsObjectTypeDeclaration(contextToken.Parent) { + return contextToken.Parent + } + return nil + default: + if ast.IsObjectTypeDeclaration(location) { + // class C extends React.Component { a: () => 1\n| } + // class C { prop = ""\n | } + if getLineOfPosition(file, contextToken.End()) != getLineOfPosition(file, position) { + return location + } + isValidKeyword := core.IfElse( + ast.IsClassLike(contextToken.Parent.Parent), + isClassMemberCompletionKeyword, + isInterfaceOrTypeLiteralCompletionKeyword, + ) + + if isValidKeyword(contextToken.Kind) || contextToken.Kind == ast.KindAsteriskToken || + ast.IsIdentifier(contextToken) && isValidKeyword(scanner.IdentifierToKeywordKind(contextToken.AsIdentifier())) { + return contextToken.Parent.Parent + } + } + + return nil + } +} + +func isFromObjectTypeDeclaration(node *ast.Node) bool { + return node.Parent != nil && ast.IsClassOrTypeElement(node.Parent) && ast.IsObjectTypeDeclaration(node.Parent.Parent) +} + +// Filters out completion suggestions for class elements. +func filterClassMembersList( + baseSymbols []*ast.Symbol, + existingMembers []*ast.ClassElement, + classElementModifierFlags ast.ModifierFlags, + file *ast.SourceFile, + position int, +) []*ast.Symbol { + existingMemberNames := core.Set[string]{} + for _, member := range existingMembers { + // Ignore omitted expressions for missing members. + if member.Kind != ast.KindPropertyDeclaration && + member.Kind != ast.KindMethodDeclaration && + member.Kind != ast.KindGetAccessor && + member.Kind != ast.KindSetAccessor { + continue + } + + // If this is the current item we are editing right now, do not filter it out + if isCurrentlyEditingNode(member, file, position) { + continue + } + + // Don't filter member even if the name matches if it is declared private in the list. + if member.ModifierFlags()&ast.ModifierFlagsPrivate != 0 { + continue + } + + // Do not filter it out if the static presence doesn't match. + if ast.IsStatic(member) != (classElementModifierFlags&ast.ModifierFlagsStatic != 0) { + continue + } + + existingName := ast.GetPropertyNameForPropertyNameNode(member.Name()) + if existingName != "" { + existingMemberNames.Add(existingName) + } + } + + return core.Filter(baseSymbols, func(propertySymbol *ast.Symbol) bool { + return !existingMemberNames.Has(propertySymbol.Name) && + len(propertySymbol.Declarations) > 0 && + checker.GetDeclarationModifierFlagsFromSymbol(propertySymbol)&ast.ModifierFlagsPrivate == 0 && + !(propertySymbol.ValueDeclaration != nil && ast.IsPrivateIdentifierClassElementDeclaration(propertySymbol.ValueDeclaration)) + }) +} + +func tryGetContainingJsxElement(contextToken *ast.Node, file *ast.SourceFile) *ast.JsxOpeningLikeElement { + if contextToken == nil { + return nil + } + + parent := contextToken.Parent + switch contextToken.Kind { + case ast.KindGreaterThanToken, ast.KindLessThanSlashToken, ast.KindSlashToken, ast.KindIdentifier, + ast.KindPropertyAccessExpression, ast.KindJsxAttributes, ast.KindJsxAttribute, ast.KindJsxSpreadAttribute: + if parent != nil && (parent.Kind == ast.KindJsxSelfClosingElement || parent.Kind == ast.KindJsxOpeningElement) { + if contextToken.Kind == ast.KindGreaterThanToken { + precedingToken := astnav.FindPrecedingToken(file, contextToken.Pos()) + if len(parent.TypeArguments()) == 0 || + precedingToken != nil && precedingToken.Kind == ast.KindSlashToken { + return nil + } + } + return parent + } + // The context token is the closing } or " of an attribute, which means + // its parent is a JsxExpression, whose parent is a JsxAttribute, + // whose parent is a JsxOpeningLikeElement + case ast.KindStringLiteral: + if parent != nil && (parent.Kind == ast.KindJsxAttribute || parent.Kind == ast.KindJsxSpreadAttribute) { + // Currently we parse JsxOpeningLikeElement as: + // JsxOpeningLikeElement + // attributes: JsxAttributes + // properties: NodeArray + return parent.Parent.Parent + } + case ast.KindCloseBraceToken: + if parent != nil && parent.Kind == ast.KindJsxExpression && + parent.Parent != nil && parent.Parent.Kind == ast.KindJsxAttribute { + // Currently we parse JsxOpeningLikeElement as: + // JsxOpeningLikeElement + // attributes: JsxAttributes + // properties: NodeArray + // each JsxAttribute can have initializer as JsxExpression + return parent.Parent.Parent.Parent + } + if parent != nil && parent.Kind == ast.KindJsxSpreadAttribute { + // Currently we parse JsxOpeningLikeElement as: + // JsxOpeningLikeElement + // attributes: JsxAttributes + // properties: NodeArray + return parent.Parent.Parent + } + } + + return nil +} + +// Filters out completion suggestions from 'symbols' according to existing JSX attributes. +// @returns Symbols to be suggested in a JSX element, barring those whose attributes +// do not occur at the current position and have not otherwise been typed. +func filterJsxAttributes( + symbols []*ast.Symbol, + attributes []*ast.JsxAttributeLike, + file *ast.SourceFile, + position int, + typeChecker *checker.Checker, +) (filteredMembers []*ast.Symbol, spreadMemberNames *core.Set[string]) { + existingNames := core.Set[string]{} + membersDeclaredBySpreadAssignment := core.Set[string]{} + for _, attr := range attributes { + // If this is the item we are editing right now, do not filter it out. + if isCurrentlyEditingNode(attr, file, position) { + continue + } + + if attr.Kind == ast.KindJsxAttribute { + existingNames.Add(attr.Name().Text()) + } else if ast.IsJsxSpreadAttribute(attr) { + setMemberDeclaredBySpreadAssignment(attr, &membersDeclaredBySpreadAssignment, typeChecker) + } + } + + return core.Filter(symbols, func(a *ast.Symbol) bool { return !existingNames.Has(a.Name) }), + &membersDeclaredBySpreadAssignment +} + +func isTypeKeywordTokenOrIdentifier(node *ast.Node) bool { + return ast.IsTypeKeywordToken(node) || + ast.IsIdentifier(node) && scanner.IdentifierToKeywordKind(node.AsIdentifier()) == ast.KindTypeKeyword +} + +// Returns the default commit characters for completion items, if that capability is supported. +// Otherwise, if item commit characters are supported, sets the commit characters on each item. +func setCommitCharacters( + clientOptions *lsproto.CompletionClientCapabilities, + items []*lsproto.CompletionItem, + defaultCommitCharacters *[]string, +) *lsproto.CompletionItemDefaults { + var itemDefaults *lsproto.CompletionItemDefaults + supportsItemCommitCharacters := ptrIsTrue(clientOptions.CompletionItem.CommitCharactersSupport) + if supportsDefaultCommitCharacters(clientOptions) && supportsItemCommitCharacters { + itemDefaults = &lsproto.CompletionItemDefaults{ + CommitCharacters: defaultCommitCharacters, + } + } else if supportsItemCommitCharacters { + for _, item := range items { + if item.CommitCharacters == nil { + item.CommitCharacters = defaultCommitCharacters + } + } + } + + return itemDefaults +} + +func specificKeywordCompletionInfo( + clientOptions *lsproto.CompletionClientCapabilities, + items []*lsproto.CompletionItem, + isNewIdentifierLocation bool, +) *lsproto.CompletionList { + defaultCommitCharacters := getDefaultCommitCharacters(isNewIdentifierLocation) + itemDefaults := setCommitCharacters( + clientOptions, + items, + &defaultCommitCharacters, + ) + return &lsproto.CompletionList{ + IsIncomplete: false, + ItemDefaults: itemDefaults, + Items: items, + } +} diff --git a/internal/ls/completions_test.go b/internal/ls/completions_test.go index 31675cbf43..a4dd1c056d 100644 --- a/internal/ls/completions_test.go +++ b/internal/ls/completions_test.go @@ -1,8 +1,10 @@ package ls_test import ( + "slices" "testing" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/microsoft/typescript-go/internal/bundled" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/ls" @@ -15,12 +17,19 @@ import ( var defaultCommitCharacters = []string{".", ",", ";"} type testCase struct { - name string - files map[string]string - expected map[string]*lsproto.CompletionList + name string + files map[string]string + expectedResult map[string]*testCaseResult + mainFileName string } -const mainFileName = "/index.ts" +type testCaseResult struct { + list *lsproto.CompletionList + isIncludes bool + excludes []string +} + +const defaultMainFileName = "/index.ts" func TestCompletions(t *testing.T) { t.Parallel() @@ -37,10 +46,13 @@ func TestCompletions(t *testing.T) { sortTextLocationPriority := ptrTo(string(ls.SortTextLocationPriority)) sortTextLocalDeclarationPriority := ptrTo(string(ls.SortTextLocalDeclarationPriority)) sortTextDeprecatedLocationPriority := ptrTo(string(ls.DeprecateSortText(ls.SortTextLocationPriority))) + sortTextGlobalsOrKeywords := ptrTo(string(ls.SortTextGlobalsOrKeywords)) fieldKind := ptrTo(lsproto.CompletionItemKindField) methodKind := ptrTo(lsproto.CompletionItemKindMethod) functionKind := ptrTo(lsproto.CompletionItemKindFunction) variableKind := ptrTo(lsproto.CompletionItemKindVariable) + classKind := ptrTo(lsproto.CompletionItemKindClass) + keywordKind := ptrTo(lsproto.CompletionItemKindKeyword) stringMembers := []*lsproto.CompletionItem{ {Label: "charAt", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, @@ -95,7 +107,7 @@ func TestCompletions(t *testing.T) { { name: "basicInterfaceMembers", files: map[string]string{ - mainFileName: `export {}; + defaultMainFileName: `export {}; interface Point { x: number; y: number; @@ -103,47 +115,49 @@ interface Point { declare const p: Point; p./*a*/`, }, - expected: map[string]*lsproto.CompletionList{ + expectedResult: map[string]*testCaseResult{ "a": { - IsIncomplete: false, - ItemDefaults: itemDefaults, - Items: []*lsproto.CompletionItem{ - { - Label: "x", - Kind: fieldKind, - SortText: sortTextLocationPriority, - FilterText: ptrTo(".x"), - InsertTextFormat: insertTextFormatPlainText, - TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ - InsertReplaceEdit: &lsproto.InsertReplaceEdit{ - NewText: "x", - Insert: lsproto.Range{ - Start: lsproto.Position{Line: 6, Character: 2}, - End: lsproto.Position{Line: 6, Character: 2}, - }, - Replace: lsproto.Range{ - Start: lsproto.Position{Line: 6, Character: 2}, - End: lsproto.Position{Line: 6, Character: 2}, + list: &lsproto.CompletionList{ + IsIncomplete: false, + ItemDefaults: itemDefaults, + Items: []*lsproto.CompletionItem{ + { + Label: "x", + Kind: fieldKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".x"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "x", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 6, Character: 2}, + End: lsproto.Position{Line: 6, Character: 2}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 6, Character: 2}, + End: lsproto.Position{Line: 6, Character: 2}, + }, }, }, }, - }, - { - Label: "y", - Kind: fieldKind, - SortText: sortTextLocationPriority, - FilterText: ptrTo(".y"), - InsertTextFormat: insertTextFormatPlainText, - TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ - InsertReplaceEdit: &lsproto.InsertReplaceEdit{ - NewText: "y", - Insert: lsproto.Range{ - Start: lsproto.Position{Line: 6, Character: 2}, - End: lsproto.Position{Line: 6, Character: 2}, - }, - Replace: lsproto.Range{ - Start: lsproto.Position{Line: 6, Character: 2}, - End: lsproto.Position{Line: 6, Character: 2}, + { + Label: "y", + Kind: fieldKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".y"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "y", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 6, Character: 2}, + End: lsproto.Position{Line: 6, Character: 2}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 6, Character: 2}, + End: lsproto.Position{Line: 6, Character: 2}, + }, }, }, }, @@ -156,7 +170,7 @@ p./*a*/`, name: "basicInterfaceMembersOptional", files: map[string]string{ "/tsconfig.json": `{ "compilerOptions": { "strict": true } }`, - mainFileName: `export {}; + defaultMainFileName: `export {}; interface Point { x: number; y: number; @@ -164,41 +178,43 @@ interface Point { declare const p: Point | undefined; p./*a*/`, }, - expected: map[string]*lsproto.CompletionList{ + expectedResult: map[string]*testCaseResult{ "a": { - IsIncomplete: false, - ItemDefaults: itemDefaults, - Items: []*lsproto.CompletionItem{ - { - Label: "x", - Kind: fieldKind, - SortText: sortTextLocationPriority, - FilterText: ptrTo(".?.x"), - InsertText: ptrTo("?.x"), - InsertTextFormat: insertTextFormatPlainText, - TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ - TextEdit: &lsproto.TextEdit{ - NewText: "?.x", - Range: lsproto.Range{ - Start: lsproto.Position{Line: 6, Character: 1}, - End: lsproto.Position{Line: 6, Character: 2}, + list: &lsproto.CompletionList{ + IsIncomplete: false, + ItemDefaults: itemDefaults, + Items: []*lsproto.CompletionItem{ + { + Label: "x", + Kind: fieldKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".?.x"), + InsertText: ptrTo("?.x"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + TextEdit: &lsproto.TextEdit{ + NewText: "?.x", + Range: lsproto.Range{ + Start: lsproto.Position{Line: 6, Character: 1}, + End: lsproto.Position{Line: 6, Character: 2}, + }, }, }, }, - }, - { - Label: "y", - Kind: fieldKind, - SortText: sortTextLocationPriority, - FilterText: ptrTo(".?.y"), - InsertText: ptrTo("?.y"), - InsertTextFormat: insertTextFormatPlainText, - TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ - TextEdit: &lsproto.TextEdit{ - NewText: "?.y", - Range: lsproto.Range{ - Start: lsproto.Position{Line: 6, Character: 1}, - End: lsproto.Position{Line: 6, Character: 2}, + { + Label: "y", + Kind: fieldKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".?.y"), + InsertText: ptrTo("?.y"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + TextEdit: &lsproto.TextEdit{ + NewText: "?.y", + Range: lsproto.Range{ + Start: lsproto.Position{Line: 6, Character: 1}, + End: lsproto.Position{Line: 6, Character: 2}, + }, }, }, }, @@ -210,31 +226,33 @@ p./*a*/`, { name: "objectLiteralType", files: map[string]string{ - mainFileName: `export {}; + defaultMainFileName: `export {}; let x = { foo: 123 }; x./*a*/`, }, - expected: map[string]*lsproto.CompletionList{ + expectedResult: map[string]*testCaseResult{ "a": { - IsIncomplete: false, - ItemDefaults: itemDefaults, - Items: []*lsproto.CompletionItem{ - { - Label: "foo", - Kind: fieldKind, - SortText: sortTextLocationPriority, - FilterText: ptrTo(".foo"), - InsertTextFormat: ptrTo(lsproto.InsertTextFormatPlainText), - TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ - InsertReplaceEdit: &lsproto.InsertReplaceEdit{ - NewText: "foo", - Insert: lsproto.Range{ - Start: lsproto.Position{Line: 2, Character: 2}, - End: lsproto.Position{Line: 2, Character: 2}, - }, - Replace: lsproto.Range{ - Start: lsproto.Position{Line: 2, Character: 2}, - End: lsproto.Position{Line: 2, Character: 2}, + list: &lsproto.CompletionList{ + IsIncomplete: false, + ItemDefaults: itemDefaults, + Items: []*lsproto.CompletionItem{ + { + Label: "foo", + Kind: fieldKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".foo"), + InsertTextFormat: ptrTo(lsproto.InsertTextFormatPlainText), + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "foo", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 2, Character: 2}, + End: lsproto.Position{Line: 2, Character: 2}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 2, Character: 2}, + End: lsproto.Position{Line: 2, Character: 2}, + }, }, }, }, @@ -246,53 +264,55 @@ x./*a*/`, { name: "basicClassMembers", files: map[string]string{ - mainFileName: ` + defaultMainFileName: ` class n { constructor (public x: number, public y: number, private z: string) { } } var t = new n(0, 1, '');t./*a*/`, }, - expected: map[string]*lsproto.CompletionList{ + expectedResult: map[string]*testCaseResult{ "a": { - IsIncomplete: false, - ItemDefaults: itemDefaults, - Items: []*lsproto.CompletionItem{ - { - Label: "x", - Kind: fieldKind, - SortText: sortTextLocationPriority, - FilterText: ptrTo(".x"), - InsertTextFormat: insertTextFormatPlainText, - TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ - InsertReplaceEdit: &lsproto.InsertReplaceEdit{ - NewText: "x", - Insert: lsproto.Range{ - Start: lsproto.Position{Line: 3, Character: 26}, - End: lsproto.Position{Line: 3, Character: 26}, - }, - Replace: lsproto.Range{ - Start: lsproto.Position{Line: 3, Character: 26}, - End: lsproto.Position{Line: 3, Character: 26}, + list: &lsproto.CompletionList{ + IsIncomplete: false, + ItemDefaults: itemDefaults, + Items: []*lsproto.CompletionItem{ + { + Label: "x", + Kind: fieldKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".x"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "x", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 3, Character: 26}, + End: lsproto.Position{Line: 3, Character: 26}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 3, Character: 26}, + End: lsproto.Position{Line: 3, Character: 26}, + }, }, }, }, - }, - { - Label: "y", - Kind: fieldKind, - SortText: sortTextLocationPriority, - FilterText: ptrTo(".y"), - InsertTextFormat: insertTextFormatPlainText, - TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ - InsertReplaceEdit: &lsproto.InsertReplaceEdit{ - NewText: "y", - Insert: lsproto.Range{ - Start: lsproto.Position{Line: 3, Character: 26}, - End: lsproto.Position{Line: 3, Character: 26}, - }, - Replace: lsproto.Range{ - Start: lsproto.Position{Line: 3, Character: 26}, - End: lsproto.Position{Line: 3, Character: 26}, + { + Label: "y", + Kind: fieldKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".y"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "y", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 3, Character: 26}, + End: lsproto.Position{Line: 3, Character: 26}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 3, Character: 26}, + End: lsproto.Position{Line: 3, Character: 26}, + }, }, }, }, @@ -304,7 +324,7 @@ var t = new n(0, 1, '');t./*a*/`, { name: "cloduleAsBaseClass", files: map[string]string{ - mainFileName: ` + defaultMainFileName: ` class A { constructor(x: number) { } foo() { } @@ -326,247 +346,249 @@ class D extends A { D./*a*/`, }, - expected: map[string]*lsproto.CompletionList{ + expectedResult: map[string]*testCaseResult{ "a": { - IsIncomplete: false, - ItemDefaults: itemDefaults, - Items: []*lsproto.CompletionItem{ // !!! `funcionMembersPlus` - { - Label: "bar", - Kind: methodKind, - SortText: sortTextLocalDeclarationPriority, - FilterText: ptrTo(".bar"), - InsertTextFormat: insertTextFormatPlainText, - TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ - InsertReplaceEdit: &lsproto.InsertReplaceEdit{ - NewText: "bar", - Insert: lsproto.Range{ - Start: lsproto.Position{Line: 19, Character: 2}, - End: lsproto.Position{Line: 19, Character: 2}, - }, - Replace: lsproto.Range{ - Start: lsproto.Position{Line: 19, Character: 2}, - End: lsproto.Position{Line: 19, Character: 2}, + list: &lsproto.CompletionList{ + IsIncomplete: false, + ItemDefaults: itemDefaults, + Items: []*lsproto.CompletionItem{ // !!! `funcionMembersPlus` + { + Label: "bar", + Kind: methodKind, + SortText: sortTextLocalDeclarationPriority, + FilterText: ptrTo(".bar"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "bar", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, }, }, }, - }, - { - Label: "bar2", - Kind: methodKind, - SortText: sortTextLocalDeclarationPriority, - FilterText: ptrTo(".bar2"), - InsertTextFormat: insertTextFormatPlainText, - TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ - InsertReplaceEdit: &lsproto.InsertReplaceEdit{ - NewText: "bar2", - Insert: lsproto.Range{ - Start: lsproto.Position{Line: 19, Character: 2}, - End: lsproto.Position{Line: 19, Character: 2}, - }, - Replace: lsproto.Range{ - Start: lsproto.Position{Line: 19, Character: 2}, - End: lsproto.Position{Line: 19, Character: 2}, + { + Label: "bar2", + Kind: methodKind, + SortText: sortTextLocalDeclarationPriority, + FilterText: ptrTo(".bar2"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "bar2", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, }, }, }, - }, - { - Label: "apply", - Kind: methodKind, - SortText: sortTextLocationPriority, - FilterText: ptrTo(".apply"), - InsertTextFormat: insertTextFormatPlainText, - TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ - InsertReplaceEdit: &lsproto.InsertReplaceEdit{ - NewText: "apply", - Insert: lsproto.Range{ - Start: lsproto.Position{Line: 19, Character: 2}, - End: lsproto.Position{Line: 19, Character: 2}, - }, - Replace: lsproto.Range{ - Start: lsproto.Position{Line: 19, Character: 2}, - End: lsproto.Position{Line: 19, Character: 2}, + { + Label: "apply", + Kind: methodKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".apply"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "apply", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, }, }, }, - }, - { - Label: "arguments", - Kind: fieldKind, - SortText: sortTextLocationPriority, - FilterText: ptrTo(".arguments"), - InsertTextFormat: insertTextFormatPlainText, - TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ - InsertReplaceEdit: &lsproto.InsertReplaceEdit{ - NewText: "arguments", - Insert: lsproto.Range{ - Start: lsproto.Position{Line: 19, Character: 2}, - End: lsproto.Position{Line: 19, Character: 2}, - }, - Replace: lsproto.Range{ - Start: lsproto.Position{Line: 19, Character: 2}, - End: lsproto.Position{Line: 19, Character: 2}, + { + Label: "arguments", + Kind: fieldKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".arguments"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "arguments", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, }, }, }, - }, - { - Label: "baz", - Kind: functionKind, - SortText: sortTextLocationPriority, - FilterText: ptrTo(".baz"), - InsertTextFormat: insertTextFormatPlainText, - TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ - InsertReplaceEdit: &lsproto.InsertReplaceEdit{ - NewText: "baz", - Insert: lsproto.Range{ - Start: lsproto.Position{Line: 19, Character: 2}, - End: lsproto.Position{Line: 19, Character: 2}, - }, - Replace: lsproto.Range{ - Start: lsproto.Position{Line: 19, Character: 2}, - End: lsproto.Position{Line: 19, Character: 2}, + { + Label: "baz", + Kind: functionKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".baz"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "baz", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, }, }, }, - }, - { - Label: "bind", - Kind: methodKind, - SortText: sortTextLocationPriority, - FilterText: ptrTo(".bind"), - InsertTextFormat: insertTextFormatPlainText, - TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ - InsertReplaceEdit: &lsproto.InsertReplaceEdit{ - NewText: "bind", - Insert: lsproto.Range{ - Start: lsproto.Position{Line: 19, Character: 2}, - End: lsproto.Position{Line: 19, Character: 2}, - }, - Replace: lsproto.Range{ - Start: lsproto.Position{Line: 19, Character: 2}, - End: lsproto.Position{Line: 19, Character: 2}, + { + Label: "bind", + Kind: methodKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".bind"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "bind", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, }, }, }, - }, - { - Label: "call", - Kind: methodKind, - SortText: sortTextLocationPriority, - FilterText: ptrTo(".call"), - InsertTextFormat: insertTextFormatPlainText, - TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ - InsertReplaceEdit: &lsproto.InsertReplaceEdit{ - NewText: "call", - Insert: lsproto.Range{ - Start: lsproto.Position{Line: 19, Character: 2}, - End: lsproto.Position{Line: 19, Character: 2}, - }, - Replace: lsproto.Range{ - Start: lsproto.Position{Line: 19, Character: 2}, - End: lsproto.Position{Line: 19, Character: 2}, + { + Label: "call", + Kind: methodKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".call"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "call", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, }, }, }, - }, - { - Label: "caller", - Kind: fieldKind, - SortText: sortTextLocationPriority, - FilterText: ptrTo(".caller"), - InsertTextFormat: insertTextFormatPlainText, - TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ - InsertReplaceEdit: &lsproto.InsertReplaceEdit{ - NewText: "caller", - Insert: lsproto.Range{ - Start: lsproto.Position{Line: 19, Character: 2}, - End: lsproto.Position{Line: 19, Character: 2}, - }, - Replace: lsproto.Range{ - Start: lsproto.Position{Line: 19, Character: 2}, - End: lsproto.Position{Line: 19, Character: 2}, + { + Label: "caller", + Kind: fieldKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".caller"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "caller", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, }, }, }, - }, - { - Label: "length", - Kind: fieldKind, - SortText: sortTextLocationPriority, - FilterText: ptrTo(".length"), - InsertTextFormat: insertTextFormatPlainText, - TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ - InsertReplaceEdit: &lsproto.InsertReplaceEdit{ - NewText: "length", - Insert: lsproto.Range{ - Start: lsproto.Position{Line: 19, Character: 2}, - End: lsproto.Position{Line: 19, Character: 2}, - }, - Replace: lsproto.Range{ - Start: lsproto.Position{Line: 19, Character: 2}, - End: lsproto.Position{Line: 19, Character: 2}, + { + Label: "length", + Kind: fieldKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".length"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "length", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, }, }, }, - }, - { - Label: "prototype", - Kind: fieldKind, - SortText: sortTextLocationPriority, - FilterText: ptrTo(".prototype"), - InsertTextFormat: insertTextFormatPlainText, - TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ - InsertReplaceEdit: &lsproto.InsertReplaceEdit{ - NewText: "prototype", - Insert: lsproto.Range{ - Start: lsproto.Position{Line: 19, Character: 2}, - End: lsproto.Position{Line: 19, Character: 2}, - }, - Replace: lsproto.Range{ - Start: lsproto.Position{Line: 19, Character: 2}, - End: lsproto.Position{Line: 19, Character: 2}, + { + Label: "prototype", + Kind: fieldKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".prototype"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "prototype", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, }, }, }, - }, - { - Label: "toString", - Kind: methodKind, - SortText: sortTextLocationPriority, - FilterText: ptrTo(".toString"), - InsertTextFormat: insertTextFormatPlainText, - TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ - InsertReplaceEdit: &lsproto.InsertReplaceEdit{ - NewText: "toString", - Insert: lsproto.Range{ - Start: lsproto.Position{Line: 19, Character: 2}, - End: lsproto.Position{Line: 19, Character: 2}, - }, - Replace: lsproto.Range{ - Start: lsproto.Position{Line: 19, Character: 2}, - End: lsproto.Position{Line: 19, Character: 2}, + { + Label: "toString", + Kind: methodKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".toString"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "toString", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, }, }, }, - }, - { - Label: "x", - Kind: variableKind, - SortText: sortTextLocationPriority, - FilterText: ptrTo(".x"), - InsertTextFormat: insertTextFormatPlainText, - TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ - InsertReplaceEdit: &lsproto.InsertReplaceEdit{ - NewText: "x", - Insert: lsproto.Range{ - Start: lsproto.Position{Line: 19, Character: 2}, - End: lsproto.Position{Line: 19, Character: 2}, - }, - Replace: lsproto.Range{ - Start: lsproto.Position{Line: 19, Character: 2}, - End: lsproto.Position{Line: 19, Character: 2}, + { + Label: "x", + Kind: variableKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".x"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "x", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, }, }, }, @@ -578,7 +600,7 @@ D./*a*/`, { name: "lambdaThisMembers", files: map[string]string{ - mainFileName: `class Foo { + defaultMainFileName: `class Foo { a: number; b() { var x = () => { @@ -587,149 +609,777 @@ D./*a*/`, } }`, }, - expected: map[string]*lsproto.CompletionList{ + expectedResult: map[string]*testCaseResult{ "": { - IsIncomplete: false, - ItemDefaults: itemDefaults, - Items: []*lsproto.CompletionItem{ - { - Label: "a", - Kind: fieldKind, - SortText: sortTextLocationPriority, - FilterText: ptrTo(".a"), - InsertTextFormat: insertTextFormatPlainText, - TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + list: &lsproto.CompletionList{ + IsIncomplete: false, + ItemDefaults: itemDefaults, + Items: []*lsproto.CompletionItem{ + { + Label: "a", + Kind: fieldKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".a"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "a", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 4, Character: 17}, + End: lsproto.Position{Line: 4, Character: 17}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 4, Character: 17}, + End: lsproto.Position{Line: 4, Character: 17}, + }, + }, + }, + }, + { + Label: "b", + Kind: methodKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".b"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "b", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 4, Character: 17}, + End: lsproto.Position{Line: 4, Character: 17}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 4, Character: 17}, + End: lsproto.Position{Line: 4, Character: 17}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "memberCompletionInForEach1", + files: map[string]string{ + defaultMainFileName: `var x: string[] = []; +x.forEach(function (y) { y./*1*/`, + }, + expectedResult: map[string]*testCaseResult{ + "1": { + list: &lsproto.CompletionList{ + IsIncomplete: false, + ItemDefaults: itemDefaults, + Items: core.Map(stringMembers, func(basicItem *lsproto.CompletionItem) *lsproto.CompletionItem { + item := *basicItem + item.FilterText = ptrTo("." + item.Label) + item.TextEdit = &lsproto.TextEditOrInsertReplaceEdit{ InsertReplaceEdit: &lsproto.InsertReplaceEdit{ - NewText: "a", + NewText: item.Label, Insert: lsproto.Range{ - Start: lsproto.Position{Line: 4, Character: 17}, - End: lsproto.Position{Line: 4, Character: 17}, + Start: lsproto.Position{Line: 1, Character: 27}, + End: lsproto.Position{Line: 1, Character: 27}, }, Replace: lsproto.Range{ - Start: lsproto.Position{Line: 4, Character: 17}, - End: lsproto.Position{Line: 4, Character: 17}, + Start: lsproto.Position{Line: 1, Character: 27}, + End: lsproto.Position{Line: 1, Character: 27}, + }, + }, + } + return &item + }), + }, + }, + }, + }, + { + name: "completionsTuple", + files: map[string]string{ + defaultMainFileName: `declare const x: [number, number]; +x./**/;`, + }, + expectedResult: map[string]*testCaseResult{ + "": { + list: &lsproto.CompletionList{ + IsIncomplete: false, + ItemDefaults: itemDefaults, + Items: append([]*lsproto.CompletionItem{ + { + Label: "0", + Kind: fieldKind, + SortText: sortTextLocationPriority, + InsertText: ptrTo("[0]"), + InsertTextFormat: insertTextFormatPlainText, + FilterText: ptrTo(".[0]"), + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + TextEdit: &lsproto.TextEdit{ + NewText: "[0]", + Range: lsproto.Range{ + Start: lsproto.Position{Line: 1, Character: 1}, + End: lsproto.Position{Line: 1, Character: 2}, + }, }, }, }, - }, - { - Label: "b", - Kind: methodKind, - SortText: sortTextLocationPriority, - FilterText: ptrTo(".b"), - InsertTextFormat: insertTextFormatPlainText, - TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + { + Label: "1", + Kind: fieldKind, + SortText: sortTextLocationPriority, + InsertText: ptrTo("[1]"), + InsertTextFormat: insertTextFormatPlainText, + FilterText: ptrTo(".[1]"), + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + TextEdit: &lsproto.TextEdit{ + NewText: "[1]", + Range: lsproto.Range{ + Start: lsproto.Position{Line: 1, Character: 1}, + End: lsproto.Position{Line: 1, Character: 2}, + }, + }, + }, + }, + }, core.Map(arrayMembers, func(basicItem *lsproto.CompletionItem) *lsproto.CompletionItem { + item := *basicItem + item.FilterText = ptrTo("." + item.Label) + item.TextEdit = &lsproto.TextEditOrInsertReplaceEdit{ InsertReplaceEdit: &lsproto.InsertReplaceEdit{ - NewText: "b", + NewText: item.Label, Insert: lsproto.Range{ - Start: lsproto.Position{Line: 4, Character: 17}, - End: lsproto.Position{Line: 4, Character: 17}, + Start: lsproto.Position{Line: 1, Character: 2}, + End: lsproto.Position{Line: 1, Character: 2}, }, Replace: lsproto.Range{ - Start: lsproto.Position{Line: 4, Character: 17}, - End: lsproto.Position{Line: 4, Character: 17}, + Start: lsproto.Position{Line: 1, Character: 2}, + End: lsproto.Position{Line: 1, Character: 2}, }, }, + } + return &item + })...), + }, + }, + }, + }, + { + name: "augmentedTypesClass3Fourslash", + files: map[string]string{ + defaultMainFileName: `class c5b { public foo() { } } +namespace c5b { export var y = 2; } // should be ok +/*3*/`, + }, + expectedResult: map[string]*testCaseResult{ + "3": { + list: &lsproto.CompletionList{ + IsIncomplete: false, + ItemDefaults: itemDefaults, + Items: []*lsproto.CompletionItem{ + { + Label: "c5b", + Kind: classKind, + SortText: sortTextLocationPriority, + InsertTextFormat: insertTextFormatPlainText, }, }, }, + isIncludes: true, }, }, }, { - name: "memberCompletionInForEach1", + name: "objectLiteralBindingInParameter", files: map[string]string{ - mainFileName: `var x: string[] = []; -x.forEach(function (y) { y./*1*/`, + defaultMainFileName: `interface I { x1: number; x2: string } +function f(cb: (ev: I) => any) { } +f(({/*1*/}) => 0); + +[null].reduce(({/*2*/}, b) => b); + +interface Foo { + m(x: { x1: number, x2: number }): void; + prop: I; +} +let x: Foo = { + m({ /*3*/ }) { + }, + get prop(): I { return undefined; }, + set prop({ /*4*/ }) { + } +};`, }, - expected: map[string]*lsproto.CompletionList{ + expectedResult: map[string]*testCaseResult{ "1": { - IsIncomplete: false, - ItemDefaults: itemDefaults, - Items: core.Map(stringMembers, func(basicItem *lsproto.CompletionItem) *lsproto.CompletionItem { - item := *basicItem - item.FilterText = ptrTo("." + item.Label) - item.TextEdit = &lsproto.TextEditOrInsertReplaceEdit{ - InsertReplaceEdit: &lsproto.InsertReplaceEdit{ - NewText: item.Label, - Insert: lsproto.Range{ - Start: lsproto.Position{Line: 1, Character: 27}, - End: lsproto.Position{Line: 1, Character: 27}, - }, - Replace: lsproto.Range{ - Start: lsproto.Position{Line: 1, Character: 27}, - End: lsproto.Position{Line: 1, Character: 27}, - }, + list: &lsproto.CompletionList{ + IsIncomplete: false, + ItemDefaults: itemDefaults, + Items: []*lsproto.CompletionItem{ + { + Label: "x1", + Kind: fieldKind, + SortText: sortTextLocationPriority, + InsertTextFormat: insertTextFormatPlainText, + }, + { + Label: "x2", + Kind: fieldKind, + SortText: sortTextLocationPriority, + InsertTextFormat: insertTextFormatPlainText, + }, + }, + }, + }, + "2": { + list: &lsproto.CompletionList{ + IsIncomplete: false, + ItemDefaults: itemDefaults, + Items: []*lsproto.CompletionItem{ + { + Label: "x1", + Kind: fieldKind, + SortText: sortTextLocationPriority, + InsertTextFormat: insertTextFormatPlainText, + }, + { + Label: "x2", + Kind: fieldKind, + SortText: sortTextLocationPriority, + InsertTextFormat: insertTextFormatPlainText, + }, + }, + }, + }, + "3": { + list: &lsproto.CompletionList{ + IsIncomplete: false, + ItemDefaults: itemDefaults, + Items: []*lsproto.CompletionItem{ + { + Label: "x1", + Kind: fieldKind, + SortText: sortTextLocationPriority, + InsertTextFormat: insertTextFormatPlainText, }, - } - return &item - }), + { + Label: "x2", + Kind: fieldKind, + SortText: sortTextLocationPriority, + InsertTextFormat: insertTextFormatPlainText, + }, + }, + }, }, }, }, { - name: "completionsTuple", + name: "completionListInTypeLiteralInTypeParameter3", files: map[string]string{ - mainFileName: `declare const x: [number, number]; -x./**/;`, + defaultMainFileName: `interface Foo { + one: string; + two: number; +} + +interface Bar { + foo: T; +} + +var foobar: Bar<{ one: string, /**/`, }, - expected: map[string]*lsproto.CompletionList{ + expectedResult: map[string]*testCaseResult{ "": { - IsIncomplete: false, - ItemDefaults: itemDefaults, - Items: append([]*lsproto.CompletionItem{ - { - Label: "0", - Kind: fieldKind, - SortText: sortTextLocationPriority, - InsertText: ptrTo("[0]"), - InsertTextFormat: insertTextFormatPlainText, - FilterText: ptrTo(".[0]"), - TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ - TextEdit: &lsproto.TextEdit{ - NewText: "[0]", - Range: lsproto.Range{ - Start: lsproto.Position{Line: 1, Character: 1}, - End: lsproto.Position{Line: 1, Character: 2}, + list: &lsproto.CompletionList{ + IsIncomplete: false, + ItemDefaults: &lsproto.CompletionItemDefaults{ + CommitCharacters: &[]string{}, + }, + Items: []*lsproto.CompletionItem{ + { + Label: "two", + Kind: fieldKind, + SortText: sortTextLocationPriority, + InsertTextFormat: insertTextFormatPlainText, + }, + }, + }, + }, + }, + }, + { + name: "completionListInImportClause04", + files: map[string]string{ + defaultMainFileName: `import {/*1*/} from './foo';`, + "/foo.d.ts": `declare class Foo { + static prop1(x: number): number; + static prop1(x: string): string; + static prop2(x: boolean): boolean; +} +export = Foo;`, + }, + expectedResult: map[string]*testCaseResult{ + "1": { + list: &lsproto.CompletionList{ + IsIncomplete: false, + ItemDefaults: itemDefaults, + Items: []*lsproto.CompletionItem{ + { + Label: "prop1", + Kind: methodKind, + SortText: sortTextLocationPriority, + InsertTextFormat: insertTextFormatPlainText, + }, + { + Label: "prop2", + Kind: methodKind, + SortText: sortTextLocationPriority, + InsertTextFormat: insertTextFormatPlainText, + }, + { + Label: "prototype", + Kind: fieldKind, + SortText: sortTextLocationPriority, + InsertTextFormat: insertTextFormatPlainText, + }, + { + Label: "type", + Kind: keywordKind, + SortText: sortTextGlobalsOrKeywords, + }, + }, + }, + }, + }, + }, + { + name: "completionListForImportAttributes", + files: map[string]string{ + defaultMainFileName: `declare global { + interface ImportAttributes { + type: "json", + "resolution-mode": "import" + } +} +const str = "hello"; + +import * as t1 from "./a" with { /*1*/ }; +import * as t3 from "./a" with { type: "json", /*3*/ }; +import * as t4 from "./a" with { type: /*4*/ };`, + "/a.ts": `export default {};`, + "/tsconfig.json": `{ "compilerOptions": { "module": "esnext", "target": "esnext" } }`, + }, + expectedResult: map[string]*testCaseResult{ + "1": { + list: &lsproto.CompletionList{ + IsIncomplete: false, + ItemDefaults: itemDefaults, + Items: []*lsproto.CompletionItem{ + { + Label: "resolution-mode", + Kind: fieldKind, + SortText: sortTextLocationPriority, + InsertTextFormat: insertTextFormatPlainText, + }, + { + Label: "type", + Kind: fieldKind, + SortText: sortTextLocationPriority, + InsertTextFormat: insertTextFormatPlainText, + }, + }, + }, + }, + }, + }, + { + name: "completionsInExport_invalid", + files: map[string]string{ + defaultMainFileName: `function topLevel() {} +if (!!true) { + const blockScoped = 0; + export { /**/ }; +}`, + }, + expectedResult: map[string]*testCaseResult{ + "": { + list: &lsproto.CompletionList{ + IsIncomplete: false, + ItemDefaults: itemDefaults, + Items: []*lsproto.CompletionItem{ + { + Label: "topLevel", + Kind: functionKind, + SortText: sortTextLocationPriority, + InsertTextFormat: insertTextFormatPlainText, + }, + { + Label: "type", + Kind: keywordKind, + SortText: sortTextGlobalsOrKeywords, + }, + }, + }, + }, + }, + }, + { + name: "completionListAtIdentifierDefinitionLocations_parameters", + files: map[string]string{ + defaultMainFileName: `var aa = 1; +class bar5{ constructor(public /*constructorParameter1*/`, + }, + expectedResult: map[string]*testCaseResult{ + "constructorParameter1": { + list: &lsproto.CompletionList{ + IsIncomplete: false, + ItemDefaults: &lsproto.CompletionItemDefaults{ + CommitCharacters: &[]string{}, + }, + Items: []*lsproto.CompletionItem{ + { + Label: "override", + Kind: keywordKind, + SortText: sortTextGlobalsOrKeywords, + }, + { + Label: "private", + Kind: keywordKind, + SortText: sortTextGlobalsOrKeywords, + }, + { + Label: "protected", + Kind: keywordKind, + SortText: sortTextGlobalsOrKeywords, + }, + { + Label: "public", + Kind: keywordKind, + SortText: sortTextGlobalsOrKeywords, + }, + { + Label: "readonly", + Kind: keywordKind, + SortText: sortTextGlobalsOrKeywords, + }, + }, + }, + }, + }, + }, + { + name: "completionEntryForClassMembers_StaticWhenBaseTypeIsNotResolved", + files: map[string]string{ + defaultMainFileName: `import React from 'react' +class Slider extends React.Component { + static defau/**/ltProps = { + onMouseDown: () => { }, + onMouseUp: () => { }, + unit: 'px', + } + handleChange = () => 10; +}`, + "/node_modules/@types/react/index.d.ts": `export = React; +export as namespace React; +declare namespace React { + function createElement(): any; + interface Component

{ } + class Component { + static contextType?: any; + context: any; + constructor(props: Readonly

); + setState( + state: ((prevState: Readonly, props: Readonly

) => (Pick | S | null)) | (Pick | S | null), + callback?: () => void + ): void; + } +}`, + }, + expectedResult: map[string]*testCaseResult{ + "": { + list: &lsproto.CompletionList{ + IsIncomplete: false, + ItemDefaults: &lsproto.CompletionItemDefaults{ + CommitCharacters: &[]string{}, + }, + Items: []*lsproto.CompletionItem{ + { + Label: "contextType?", + Kind: fieldKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo("contextType"), + InsertText: ptrTo("contextType"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "contextType", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 2, Character: 11}, + End: lsproto.Position{Line: 2, Character: 16}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 2, Character: 11}, + End: lsproto.Position{Line: 2, Character: 23}, + }, }, }, }, + { + Label: "abstract", + Kind: keywordKind, + SortText: sortTextGlobalsOrKeywords, + }, + { + Label: "accessor", + Kind: keywordKind, + SortText: sortTextGlobalsOrKeywords, + }, + { + Label: "async", + Kind: keywordKind, + SortText: sortTextGlobalsOrKeywords, + }, + { + Label: "constructor", + Kind: keywordKind, + SortText: sortTextGlobalsOrKeywords, + }, + { + Label: "declare", + Kind: keywordKind, + SortText: sortTextGlobalsOrKeywords, + }, + { + Label: "get", + Kind: keywordKind, + SortText: sortTextGlobalsOrKeywords, + }, + { + Label: "override", + Kind: keywordKind, + SortText: sortTextGlobalsOrKeywords, + }, + { + Label: "private", + Kind: keywordKind, + SortText: sortTextGlobalsOrKeywords, + }, + { + Label: "protected", + Kind: keywordKind, + SortText: sortTextGlobalsOrKeywords, + }, + { + Label: "public", + Kind: keywordKind, + SortText: sortTextGlobalsOrKeywords, + }, + { + Label: "readonly", + Kind: keywordKind, + SortText: sortTextGlobalsOrKeywords, + }, + { + Label: "set", + Kind: keywordKind, + SortText: sortTextGlobalsOrKeywords, + }, + { + Label: "static", + Kind: keywordKind, + SortText: sortTextGlobalsOrKeywords, + }, }, - { - Label: "1", - Kind: fieldKind, - SortText: sortTextLocationPriority, - InsertText: ptrTo("[1]"), - InsertTextFormat: insertTextFormatPlainText, - FilterText: ptrTo(".[1]"), - TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ - TextEdit: &lsproto.TextEdit{ - NewText: "[1]", - Range: lsproto.Range{ - Start: lsproto.Position{Line: 1, Character: 1}, - End: lsproto.Position{Line: 1, Character: 2}, + }, + }, + }, + }, + { + name: "completionsInJsxTag", + mainFileName: "/index.tsx", + files: map[string]string{ + "/index.tsx": `declare namespace JSX { + interface Element {} + interface IntrinsicElements { + div: { + /** Doc */ + foo: string + /** Label docs */ + "aria-label": string + } + } +} +class Foo { + render() { +

; +
+ } +}`, + "/tsconfig.json": `{ "compilerOptions": { "jsx": "preserve" } }`, + }, + expectedResult: map[string]*testCaseResult{ + "1": { + list: nil, // !!! jsx + }, + "2": { + list: nil, // !!! jsx + }, + }, + }, + { + name: "completionsDotDotDotInObjectLiteral1", + files: map[string]string{ + defaultMainFileName: `const foo = { b: 100 }; +const bar: { + a: number; + b: number; +} = { + a: 42, + .../*1*/ +};`, + }, + expectedResult: map[string]*testCaseResult{ + "1": { + list: &lsproto.CompletionList{ + IsIncomplete: false, + ItemDefaults: itemDefaults, + Items: []*lsproto.CompletionItem{ + { + Label: "foo", + Kind: variableKind, + SortText: sortTextLocationPriority, + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "foo", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 6, Character: 5}, + End: lsproto.Position{Line: 6, Character: 5}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 6, Character: 5}, + End: lsproto.Position{Line: 6, Character: 5}, + }, }, }, }, }, - }, core.Map(arrayMembers, func(basicItem *lsproto.CompletionItem) *lsproto.CompletionItem { - item := *basicItem - item.FilterText = ptrTo("." + item.Label) - item.TextEdit = &lsproto.TextEditOrInsertReplaceEdit{ - InsertReplaceEdit: &lsproto.InsertReplaceEdit{ - NewText: item.Label, - Insert: lsproto.Range{ - Start: lsproto.Position{Line: 1, Character: 2}, - End: lsproto.Position{Line: 1, Character: 2}, - }, - Replace: lsproto.Range{ - Start: lsproto.Position{Line: 1, Character: 2}, - End: lsproto.Position{Line: 1, Character: 2}, + }, + isIncludes: true, + excludes: []string{"b"}, + }, + }, + }, + { + name: "extendsKeywordCompletion2", + files: map[string]string{ + defaultMainFileName: `function f1() {} +function f2() {}`, + }, + expectedResult: map[string]*testCaseResult{ + "1": { + list: &lsproto.CompletionList{ + IsIncomplete: false, + ItemDefaults: itemDefaults, + Items: []*lsproto.CompletionItem{ + { + Label: "extends", + Kind: keywordKind, + SortText: sortTextGlobalsOrKeywords, + }, + }, + }, + isIncludes: true, + }, + "2": { + list: &lsproto.CompletionList{ + IsIncomplete: false, + ItemDefaults: itemDefaults, + Items: []*lsproto.CompletionItem{ + { + Label: "extends", + Kind: keywordKind, + SortText: sortTextGlobalsOrKeywords, + }, + }, + }, + isIncludes: true, + }, + }, + }, + { + name: "paths.ts", + files: map[string]string{ + defaultMainFileName: `import { + CharacterCodes, + compareStringsCaseInsensitive, + compareStringsCaseSensitive, + compareValues, + Comparison, + Debug, + endsWith, + equateStringsCaseInsensitive, + equateStringsCaseSensitive, + GetCanonicalFileName, + getDeclarationFileExtension, + getStringComparer, + identity, + lastOrUndefined, + Path, + some, + startsWith, +} from "./_namespaces/ts.js"; + +/** + * Internally, we represent paths as strings with '/' as the directory separator. + * When we make system calls (eg: LanguageServiceHost.getDirectory()), + * we expect the host to correctly handle paths in our specified format. + * + * @internal + */ +export const directorySeparator = "/"; +/** @internal */ +export const altDirectorySeparator = "\\"; +const urlSchemeSeparator = "://"; +const backslashRegExp = /\\/g; + +b/*a*/ + +//// Path Tests + +/** + * Determines whether a charCode corresponds to '/' or '\'. + * + * @internal + */ +export function isAnyDirectorySeparator(charCode: number): boolean { + return charCode === CharacterCodes.slash || charCode === CharacterCodes.backslash; +}`, + }, + expectedResult: map[string]*testCaseResult{ + "a": { + list: &lsproto.CompletionList{ + IsIncomplete: false, + ItemDefaults: itemDefaults, + Items: []*lsproto.CompletionItem{ + { + Label: "CharacterCodes", + Kind: variableKind, + SortText: sortTextLocationPriority, + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "CharacterCodes", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 27, Character: 0}, + End: lsproto.Position{Line: 27, Character: 1}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 27, Character: 0}, + End: lsproto.Position{Line: 27, Character: 1}, + }, + }, }, }, - } - return &item - })...), + }, + }, + isIncludes: true, }, }, }, @@ -737,19 +1387,22 @@ x./**/;`, for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { t.Parallel() - runTest(t, testCase.files, testCase.expected) + runTest(t, testCase.files, testCase.expectedResult, testCase.mainFileName) }) } } -func runTest(t *testing.T, files map[string]string, expected map[string]*lsproto.CompletionList) { +func runTest(t *testing.T, files map[string]string, expected map[string]*testCaseResult, mainFileName string) { + if mainFileName == "" { + mainFileName = defaultMainFileName + } parsedFiles := make(map[string]string) var markerPositions map[string]*lstestutil.Marker for fileName, content := range files { if fileName == mainFileName { testData := lstestutil.ParseTestData("", content, fileName) markerPositions = testData.MarkerPositions - parsedFiles[fileName] = testData.Files[0].Content // !!! Assumes no usage of @filename + parsedFiles[fileName] = testData.Files[0].Content // !!! Assumes no usage of @filename, markers only on main file } else { parsedFiles[fileName] = content } @@ -779,13 +1432,38 @@ func runTest(t *testing.T, files map[string]string, expected map[string]*lsproto t.Fatalf("No marker found for '%s'", markerName) } completionList := languageService.ProvideCompletion( - "/index.ts", + mainFileName, marker.Position, context, capabilities, preferences) - assert.DeepEqual(t, completionList, expectedResult) + if expectedResult.isIncludes { + assertIncludesItem(t, completionList, expectedResult.list) + } else { + assert.DeepEqual(t, completionList, expectedResult.list) + } + for _, excludedLabel := range expectedResult.excludes { + for _, item := range completionList.Items { + if item.Label == excludedLabel { + t.Fatalf("Label %s should not be included in completion list", excludedLabel) + } + } + } + } +} + +func assertIncludesItem(t *testing.T, actual *lsproto.CompletionList, expected *lsproto.CompletionList) bool { + assert.DeepEqual(t, actual, expected, cmpopts.IgnoreFields(lsproto.CompletionList{}, "Items")) + for _, item := range expected.Items { + index := slices.IndexFunc(actual.Items, func(actualItem *lsproto.CompletionItem) bool { + return actualItem.Label == item.Label + }) + if index == -1 { + t.Fatalf("Label %s not found in actual items. Actual items: %v", item.Label, actual.Items) + } + assert.DeepEqual(t, actual.Items[index], item) } + return false } func createLanguageService(fileName string, files map[string]string) *ls.LanguageService { diff --git a/internal/ls/symbol_display.go b/internal/ls/symbol_display.go index 777eea8614..b800044c90 100644 --- a/internal/ls/symbol_display.go +++ b/internal/ls/symbol_display.go @@ -288,15 +288,18 @@ func isLocalVariableOrFunction(symbol *ast.Symbol) bool { } // If the parent is not source file or module block, it is a local variable. - for parent := decl.Parent; !ast.IsFunctionBlock(parent); parent = parent.Parent { + parent := decl.Parent + for ; !ast.IsFunctionBlock(parent); parent = parent.Parent { // Reached source file or module block if parent.Kind == ast.KindSourceFile || parent.Kind == ast.KindModuleBlock { - continue + break } } - // Parent is in function block. - return true + if ast.IsFunctionBlock(parent) { + // Parent is in function block. + return true + } } return false } diff --git a/internal/ls/utilities.go b/internal/ls/utilities.go index 71d0c31106..2f159d5639 100644 --- a/internal/ls/utilities.go +++ b/internal/ls/utilities.go @@ -46,10 +46,29 @@ func isInComment(file *ast.SourceFile, position int, tokenAtPosition *ast.Node) return nil } -// !!! // Replaces last(node.getChildren(sourceFile)) func getLastChild(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node { - return nil + lastChildNode := getLastVisitedChild(node, sourceFile) + if ast.IsJSDocSingleCommentNode(node) { + return nil + } + var tokenStartPos int + if lastChildNode != nil { + tokenStartPos = lastChildNode.End() + } else { + tokenStartPos = node.Pos() + } + var lastToken *ast.Node + scanner := scanner.GetScannerForSourceFile(sourceFile, tokenStartPos) + for startPos := tokenStartPos; startPos < node.End(); { + tokenKind := scanner.Token() + tokenFullStart := scanner.TokenFullStart() + tokenEnd := scanner.TokenEnd() + lastToken = sourceFile.GetOrCreateToken(tokenKind, tokenFullStart, tokenEnd, node) + startPos = tokenEnd + scanner.Scan() + } + return core.IfElse(lastToken != nil, lastToken, lastChildNode) } func getLastToken(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node { @@ -57,6 +76,10 @@ func getLastToken(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node { return nil } + if ast.IsTokenKind(node.Kind) || ast.IsIdentifier(node) { + return nil + } + assertHasRealPosition(node) lastChild := getLastChild(node, sourceFile) @@ -71,9 +94,84 @@ func getLastToken(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node { } } -// !!! +// Gets the last visited child of the given node. +// NOTE: This doesn't include unvisited tokens; for this, use `getLastChild` or `getLastToken`. +func getLastVisitedChild(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node { + var lastChild *ast.Node + + visitNode := func(n *ast.Node, _ *ast.NodeVisitor) *ast.Node { + if !(n == nil || node.Flags&ast.NodeFlagsReparsed != 0) { + lastChild = n + } + return n + } + visitNodeList := func(nodeList *ast.NodeList, _ *ast.NodeVisitor) *ast.NodeList { + if nodeList != nil && len(nodeList.Nodes) > 0 && !ast.IsJSDocSingleCommentNodeList(node, nodeList) { + for i := len(nodeList.Nodes) - 1; i >= 0; i-- { + if nodeList.Nodes[i].Flags&ast.NodeFlagsReparsed == 0 { + lastChild = nodeList.Nodes[i] + break + } + } + } + return nodeList + } + + nodeVisitor := ast.NewNodeVisitor(core.Identity, nil, ast.NodeVisitorHooks{ + VisitNode: visitNode, + VisitToken: visitNode, + VisitNodes: visitNodeList, + VisitModifiers: func(modifiers *ast.ModifierList, visitor *ast.NodeVisitor) *ast.ModifierList { + if modifiers != nil { + visitNodeList(&modifiers.NodeList, visitor) + } + return modifiers + }, + }) + + astnav.VisitEachChildAndJSDoc(node, sourceFile, nodeVisitor) + return lastChild +} + func getFirstToken(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node { - return nil + if ast.IsIdentifier(node) || ast.IsTokenKind(node.Kind) { + return nil + } + assertHasRealPosition(node) + var firstChild *ast.Node + node.ForEachChild(func(n *ast.Node) bool { + if n == nil || node.Flags&ast.NodeFlagsReparsed != 0 { + return false + } + firstChild = n + return true + }) + + var tokenEndPosition int + if firstChild != nil { + tokenEndPosition = firstChild.Pos() + } else { + tokenEndPosition = node.End() + } + scanner := scanner.GetScannerForSourceFile(sourceFile, node.Pos()) + var firstToken *ast.Node + if node.Pos() < tokenEndPosition { + tokenKind := scanner.Token() + tokenFullStart := scanner.TokenFullStart() + tokenEnd := scanner.TokenEnd() + firstToken = sourceFile.GetOrCreateToken(tokenKind, tokenFullStart, tokenEnd, node) + } + + if firstToken != nil { + return firstToken + } + if firstChild == nil { + return nil + } + if firstChild.Kind < ast.KindFirstNode { + return firstChild + } + return getFirstToken(firstChild, sourceFile) } func assertHasRealPosition(node *ast.Node) { @@ -82,6 +180,10 @@ func assertHasRealPosition(node *ast.Node) { } } +func hasChildOfKind(containingNode *ast.Node, kind ast.Kind, sourceFile *ast.SourceFile) bool { + return findChildOfKind(containingNode, kind, sourceFile) != nil +} + func findChildOfKind(containingNode *ast.Node, kind ast.Kind, sourceFile *ast.SourceFile) *ast.Node { lastNodePos := containingNode.Pos() scanner := scanner.GetScannerForSourceFile(sourceFile, lastNodePos) @@ -345,6 +447,9 @@ func probablyUsesSemicolons(file *ast.SourceFile) bool { } // If even 2/5 places have a semicolon, the user probably wants semicolons + if withoutSemicolon == 0 { + return true + } return withSemicolon/withoutSemicolon > 1/nStatementsToObserve } @@ -425,3 +530,200 @@ func isArgumentOfElementAccessExpression(node *ast.Node) bool { func isTagName(node *ast.Node) bool { return node.Parent != nil && ast.IsJSDocTag(node.Parent) && node.Parent.TagName() == node } + +// Assumes `candidate.pos <= position` holds. +func positionBelongsToNode(candidate *ast.Node, position int, file *ast.SourceFile) bool { + if candidate.Pos() > position { + panic("Expected candidate.pos <= position") + } + return position < candidate.End() || !isCompletedNode(candidate, file) +} + +func isCompletedNode(n *ast.Node, sourceFile *ast.SourceFile) bool { + if n == nil || ast.NodeIsMissing(n) { + return false + } + + switch n.Kind { + case ast.KindClassDeclaration, + ast.KindInterfaceDeclaration, + ast.KindEnumDeclaration, + ast.KindObjectLiteralExpression, + ast.KindObjectBindingPattern, + ast.KindTypeLiteral, + ast.KindBlock, + ast.KindModuleBlock, + ast.KindCaseBlock, + ast.KindNamedImports, + ast.KindNamedExports: + return nodeEndsWith(n, ast.KindCloseBraceToken, sourceFile) + + case ast.KindCatchClause: + return isCompletedNode(n.AsCatchClause().Block, sourceFile) + + case ast.KindNewExpression: + if n.AsNewExpression().Arguments == nil { + return true + } + fallthrough + + case ast.KindCallExpression, + ast.KindParenthesizedExpression, + ast.KindParenthesizedType: + return nodeEndsWith(n, ast.KindCloseParenToken, sourceFile) + + case ast.KindFunctionType, + ast.KindConstructorType: + return isCompletedNode(n.Type(), sourceFile) + + case ast.KindConstructor, + ast.KindGetAccessor, + ast.KindSetAccessor, + ast.KindFunctionDeclaration, + ast.KindFunctionExpression, + ast.KindMethodDeclaration, + ast.KindMethodSignature, + ast.KindConstructSignature, + ast.KindCallSignature, + ast.KindArrowFunction: + if n.Body() != nil { + return isCompletedNode(n.Body(), sourceFile) + } + if n.Type() != nil { + return isCompletedNode(n.Type(), sourceFile) + } + // Even though type parameters can be unclosed, we can get away with + // having at least a closing paren. + return hasChildOfKind(n, ast.KindCloseParenToken, sourceFile) + + case ast.KindModuleDeclaration: + return n.AsModuleDeclaration().Body != nil && isCompletedNode(n.AsModuleDeclaration().Body, sourceFile) + + case ast.KindIfStatement: + if n.AsIfStatement().ElseStatement != nil { + return isCompletedNode(n.AsIfStatement().ElseStatement, sourceFile) + } + return isCompletedNode(n.AsIfStatement().ThenStatement, sourceFile) + + case ast.KindExpressionStatement: + return isCompletedNode(n.AsExpressionStatement().Expression, sourceFile) || + hasChildOfKind(n, ast.KindSemicolonToken, sourceFile) + + case ast.KindArrayLiteralExpression, + ast.KindArrayBindingPattern, + ast.KindElementAccessExpression, + ast.KindComputedPropertyName, + ast.KindTupleType: + return nodeEndsWith(n, ast.KindCloseBracketToken, sourceFile) + + case ast.KindIndexSignature: + if n.AsIndexSignatureDeclaration().Type != nil { + return isCompletedNode(n.AsIndexSignatureDeclaration().Type, sourceFile) + } + return hasChildOfKind(n, ast.KindCloseBracketToken, sourceFile) + + case ast.KindCaseClause, + ast.KindDefaultClause: + // there is no such thing as terminator token for CaseClause/DefaultClause so for simplicity always consider them non-completed + return false + + case ast.KindForStatement, + ast.KindForInStatement, + ast.KindForOfStatement, + ast.KindWhileStatement: + return isCompletedNode(n.Statement(), sourceFile) + case ast.KindDoStatement: + // rough approximation: if DoStatement has While keyword - then if node is completed is checking the presence of ')'; + if hasChildOfKind(n, ast.KindWhileKeyword, sourceFile) { + return nodeEndsWith(n, ast.KindCloseParenToken, sourceFile) + } + return isCompletedNode(n.AsDoStatement().Statement, sourceFile) + + case ast.KindTypeQuery: + return isCompletedNode(n.AsTypeQueryNode().ExprName, sourceFile) + + case ast.KindTypeOfExpression, + ast.KindDeleteExpression, + ast.KindVoidExpression, + ast.KindYieldExpression, + ast.KindSpreadElement: + return isCompletedNode(n.Expression(), sourceFile) + + case ast.KindTaggedTemplateExpression: + return isCompletedNode(n.AsTaggedTemplateExpression().Template, sourceFile) + + case ast.KindTemplateExpression: + if n.AsTemplateExpression().TemplateSpans == nil { + return false + } + lastSpan := core.LastOrNil(n.AsTemplateExpression().TemplateSpans.Nodes) + return isCompletedNode(lastSpan, sourceFile) + + case ast.KindTemplateSpan: + return ast.NodeIsPresent(n.AsTemplateSpan().Literal) + + case ast.KindExportDeclaration, + ast.KindImportDeclaration: + return ast.NodeIsPresent(n.ModuleSpecifier()) + + case ast.KindPrefixUnaryExpression: + return isCompletedNode(n.AsPrefixUnaryExpression().Operand, sourceFile) + + case ast.KindBinaryExpression: + return isCompletedNode(n.AsBinaryExpression().Right, sourceFile) + + case ast.KindConditionalExpression: + return isCompletedNode(n.AsConditionalExpression().WhenFalse, sourceFile) + + default: + return true + } +} + +// Checks if node ends with 'expectedLastToken'. +// If child at position 'length - 1' is 'SemicolonToken' it is skipped and 'expectedLastToken' is compared with child at position 'length - 2'. +func nodeEndsWith(n *ast.Node, expectedLastToken ast.Kind, sourceFile *ast.SourceFile) bool { + lastChildNode := getLastVisitedChild(n, sourceFile) + var lastNodeAndTokens []*ast.Node + var tokenStartPos int + if lastChildNode != nil { + lastNodeAndTokens = []*ast.Node{lastChildNode} + tokenStartPos = lastChildNode.End() + } else { + tokenStartPos = n.Pos() + } + scanner := scanner.GetScannerForSourceFile(sourceFile, tokenStartPos) + for startPos := tokenStartPos; startPos < n.End(); { + tokenKind := scanner.Token() + tokenFullStart := scanner.TokenFullStart() + tokenEnd := scanner.TokenEnd() + token := sourceFile.GetOrCreateToken(tokenKind, tokenFullStart, tokenEnd, n) + lastNodeAndTokens = append(lastNodeAndTokens, token) + startPos = tokenEnd + scanner.Scan() + } + if len(lastNodeAndTokens) == 0 { + return false + } + lastChild := lastNodeAndTokens[len(lastNodeAndTokens)-1] + if lastChild.Kind == expectedLastToken { + return true + } else if lastChild.Kind == ast.KindSemicolonToken && len(lastNodeAndTokens) > 1 { + return lastNodeAndTokens[len(lastNodeAndTokens)-2].Kind == expectedLastToken + } + return false +} + +// Returns the node in an `extends` or `implements` clause of a class or interface. +func getAllSuperTypeNodes(node *ast.Node) []*ast.TypeNode { + if ast.IsInterfaceDeclaration(node) { + return ast.GetHeritageElements(node, ast.KindExtendsKeyword) + } + if ast.IsClassLike(node) { + return append( + []*ast.Node{ast.GetClassExtendsHeritageElement(node)}, + ast.GetImplementsTypeNodes(node)..., + ) + } + return nil +} diff --git a/internal/scanner/scanner.go b/internal/scanner/scanner.go index cc546e7418..bf8289c3f1 100644 --- a/internal/scanner/scanner.go +++ b/internal/scanner/scanner.go @@ -3,6 +3,7 @@ package scanner import ( "fmt" "iter" + "maps" "strconv" "strings" "unicode" @@ -117,69 +118,73 @@ var textToKeyword = map[string]ast.Kind{ "of": ast.KindOfKeyword, } -var textToToken = map[string]ast.Kind{ - "{": ast.KindOpenBraceToken, - "}": ast.KindCloseBraceToken, - "(": ast.KindOpenParenToken, - ")": ast.KindCloseParenToken, - "[": ast.KindOpenBracketToken, - "]": ast.KindCloseBracketToken, - ".": ast.KindDotToken, - "...": ast.KindDotDotDotToken, - ";": ast.KindSemicolonToken, - ",": ast.KindCommaToken, - "<": ast.KindLessThanToken, - ">": ast.KindGreaterThanToken, - "<=": ast.KindLessThanEqualsToken, - ">=": ast.KindGreaterThanEqualsToken, - "==": ast.KindEqualsEqualsToken, - "!=": ast.KindExclamationEqualsToken, - "===": ast.KindEqualsEqualsEqualsToken, - "!==": ast.KindExclamationEqualsEqualsToken, - "=>": ast.KindEqualsGreaterThanToken, - "+": ast.KindPlusToken, - "-": ast.KindMinusToken, - "**": ast.KindAsteriskAsteriskToken, - "*": ast.KindAsteriskToken, - "/": ast.KindSlashToken, - "%": ast.KindPercentToken, - "++": ast.KindPlusPlusToken, - "--": ast.KindMinusMinusToken, - "<<": ast.KindLessThanLessThanToken, - ">": ast.KindGreaterThanGreaterThanToken, - ">>>": ast.KindGreaterThanGreaterThanGreaterThanToken, - "&": ast.KindAmpersandToken, - "|": ast.KindBarToken, - "^": ast.KindCaretToken, - "!": ast.KindExclamationToken, - "~": ast.KindTildeToken, - "&&": ast.KindAmpersandAmpersandToken, - "||": ast.KindBarBarToken, - "?": ast.KindQuestionToken, - "??": ast.KindQuestionQuestionToken, - "?.": ast.KindQuestionDotToken, - ":": ast.KindColonToken, - "=": ast.KindEqualsToken, - "+=": ast.KindPlusEqualsToken, - "-=": ast.KindMinusEqualsToken, - "*=": ast.KindAsteriskEqualsToken, - "**=": ast.KindAsteriskAsteriskEqualsToken, - "/=": ast.KindSlashEqualsToken, - "%=": ast.KindPercentEqualsToken, - "<<=": ast.KindLessThanLessThanEqualsToken, - ">>=": ast.KindGreaterThanGreaterThanEqualsToken, - ">>>=": ast.KindGreaterThanGreaterThanGreaterThanEqualsToken, - "&=": ast.KindAmpersandEqualsToken, - "|=": ast.KindBarEqualsToken, - "^=": ast.KindCaretEqualsToken, - "||=": ast.KindBarBarEqualsToken, - "&&=": ast.KindAmpersandAmpersandEqualsToken, - "??=": ast.KindQuestionQuestionEqualsToken, - "@": ast.KindAtToken, - "#": ast.KindHashToken, - "`": ast.KindBacktickToken, -} +var textToToken = func() map[string]ast.Kind { + m := map[string]ast.Kind{ + "{": ast.KindOpenBraceToken, + "}": ast.KindCloseBraceToken, + "(": ast.KindOpenParenToken, + ")": ast.KindCloseParenToken, + "[": ast.KindOpenBracketToken, + "]": ast.KindCloseBracketToken, + ".": ast.KindDotToken, + "...": ast.KindDotDotDotToken, + ";": ast.KindSemicolonToken, + ",": ast.KindCommaToken, + "<": ast.KindLessThanToken, + ">": ast.KindGreaterThanToken, + "<=": ast.KindLessThanEqualsToken, + ">=": ast.KindGreaterThanEqualsToken, + "==": ast.KindEqualsEqualsToken, + "!=": ast.KindExclamationEqualsToken, + "===": ast.KindEqualsEqualsEqualsToken, + "!==": ast.KindExclamationEqualsEqualsToken, + "=>": ast.KindEqualsGreaterThanToken, + "+": ast.KindPlusToken, + "-": ast.KindMinusToken, + "**": ast.KindAsteriskAsteriskToken, + "*": ast.KindAsteriskToken, + "/": ast.KindSlashToken, + "%": ast.KindPercentToken, + "++": ast.KindPlusPlusToken, + "--": ast.KindMinusMinusToken, + "<<": ast.KindLessThanLessThanToken, + ">": ast.KindGreaterThanGreaterThanToken, + ">>>": ast.KindGreaterThanGreaterThanGreaterThanToken, + "&": ast.KindAmpersandToken, + "|": ast.KindBarToken, + "^": ast.KindCaretToken, + "!": ast.KindExclamationToken, + "~": ast.KindTildeToken, + "&&": ast.KindAmpersandAmpersandToken, + "||": ast.KindBarBarToken, + "?": ast.KindQuestionToken, + "??": ast.KindQuestionQuestionToken, + "?.": ast.KindQuestionDotToken, + ":": ast.KindColonToken, + "=": ast.KindEqualsToken, + "+=": ast.KindPlusEqualsToken, + "-=": ast.KindMinusEqualsToken, + "*=": ast.KindAsteriskEqualsToken, + "**=": ast.KindAsteriskAsteriskEqualsToken, + "/=": ast.KindSlashEqualsToken, + "%=": ast.KindPercentEqualsToken, + "<<=": ast.KindLessThanLessThanEqualsToken, + ">>=": ast.KindGreaterThanGreaterThanEqualsToken, + ">>>=": ast.KindGreaterThanGreaterThanGreaterThanEqualsToken, + "&=": ast.KindAmpersandEqualsToken, + "|=": ast.KindBarEqualsToken, + "^=": ast.KindCaretEqualsToken, + "||=": ast.KindBarBarEqualsToken, + "&&=": ast.KindAmpersandAmpersandEqualsToken, + "??=": ast.KindQuestionQuestionEqualsToken, + "@": ast.KindAtToken, + "#": ast.KindHashToken, + "`": ast.KindBacktickToken, + } + maps.Copy(m, textToKeyword) + return m +}() // As per ECMAScript Language Specification 5th Edition, Section 7.6: ISyntaxToken Names and Identifiers // IdentifierStart :: @@ -2013,10 +2018,7 @@ func isInUnicodeRanges(cp rune, ranges []rune) bool { var tokenToText map[ast.Kind]string func init() { - tokenToText = make(map[ast.Kind]string, len(textToKeyword)+len(textToToken)) - for text, key := range textToKeyword { - tokenToText[key] = text - } + tokenToText = make(map[ast.Kind]string, len(textToToken)) for text, key := range textToToken { tokenToText[key] = text }