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
177 lines
6.0 KiB
Go
177 lines
6.0 KiB
Go
package languages
|
|
|
|
import (
|
|
"bytes"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/zzet/gortex/internal/config"
|
|
"github.com/zzet/gortex/internal/graph"
|
|
"github.com/zzet/gortex/internal/parser"
|
|
)
|
|
|
|
// compiledChunkPattern is a ChunkPattern with its regex compiled once
|
|
// at registration time — the spec is rejected wholesale if any pattern
|
|
// fails to compile, so Extract never re-validates.
|
|
type compiledChunkPattern struct {
|
|
kind graph.NodeKind
|
|
re *regexp.Regexp
|
|
nameGroup int
|
|
}
|
|
|
|
// RegexChunkerExtractor is a parser.Extractor that derives a coarse
|
|
// symbol outline from regex patterns alone — the documented "register a
|
|
// regex fallback chunker for a grammar-less language without writing Go"
|
|
// entry point. It emits the file node, then one node per pattern match
|
|
// (named by the configured capture group) plus an EdgeDefines from the
|
|
// file to each. Nodes are deduplicated by ID.
|
|
type RegexChunkerExtractor struct {
|
|
language string
|
|
exts []string
|
|
patterns []compiledChunkPattern
|
|
}
|
|
|
|
func (e *RegexChunkerExtractor) Language() string { return e.language }
|
|
func (e *RegexChunkerExtractor) Extensions() []string { return e.exts }
|
|
|
|
// Extract scans src with every compiled pattern, emitting one node per
|
|
// match and an EdgeDefines from the file. The file node always comes
|
|
// first; per-match nodes are deduplicated by ID. Never returns an error
|
|
// — a grammar-less outline degrades silently to just the file node when
|
|
// nothing matches.
|
|
func (e *RegexChunkerExtractor) Extract(filePath string, src []byte) (*parser.ExtractionResult, error) {
|
|
result := &parser.ExtractionResult{}
|
|
fileNode := &graph.Node{
|
|
ID: filePath,
|
|
Kind: graph.KindFile,
|
|
Name: filePath,
|
|
FilePath: filePath,
|
|
StartLine: 1,
|
|
EndLine: bytes.Count(src, []byte("\n")) + 1,
|
|
Language: e.language,
|
|
}
|
|
result.Nodes = append(result.Nodes, fileNode)
|
|
|
|
seen := map[string]bool{filePath: true}
|
|
for _, p := range e.patterns {
|
|
matches := p.re.FindAllSubmatchIndex(src, -1)
|
|
for _, m := range matches {
|
|
// Submatch index 2*g..2*g+1 brackets capture group g. A
|
|
// negative start means the optional group didn't participate.
|
|
gi := 2 * p.nameGroup
|
|
if gi+1 >= len(m) || m[gi] < 0 {
|
|
continue
|
|
}
|
|
name := string(src[m[gi]:m[gi+1]])
|
|
if name == "" {
|
|
continue
|
|
}
|
|
id := filePath + "::" + name
|
|
if seen[id] {
|
|
continue
|
|
}
|
|
seen[id] = true
|
|
|
|
// StartLine is computed from the name-capture byte offset
|
|
// (count of newlines before it) rather than the whole-match
|
|
// start: a leading `\s*` in the pattern can pull the match
|
|
// start onto a preceding blank line, while the name capture
|
|
// always sits on the symbol's own line.
|
|
startLine := bytes.Count(src[:m[gi]], []byte("\n")) + 1
|
|
node := &graph.Node{
|
|
ID: id,
|
|
Kind: p.kind,
|
|
Name: name,
|
|
FilePath: filePath,
|
|
StartLine: startLine,
|
|
Language: e.language,
|
|
}
|
|
result.Nodes = append(result.Nodes, node)
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: filePath,
|
|
To: id,
|
|
Kind: graph.EdgeDefines,
|
|
FilePath: filePath,
|
|
Line: startLine,
|
|
})
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
var _ parser.Extractor = (*RegexChunkerExtractor)(nil)
|
|
|
|
// compileChunkPatterns compiles each pattern of a spec, dropping a
|
|
// pattern whose Kind is not a valid graph node kind (logged) and
|
|
// returning ok=false when any regex fails to compile — the caller then
|
|
// skips the whole spec. A NameGroup of zero defaults to group 1.
|
|
func compileChunkPatterns(spec config.FallbackChunkerSpec, log *zap.Logger) ([]compiledChunkPattern, bool) {
|
|
out := make([]compiledChunkPattern, 0, len(spec.Patterns))
|
|
for _, p := range spec.Patterns {
|
|
kind := graph.NodeKind(strings.TrimSpace(p.Kind))
|
|
if !graph.ValidNodeKind(kind) {
|
|
log.Warn("fallback chunker: dropping pattern with invalid kind",
|
|
zap.String("language", spec.Language), zap.String("kind", p.Kind))
|
|
continue
|
|
}
|
|
re, err := regexp.Compile(p.Regex)
|
|
if err != nil {
|
|
log.Warn("fallback chunker: skipped — pattern failed to compile",
|
|
zap.String("language", spec.Language), zap.String("regex", p.Regex), zap.Error(err))
|
|
return nil, false
|
|
}
|
|
group := p.NameGroup
|
|
if group <= 0 {
|
|
group = 1
|
|
}
|
|
out = append(out, compiledChunkPattern{kind: kind, re: re, nameGroup: group})
|
|
}
|
|
return out, true
|
|
}
|
|
|
|
// RegisterFallbackChunkers registers every configured regex fallback
|
|
// chunker, mirroring RegisterCustomGrammars: a spec whose language or
|
|
// any extension collides with a built-in (or already registered)
|
|
// extractor is skipped — built-ins win — as is one that fails to
|
|
// validate (no usable pattern, or a bad regex), each with a logged
|
|
// warning so a typo never aborts startup.
|
|
func RegisterFallbackChunkers(reg *parser.Registry, specs []config.FallbackChunkerSpec, log *zap.Logger) {
|
|
if reg == nil {
|
|
return
|
|
}
|
|
if log == nil {
|
|
log = zap.NewNop()
|
|
}
|
|
for _, spec := range specs {
|
|
lang := strings.TrimSpace(spec.Language)
|
|
exts := normalizeGrammarExtensions(spec.Extensions)
|
|
if lang == "" || len(exts) == 0 || len(spec.Patterns) == 0 {
|
|
log.Warn("fallback chunker: skipped — language, extensions and patterns are all required",
|
|
zap.String("language", spec.Language))
|
|
continue
|
|
}
|
|
if _, exists := reg.GetByLanguage(lang); exists {
|
|
log.Warn("fallback chunker: skipped — language already registered by a built-in extractor",
|
|
zap.String("language", lang))
|
|
continue
|
|
}
|
|
if conflict := firstClaimedExtension(reg, exts); conflict != "" {
|
|
log.Warn("fallback chunker: skipped — extension already claimed by a built-in extractor",
|
|
zap.String("language", lang), zap.String("extension", conflict))
|
|
continue
|
|
}
|
|
patterns, ok := compileChunkPatterns(spec, log)
|
|
if !ok || len(patterns) == 0 {
|
|
log.Warn("fallback chunker: skipped — no usable pattern",
|
|
zap.String("language", lang))
|
|
continue
|
|
}
|
|
reg.Register(&RegexChunkerExtractor{language: lang, exts: exts, patterns: patterns})
|
|
log.Info("fallback chunker registered",
|
|
zap.String("language", lang), zap.Strings("extensions", exts),
|
|
zap.Int("patterns", len(patterns)))
|
|
}
|
|
}
|