项目文件夹

文件
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

607 行
20 KiB
Go

// Package tsitter is a thin compatibility shim over
// github.com/tree-sitter/go-tree-sitter. Its surface intentionally
// mirrors the smacker/go-tree-sitter API so that the ~90 language
// extractors in gortex can be migrated by changing only import paths —
// method names and signatures stay the same.
//
// The shim wraps native tree-sitter types (Node, Tree, Parser, Query)
// and adapts:
// - Type() → Kind()
// - Content(src) → Utf8Text(src)
// - StartPoint/EndPoint → StartPosition/EndPosition (uint32 Row/Column)
// - int-indexed children → uint-indexed (internally converted)
// - ParseCtx(ctx, old, src) built on top of ParseWithOptions
//
// The cursor-based query iteration is not re-exposed here: it lives in
// the parent parser package, which uses the official API directly.
package tsitter
import (
"context"
"errors"
"fmt"
"iter"
"sync"
"unsafe"
ts "github.com/tree-sitter/go-tree-sitter"
)
// Language is a parser language. Exposed as an alias so grammar
// sub-packages can return *ts.Language directly.
type Language = ts.Language
// NewLanguage constructs a Language from a grammar's raw C pointer —
// used by the per-language shim sub-packages.
func NewLanguage(ptr unsafe.Pointer) *Language { return ts.NewLanguage(ptr) }
// Point mirrors the smacker Point layout (uint32 row/column).
type Point struct {
Row uint32
Column uint32
}
func fromTSPoint(p ts.Point) Point {
return Point{Row: uint32(p.Row), Column: uint32(p.Column)}
}
// Node wraps *ts.Node with smacker-compatible method names. Nodes are
// valid for the lifetime of their Tree; copying by value is cheap
// (single C struct field).
type Node struct {
inner ts.Node
// set true when constructed; distinguishes the zero value from a real node.
valid bool
// langKey is the stable C-pointer identity of this node's language,
// propagated from the root through every navigation step so Type()
// can resolve the node kind from a per-language table with no CGO
// call and no allocation. Nil means "not stamped" — Type() then
// derives the language, which is slower but still correct.
langKey unsafe.Pointer
// arena bump-allocates Node wrappers in chunks so a deep tree walk
// produces a few large backing arrays instead of millions of tiny heap
// objects. The per-object GC mark cost dominated CPU when indexing a
// large TS monorepo (vscode: ~70% of cycles in GC, scanning millions of
// 1-node spans). Chunks are never explicitly freed — they are reachable
// only through the Nodes that point into them and are reclaimed by the
// GC once the tree's nodes are dropped. A nil arena falls back to a plain
// heap Node (zero-value and SetInner-pooled nodes have no arena).
arena *nodeArena
}
// nodeArena is a per-tree bump allocator for Node wrappers. It is not
// safe for concurrent use; each parse tree is walked by a single
// goroutine, and distinct files use distinct trees (and arenas).
//
// Arenas are pooled and reused across files (see arenaPool): a Tree takes
// one on its first RootNode() and returns it on Close(). reset() rewinds
// the allocation cursor but RETAINS the backing chunks, so a warm pool
// serves each file's node count with no fresh allocation. This is the
// dominant GC-pressure lever on a large index: profiling vscode and
// kubernetes put tree-sitter Node wrappers at 70–82% of every byte
// allocated, almost all of it per-file chunk garbage. Retaining the chunks
// turns that churn into a bounded, reused working set.
type nodeArena struct {
chunks [][]Node // backing arrays, retained across resets for reuse
ci int // index of the current chunk within chunks
used int // slots used in chunks[ci]
}
const (
// arenaFirstChunk keeps the first backing array small so a file with a
// handful of nodes (the common case in a many-small-files repo) wastes
// little; chunks then double up to arenaMaxChunk so a deep file still
// ends up with only a few large objects.
arenaFirstChunk = 64
arenaMaxChunk = 4096
)
func newNodeArena() *nodeArena { return &nodeArena{} }
// alloc returns a pointer to a fresh Node. The pointer is stable for the
// life of the arena: a chunk is never resized in place — when the current
// chunk fills, allocation advances to the next retained chunk, or appends a
// geometrically larger one past the high-water mark, so earlier &chunk[i]
// pointers never move. Callers overwrite all fields of the returned Node
// immediately, so a reused slot needs no zeroing here; reset() clears stale
// slots when the arena is recycled.
func (a *nodeArena) alloc() *Node {
switch {
case len(a.chunks) == 0:
a.chunks = append(a.chunks, make([]Node, arenaFirstChunk))
a.ci, a.used = 0, 0
case a.used >= len(a.chunks[a.ci]):
a.ci++
if a.ci >= len(a.chunks) {
size := len(a.chunks[a.ci-1]) * 2
if size > arenaMaxChunk {
size = arenaMaxChunk
}
a.chunks = append(a.chunks, make([]Node, size))
}
a.used = 0
}
n := &a.chunks[a.ci][a.used]
a.used++
return n
}
// reset rewinds the allocation cursor to the start while retaining the
// backing chunks for reuse. It clears the slots touched since the last
// reset so a stale Node value — whose embedded ts.Node pins its now-closed
// *ts.Tree — cannot survive into the next file and leak. Clearing only the
// used prefix keeps the cost proportional to the file just processed, not
// the high-water capacity.
func (a *nodeArena) reset() {
for i := 0; i <= a.ci && i < len(a.chunks); i++ {
end := len(a.chunks[i])
if i == a.ci {
end = a.used
}
clear(a.chunks[i][:end])
}
a.ci, a.used = 0, 0
}
// arenaPool recycles per-tree arenas — with their retained chunks — across
// files. A Tree gets one lazily on its first RootNode() and returns it on
// Close(). sync.Pool may drop entries under GC pressure; a cold Get then
// just starts with no chunks and warms up again, so correctness never
// depends on retention.
var arenaPool = sync.Pool{New: func() any { return &nodeArena{} }}
func getArena() *nodeArena { return arenaPool.Get().(*nodeArena) }
func putArena(a *nodeArena) {
if a == nil {
return
}
a.reset()
arenaPool.Put(a)
}
// WrapNode wraps a value Node from the new API into our shim. It derives
// the language key eagerly so navigation from the result stays alloc-free,
// and seeds a fresh arena so the subtree walk below it allocates in chunks.
func WrapNode(n ts.Node) *Node {
a := newNodeArena()
nn := a.alloc()
nn.inner = n
nn.valid = true
nn.langKey = unsafe.Pointer(n.Language().Inner)
nn.arena = a
return nn
}
// WrapVal wraps a ts.Node reached from n (e.g. a query capture),
// carrying n's language key so Type() on the result and its descendants
// needs neither CGO nor allocation.
func (n *Node) WrapVal(c ts.Node) *Node {
if n.arena == nil {
return &Node{inner: c, valid: true, langKey: n.langKey}
}
nn := n.arena.alloc()
nn.inner = c
nn.valid = true
nn.langKey = n.langKey
nn.arena = n.arena
return nn
}
// SetInner overwrites the receiver's wrapped ts.Node and marks it
// valid. Lets callers reuse a *Node out of a pool / backing slice
// instead of allocating a new one per query match — see EachMatch in
// internal/parser/treesitter.go. The receiver must already exist
// (caller-owned), so SetInner cannot be used on a nil pointer.
func (n *Node) SetInner(inner ts.Node) {
n.inner = inner
n.valid = true
}
// Inner returns a pointer to the underlying ts.Node. Internal use by
// the parser package's query runners.
func (n *Node) Inner() *ts.Node {
if n == nil || !n.valid {
return nil
}
return &n.inner
}
// Type returns the node kind string ("identifier", "function_declaration", …).
func (n *Node) Type() string { return internedKind(n.inner, n.langKey) }
// kindTables memoises per-language node-kind name tables. tree-sitter
// node kinds are a small fixed set (NodeKindCount — typically <300),
// but ts.Node.Kind() crosses CGO and allocates a fresh Go string on
// every call; a single index walks millions of nodes, and profiling
// put node-kind GoString conversions at ~22% of all allocations. The
// table turns Type() into a slice index — zero CGO, zero allocation —
// after a one-time build per language.
//
// Keyed by the C TSLanguage pointer (stable per registered grammar);
// sync.Map because indexing runs many languages concurrently.
var kindTables sync.Map // unsafe.Pointer(*C.TSLanguage) -> []string
// internedKind returns a node's type name from the per-language table,
// building it on first use. Equivalent to ts.Node.Kind() because
// ts_node_type is itself ts_language_symbol_name(language, symbol).
// Falls back to the allocating Kind() only for an out-of-range symbol
// id, which a well-formed grammar never produces.
func internedKind(n ts.Node, key unsafe.Pointer) string {
// key is the node's language identity, stamped at wrap time. A nil
// key means the node was built on a path that didn't stamp it —
// derive it the slow way so Type() still returns the right answer.
if key == nil {
key = unsafe.Pointer(n.Language().Inner)
}
id := n.KindId()
v, ok := kindTables.Load(key)
if !ok {
lang := n.Language()
cnt := lang.NodeKindCount()
names := make([]string, cnt)
for i := uint32(0); i < cnt; i++ {
names[i] = lang.NodeKindForId(uint16(i))
}
v, _ = kindTables.LoadOrStore(key, names)
}
names := v.([]string)
if int(id) < len(names) {
return names[id]
}
return n.Kind()
}
// Content returns the UTF-8 text of the node as a slice of src.
func (n *Node) Content(src []byte) string { return n.inner.Utf8Text(src) }
// StartPoint returns the (row, column) position of the node start.
func (n *Node) StartPoint() Point { return fromTSPoint(n.inner.StartPosition()) }
// EndPoint returns the (row, column) position one past the node end.
func (n *Node) EndPoint() Point { return fromTSPoint(n.inner.EndPosition()) }
// StartByte returns the byte offset of the node start.
func (n *Node) StartByte() uint32 { return uint32(n.inner.StartByte()) }
// EndByte returns the byte offset one past the node end.
func (n *Node) EndByte() uint32 { return uint32(n.inner.EndByte()) }
// ChildCount returns the number of children (named + anonymous).
func (n *Node) ChildCount() uint32 { return uint32(n.inner.ChildCount()) }
// NamedChildCount returns the number of named children.
func (n *Node) NamedChildCount() uint32 { return uint32(n.inner.NamedChildCount()) }
// Child returns the i-th child (named or anonymous) or nil. It reaches the
// child through a direct C call that returns the node by value, so the result
// is bump-allocated in the arena with no go-tree-sitter heap node (newNode).
func (n *Node) Child(i int) *Node {
if i < 0 {
return nil
}
c, ok := childDirect(n.inner, i)
if !ok {
return nil
}
return n.WrapVal(c)
}
// NamedChild returns the i-th named child or nil. Like Child, it avoids
// go-tree-sitter's per-node heap allocation.
func (n *Node) NamedChild(i int) *Node {
if i < 0 {
return nil
}
c, ok := namedChildDirect(n.inner, i)
if !ok {
return nil
}
return n.WrapVal(c)
}
// NamedChildren yields n's named children, in order, walking the sibling
// chain once with a tree-sitter cursor. Visiting every named child costs
// O(total children). The index form
//
// for i := 0; i < int(n.NamedChildCount()); i++ { c := n.NamedChild(i); … }
//
// is O(N^2): each NamedChild(i) re-walks the child list from the first
// child to reach position i, so a loop over a very wide node (e.g. a
// generated file's program root with thousands of top-level siblings)
// degrades quadratically. This iterator stays linear.
//
// The visited set and order are identical to the NamedChild index form:
// anonymous (unnamed) children are skipped and named children are
// yielded in their natural child order.
func (n *Node) NamedChildren() iter.Seq[*Node] {
return func(yield func(*Node) bool) {
if n == nil || !n.valid {
return
}
cursor := n.inner.Walk()
defer cursor.Close()
if !cursor.GotoFirstChild() {
return
}
for {
c := cursorCurrentNode(cursor)
if c.IsNamed() {
if !yield(n.WrapVal(c)) {
return
}
}
if !cursor.GotoNextSibling() {
return
}
}
}
}
// ChildByFieldName returns the first child with the given field name or nil.
// Uses a direct C call so the result is arena-allocated with no heap node.
func (n *Node) ChildByFieldName(name string) *Node {
c, ok := childByFieldNameDirect(n.inner, name)
if !ok {
return nil
}
return n.WrapVal(c)
}
// FieldNameForChild returns the field name of the i-th child, or "" if none.
func (n *Node) FieldNameForChild(i int) string {
if i < 0 {
return ""
}
return n.inner.FieldNameForChild(uint32(i))
}
// Parent returns the parent node or nil for the root. Avoids
// go-tree-sitter's per-node heap allocation via a direct C call.
func (n *Node) Parent() *Node {
c, ok := parentDirect(n.inner)
if !ok {
return nil
}
return n.WrapVal(c)
}
// NextSibling returns the next sibling (named or anonymous) or nil. Direct C
// call, arena-allocated result, no heap node.
func (n *Node) NextSibling() *Node {
c, ok := nextSiblingDirect(n.inner)
if !ok {
return nil
}
return n.WrapVal(c)
}
// PrevSibling returns the previous sibling (named or anonymous) or nil.
func (n *Node) PrevSibling() *Node {
c, ok := prevSiblingDirect(n.inner)
if !ok {
return nil
}
return n.WrapVal(c)
}
// NextNamedSibling returns the next named sibling or nil.
func (n *Node) NextNamedSibling() *Node {
c, ok := nextNamedSiblingDirect(n.inner)
if !ok {
return nil
}
return n.WrapVal(c)
}
// PrevNamedSibling returns the previous named sibling or nil.
func (n *Node) PrevNamedSibling() *Node {
c, ok := prevNamedSiblingDirect(n.inner)
if !ok {
return nil
}
return n.WrapVal(c)
}
// IsNamed reports whether the node corresponds to a named grammar rule.
func (n *Node) IsNamed() bool { return n.inner.IsNamed() }
// IsMissing reports whether the parser inserted this node to recover from an error.
func (n *Node) IsMissing() bool { return n.inner.IsMissing() }
// IsError reports whether this is a synthetic ERROR node.
func (n *Node) IsError() bool { return n.inner.IsError() }
// HasError reports whether the subtree under this node contains any ERROR nodes.
func (n *Node) HasError() bool { return n.inner.HasError() }
// String returns the s-expression representation of the node.
func (n *Node) String() string { return n.inner.ToSexp() }
// Id returns a stable numeric identity for the underlying node. Safe
// to use as a map key; equal across multiple wrappers of the same
// tree-sitter node. (Required because our shim creates a fresh *Node
// on every traversal, so pointer identity is not meaningful.)
func (n *Node) Id() uintptr {
if n == nil {
return 0
}
return n.inner.Id()
}
// Equal reports whether two shim Nodes wrap the same underlying
// tree-sitter node. Prefer this to `==` pointer comparison — our
// wrappers are freshly allocated on every navigation.
func (n *Node) Equal(other *Node) bool {
if n == nil || other == nil {
return n == other
}
return n.inner.Equals(other.inner)
}
// Tree wraps *ts.Tree.
type Tree struct {
inner *ts.Tree
arena *nodeArena // pooled; taken lazily on first RootNode, returned on Close
}
// WrapTree wraps a *ts.Tree for internal use by the parser package.
func WrapTree(t *ts.Tree) *Tree { return &Tree{inner: t} }
// Inner exposes the underlying *ts.Tree for internal use.
func (t *Tree) Inner() *ts.Tree { return t.inner }
// RootNode returns the root node of the parse tree, stamped with the
// tree's language so Type() lookups across the walk need no CGO call.
func (t *Tree) RootNode() *Node {
root := t.inner.RootNode()
if root == nil {
return nil
}
// Take a pooled arena on first use and reuse it for any later RootNode
// call on the same tree, so every node walked from this tree allocates
// into one recycled arena. Close() returns it to the pool.
if t.arena == nil {
t.arena = getArena()
}
a := t.arena
nn := a.alloc()
nn.inner = *root
nn.valid = true
nn.langKey = unsafe.Pointer(root.Language().Inner)
nn.arena = a
return nn
}
// Close releases the tree's C resources and recycles its node arena.
//
// The arena is returned to the pool AFTER the C tree is freed: by the
// Tree's contract every Node wrapper is dead once Close returns, so the
// chunks the arena retains hold nothing live. putArena's reset() clears any
// stale slot, so a recycled arena never pins a closed tree.
func (t *Tree) Close() {
if t == nil {
return
}
if t.inner != nil {
t.inner.Close()
t.inner = nil
}
if t.arena != nil {
putArena(t.arena)
t.arena = nil
}
}
// Parser wraps *ts.Parser with a ParseCtx that honours ctx cancellation
// via the new API's progress callback hook.
type Parser struct {
inner *ts.Parser
}
// NewParser allocates a fresh parser. The caller must Close it.
func NewParser() *Parser { return &Parser{inner: ts.NewParser()} }
// Close releases the parser's C resources.
func (p *Parser) Close() {
if p != nil && p.inner != nil {
p.inner.Close()
p.inner = nil
}
}
// SetLanguage binds a grammar to the parser. Errors from the new API
// (incompatible ABI versions) are swallowed to keep the smacker-style
// void return; callers trust build-time grammar selection.
func (p *Parser) SetLanguage(lang *Language) { _ = p.inner.SetLanguage(lang) }
// Reset clears retained parse state (finished tree, old-tree refs,
// stack, cached token) so a parser that parsed cleanly can be reused
// for an unrelated document. It does not clear the bound language.
//
// Reset does NOT fully sanitise a parser whose parse was cancelled:
// ts_parser_reset leaves the C parser's canceled_balancing flag set,
// so a parse cancelled during the balancing phase poisons the parser
// permanently — the next Parse jumps to the balance label and aborts
// the process on an internal assertion. Discard (Close) an errored
// parser; never Reset-and-reuse it. See parser.ParseFile.
func (p *Parser) Reset() {
if p != nil && p.inner != nil {
p.inner.Reset()
}
}
// ParseCtx parses src under ctx's deadline, returning a *Tree the
// caller must Close. Cancellation is polled via a ProgressCallback;
// exact-to-the-byte interruption isn't guaranteed — tree-sitter calls
// the callback at its own cadence.
func (p *Parser) ParseCtx(ctx context.Context, old *Tree, src []byte) (*Tree, error) {
var oldTree *ts.Tree
if old != nil {
oldTree = old.inner
}
cancelled := false
opts := &ts.ParseOptions{
ProgressCallback: func(_ ts.ParseState) bool {
if ctx.Err() != nil {
cancelled = true
return true // true aborts the parse
}
return false
},
}
tree := p.inner.ParseWithOptions(func(offset int, _ ts.Point) []byte {
if offset >= len(src) {
return nil
}
return src[offset:]
}, oldTree, opts)
if tree == nil {
if cancelled {
if err := ctx.Err(); err != nil {
return nil, err
}
return nil, errors.New("tree-sitter: parse cancelled")
}
return nil, fmt.Errorf("tree-sitter: parse returned nil")
}
return &Tree{inner: tree}, nil
}
// Query is a compiled tree-sitter query. It caches CaptureNames so
// capture-id → name lookups are O(1) and don't cross into CGO.
type Query struct {
inner *ts.Query
names []string
}
// NewQuery compiles a query pattern against a language. Signature
// matches smacker's (pattern, lang) order, which is the argument order
// most of our language adapters expect.
func NewQuery(pattern []byte, lang *Language) (*Query, error) {
q, qerr := ts.NewQuery(lang, string(pattern))
if qerr != nil {
return nil, errors.New(qerr.Error())
}
return &Query{inner: q, names: q.CaptureNames()}, nil
}
// Inner exposes the underlying *ts.Query for internal query runners.
func (q *Query) Inner() *ts.Query { return q.inner }
// Close releases the query's C resources.
func (q *Query) Close() {
if q != nil && q.inner != nil {
q.inner.Close()
q.inner = nil
}
}
// CaptureNameForId returns the capture name for a capture index.
func (q *Query) CaptureNameForId(id uint32) string {
if int(id) >= len(q.names) {
return ""
}
return q.names[id]
}