Skip to content

Commit 60f706c

Browse files
authored
Fix findRightmostValidToken (#820)
1 parent ac7e05c commit 60f706c

File tree

2 files changed

+104
-19
lines changed

2 files changed

+104
-19
lines changed

internal/astnav/tokens.go

+49-18
Original file line numberDiff line numberDiff line change
@@ -428,26 +428,26 @@ func findRightmostValidToken(endPos int, sourceFile *ast.SourceFile, containingN
428428
}
429429

430430
var rightmostValidNode *ast.Node
431-
var rightmostVisitedNode *ast.Node
431+
rightmostVisitedNodes := make([]*ast.Node, 0, 1) // Nodes after the last valid node.
432432
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
433+
shouldVisitNode := func(node *ast.Node) bool {
434+
// Node is synthetic or out of the desired range: don't visit it.
435+
return !(node.Flags&ast.NodeFlagsReparsed != 0 ||
436+
node.End() > endPos || getStartOfNode(node, sourceFile, !excludeJSDoc /*includeJSDoc*/) >= position)
444437
}
445438
visitNode := func(node *ast.Node, _ *ast.NodeVisitor) *ast.Node {
446439
if node == nil {
447440
return node
448441
}
449442
hasChildren = true
450-
test(node)
443+
if !shouldVisitNode(node) {
444+
return node
445+
}
446+
rightmostVisitedNodes = append(rightmostVisitedNodes, node)
447+
if isValidPrecedingNode(node, sourceFile) {
448+
rightmostValidNode = node
449+
rightmostVisitedNodes = rightmostVisitedNodes[:0]
450+
}
451451
return node
452452
}
453453
visitNodes := func(nodeList *ast.NodeList, _ *ast.NodeVisitor) *ast.NodeList {
@@ -462,11 +462,23 @@ func findRightmostValidToken(endPos int, sourceFile *ast.SourceFile, containingN
462462
}
463463
return comparisonLessThan
464464
})
465+
validIndex := -1
465466
for i := index - 1; i >= 0; i-- {
466-
if test(nodeList.Nodes[i]) {
467+
if !shouldVisitNode(nodeList.Nodes[i]) {
468+
continue
469+
}
470+
if isValidPrecedingNode(nodeList.Nodes[i], sourceFile) {
471+
validIndex = i
472+
rightmostValidNode = nodeList.Nodes[i]
467473
break
468474
}
469475
}
476+
for i := validIndex + 1; i < index; i++ {
477+
if !shouldVisitNode(nodeList.Nodes[i]) {
478+
continue
479+
}
480+
rightmostVisitedNodes = append(rightmostVisitedNodes, nodeList.Nodes[i])
481+
}
470482
}
471483
return nodeList
472484
}
@@ -485,19 +497,37 @@ func findRightmostValidToken(endPos int, sourceFile *ast.SourceFile, containingN
485497

486498
// Three cases:
487499
// 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.
500+
// 2. The answer is one of the unvisited tokens that occur after the rightmost valid node.
489501
// 3. The current node is a childless, token-less node. The answer is the current node.
490502

491-
// Case 2: Look at trailing tokens.
503+
// Case 2: Look at unvisited trailing tokens that occur in between the rightmost visited nodes.
492504
if !ast.IsJSDocCommentContainingNode(n) { // JSDoc nodes don't include trivia tokens as children.
493505
var startPos int
494-
if rightmostVisitedNode != nil {
495-
startPos = rightmostVisitedNode.End()
506+
if rightmostValidNode != nil {
507+
startPos = rightmostValidNode.End()
496508
} else {
497509
startPos = n.Pos()
498510
}
499511
scanner := scanner.GetScannerForSourceFile(sourceFile, startPos)
500512
var tokens []*ast.Node
513+
for _, visitedNode := range rightmostVisitedNodes {
514+
// Trailing tokens that occur before this node.
515+
for startPos < min(visitedNode.Pos(), position) {
516+
tokenStart := scanner.TokenStart()
517+
if tokenStart >= position {
518+
break
519+
}
520+
token := scanner.Token()
521+
tokenFullStart := scanner.TokenFullStart()
522+
tokenEnd := scanner.TokenEnd()
523+
startPos = tokenEnd
524+
tokens = append(tokens, sourceFile.GetOrCreateToken(token, tokenFullStart, tokenEnd, n))
525+
scanner.Scan()
526+
}
527+
startPos = visitedNode.End()
528+
scanner.ResetPos(startPos)
529+
}
530+
// Trailing tokens after last visited node.
501531
for startPos < min(endPos, position) {
502532
tokenStart := scanner.TokenStart()
503533
if tokenStart >= position {
@@ -510,6 +540,7 @@ func findRightmostValidToken(endPos int, sourceFile *ast.SourceFile, containingN
510540
tokens = append(tokens, sourceFile.GetOrCreateToken(token, tokenFullStart, tokenEnd, n))
511541
scanner.Scan()
512542
}
543+
513544
lastToken := len(tokens) - 1
514545
// Find preceding valid token.
515546
for i := lastToken; i >= 0; i-- {

internal/astnav/tokens_test.go

+55-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ func baselineTokens(t *testing.T, testName string, includeEOF bool, getTSTokens
8383
fileText, err := os.ReadFile(fileName)
8484
assert.NilError(t, err)
8585

86-
positions := make([]int, len(fileText))
86+
positions := make([]int, len(fileText)+core.IfElse(includeEOF, 1, 0))
8787
for i := range positions {
8888
positions[i] = i
8989
}
@@ -352,6 +352,60 @@ func TestFindPrecedingToken(t *testing.T) {
352352
})
353353
}
354354

355+
func TestUnitFindPrecedingToken(t *testing.T) {
356+
t.Parallel()
357+
fileContent := `import {
358+
CharacterCodes,
359+
compareStringsCaseInsensitive,
360+
compareStringsCaseSensitive,
361+
compareValues,
362+
Comparison,
363+
Debug,
364+
endsWith,
365+
equateStringsCaseInsensitive,
366+
equateStringsCaseSensitive,
367+
GetCanonicalFileName,
368+
getDeclarationFileExtension,
369+
getStringComparer,
370+
identity,
371+
lastOrUndefined,
372+
Path,
373+
some,
374+
startsWith,
375+
} from "./_namespaces/ts.js";
376+
377+
/**
378+
* Internally, we represent paths as strings with '/' as the directory separator.
379+
* When we make system calls (eg: LanguageServiceHost.getDirectory()),
380+
* we expect the host to correctly handle paths in our specified format.
381+
*
382+
* @internal
383+
*/
384+
export const directorySeparator = "/";
385+
/** @internal */
386+
export const altDirectorySeparator = "\\";
387+
const urlSchemeSeparator = "://";
388+
const backslashRegExp = /\\/g;
389+
390+
391+
backslashRegExp.
392+
393+
//Path Tests
394+
395+
/**
396+
* Determines whether a charCode corresponds to '/' or '\'.
397+
*
398+
* @internal
399+
*/
400+
export function isAnyDirectorySeparator(charCode: number): boolean {
401+
return charCode === CharacterCodes.slash || charCode === CharacterCodes.backslash;
402+
}`
403+
file := parser.ParseSourceFile("/file.ts", "/file.ts", fileContent, core.ScriptTargetLatest, scanner.JSDocParsingModeParseAll)
404+
position := 839
405+
token := astnav.FindPrecedingToken(file, position)
406+
assert.Equal(t, token.Kind, ast.KindDotToken)
407+
}
408+
355409
func tsFindPrecedingTokens(t *testing.T, fileText string, positions []int) []*tokenInfo {
356410
dir := t.TempDir()
357411
err := os.WriteFile(filepath.Join(dir, "file.ts"), []byte(fileText), 0o644)

0 commit comments

Comments
 (0)