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

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)))
}
}