Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions internal/ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -9657,6 +9657,10 @@ func IsJSDocParameterTag(node *Node) bool {
return node.Kind == KindJSDocParameterTag
}

func IsJSDocPropertyTag(node *Node) bool {
return node.Kind == KindJSDocPropertyTag
}

// JSDocReturnTag
type JSDocReturnTag struct {
JSDocTagBase
Expand Down Expand Up @@ -10133,6 +10137,8 @@ func (node *JSDocCallbackTag) Clone(f NodeFactoryCoercible) *Node {
return cloneNode(f.AsNodeFactory().NewJSDocCallbackTag(node.TagName, node.TypeExpression, node.FullName, node.Comment), node.AsNode(), f.AsNodeFactory().hooks)
}

func (node *JSDocCallbackTag) Name() *DeclarationName { return node.FullName }

func IsJSDocCallbackTag(node *Node) bool {
return node.Kind == KindJSDocCallbackTag
}
Expand Down Expand Up @@ -10282,6 +10288,10 @@ func (node *JSDocSignature) Clone(f NodeFactoryCoercible) *Node {
return cloneNode(f.AsNodeFactory().NewJSDocSignature(node.TypeParameters, node.Parameters, node.Type), node.AsNode(), f.AsNodeFactory().hooks)
}

func IsJSDocSignature(node *Node) bool {
return node.Kind == KindJSDocSignature
}

// JSDocNameReference
type JSDocNameReference struct {
TypeNodeBase
Expand Down Expand Up @@ -10315,6 +10325,10 @@ func (node *JSDocNameReference) Clone(f NodeFactoryCoercible) *Node {

func (node *JSDocNameReference) Name() *EntityName { return node.name }

func IsJSDocNameReference(node *Node) bool {
return node.Kind == KindJSDocNameReference
}

// PatternAmbientModule

type PatternAmbientModule struct {
Expand Down
26 changes: 18 additions & 8 deletions internal/ast/utilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,10 @@ func IsClassLike(node *Node) bool {
return node.Kind == KindClassDeclaration || node.Kind == KindClassExpression
}

func IsClassOrInterfaceLike(node *Node) bool {
return node.Kind == KindClassDeclaration || node.Kind == KindClassExpression || node.Kind == KindInterfaceDeclaration
}

func IsClassElement(node *Node) bool {
switch node.Kind {
case KindConstructor,
Expand Down Expand Up @@ -1899,13 +1903,11 @@ func IsExpressionNode(node *Node) bool {
for node.Parent.Kind == KindQualifiedName {
node = node.Parent
}
return IsTypeQueryNode(node.Parent) || IsJSDocLinkLike(node.Parent) || isJSXTagName(node)
case KindJSDocMemberName:
return IsTypeQueryNode(node.Parent) || IsJSDocLinkLike(node.Parent) || isJSXTagName(node)
return IsTypeQueryNode(node.Parent) || IsJSDocLinkLike(node.Parent) || IsJSDocNameReference(node.Parent) || isJSXTagName(node)
case KindPrivateIdentifier:
return IsBinaryExpression(node.Parent) && node.Parent.AsBinaryExpression().Left == node && node.Parent.AsBinaryExpression().OperatorToken.Kind == KindInKeyword
case KindIdentifier:
if IsTypeQueryNode(node.Parent) || IsJSDocLinkLike(node.Parent) || isJSXTagName(node) {
if IsTypeQueryNode(node.Parent) || IsJSDocLinkLike(node.Parent) || IsJSDocNameReference(node.Parent) || isJSXTagName(node) {
return true
}
fallthrough
Expand Down Expand Up @@ -2045,7 +2047,9 @@ func isPartOfTypeNodeInParent(node *Node) bool {

func isPartOfTypeExpressionWithTypeArguments(node *Node) bool {
parent := node.Parent
return IsHeritageClause(parent) && (!IsClassLike(parent.Parent) || parent.AsHeritageClause().Token == KindImplementsKeyword)
return IsHeritageClause(parent) && (!IsClassLike(parent.Parent) || parent.AsHeritageClause().Token == KindImplementsKeyword) ||
IsJSDocImplementsTag(parent) ||
IsJSDocAugmentsTag(parent)
}

func IsJSDocLinkLike(node *Node) bool {
Expand Down Expand Up @@ -2620,12 +2624,12 @@ func IsParseTreeNode(node *Node) bool {
return node.Flags&NodeFlagsSynthesized == 0
}

// Returns a token if position is in [start-of-leading-trivia, end), includes JSDoc only in JS files
func GetNodeAtPosition(file *SourceFile, position int, isJavaScriptFile bool) *Node {
// Returns a token if position is in [start-of-leading-trivia, end), includes JSDoc only if requested
func GetNodeAtPosition(file *SourceFile, position int, includeJSDoc bool) *Node {
current := file.AsNode()
for {
var child *Node
if isJavaScriptFile {
if includeJSDoc {
for _, jsdoc := range current.JSDoc(file) {
if nodeContainsPosition(jsdoc, position) {
child = jsdoc
Expand Down Expand Up @@ -3867,3 +3871,9 @@ func GetRestIndicatorOfBindingOrAssignmentElement(bindingElement *Node) *Node {
}
return nil
}

func IsJSDocNameReferenceContext(node *Node) bool {
return node.Flags&NodeFlagsJSDoc != 0 && FindAncestor(node, func(node *Node) bool {
return IsJSDocNameReference(node) || IsJSDocLinkLike(node)
}) != nil
}
22 changes: 20 additions & 2 deletions internal/astnav/tokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,27 @@ import (
)

func GetTouchingPropertyName(sourceFile *ast.SourceFile, position int) *ast.Node {
return getTokenAtPosition(sourceFile, position, false /*allowPositionInLeadingTrivia*/, func(node *ast.Node) bool {
return getReparsedNodeForNode(getTokenAtPosition(sourceFile, position, false /*allowPositionInLeadingTrivia*/, func(node *ast.Node) bool {
return ast.IsPropertyNameLiteral(node) || ast.IsKeywordKind(node.Kind) || ast.IsPrivateIdentifier(node)
})
}))
}

// If the given node is a declaration name node in a JSDoc comment that is subject to reparsing, return the declaration name node
// for the corresponding reparsed construct. Otherwise, just return the node.
func getReparsedNodeForNode(node *ast.Node) *ast.Node {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a replacement for the bidirectional mapping plan we had originally conceived?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is reasonably performant and not used in anything time critical. I haven't really seen a need for anything else.

if node.Flags&ast.NodeFlagsJSDoc != 0 && (ast.IsIdentifier(node) || ast.IsPrivateIdentifier(node)) {
parent := node.Parent
if (ast.IsJSDocTypedefTag(parent) || ast.IsJSDocCallbackTag(parent) || ast.IsJSDocPropertyTag(parent) || ast.IsJSDocParameterTag(parent) || ast.IsImportClause(parent) || ast.IsImportSpecifier(parent)) && parent.Name() == node {
// Reparsing preserves the location of the name. Thus, a search at the position of the name with JSDoc excluded
// finds the containing reparsed declaration node.
if reparsed := ast.GetNodeAtPosition(ast.GetSourceFileOfNode(node), node.Pos(), false); reparsed != nil {
if name := reparsed.Name(); name != nil && name.Pos() == node.Pos() {
return name
}
}
}
}
return node
}

func GetTouchingToken(sourceFile *ast.SourceFile, position int) *ast.Node {
Expand Down
59 changes: 35 additions & 24 deletions internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -2368,10 +2368,11 @@ func (c *Checker) checkJSDocComment(node *ast.Node, location *ast.Node) {
func (c *Checker) resolveJSDocMemberName(name *ast.Node, location *ast.Node) *ast.Symbol {
if name != nil && ast.IsEntityName(name) {
meaning := ast.SymbolFlagsType | ast.SymbolFlagsNamespace | ast.SymbolFlagsValue
symbol := c.resolveEntityName(name, meaning, true /*ignoreErrors*/, true /*dontResolveAlias*/, location)
if symbol == nil && ast.IsQualifiedName(name) {
symbol := c.resolveJSDocMemberName(name.AsQualifiedName().Left, location)
if symbol != nil {
if symbol := c.resolveEntityName(name, meaning, true /*ignoreErrors*/, true /*dontResolveAlias*/, location); symbol != nil {
return symbol
}
Comment on lines +2371 to +2373
Copy link

Copilot AI Sep 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The early return pattern introduced here removes the original variable assignment and changes the control flow. While functionally correct, this creates inconsistency with the original nested if-else structure that was handling the symbol resolution. Consider maintaining the original structure for better code readability and consistency.

Copilot uses AI. Check for mistakes.

if ast.IsQualifiedName(name) {
if symbol := c.resolveJSDocMemberName(name.AsQualifiedName().Left, location); symbol != nil {
var t *Type
if symbol.Flags&ast.SymbolFlagsValue != 0 {
proto := c.getPropertyOfType(c.getTypeOfSymbol(symbol), "prototype")
Expand Down Expand Up @@ -30203,19 +30204,15 @@ func (c *Checker) getSymbolAtLocation(node *ast.Node, ignoreErrors bool) *ast.Sy
return c.getSymbolOfDeclaration(grandParent)
}

if node.Kind == ast.KindIdentifier {
if ast.IsIdentifier(node) {
if isInRightSideOfImportOrExportAssignment(node) {
return c.getSymbolOfNameOrPropertyAccessExpression(node)
} else if parent.Kind == ast.KindBindingElement &&
grandParent.Kind == ast.KindObjectBindingPattern &&
node == parent.AsBindingElement().PropertyName {
} else if ast.IsBindingElement(parent) && ast.IsObjectBindingPattern(grandParent) && node == parent.PropertyName() {
typeOfPattern := c.getTypeOfNode(grandParent)
propertyDeclaration := c.getPropertyOfType(typeOfPattern, node.Text())

if propertyDeclaration != nil {
if propertyDeclaration := c.getPropertyOfType(typeOfPattern, node.Text()); propertyDeclaration != nil {
return propertyDeclaration
}
} else if ast.IsMetaProperty(parent) && parent.AsMetaProperty().Name() == node {
} else if ast.IsMetaProperty(parent) && parent.Name() == node {
metaProp := parent.AsMetaProperty()
if metaProp.KeywordToken == ast.KindNewKeyword && node.Text() == "target" {
// `target` in `new.target`
Expand All @@ -30230,6 +30227,14 @@ func (c *Checker) getSymbolAtLocation(node *ast.Node, ignoreErrors bool) *ast.Sy
}
// no other meta properties are valid syntax, thus no others should have symbols
return nil
} else if ast.IsJSDocParameterTag(parent) && parent.Name() == node {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it odd to have this one special case here in the checker? I don't think we have any cases like this elsewhere after the reparser change.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do have a few. We used to have logic in the binder that would bind declaration names in JSDoc nodes, but we don't do that anymore (which is good). The logic also can't be in getReparsedNodeForNode because that function is supposed to preserve the location of the node (it basically just returns the cloned copy of an identifier created by re-parse). So, I think it's in the right place.

if fn := ast.GetNodeAtPosition(ast.GetSourceFileOfNode(node), node.Pos(), false); fn != nil && ast.IsFunctionLike(fn) {
for _, param := range fn.Parameters() {
if param.Name().Text() == node.Text() {
return c.getSymbolOfNode(param)
}
}
}
}
}

Expand Down Expand Up @@ -30418,28 +30423,32 @@ func (c *Checker) getSymbolOfNameOrPropertyAccessExpression(name *ast.Node) *ast
// Missing entity name.
return nil
}

if name.Kind == ast.KindIdentifier {
isJSDoc := ast.IsJSDocNameReferenceContext(name)
if ast.IsIdentifier(name) {
if ast.IsJsxTagName(name) && isJsxIntrinsicTagName(name) {
symbol := c.getIntrinsicTagSymbol(name.Parent)
return core.IfElse(symbol == c.unknownSymbol, nil, symbol)
}
result := c.resolveEntityName(
name,
ast.SymbolFlagsValue, /*meaning*/
true, /*ignoreErrors*/
true, /*dontResolveAlias*/
nil /*location*/)
meaning := core.IfElse(isJSDoc, ast.SymbolFlagsValue|ast.SymbolFlagsType|ast.SymbolFlagsNamespace, ast.SymbolFlagsValue)
result := c.resolveEntityName(name, meaning, true /*ignoreErrors*/, true /*dontResolveAlias*/, nil /*location*/)
if result == nil && isJSDoc {
if container := ast.FindAncestor(name, ast.IsClassOrInterfaceLike); container != nil {
symbol := c.getSymbolOfDeclaration(container)
// Handle unqualified references to class static members and class or interface instance members
if result = c.getMergedSymbol(c.getSymbol(c.getExportsOfSymbol(symbol), name.Text(), meaning)); result == nil {
result = c.getPropertyOfType(c.getDeclaredTypeOfSymbol(symbol), name.Text())
}
}
}
return result
} else if ast.IsPrivateIdentifier(name) {
return c.getSymbolForPrivateIdentifierExpression(name)
} else if name.Kind == ast.KindPropertyAccessExpression || name.Kind == ast.KindQualifiedName {
} else if ast.IsPropertyAccessExpression(name) || ast.IsQualifiedName(name) {
links := c.symbolNodeLinks.Get(name)
if links.resolvedSymbol != nil {
return links.resolvedSymbol
}

if name.Kind == ast.KindPropertyAccessExpression {
if ast.IsPropertyAccessExpression(name) {
c.checkPropertyAccessExpression(name, CheckModeNormal, false /*writeOnly*/)
if links.resolvedSymbol == nil {
links.resolvedSymbol = c.getApplicableIndexSymbol(
Expand All @@ -30450,7 +30459,9 @@ func (c *Checker) getSymbolOfNameOrPropertyAccessExpression(name *ast.Node) *ast
} else {
c.checkQualifiedName(name, CheckModeNormal)
}

if links.resolvedSymbol == nil && isJSDoc && ast.IsQualifiedName(name) {
return c.resolveJSDocMemberName(name, nil)
}
return links.resolvedSymbol
}
} else if ast.IsEntityName(name) && isTypeReferenceIdentifier(name) {
Expand Down
8 changes: 5 additions & 3 deletions internal/ls/symbols.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ func (l *LanguageService) ProvideDocumentSymbols(ctx context.Context, documentUR
func (l *LanguageService) getDocumentSymbolsForChildren(ctx context.Context, node *ast.Node) []*lsproto.DocumentSymbol {
var symbols []*lsproto.DocumentSymbol
addSymbolForNode := func(node *ast.Node, children []*lsproto.DocumentSymbol) {
symbol := l.newDocumentSymbol(node, children)
if symbol != nil {
symbols = append(symbols, symbol)
if node.Flags&ast.NodeFlagsReparsed == 0 {
symbol := l.newDocumentSymbol(node, children)
if symbol != nil {
symbols = append(symbols, symbol)
}
}
}
var visit func(*ast.Node) bool
Expand Down
24 changes: 11 additions & 13 deletions internal/ls/utilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -1259,14 +1259,14 @@ func getAdjustedLocationForExportDeclaration(node *ast.ExportDeclaration, forRen

func getMeaningFromLocation(node *ast.Node) ast.SemanticMeaning {
// todo: check if this function needs to be changed for jsdoc updates

node = getAdjustedLocation(node, false /*forRename*/, nil)
parent := node.Parent
if node.Kind == ast.KindSourceFile {
switch {
case ast.IsSourceFile(node):
return ast.SemanticMeaningValue
} else if ast.NodeKindIs(node, ast.KindExportAssignment, ast.KindExportSpecifier, ast.KindExternalModuleReference, ast.KindImportSpecifier, ast.KindImportClause) || parent.Kind == ast.KindImportEqualsDeclaration && node == parent.Name() {
case ast.NodeKindIs(node, ast.KindExportAssignment, ast.KindExportSpecifier, ast.KindExternalModuleReference, ast.KindImportSpecifier, ast.KindImportClause) || parent.Kind == ast.KindImportEqualsDeclaration && node == parent.Name():
return ast.SemanticMeaningAll
} else if isInRightSideOfInternalImportEqualsDeclaration(node) {
case isInRightSideOfInternalImportEqualsDeclaration(node):
// import a = |b|; // Namespace
// import a = |b.c|; // Value, type, namespace
// import a = |b.c|.d; // Namespace
Expand All @@ -1278,22 +1278,20 @@ func getMeaningFromLocation(node *ast.Node) ast.SemanticMeaning {
return ast.SemanticMeaningNamespace
}
return ast.SemanticMeaningAll
} else if ast.IsDeclarationName(node) {
case ast.IsDeclarationName(node):
return getMeaningFromDeclaration(parent)
} else if ast.IsEntityName(node) && ast.FindAncestor(node, func(*ast.Node) bool {
return node.Kind == ast.KindJSDocNameReference || ast.IsJSDocLinkLike(node) || node.Kind == ast.KindJSDocMemberName
}) != nil {
case ast.IsEntityName(node) && ast.IsJSDocNameReferenceContext(node):
return ast.SemanticMeaningAll
} else if isTypeReference(node) {
case isTypeReference(node):
return ast.SemanticMeaningType
} else if isNamespaceReference(node) {
case isNamespaceReference(node):
return ast.SemanticMeaningNamespace
} else if parent.Kind == ast.KindTypeParameter {
case ast.IsTypeParameterDeclaration(parent):
return ast.SemanticMeaningType
} else if parent.Kind == ast.KindLiteralType {
case ast.IsLiteralTypeNode(parent):
// This might be T["name"], which is actually referencing a property and not a type. So allow both meanings.
return ast.SemanticMeaningType | ast.SemanticMeaningValue
} else {
default:
return ast.SemanticMeaningValue
}
}
Expand Down
4 changes: 2 additions & 2 deletions internal/parser/jsdoc.go
Original file line number Diff line number Diff line change
Expand Up @@ -918,7 +918,7 @@ func (p *Parser) parseTypedefTag(start int, tagName *ast.IdentifierNode, indent
if childTypeTag != nil && childTypeTag.TypeExpression != nil && !isObjectOrObjectArrayTypeReference(childTypeTag.TypeExpression.Type()) {
typeExpression = childTypeTag.TypeExpression
} else {
typeExpression = p.finishNode(jsdocTypeLiteral, start)
typeExpression = p.finishNode(jsdocTypeLiteral, jsdocPropertyTags[0].Pos())
}
}
}
Expand Down Expand Up @@ -989,7 +989,7 @@ func (p *Parser) parseCallbackTag(start int, tagName *ast.IdentifierNode, indent
fullName := p.parseJSDocIdentifierName(nil)
p.skipWhitespace()
comment := p.parseTagComments(indent, nil)
typeExpression := p.parseJSDocSignature(start, indent)
typeExpression := p.parseJSDocSignature(p.nodePos(), indent)
if comment == nil {
comment = p.parseTrailingTagComments(start, p.nodePos(), indent, indentText)
}
Expand Down
4 changes: 2 additions & 2 deletions internal/parser/reparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ func (p *Parser) reparseJSDocSignature(jsSignature *ast.Node, fun *ast.Node, jsD
if jsSignature.Type() != nil && jsSignature.Type().AsJSDocReturnTag().TypeExpression != nil {
signature.FunctionLikeData().Type = p.factory.DeepCloneReparse(jsSignature.Type().AsJSDocReturnTag().TypeExpression.Type())
}
loc := tag
loc := jsSignature
if tag.Kind == ast.KindJSDocOverloadTag {
loc = tag.AsJSDocOverloadTag().TagName
Comment on lines 195 to 196
Copy link

Copilot AI Sep 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable assignment has changed from tag to jsSignature, but the conditional check on line 195-197 still references tag. This creates a potential inconsistency where loc might not match the intended logic in the conditional block.

Suggested change
if tag.Kind == ast.KindJSDocOverloadTag {
loc = tag.AsJSDocOverloadTag().TagName
if jsSignature.Kind == ast.KindJSDocOverloadTag {
loc = jsSignature.AsJSDocOverloadTag().TagName

Copilot uses AI. Check for mistakes.

}
Expand All @@ -213,7 +213,7 @@ func (p *Parser) reparseJSDocTypeLiteral(t *ast.TypeNode) *ast.Node {
if name.Kind == ast.KindQualifiedName {
name = name.AsQualifiedName().Right
}
property := p.factory.NewPropertySignatureDeclaration(nil, name, p.makeQuestionIfOptional(jsprop), nil, nil)
property := p.factory.NewPropertySignatureDeclaration(nil, p.factory.DeepCloneReparse(name), p.makeQuestionIfOptional(jsprop), nil, nil)
if jsprop.TypeExpression != nil {
property.AsPropertySignatureDeclaration().Type = p.reparseJSDocTypeLiteral(jsprop.TypeExpression.Type())
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
// === findAllReferences ===
// === /foo.js ===

// --- (line: 9) skipped ---
// * @param {unknown} x
// /**
// * @overload
// * @param {number} [|x|]
// * @returns {number}
// *
// * @overload
// * @param {string} [|x|]
// * @returns {string}
// *
// * @param {unknown} [|x|]
// * @returns {unknown}
// */
// function foo(x/*FIND ALL REFS*/[|x|]) {
Expand Down
Loading
Loading