Files
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

135 lines
4.2 KiB
Go

// Package forest is the long-tail language adapter. It wraps any
// alexaandru/go-sitter-forest grammar (510+ supported) as a Gortex
// Extractor with signature-only depth: function / method / type /
// interface / variable / constant nodes plus EdgeDefines from the
// file. Calls and imports are best-effort via @reference.call captures
// when the grammar ships a tags.scm.
//
// Top-tier languages (Go, TS, Python, Rust, …) keep their bespoke
// extractors in internal/parser/languages — those have hand-tuned
// queries that emit Gortex-specific edges (ORM, contracts, dataflow)
// the generic walker can't produce. forest is for everything else.
package forest
import (
"sync"
"unsafe"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser"
sitter "github.com/zzet/gortex/internal/parser/tsitter"
)
// GetLanguageFn returns the raw C *TSLanguage pointer. Every forest
// language module exposes one as `forestpkg.GetLanguage`.
type GetLanguageFn func() unsafe.Pointer
// GetQueryFn returns the bytes of a named .scm query bundled with the
// grammar. Forest grammars ship a subset of {tags, highlights, locals,
// folds, indents, injections}; we only read tags.scm. The opts
// argument is forest's preference flag (NvimFirst / NativeFirst /
// NvimOnly / NativeOnly). nil is acceptable when the grammar has no
// queries.
type GetQueryFn func(kind string, opts ...byte) []byte
// Extractor is a generic forest-backed signature-only extractor.
// Construct one per language via New() and register it with the
// parser registry exactly like any other Extractor.
type Extractor struct {
language string
extensions []string
getLang GetLanguageFn
getQuery GetQueryFn
once sync.Once
initErr error
lang *sitter.Language
tagsQ *parser.PreparedQuery // nil when grammar ships no tags.scm
}
// New builds a forest-backed Extractor. getQuery may be nil for
// grammars that do not expose any .scm queries — extraction then
// falls back to the generic node-kind walker.
func New(language string, extensions []string, getLang GetLanguageFn, getQuery GetQueryFn) *Extractor {
return &Extractor{
language: language,
extensions: extensions,
getLang: getLang,
getQuery: getQuery,
}
}
func (e *Extractor) Language() string { return e.language }
func (e *Extractor) Extensions() []string { return e.extensions }
// init compiles the language pointer + tags.scm query exactly once.
// Errors are sticky: once an init has failed, every subsequent
// Extract returns the same error rather than retrying CGO setup on
// every file (which would slow indexing to a crawl on a broken
// grammar).
func (e *Extractor) init() error {
e.once.Do(func() {
ptr := e.getLang()
if ptr == nil {
e.initErr = errNilLanguage
return
}
e.lang = sitter.NewLanguage(ptr)
if e.getQuery != nil {
pattern := e.getQuery("tags")
if len(pattern) > 0 {
q, err := parser.NewPreparedQuery(string(pattern), e.lang)
if err == nil {
e.tagsQ = q
}
// Compile failure is non-fatal: drop to walker.
}
}
})
return e.initErr
}
// Extract parses src with the bundled grammar and emits one file
// node, plus one node per detected definition (function / method /
// type / interface / variable / constant / field / module). Edges:
// EdgeDefines from the file to every definition; EdgeCalls
// (unresolved::name) for @reference.call captures when tags.scm is
// present.
func (e *Extractor) Extract(filePath string, src []byte) (*parser.ExtractionResult, error) {
if err := e.init(); err != nil {
return nil, err
}
tree, err := parser.ParseFile(src, e.lang)
if err != nil {
return nil, err
}
defer tree.Close()
root := tree.RootNode()
result := &parser.ExtractionResult{}
fileNode := &graph.Node{
ID: filePath,
Kind: graph.KindFile,
Name: filePath,
FilePath: filePath,
StartLine: 1,
EndLine: int(root.EndPoint().Row) + 1,
Language: e.language,
}
result.Nodes = append(result.Nodes, fileNode)
if e.tagsQ != nil {
e.extractByTags(root, src, filePath, fileNode, result)
return result, nil
}
e.extractByWalker(root, src, filePath, fileNode, result)
return result, nil
}
var _ parser.Extractor = (*Extractor)(nil)