Skip to content

Commit 28722fa

Browse files
authored
Add findPrecedingToken (#804)
1 parent 222ef9b commit 28722fa

File tree

3 files changed

+454
-29
lines changed

3 files changed

+454
-29
lines changed

internal/astnav/tokens.go

+283
Original file line numberDiff line numberDiff line change
@@ -246,3 +246,286 @@ func visitEachChildAndJSDoc(node *ast.Node, sourceFile *ast.SourceFile, visitor
246246
}
247247
node.VisitEachChild(visitor)
248248
}
249+
250+
const (
251+
comparisonLessThan = -1
252+
comparisonEqualTo = 0
253+
comparisonGreaterThan = 1
254+
)
255+
256+
// Finds the leftmost token satisfying `position < token.End()`.
257+
// If the leftmost token satisfying `position < token.End()` is invalid, or if position
258+
// is in the trivia of that leftmost token,
259+
// we will find the rightmost valid token with `token.End() <= position`.
260+
func FindPrecedingToken(sourceFile *ast.SourceFile, position int) *ast.Node {
261+
return FindPrecedingTokenEx(sourceFile, position, nil, false)
262+
}
263+
264+
func FindPrecedingTokenEx(sourceFile *ast.SourceFile, position int, startNode *ast.Node, excludeJSDoc bool) *ast.Node {
265+
var find func(node *ast.Node) *ast.Node
266+
find = func(n *ast.Node) *ast.Node {
267+
if ast.IsNonWhitespaceToken(n) {
268+
return n
269+
}
270+
271+
// `foundChild` is the leftmost node that contains the target position.
272+
// `prevChild` is the last visited child of the current node.
273+
var foundChild, prevChild *ast.Node
274+
visitNode := func(node *ast.Node, _ *ast.NodeVisitor) *ast.Node {
275+
// skip synthesized nodes (that will exist now because of jsdoc handling)
276+
if node == nil || node.Flags&ast.NodeFlagsReparsed != 0 {
277+
return node
278+
}
279+
if foundChild != nil { // We cannot abort visiting children, so once the desired child is found, we do nothing.
280+
return node
281+
}
282+
if position < node.End() && (prevChild == nil || prevChild.End() <= position) {
283+
foundChild = node
284+
} else {
285+
prevChild = node
286+
}
287+
return node
288+
}
289+
visitNodes := func(nodeList *ast.NodeList, _ *ast.NodeVisitor) *ast.NodeList {
290+
if foundChild != nil {
291+
return nodeList
292+
}
293+
if nodeList != nil && len(nodeList.Nodes) > 0 {
294+
nodes := nodeList.Nodes
295+
if isJSDocSingleCommentNodeList(n, nodeList) {
296+
return nodeList
297+
}
298+
index, match := core.BinarySearchUniqueFunc(nodes, func(middle int, _ *ast.Node) int {
299+
// synthetic jsdoc nodes should have jsdocNode.End() <= n.Pos()
300+
if nodes[middle].Flags&ast.NodeFlagsReparsed != 0 {
301+
return comparisonLessThan
302+
}
303+
if position < nodes[middle].End() {
304+
if middle == 0 || position >= nodes[middle-1].End() {
305+
return comparisonEqualTo
306+
}
307+
return comparisonGreaterThan
308+
}
309+
return comparisonLessThan
310+
})
311+
312+
if match {
313+
foundChild = nodes[index]
314+
}
315+
316+
validLookupIndex := core.IfElse(match, index-1, len(nodes)-1)
317+
for i := validLookupIndex; i >= 0; i-- {
318+
if nodes[i].Flags&ast.NodeFlagsReparsed != 0 {
319+
continue
320+
}
321+
if prevChild == nil {
322+
prevChild = nodes[i]
323+
}
324+
}
325+
}
326+
return nodeList
327+
}
328+
nodeVisitor := ast.NewNodeVisitor(core.Identity, nil, ast.NodeVisitorHooks{
329+
VisitNode: visitNode,
330+
VisitToken: visitNode,
331+
VisitNodes: visitNodes,
332+
VisitModifiers: func(modifiers *ast.ModifierList, visitor *ast.NodeVisitor) *ast.ModifierList {
333+
if modifiers != nil {
334+
visitNodes(&modifiers.NodeList, visitor)
335+
}
336+
return modifiers
337+
},
338+
})
339+
visitEachChildAndJSDoc(n, sourceFile, nodeVisitor)
340+
341+
if foundChild != nil {
342+
// Note that the span of a node's tokens is [getStartOfNode(node, ...), node.end).
343+
// Given that `position < child.end` and child has constituent tokens, we distinguish these cases:
344+
// 1) `position` precedes `child`'s tokens or `child` has no tokens (ie: in a comment or whitespace preceding `child`):
345+
// we need to find the last token in a previous child node or child tokens.
346+
// 2) `position` is within the same span: we recurse on `child`.
347+
start := getStartOfNode(foundChild, sourceFile, !excludeJSDoc /*includeJSDoc*/)
348+
lookInPreviousChild := start >= position || // cursor in the leading trivia or preceding tokens
349+
!isValidPrecedingNode(foundChild, sourceFile)
350+
if lookInPreviousChild {
351+
if position >= foundChild.Pos() {
352+
// Find jsdoc preceding the foundChild.
353+
var jsDoc *ast.Node
354+
nodeJSDoc := n.JSDoc(sourceFile)
355+
for i := len(nodeJSDoc) - 1; i >= 0; i-- {
356+
if nodeJSDoc[i].Pos() >= foundChild.Pos() {
357+
jsDoc = nodeJSDoc[i]
358+
break
359+
}
360+
}
361+
if jsDoc != nil {
362+
if !excludeJSDoc {
363+
return find(jsDoc)
364+
} else {
365+
return findRightmostValidToken(jsDoc.End(), sourceFile, n, position, excludeJSDoc)
366+
}
367+
}
368+
return findRightmostValidToken(foundChild.Pos(), sourceFile, n, -1 /*position*/, excludeJSDoc)
369+
} else { // Answer is in tokens between two visited children.
370+
return findRightmostValidToken(foundChild.Pos(), sourceFile, n, position, excludeJSDoc)
371+
}
372+
} else {
373+
// position is in [foundChild.getStart(), foundChild.End): recur.
374+
return find(foundChild)
375+
}
376+
}
377+
378+
// We have two cases here: either the position is at the end of the file,
379+
// or the desired token is in the unvisited trailing tokens of the current node.
380+
if position >= n.End() {
381+
return findRightmostValidToken(n.End(), sourceFile, n, -1 /*position*/, excludeJSDoc)
382+
} else {
383+
return findRightmostValidToken(n.End(), sourceFile, n, position, excludeJSDoc)
384+
}
385+
}
386+
387+
var node *ast.Node
388+
if startNode != nil {
389+
node = startNode
390+
} else {
391+
node = sourceFile.AsNode()
392+
}
393+
result := find(node)
394+
if result != nil && ast.IsWhitespaceOnlyJsxText(result) {
395+
panic("Expected result to be a non-whitespace token.")
396+
}
397+
return result
398+
}
399+
400+
func isValidPrecedingNode(node *ast.Node, sourceFile *ast.SourceFile) bool {
401+
start := getStartOfNode(node, sourceFile, false /*includeJSDoc*/)
402+
width := node.End() - start
403+
return !(ast.IsWhitespaceOnlyJsxText(node) || width == 0)
404+
}
405+
406+
func getStartOfNode(node *ast.Node, file *ast.SourceFile, includeJSDoc bool) int {
407+
return scanner.GetTokenPosOfNode(node, file, includeJSDoc)
408+
}
409+
410+
// If this is a single comment JSDoc, we do not visit the comment node.
411+
func isJSDocSingleCommentNodeList(parent *ast.Node, nodeList *ast.NodeList) bool {
412+
return parent.Kind == ast.KindJSDoc && nodeList == parent.AsJSDoc().Comment && nodeList != nil && len(nodeList.Nodes) == 1
413+
}
414+
415+
// Looks for rightmost valid token in the range [startPos, endPos).
416+
// If position is >= 0, looks for rightmost valid token that precedes or touches that position.
417+
func findRightmostValidToken(endPos int, sourceFile *ast.SourceFile, containingNode *ast.Node, position int, excludeJSDoc bool) *ast.Node {
418+
if position == -1 {
419+
position = containingNode.End()
420+
}
421+
var find func(n *ast.Node) *ast.Node
422+
find = func(n *ast.Node) *ast.Node {
423+
if n == nil {
424+
return nil
425+
}
426+
if ast.IsNonWhitespaceToken(n) {
427+
return n
428+
}
429+
430+
var rightmostValidNode *ast.Node
431+
var rightmostVisitedNode *ast.Node
432+
hasChildren := false
433+
test := func(node *ast.Node) bool {
434+
if node.Flags&ast.NodeFlagsReparsed != 0 ||
435+
node.End() > endPos || getStartOfNode(node, sourceFile, !excludeJSDoc /*includeJSDoc*/) >= position {
436+
return false
437+
}
438+
rightmostVisitedNode = node
439+
if isValidPrecedingNode(node, sourceFile) {
440+
rightmostValidNode = node
441+
return true
442+
}
443+
return false
444+
}
445+
visitNode := func(node *ast.Node, _ *ast.NodeVisitor) *ast.Node {
446+
if node == nil {
447+
return node
448+
}
449+
hasChildren = true
450+
test(node)
451+
return node
452+
}
453+
visitNodes := func(nodeList *ast.NodeList, _ *ast.NodeVisitor) *ast.NodeList {
454+
if nodeList != nil && len(nodeList.Nodes) > 0 {
455+
if isJSDocSingleCommentNodeList(n, nodeList) {
456+
return nodeList
457+
}
458+
hasChildren = true
459+
index, _ := core.BinarySearchUniqueFunc(nodeList.Nodes, func(middle int, node *ast.Node) int {
460+
if node.End() > endPos {
461+
return comparisonGreaterThan
462+
}
463+
return comparisonLessThan
464+
})
465+
for i := index - 1; i >= 0; i-- {
466+
if test(nodeList.Nodes[i]) {
467+
break
468+
}
469+
}
470+
}
471+
return nodeList
472+
}
473+
nodeVisitor := ast.NewNodeVisitor(core.Identity, nil, ast.NodeVisitorHooks{
474+
VisitNode: visitNode,
475+
VisitToken: visitNode,
476+
VisitNodes: visitNodes,
477+
VisitModifiers: func(modifiers *ast.ModifierList, visitor *ast.NodeVisitor) *ast.ModifierList {
478+
if modifiers != nil {
479+
visitNodes(&modifiers.NodeList, visitor)
480+
}
481+
return modifiers
482+
},
483+
})
484+
visitEachChildAndJSDoc(n, sourceFile, nodeVisitor)
485+
486+
// Three cases:
487+
// 1. The answer is a token of `rightmostValidNode`.
488+
// 2. The answer is one of the unvisited tokens that occur after the last visited node.
489+
// 3. The current node is a childless, token-less node. The answer is the current node.
490+
491+
// Case 2: Look at trailing tokens.
492+
if !ast.IsJSDocCommentContainingNode(n) { // JSDoc nodes don't include trivia tokens as children.
493+
var startPos int
494+
if rightmostVisitedNode != nil {
495+
startPos = rightmostVisitedNode.End()
496+
} else {
497+
startPos = n.Pos()
498+
}
499+
scanner := scanner.GetScannerForSourceFile(sourceFile, startPos)
500+
var tokens []*ast.Node
501+
for startPos < min(endPos, position) {
502+
tokenStart := scanner.TokenStart()
503+
if tokenStart >= position {
504+
break
505+
}
506+
token := scanner.Token()
507+
tokenFullStart := scanner.TokenFullStart()
508+
tokenEnd := scanner.TokenEnd()
509+
startPos = tokenEnd
510+
tokens = append(tokens, sourceFile.GetOrCreateToken(token, tokenFullStart, tokenEnd, n))
511+
scanner.Scan()
512+
}
513+
lastToken := len(tokens) - 1
514+
// Find preceding valid token.
515+
for i := lastToken; i >= 0; i-- {
516+
if !ast.IsWhitespaceOnlyJsxText(tokens[i]) {
517+
return tokens[i]
518+
}
519+
}
520+
}
521+
522+
// Case 3: childless node.
523+
if !hasChildren {
524+
return n
525+
}
526+
// Case 1: recur on rightmostValidNode.
527+
return find(rightmostValidNode)
528+
}
529+
530+
return find(containingNode)
531+
}

0 commit comments

Comments
 (0)