Skip to content

Commit e33ba54

Browse files
committed
track next node and avoid scanning into it
1 parent d38f992 commit e33ba54

File tree

2 files changed

+108
-12
lines changed

2 files changed

+108
-12
lines changed

internal/astnav/tokens.go

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,18 @@ func GetTokenAtPosition(sourceFile *ast.SourceFile, position int) *ast.Node {
4040
return getTokenAtPosition(sourceFile, position, true /*allowPositionInLeadingTrivia*/, nil)
4141
}
4242

43+
func shouldVisitNode(node *ast.Node, allowReparsed bool) bool {
44+
// Skip reparsed nodes unless:
45+
// 1. The node itself is AsExpression or SatisfiesExpression, OR
46+
// 2. We're already inside an AsExpression or SatisfiesExpression (allowReparsed=true)
47+
// These are special cases where reparsed nodes from JSDoc type assertions
48+
// should still be navigable to reach identifiers.
49+
isSpecialReparsed := node.Flags&ast.NodeFlagsReparsed != 0 &&
50+
(node.Kind == ast.KindAsExpression || node.Kind == ast.KindSatisfiesExpression)
51+
52+
return node.Flags&ast.NodeFlagsReparsed == 0 || isSpecialReparsed || allowReparsed
53+
}
54+
4355
func getTokenAtPosition(
4456
sourceFile *ast.SourceFile,
4557
position int,
@@ -65,6 +77,9 @@ func getTokenAtPosition(
6577
// `left` tracks the lower boundary of the node/token that could be returned,
6678
// and is eventually the scanner's start position, if the scanner is used.
6779
left := 0
80+
// `nodeAfterLeft` tracks the first node we visit after visiting the node that advances `left`.
81+
// When scanning in between nodes for token, we should only scan up to the start of `nodeAfterLeft`.
82+
var nodeAfterLeft *ast.Node
6883
// `allowReparsed` is set when we're navigating inside an AsExpression or
6984
// SatisfiesExpression, which allows visiting their reparsed children to reach
7085
// the actual identifier from JSDoc type assertions.
@@ -91,16 +106,14 @@ func getTokenAtPosition(
91106
visitNode := func(node *ast.Node, _ *ast.NodeVisitor) *ast.Node {
92107
// We can't abort visiting children, so once a match is found, we set `next`
93108
// and do nothing on subsequent visits.
94-
if node != nil && next == nil {
95-
// Skip reparsed nodes unless:
96-
// 1. The node itself is AsExpression or SatisfiesExpression, OR
97-
// 2. We're already inside an AsExpression or SatisfiesExpression (allowReparsed=true)
98-
// These are special cases where reparsed nodes from JSDoc type assertions
99-
// should still be navigable to reach identifiers.
100-
isSpecialReparsed := node.Flags&ast.NodeFlagsReparsed != 0 &&
101-
(node.Kind == ast.KindAsExpression || node.Kind == ast.KindSatisfiesExpression)
102-
103-
if node.Flags&ast.NodeFlagsReparsed == 0 || isSpecialReparsed || allowReparsed {
109+
if node == nil {
110+
return nil
111+
}
112+
if nodeAfterLeft == nil {
113+
nodeAfterLeft = node
114+
}
115+
if next == nil {
116+
if shouldVisitNode(node, allowReparsed) {
104117
result := testNode(node)
105118
switch result {
106119
case -1:
@@ -111,6 +124,7 @@ func getTokenAtPosition(
111124
// all its leading trivia in its position.
112125
left = node.End()
113126
}
127+
nodeAfterLeft = nil
114128
case 0:
115129
next = node
116130
}
@@ -120,12 +134,25 @@ func getTokenAtPosition(
120134
}
121135

122136
visitNodeList := func(nodeList *ast.NodeList, _ *ast.NodeVisitor) *ast.NodeList {
123-
if nodeList != nil && len(nodeList.Nodes) > 0 && next == nil {
137+
if nodeList == nil || len(nodeList.Nodes) == 0 {
138+
return nodeList
139+
}
140+
if nodeAfterLeft == nil {
141+
for _, node := range nodeList.Nodes {
142+
if shouldVisitNode(node, allowReparsed) {
143+
nodeAfterLeft = node
144+
break
145+
}
146+
}
147+
}
148+
if next == nil {
124149
if nodeList.End() == position && includePrecedingTokenAtEndPosition != nil {
125150
left = nodeList.End()
151+
nodeAfterLeft = nil
126152
prevSubtree = nodeList.Nodes[len(nodeList.Nodes)-1]
127153
} else if nodeList.End() <= position {
128154
left = nodeList.End()
155+
nodeAfterLeft = nil
129156
} else if nodeList.Pos() <= position {
130157
nodes := nodeList.Nodes
131158
index, match := core.BinarySearchUniqueFunc(nodes, func(middle int, node *ast.Node) int {
@@ -135,6 +162,13 @@ func getTokenAtPosition(
135162
cmp := testNode(node)
136163
if cmp < 0 {
137164
left = node.End()
165+
nodeAfterLeft = nil
166+
for i := middle + 1; i < len(nodes); i++ {
167+
if shouldVisitNode(nodes[i], allowReparsed) {
168+
nodeAfterLeft = nodes[i]
169+
break
170+
}
171+
}
138172
}
139173
return cmp
140174
})
@@ -147,6 +181,11 @@ func getTokenAtPosition(
147181
cmp := testNode(node)
148182
if cmp < 0 {
149183
left = node.End()
184+
if middle+1 < len(nodes) {
185+
nodeAfterLeft = nodes[middle+1]
186+
} else {
187+
nodeAfterLeft = nil
188+
}
150189
}
151190
return cmp
152191
})
@@ -182,12 +221,33 @@ func getTokenAtPosition(
182221
return current
183222
}
184223
scanner := scanner.GetScannerForSourceFile(sourceFile, left)
185-
for left < current.End() {
224+
end := current.End()
225+
// We should only scan up to the start of the next node in the AST after the node ending at position `left`.
226+
// It is necessary to enforce this invariant in cases where `position` occurs in between two node/tokens,
227+
// such that we would not find a token in the loop below before we reach the next node.
228+
// We can fall into this case when `allowPositionInLeadingTrivia` is false and `position` is in a leading trivia,
229+
// or when `position` would be in the leading trivia of a node but this node is inside JSDoc:
230+
// ```
231+
// /**
232+
// * @type {{
233+
// */*$*/ identifier: boolean;
234+
// * }}
235+
// */
236+
// ```
237+
// The position of marker '$' falls in between the asterisk token and the identifier token, but is not
238+
// part of the leading trivia for `identifier`.
239+
if nodeAfterLeft != nil {
240+
end = nodeAfterLeft.Pos()
241+
}
242+
for left < end {
186243
token := scanner.Token()
187244
tokenFullStart := scanner.TokenFullStart()
188245
tokenStart := core.IfElse(allowPositionInLeadingTrivia, tokenFullStart, scanner.TokenStart())
189246
tokenEnd := scanner.TokenEnd()
190247
flags := scanner.TokenFlags()
248+
if tokenEnd > end {
249+
break
250+
}
191251
if tokenStart <= position && (position < tokenEnd) {
192252
if token == ast.KindIdentifier || !ast.IsTokenKind(token) {
193253
if ast.IsJSDocKind(current.Kind) {
@@ -210,6 +270,7 @@ func getTokenAtPosition(
210270
}
211271
current = next
212272
left = current.Pos()
273+
nodeAfterLeft = nil
213274
next = nil
214275
// When navigating into AsExpression or SatisfiesExpression, allow visiting
215276
// their reparsed children to reach identifiers from JSDoc type assertions.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package fourslash_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/microsoft/typescript-go/internal/fourslash"
7+
. "github.com/microsoft/typescript-go/internal/fourslash/tests/util"
8+
"github.com/microsoft/typescript-go/internal/testutil"
9+
)
10+
11+
func TestCompletionsJSDocTrivia(t *testing.T) {
12+
t.Parallel()
13+
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
14+
15+
const content = `// @noLib: true
16+
/**
17+
* @type {{
18+
* 'string-property': boolean;
19+
*/*$*/ identifierProperty: boolean;
20+
* }}
21+
*/
22+
var someVariable;`
23+
24+
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
25+
defer done()
26+
f.GoToMarker(t, "$")
27+
f.VerifyCompletions(t, nil, &fourslash.CompletionsExpectedList{
28+
IsIncomplete: false,
29+
ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{
30+
CommitCharacters: &[]string{".", ",", ";"},
31+
EditRange: Ignored,
32+
},
33+
Items: &fourslash.CompletionsExpectedItems{},
34+
})
35+
}

0 commit comments

Comments
 (0)