a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
174 lines
4.7 KiB
Go
174 lines
4.7 KiB
Go
package parser
|
|
|
|
import (
|
|
"fmt"
|
|
"sync/atomic"
|
|
|
|
sitter "github.com/zzet/gortex/internal/parser/tsitter"
|
|
)
|
|
|
|
// ParseTree is a ref-counted handle to a tree-sitter parse tree plus
|
|
// the source bytes the tree was parsed from. The language extractor
|
|
// produces it; the indexer hands it to contract extractors and the
|
|
// post-pass resolvers and closes it (decrementing refs) when each
|
|
// consumer is done.
|
|
//
|
|
// Lifetime: a ParseTree starts with refs=1. Every consumer that
|
|
// retains the handle (e.g. caches it across calls) must call Acquire
|
|
// to bump the count and Release to drop it. Single-shot consumers
|
|
// that read and return don't need to touch the count — the producer's
|
|
// close balances the producer's create.
|
|
//
|
|
// ParseTree is safe for concurrent reads (tree-sitter trees are
|
|
// immutable after parse) but not for concurrent close. Acquire/Release
|
|
// are atomic; closing happens at most once when refs reach 0.
|
|
type ParseTree struct {
|
|
tree *sitter.Tree
|
|
src []byte
|
|
lang string
|
|
refs atomic.Int32
|
|
}
|
|
|
|
// NewParseTree wraps an already-parsed *sitter.Tree with the source
|
|
// bytes and language code. The returned handle starts with refs=1;
|
|
// the producer must Close (or Release) it once.
|
|
func NewParseTree(tree *sitter.Tree, src []byte, lang string) *ParseTree {
|
|
pt := &ParseTree{tree: tree, src: src, lang: lang}
|
|
pt.refs.Store(1)
|
|
return pt
|
|
}
|
|
|
|
// Tree returns the underlying tree. Returns nil if the ParseTree has
|
|
// been closed.
|
|
func (pt *ParseTree) Tree() *sitter.Tree {
|
|
if pt == nil {
|
|
return nil
|
|
}
|
|
return pt.tree
|
|
}
|
|
|
|
// Source returns the source bytes the tree was parsed from. Same
|
|
// slice the extractor was handed; do not mutate.
|
|
func (pt *ParseTree) Source() []byte {
|
|
if pt == nil {
|
|
return nil
|
|
}
|
|
return pt.src
|
|
}
|
|
|
|
// Lang returns the language code ("go", "typescript", …).
|
|
func (pt *ParseTree) Lang() string {
|
|
if pt == nil {
|
|
return ""
|
|
}
|
|
return pt.lang
|
|
}
|
|
|
|
// Acquire bumps the ref count. Pair every Acquire with a Release.
|
|
func (pt *ParseTree) Acquire() {
|
|
if pt == nil {
|
|
return
|
|
}
|
|
pt.refs.Add(1)
|
|
}
|
|
|
|
// Release decrements the ref count and closes the underlying tree
|
|
// when it reaches zero. Safe to call on nil.
|
|
func (pt *ParseTree) Release() {
|
|
if pt == nil {
|
|
return
|
|
}
|
|
if pt.refs.Add(-1) <= 0 {
|
|
if pt.tree != nil {
|
|
pt.tree.Close()
|
|
pt.tree = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// Close is an alias for Release that lets ParseTree satisfy a
|
|
// generic io.Closer-style contract for defer convenience.
|
|
func (pt *ParseTree) Close() {
|
|
pt.Release()
|
|
}
|
|
|
|
// HasParseErrors reports whether the underlying tree contains any
|
|
// ERROR or MISSING nodes. Returns false for a nil ParseTree so
|
|
// language extractors that drop their tree (most non-Go languages)
|
|
// don't appear to have parse errors when the signal isn't available.
|
|
func (pt *ParseTree) HasParseErrors() bool {
|
|
if pt == nil || pt.tree == nil {
|
|
return false
|
|
}
|
|
root := pt.tree.RootNode()
|
|
if root == nil {
|
|
return false
|
|
}
|
|
return root.HasError()
|
|
}
|
|
|
|
// CountParseErrors returns the number of ERROR or MISSING nodes in
|
|
// the tree. Useful for stamping a quantitative parse_errors metric
|
|
// on a KindFile node so index_health can rank the worst offenders.
|
|
// Returns 0 when the ParseTree is nil.
|
|
func (pt *ParseTree) CountParseErrors() int {
|
|
if pt == nil || pt.tree == nil {
|
|
return 0
|
|
}
|
|
root := pt.tree.RootNode()
|
|
if root == nil {
|
|
return 0
|
|
}
|
|
count := 0
|
|
walkParseErrors(root, &count)
|
|
return count
|
|
}
|
|
|
|
func walkParseErrors(n *sitter.Node, count *int) {
|
|
if n == nil {
|
|
return
|
|
}
|
|
if n.Type() == "ERROR" || n.IsMissing() {
|
|
*count++
|
|
}
|
|
childCount := int(n.ChildCount())
|
|
for i := 0; i < childCount; i++ {
|
|
walkParseErrors(n.Child(i), count)
|
|
}
|
|
}
|
|
|
|
// parseErrorLocationCap bounds the number of error locations collected so a
|
|
// pathologically broken file can't produce a multi-kilobyte errors blob.
|
|
const parseErrorLocationCap = 50
|
|
|
|
// ParseErrorLocations returns the 1-based "row:col" location of each ERROR /
|
|
// MISSING node, capped at parseErrorLocationCap. Returns nil when the tree is
|
|
// nil or clean — the per-file metadata sidecar stores these as a JSON array
|
|
// so index_health can point at where a file failed to parse.
|
|
func (pt *ParseTree) ParseErrorLocations() []string {
|
|
if pt == nil || pt.tree == nil {
|
|
return nil
|
|
}
|
|
root := pt.tree.RootNode()
|
|
if root == nil {
|
|
return nil
|
|
}
|
|
var locs []string
|
|
walkParseErrorLocations(root, &locs)
|
|
return locs
|
|
}
|
|
|
|
func walkParseErrorLocations(n *sitter.Node, locs *[]string) {
|
|
if n == nil || len(*locs) >= parseErrorLocationCap {
|
|
return
|
|
}
|
|
if n.Type() == "ERROR" || n.IsMissing() {
|
|
p := n.StartPoint()
|
|
*locs = append(*locs, fmt.Sprintf("%d:%d", p.Row+1, p.Column+1))
|
|
}
|
|
childCount := int(n.ChildCount())
|
|
for i := 0; i < childCount && len(*locs) < parseErrorLocationCap; i++ {
|
|
walkParseErrorLocations(n.Child(i), locs)
|
|
}
|
|
}
|