2025-10-15 10:12:44 +03:00

654 lines
22 KiB
Go

package astnav
import (
"fmt"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/scanner"
)
func GetTouchingPropertyName(sourceFile *ast.SourceFile, position int) *ast.Node {
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 {
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 {
return getTokenAtPosition(sourceFile, position, false /*allowPositionInLeadingTrivia*/, nil)
}
func GetTokenAtPosition(sourceFile *ast.SourceFile, position int) *ast.Node {
return getTokenAtPosition(sourceFile, position, true /*allowPositionInLeadingTrivia*/, nil)
}
func getTokenAtPosition(
sourceFile *ast.SourceFile,
position int,
allowPositionInLeadingTrivia bool,
includePrecedingTokenAtEndPosition func(node *ast.Node) bool,
) *ast.Node {
// getTokenAtPosition returns a token at the given position in the source file.
// The token can be a real node in the AST, or a synthesized token constructed
// with information from the scanner. Synthesized tokens are only created when
// needed, and they are stored in the source file's token cache such that multiple
// calls to getTokenAtPosition with the same position will return the same object
// in memory. If there is no token at the given position (possible when
// `allowPositionInLeadingTrivia` is false), the lowest node that encloses the
// position is returned.
// `next` tracks the node whose children will be visited on the next iteration.
// `prevSubtree` is a node whose end position is equal to the target position,
// only if `includePrecedingTokenAtEndPosition` is provided. Once set, the next
// iteration of the loop will test the rightmost token of `prevSubtree` to see
// if it should be returned.
var next, prevSubtree *ast.Node
current := sourceFile.AsNode()
// `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
testNode := func(node *ast.Node) int {
if node.Kind != ast.KindEndOfFile && node.End() == position && includePrecedingTokenAtEndPosition != nil {
prevSubtree = node
}
if node.End() < position || node.Kind != ast.KindEndOfFile && node.End() == position {
return -1
}
if getPosition(node, sourceFile, allowPositionInLeadingTrivia) > position {
return 1
}
return 0
}
// We zero in on the node that contains the target position by visiting each
// child and JSDoc comment of the current node. Node children are walked in
// order, while node lists are binary searched.
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()
}
case 0:
next = node
}
}
return node
}
visitNodeList := func(nodeList *ast.NodeList, _ *ast.NodeVisitor) *ast.NodeList {
if nodeList != nil && len(nodeList.Nodes) > 0 && next == nil {
if nodeList.End() == position && includePrecedingTokenAtEndPosition != nil {
left = nodeList.End()
prevSubtree = nodeList.Nodes[len(nodeList.Nodes)-1]
} else if nodeList.End() <= position {
left = nodeList.End()
} else if nodeList.Pos() <= position {
nodes := nodeList.Nodes
index, match := core.BinarySearchUniqueFunc(nodes, func(middle int, node *ast.Node) int {
if node.Flags&ast.NodeFlagsReparsed != 0 {
return 0
}
cmp := testNode(node)
if cmp < 0 {
left = node.End()
}
return cmp
})
if match && nodes[index].Flags&ast.NodeFlagsReparsed != 0 {
// filter and search again
nodes = core.Filter(nodes, func(node *ast.Node) bool {
return node.Flags&ast.NodeFlagsReparsed == 0
})
index, match = core.BinarySearchUniqueFunc(nodes, func(middle int, node *ast.Node) int {
cmp := testNode(node)
if cmp < 0 {
left = node.End()
}
return cmp
})
}
if match {
next = nodes[index]
}
}
}
return nodeList
}
for {
VisitEachChildAndJSDoc(current, sourceFile, visitNode, visitNodeList)
// If prevSubtree was set on the last iteration, it ends at the target position.
// Check if the rightmost token of prevSubtree should be returned based on the
// `includePrecedingTokenAtEndPosition` callback.
if prevSubtree != nil {
child := FindPrecedingTokenEx(sourceFile, position, prevSubtree, false /*excludeJSDoc*/)
if child != nil && child.End() == position && includePrecedingTokenAtEndPosition(child) {
// Optimization: includePrecedingTokenAtEndPosition only ever returns true
// for real AST nodes, so we don't run the scanner here.
return child
}
prevSubtree = nil
}
// No node was found that contains the target position, so we've gone as deep as
// we can in the AST. We've either found a token, or we need to run the scanner
// to construct one that isn't stored in the AST.
if next == nil {
if ast.IsTokenKind(current.Kind) || shouldSkipChild(current) {
return current
}
scanner := scanner.GetScannerForSourceFile(sourceFile, left)
for left < current.End() {
token := scanner.Token()
tokenFullStart := scanner.TokenFullStart()
tokenStart := core.IfElse(allowPositionInLeadingTrivia, tokenFullStart, scanner.TokenStart())
tokenEnd := scanner.TokenEnd()
if tokenStart <= position && (position < tokenEnd) {
if token == ast.KindIdentifier || !ast.IsTokenKind(token) {
if ast.IsJSDocKind(current.Kind) {
return current
}
panic(fmt.Sprintf("did not expect %s to have %s in its trivia", current.Kind.String(), token.String()))
}
return sourceFile.GetOrCreateToken(token, tokenFullStart, tokenEnd, current)
}
if includePrecedingTokenAtEndPosition != nil && tokenEnd == position {
prevToken := sourceFile.GetOrCreateToken(token, tokenFullStart, tokenEnd, current)
if includePrecedingTokenAtEndPosition(prevToken) {
return prevToken
}
}
left = tokenEnd
scanner.Scan()
}
return current
}
current = next
left = current.Pos()
next = nil
}
}
func getPosition(node *ast.Node, sourceFile *ast.SourceFile, allowPositionInLeadingTrivia bool) int {
if allowPositionInLeadingTrivia {
return node.Pos()
}
return scanner.GetTokenPosOfNode(node, sourceFile, true /*includeJSDoc*/)
}
func findRightmostNode(node *ast.Node) *ast.Node {
var next *ast.Node
current := node
visitNode := func(node *ast.Node, _ *ast.NodeVisitor) *ast.Node {
if node != nil {
next = node
}
return node
}
visitNodes := func(nodeList *ast.NodeList, visitor *ast.NodeVisitor) *ast.NodeList {
if nodeList != nil {
if rightmost := ast.FindLastVisibleNode(nodeList.Nodes); rightmost != nil {
next = rightmost
}
}
return nodeList
}
visitor := getNodeVisitor(visitNode, visitNodes)
for {
current.VisitEachChild(visitor)
if next == nil {
return current
}
current = next
next = nil
}
}
func VisitEachChildAndJSDoc(
node *ast.Node,
sourceFile *ast.SourceFile,
visitNode func(*ast.Node, *ast.NodeVisitor) *ast.Node,
visitNodes func(*ast.NodeList, *ast.NodeVisitor) *ast.NodeList,
) {
visitor := getNodeVisitor(visitNode, visitNodes)
if node.Flags&ast.NodeFlagsHasJSDoc != 0 {
for _, jsdoc := range node.JSDoc(sourceFile) {
if visitor.Hooks.VisitNode != nil {
visitor.Hooks.VisitNode(jsdoc, visitor)
} else {
visitor.VisitNode(jsdoc)
}
}
}
node.VisitEachChild(visitor)
}
const (
comparisonLessThan = -1
comparisonEqualTo = 0
comparisonGreaterThan = 1
)
// Finds the leftmost token satisfying `position < token.End()`.
// If the leftmost token satisfying `position < token.End()` is invalid, or if position
// is in the trivia of that leftmost token,
// we will find the rightmost valid token with `token.End() <= position`.
func FindPrecedingToken(sourceFile *ast.SourceFile, position int) *ast.Node {
return FindPrecedingTokenEx(sourceFile, position, nil, false)
}
func FindPrecedingTokenEx(sourceFile *ast.SourceFile, position int, startNode *ast.Node, excludeJSDoc bool) *ast.Node {
var find func(node *ast.Node) *ast.Node
find = func(n *ast.Node) *ast.Node {
if ast.IsNonWhitespaceToken(n) && n.Kind != ast.KindEndOfFile {
return n
}
// `foundChild` is the leftmost node that contains the target position.
// `prevChild` is the last visited child of the current node.
var foundChild, prevChild *ast.Node
visitNode := func(node *ast.Node, _ *ast.NodeVisitor) *ast.Node {
// skip synthesized nodes (that will exist now because of jsdoc handling)
if node == nil || node.Flags&ast.NodeFlagsReparsed != 0 {
return node
}
if foundChild != nil { // We cannot abort visiting children, so once the desired child is found, we do nothing.
return node
}
if position < node.End() && (prevChild == nil || prevChild.End() <= position) {
foundChild = node
} else {
prevChild = node
}
return node
}
visitNodes := func(nodeList *ast.NodeList, _ *ast.NodeVisitor) *ast.NodeList {
if foundChild != nil {
return nodeList
}
if nodeList != nil && len(nodeList.Nodes) > 0 {
nodes := nodeList.Nodes
index, match := core.BinarySearchUniqueFunc(nodes, func(middle int, _ *ast.Node) int {
// synthetic jsdoc nodes should have jsdocNode.End() <= n.Pos()
if nodes[middle].Flags&ast.NodeFlagsReparsed != 0 {
return comparisonLessThan
}
if position < nodes[middle].End() {
if middle == 0 || position >= nodes[middle-1].End() {
return comparisonEqualTo
}
return comparisonGreaterThan
}
return comparisonLessThan
})
if match {
foundChild = nodes[index]
}
validLookupIndex := core.IfElse(match, index-1, len(nodes)-1)
for i := validLookupIndex; i >= 0; i-- {
if nodes[i].Flags&ast.NodeFlagsReparsed != 0 {
continue
}
if prevChild == nil {
prevChild = nodes[i]
}
}
}
return nodeList
}
VisitEachChildAndJSDoc(n, sourceFile, visitNode, visitNodes)
if foundChild != nil {
// Note that the span of a node's tokens is [getStartOfNode(node, ...), node.end).
// Given that `position < child.end` and child has constituent tokens, we distinguish these cases:
// 1) `position` precedes `child`'s tokens or `child` has no tokens (ie: in a comment or whitespace preceding `child`):
// we need to find the last token in a previous child node or child tokens.
// 2) `position` is within the same span: we recurse on `child`.
start := GetStartOfNode(foundChild, sourceFile, !excludeJSDoc /*includeJSDoc*/)
lookInPreviousChild := start >= position || // cursor in the leading trivia or preceding tokens
!isValidPrecedingNode(foundChild, sourceFile)
if lookInPreviousChild {
if position >= foundChild.Pos() {
// Find jsdoc preceding the foundChild.
var jsDoc *ast.Node
nodeJSDoc := n.JSDoc(sourceFile)
for i := len(nodeJSDoc) - 1; i >= 0; i-- {
if nodeJSDoc[i].Pos() >= foundChild.Pos() {
jsDoc = nodeJSDoc[i]
break
}
}
if jsDoc != nil {
if !excludeJSDoc {
return find(jsDoc)
} else {
return findRightmostValidToken(jsDoc.End(), sourceFile, n, position, excludeJSDoc)
}
}
return findRightmostValidToken(foundChild.Pos(), sourceFile, n, -1 /*position*/, excludeJSDoc)
} else { // Answer is in tokens between two visited children.
return findRightmostValidToken(foundChild.Pos(), sourceFile, n, position, excludeJSDoc)
}
} else {
// position is in [foundChild.getStart(), foundChild.End): recur.
return find(foundChild)
}
}
// We have two cases here: either the position is at the end of the file,
// or the desired token is in the unvisited trailing tokens of the current node.
if position >= n.End() {
return findRightmostValidToken(n.End(), sourceFile, n, -1 /*position*/, excludeJSDoc)
} else {
return findRightmostValidToken(n.End(), sourceFile, n, position, excludeJSDoc)
}
}
var node *ast.Node
if startNode != nil {
node = startNode
} else {
node = sourceFile.AsNode()
}
result := find(node)
if result != nil && ast.IsWhitespaceOnlyJsxText(result) {
panic("Expected result to be a non-whitespace token.")
}
return result
}
func isValidPrecedingNode(node *ast.Node, sourceFile *ast.SourceFile) bool {
start := GetStartOfNode(node, sourceFile, false /*includeJSDoc*/)
width := node.End() - start
return !(ast.IsWhitespaceOnlyJsxText(node) || width == 0)
}
func GetStartOfNode(node *ast.Node, file *ast.SourceFile, includeJSDoc bool) int {
return scanner.GetTokenPosOfNode(node, file, includeJSDoc)
}
// Looks for rightmost valid token in the range [startPos, endPos).
// If position is >= 0, looks for rightmost valid token that precedes or touches that position.
func findRightmostValidToken(endPos int, sourceFile *ast.SourceFile, containingNode *ast.Node, position int, excludeJSDoc bool) *ast.Node {
if position == -1 {
position = containingNode.End()
}
var find func(n *ast.Node, endPos int) *ast.Node
find = func(n *ast.Node, endPos int) *ast.Node {
if n == nil {
return nil
}
if ast.IsNonWhitespaceToken(n) {
return n
}
var rightmostValidNode *ast.Node
rightmostVisitedNodes := make([]*ast.Node, 0, 1) // Nodes after the last valid node.
hasChildren := false
shouldVisitNode := func(node *ast.Node) bool {
// Node is synthetic or out of the desired range: don't visit it.
return !(node.Flags&ast.NodeFlagsReparsed != 0 ||
node.End() > endPos || GetStartOfNode(node, sourceFile, !excludeJSDoc /*includeJSDoc*/) >= position)
}
visitNode := func(node *ast.Node, _ *ast.NodeVisitor) *ast.Node {
if node == nil {
return node
}
hasChildren = true
if !shouldVisitNode(node) {
return node
}
rightmostVisitedNodes = append(rightmostVisitedNodes, node)
if isValidPrecedingNode(node, sourceFile) {
rightmostValidNode = node
rightmostVisitedNodes = rightmostVisitedNodes[:0]
}
return node
}
visitNodes := func(nodeList *ast.NodeList, _ *ast.NodeVisitor) *ast.NodeList {
if nodeList != nil && len(nodeList.Nodes) > 0 {
hasChildren = true
index, _ := core.BinarySearchUniqueFunc(nodeList.Nodes, func(middle int, node *ast.Node) int {
if node.End() > endPos {
return comparisonGreaterThan
}
return comparisonLessThan
})
validIndex := -1
for i := index - 1; i >= 0; i-- {
if !shouldVisitNode(nodeList.Nodes[i]) {
continue
}
if isValidPrecedingNode(nodeList.Nodes[i], sourceFile) {
validIndex = i
rightmostValidNode = nodeList.Nodes[i]
break
}
}
for i := validIndex + 1; i < index; i++ {
if !shouldVisitNode(nodeList.Nodes[i]) {
continue
}
rightmostVisitedNodes = append(rightmostVisitedNodes, nodeList.Nodes[i])
}
}
return nodeList
}
VisitEachChildAndJSDoc(n, sourceFile, visitNode, visitNodes)
// Three cases:
// 1. The answer is a token of `rightmostValidNode`.
// 2. The answer is one of the unvisited tokens that occur after the rightmost valid node.
// 3. The current node is a childless, token-less node. The answer is the current node.
// Case 2: Look at unvisited trailing tokens that occur in between the rightmost visited nodes.
if !shouldSkipChild(n) { // JSDoc nodes don't include trivia tokens as children.
var startPos int
if rightmostValidNode != nil {
startPos = rightmostValidNode.End()
} else {
startPos = n.Pos()
}
scanner := scanner.GetScannerForSourceFile(sourceFile, startPos)
var tokens []*ast.Node
for _, visitedNode := range rightmostVisitedNodes {
// Trailing tokens that occur before this node.
for startPos < min(visitedNode.Pos(), position) {
tokenStart := scanner.TokenStart()
if tokenStart >= position {
break
}
token := scanner.Token()
tokenFullStart := scanner.TokenFullStart()
tokenEnd := scanner.TokenEnd()
startPos = tokenEnd
tokens = append(tokens, sourceFile.GetOrCreateToken(token, tokenFullStart, tokenEnd, n))
scanner.Scan()
}
startPos = visitedNode.End()
scanner.ResetPos(startPos)
scanner.Scan()
}
// Trailing tokens after last visited node.
for startPos < min(endPos, position) {
tokenStart := scanner.TokenStart()
if tokenStart >= position {
break
}
token := scanner.Token()
tokenFullStart := scanner.TokenFullStart()
tokenEnd := scanner.TokenEnd()
startPos = tokenEnd
tokens = append(tokens, sourceFile.GetOrCreateToken(token, tokenFullStart, tokenEnd, n))
scanner.Scan()
}
lastToken := len(tokens) - 1
// Find preceding valid token.
for i := lastToken; i >= 0; i-- {
if !ast.IsWhitespaceOnlyJsxText(tokens[i]) {
return tokens[i]
}
}
}
// Case 3: childless node.
if !hasChildren {
if n != containingNode {
return n
}
return nil
}
// Case 1: recur on rightmostValidNode.
if rightmostValidNode != nil {
endPos = rightmostValidNode.End()
}
return find(rightmostValidNode, endPos)
}
return find(containingNode, endPos)
}
func FindNextToken(previousToken *ast.Node, parent *ast.Node, file *ast.SourceFile) *ast.Node {
var find func(n *ast.Node) *ast.Node
find = func(n *ast.Node) *ast.Node {
if ast.IsTokenKind(n.Kind) && n.Pos() == previousToken.End() {
// this is token that starts at the end of previous token - return it
return n
}
// Node that contains `previousToken` or occurs immediately after it.
var foundNode *ast.Node
visitNode := func(node *ast.Node, _ *ast.NodeVisitor) *ast.Node {
if node != nil && node.Flags&ast.NodeFlagsReparsed == 0 &&
node.Pos() <= previousToken.End() && node.End() > previousToken.End() {
foundNode = node
}
return node
}
visitNodes := func(nodeList *ast.NodeList, _ *ast.NodeVisitor) *ast.NodeList {
if nodeList != nil && len(nodeList.Nodes) > 0 && foundNode == nil {
nodes := nodeList.Nodes
index, match := core.BinarySearchUniqueFunc(nodes, func(_ int, node *ast.Node) int {
if node.Flags&ast.NodeFlagsReparsed != 0 {
return comparisonLessThan
}
if node.Pos() > previousToken.End() {
return comparisonGreaterThan
}
if node.End() <= previousToken.Pos() {
return comparisonLessThan
}
return comparisonEqualTo
})
if match {
foundNode = nodes[index]
}
}
return nodeList
}
VisitEachChildAndJSDoc(n, file, visitNode, visitNodes)
// Cases:
// 1. no answer exists
// 2. answer is an unvisited token
// 3. answer is in the visited found node
// Case 3: look for the next token inside the found node.
if foundNode != nil {
return find(foundNode)
}
startPos := previousToken.End()
// Case 2: look for the next token directly.
if startPos >= n.Pos() && startPos < n.End() {
scanner := scanner.GetScannerForSourceFile(file, startPos)
token := scanner.Token()
tokenFullStart := scanner.TokenFullStart()
tokenStart := scanner.TokenStart()
tokenEnd := scanner.TokenEnd()
if tokenStart == previousToken.End() {
return file.GetOrCreateToken(token, tokenFullStart, tokenEnd, n)
}
panic(fmt.Sprintf("Expected to find next token at %d, got token %s at %d", previousToken.End(), token, tokenStart))
}
// Case 3: no answer.
return nil
}
return find(parent)
}
func getNodeVisitor(
visitNode func(*ast.Node, *ast.NodeVisitor) *ast.Node,
visitNodes func(*ast.NodeList, *ast.NodeVisitor) *ast.NodeList,
) *ast.NodeVisitor {
var wrappedVisitNode func(*ast.Node, *ast.NodeVisitor) *ast.Node
var wrappedVisitNodes func(*ast.NodeList, *ast.NodeVisitor) *ast.NodeList
if visitNode != nil {
wrappedVisitNode = func(n *ast.Node, v *ast.NodeVisitor) *ast.Node {
if ast.IsJSDocSingleCommentNodeComment(n) {
return n
}
return visitNode(n, v)
}
}
if visitNodes != nil {
wrappedVisitNodes = func(n *ast.NodeList, v *ast.NodeVisitor) *ast.NodeList {
if ast.IsJSDocSingleCommentNodeList(n) {
return n
}
return visitNodes(n, v)
}
}
return ast.NewNodeVisitor(core.Identity, nil, ast.NodeVisitorHooks{
VisitNode: wrappedVisitNode,
VisitToken: wrappedVisitNode,
VisitNodes: wrappedVisitNodes,
VisitModifiers: func(modifiers *ast.ModifierList, visitor *ast.NodeVisitor) *ast.ModifierList {
if modifiers != nil {
wrappedVisitNodes(&modifiers.NodeList, visitor)
}
return modifiers
},
})
}
func shouldSkipChild(node *ast.Node) bool {
return node.Kind == ast.KindJSDoc ||
node.Kind == ast.KindJSDocText ||
node.Kind == ast.KindJSDocTypeLiteral ||
node.Kind == ast.KindJSDocSignature ||
ast.IsJSDocLinkLike(node) ||
ast.IsJSDocTag(node)
}