Files
filepuff-mcp/internal/parser/ast.go
T
lukaszraczylo 98a499ab20 fix(parser): guard FindNodeAtPosition against position underflow
line/col are 1-indexed and converted via uint32(line-1); a caller passing 0
wrapped to 4294967295, feeding a garbage Point into tree-sitter. Reachable from
the LSP-down AST fallback with caller-supplied coordinates. Reject non-positive
positions up front. Adds tests (the function was previously 0% covered).
2026-06-21 02:01:50 +01:00

199 lines
4.5 KiB
Go

package parser
import (
"github.com/lukaszraczylo/mcp-filepuff/pkg/protocol"
sitter "github.com/smacker/go-tree-sitter"
)
// FindNodeAtPosition finds the node at the given line and column.
func FindNodeAtPosition(tree *sitter.Tree, line, col int) *sitter.Node {
if tree == nil {
return nil
}
root := tree.RootNode()
if root == nil {
return nil
}
// Positions are 1-indexed; reject non-positive values so the uint32
// conversion below can't underflow to a huge Row/Column (line/col == 0
// would wrap to 4294967295). Reachable from the LSP-down AST fallback with
// caller-supplied coordinates.
if line < 1 || col < 1 {
return nil
}
// Convert to 0-indexed
point := sitter.Point{
Row: uint32(line - 1), // #nosec G115 - line numbers are bounded by file size
Column: uint32(col - 1), // #nosec G115 - column numbers are bounded by line length
}
return findNodeAtPoint(root, point)
}
// findNodeAtPoint recursively finds the smallest node containing the point.
func findNodeAtPoint(node *sitter.Node, point sitter.Point) *sitter.Node {
if node == nil {
return nil
}
startPoint := node.StartPoint()
endPoint := node.EndPoint()
// Check if point is within this node
if !pointInRange(point, startPoint, endPoint) {
return nil
}
// Try to find a more specific child node
for i := 0; i < int(node.ChildCount()); i++ {
child := node.Child(i)
if child == nil {
continue
}
if result := findNodeAtPoint(child, point); result != nil {
return result
}
}
// No child contains the point, return this node
return node
}
// pointInRange checks if a point is within a range.
func pointInRange(point, start, end sitter.Point) bool {
// Before start?
if point.Row < start.Row || (point.Row == start.Row && point.Column < start.Column) {
return false
}
// After end?
if point.Row > end.Row || (point.Row == end.Row && point.Column >= end.Column) {
return false
}
return true
}
// FindParentOfKind finds the nearest ancestor of the given node type.
func FindParentOfKind(node *sitter.Node, kind string) *sitter.Node {
if node == nil {
return nil
}
current := node.Parent()
for current != nil {
if current.Type() == kind {
return current
}
current = current.Parent()
}
return nil
}
// GetNodeText returns the text content of a node.
func GetNodeText(node *sitter.Node, content []byte) string {
if node == nil {
return ""
}
start := node.StartByte()
end := node.EndByte()
if int(start) >= len(content) || int(end) > len(content) {
return ""
}
return string(content[start:end])
}
// WalkTree walks the tree calling fn for each node.
// If fn returns false, the walk stops.
func WalkTree(node *sitter.Node, fn func(*sitter.Node) bool) {
if node == nil {
return
}
if !fn(node) {
return
}
for i := 0; i < int(node.ChildCount()); i++ {
WalkTree(node.Child(i), fn)
}
}
// FindNodesByKind finds all nodes of a given kind.
func FindNodesByKind(root *sitter.Node, kind string) []*sitter.Node {
var nodes []*sitter.Node
WalkTree(root, func(n *sitter.Node) bool {
if n.Type() == kind {
nodes = append(nodes, n)
}
return true
})
return nodes
}
// FindNamedChildren returns all named (non-anonymous) children of a node.
func FindNamedChildren(node *sitter.Node) []*sitter.Node {
if node == nil {
return nil
}
var children []*sitter.Node
for i := 0; i < int(node.NamedChildCount()); i++ {
if child := node.NamedChild(i); child != nil {
children = append(children, child)
}
}
return children
}
// GetChildByFieldName returns the child node with the given field name.
func GetChildByFieldName(node *sitter.Node, fieldName string) *sitter.Node {
if node == nil {
return nil
}
return node.ChildByFieldName(fieldName)
}
// NodeLocation returns the location of a node.
func NodeLocation(node *sitter.Node, filename string) protocol.Location {
if node == nil {
return protocol.Location{}
}
startPoint := node.StartPoint()
return protocol.Location{
File: filename,
Line: int(startPoint.Row) + 1,
Column: int(startPoint.Column) + 1,
}
}
// NodeRange returns the range of a node.
func NodeRange(node *sitter.Node, filename string) protocol.Range {
if node == nil {
return protocol.Range{}
}
startPoint := node.StartPoint()
endPoint := node.EndPoint()
return protocol.Range{
Start: protocol.Location{
File: filename,
Line: int(startPoint.Row) + 1,
Column: int(startPoint.Column) + 1,
},
End: protocol.Location{
File: filename,
Line: int(endPoint.Row) + 1,
Column: int(endPoint.Column) + 1,
},
}
}