Skip to content
Open
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
45 changes: 33 additions & 12 deletions internal/astnav/tokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ func getTokenAtPosition(
// `left` tracks the lower boundary of the node/token that could be returned,
// and is eventually the scanner's start position, if the scanner is used.
left := 0
// `allowReparsed` is set when we're navigating inside an AsExpression or
// SatisfiesExpression, which allows visiting their reparsed children to reach
// the actual identifier from JSDoc type assertions.
allowReparsed := false

testNode := func(node *ast.Node) int {
if node.Kind != ast.KindEndOfFile && node.End() == position && includePrecedingTokenAtEndPosition != nil {
Expand All @@ -74,7 +78,8 @@ func getTokenAtPosition(
if node.End() < position || node.Kind != ast.KindEndOfFile && node.End() == position {
return -1
}
if getPosition(node, sourceFile, allowPositionInLeadingTrivia) > position {
nodePos := getPosition(node, sourceFile, allowPositionInLeadingTrivia)
if nodePos > position {
return 1
}
return 0
Expand All @@ -86,18 +91,29 @@ func getTokenAtPosition(
visitNode := func(node *ast.Node, _ *ast.NodeVisitor) *ast.Node {
// We can't abort visiting children, so once a match is found, we set `next`
// and do nothing on subsequent visits.
if node != nil && node.Flags&ast.NodeFlagsReparsed == 0 && next == nil {
switch testNode(node) {
case -1:
if !ast.IsJSDocKind(node.Kind) {
// We can't move the left boundary into or beyond JSDoc,
// because we may end up returning the token after this JSDoc,
// constructing it with the scanner, and we need to include
// all its leading trivia in its position.
left = node.End()
if node != nil && next == nil {
// Skip reparsed nodes unless:
// 1. The node itself is AsExpression or SatisfiesExpression, OR
// 2. We're already inside an AsExpression or SatisfiesExpression (allowReparsed=true)
// These are special cases where reparsed nodes from JSDoc type assertions
// should still be navigable to reach identifiers.
isSpecialReparsed := node.Flags&ast.NodeFlagsReparsed != 0 &&
(node.Kind == ast.KindAsExpression || node.Kind == ast.KindSatisfiesExpression)

if node.Flags&ast.NodeFlagsReparsed == 0 || isSpecialReparsed || allowReparsed {
result := testNode(node)
switch result {
case -1:
if !ast.IsJSDocKind(node.Kind) {
// We can't move the left boundary into or beyond JSDoc,
// because we may end up returning the token after this JSDoc,
// constructing it with the scanner, and we need to include
// all its leading trivia in its position.
left = node.End()
}
case 0:
next = node
}
case 0:
next = node
}
}
return node
Expand Down Expand Up @@ -194,6 +210,11 @@ func getTokenAtPosition(
current = next
left = current.Pos()
next = nil
// When navigating into AsExpression or SatisfiesExpression, allow visiting
// their reparsed children to reach identifiers from JSDoc type assertions.
if current.Kind == ast.KindAsExpression || current.Kind == ast.KindSatisfiesExpression {
allowReparsed = true
}
}
}

Expand Down
46 changes: 46 additions & 0 deletions internal/astnav/tokens_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,52 @@ func TestGetTokenAtPosition(t *testing.T) {
)
})

t.Run("JSDoc type assertion", func(t *testing.T) {
t.Parallel()
fileText := `function foo(x) {
const s = /**@type {string}*/(x)
}`
file := parser.ParseSourceFile(ast.SourceFileParseOptions{
FileName: "/test.js",
Path: "/test.js",
}, fileText, core.ScriptKindJS)

// Position of 'x' inside the parenthesized expression (position 52)
position := 52

// This should not panic - it previously panicked with:
// "did not expect KindParenthesizedExpression to have KindIdentifier in its trivia"
token := astnav.GetTouchingPropertyName(file, position)
if token == nil {
t.Fatal("Expected to get a token, got nil")
}

// The function may return either the identifier itself or the containing
// parenthesized expression, depending on how the AST is structured
if token.Kind != ast.KindIdentifier && token.Kind != ast.KindParenthesizedExpression {
t.Errorf("Expected identifier or parenthesized expression, got %s", token.Kind)
}
})

t.Run("JSDoc type assertion with comment", func(t *testing.T) {
t.Parallel()
// Exact code from the issue report
fileText := `function foo(x) {
const s = /**@type {string}*/(x) // Go-to-definition on x causes panic
}`
file := parser.ParseSourceFile(ast.SourceFileParseOptions{
FileName: "/test.js",
Path: "/test.js",
}, fileText, core.ScriptKindJS)

// Find position of 'x' in the type assertion
xPos := 52 // Position of 'x' in (x)

// This should not panic
token := astnav.GetTouchingPropertyName(file, xPos)
assert.Assert(t, token != nil, "Expected to get a token")
})

t.Run("pointer equality", func(t *testing.T) {
t.Parallel()
fileText := `
Expand Down