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
135 lines
4.2 KiB
Go
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)
|