Files
zzet--gortex/internal/codegen/scanner.go
T
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

222 lines
8.2 KiB
Go

// Package codegen detects whether a source file was emitted by a code
// generator and, when possible, identifies the schema source it came
// from. Recognises the conventional Go "Code generated by … DO NOT
// EDIT." header plus the broader "@generated" marker used by Buck,
// Bazel, GraphQL Codegen, and others.
//
// Output is kept deliberately conservative: a generated-file flag
// plus optional generator tool name and source-file reference. File-
// pair linking against the actual schema (e.g. `foo.pb.go` →
// `foo.proto`) is left to the caller — only the source field
// declared inside the marker comment itself is parsed here. That
// keeps the scanner free of repo-walking state and lets it run in
// the per-file extraction path.
package codegen
import (
"bufio"
"bytes"
"path/filepath"
"regexp"
"strings"
"sync"
"github.com/zzet/gortex/internal/graph"
)
// scanBufPool holds reusable 64 KB scratch buffers for bufio.Scanner.
// Allocating a fresh buffer per call shows up as the top allocator in
// the indexer's GC-bound warmup phase (Scan runs on every file across
// every tracked repo). Pooling keeps the per-call footprint at the
// pointer header.
var scanBufPool = sync.Pool{
New: func() any {
b := make([]byte, 64*1024)
return &b
},
}
// Marker is the parsed result of scanning a file. Generated is true
// iff a recognised marker was found in the header window.
type Marker struct {
Generated bool
// Tool is the generator name extracted from `// Code generated
// by <tool>` or `// @generated by <tool>`. Empty when the marker
// was a bare `@generated`.
Tool string
// Source is the path or identifier extracted from a
// `// source: <path>` line that immediately follows a Go-style
// codegen header. Empty when the file's marker doesn't include
// one.
Source string
}
// commentLineRe enforces that the marker appears inside a real
// comment, not embedded in a prose string. The line must begin
// with whitespace and a known comment opener; anything after that
// is the comment body. We split the marker check into two passes:
// commentLineRe extracts the body, then markerRe / atGeneratedRe
// scan the body for the marker. This keeps "see the 'Code
// generated by' header" inside a doc comment from triggering, but
// still matches `/* Automatically @generated by tree-sitter */`
// where the marker isn't the first comment word.
var commentLineRe = regexp.MustCompile(`^\s*(?://|#|--|/\*|\*)\s*(.+?)\s*(?:\*/)?\s*$`)
// codegenRe matches "Code generated by <tool>." optionally
// followed by " DO NOT EDIT." within a comment body. Case-
// sensitive — the marker is a fixed convention, not natural
// language.
var codegenRe = regexp.MustCompile(`Code generated\s+(?:by\s+)?([^.]+?)\.\s*(?:DO NOT EDIT\.?)?`)
// atGeneratedRe matches the `@generated` marker used by tools
// outside the Go ecosystem (Buck, Bazel, GraphQL Codegen) within a
// comment body.
var atGeneratedRe = regexp.MustCompile(`@generated(?:\s+by\s+([^\s]+))?`)
// sourceRe matches the optional `// source: <path>` companion line
// that protoc-gen-go and friends emit immediately after the
// codegen header. Stops at end-of-line.
var sourceRe = regexp.MustCompile(`^\s*(?://|#|\*|--)\s*source:\s*([^\s]+)\s*$`)
const headerWindowLines = 10
// maxMarkerOffset bounds how far into a comment body the marker may
// appear before we stop trusting it. Tree-sitter's "Automatically
// @generated by …" puts @generated at offset 14; a generous cap of
// 20 lets that and similar prefixes through while still rejecting
// prose mentioning the marker mid-sentence.
const maxMarkerOffset = 20
// Scan walks the first headerWindowLines lines of source and
// returns a Marker. Generated stays false when no recognised marker
// fires; Tool/Source stay empty when the marker doesn't expose
// them. Lines are scanned in order; the first hit wins for both
// marker and source.
func Scan(source []byte) Marker {
var m Marker
if len(source) == 0 {
return m
}
bufPtr := scanBufPool.Get().(*[]byte)
defer scanBufPool.Put(bufPtr)
scanner := bufio.NewScanner(bytes.NewReader(source))
scanner.Buffer(*bufPtr, 1024*1024)
lineNum := 0
for scanner.Scan() && lineNum < headerWindowLines {
lineNum++
line := scanner.Text()
// Restrict marker matching to lines that are actually
// comments — guards against prose hits inside doc strings
// and inside this scanner's own source.
commentBody := commentLineRe.FindStringSubmatch(line)
if commentBody == nil {
continue
}
body := commentBody[1]
if !m.Generated {
// The marker must appear near the start of the comment
// body. Real codegen headers put the marker at column 0
// (Go's `Code generated by …`) or after a short
// adverb-like prefix (tree-sitter's
// `Automatically @generated by …`). Prose comments
// mentioning the marker as a quoted phrase have it well
// past column 20 — so we require the match index to be
// no further than that.
if hit := codegenRe.FindStringSubmatchIndex(body); hit != nil && hit[0] <= maxMarkerOffset {
m.Generated = true
m.Tool = strings.TrimSpace(body[hit[2]:hit[3]])
continue
}
if hit := atGeneratedRe.FindStringSubmatchIndex(body); hit != nil && hit[0] <= maxMarkerOffset {
m.Generated = true
if hit[2] >= 0 {
m.Tool = strings.TrimSpace(body[hit[2]:hit[3]])
}
continue
}
}
if m.Source == "" {
if hit := sourceRe.FindStringSubmatch(line); hit != nil {
m.Source = strings.TrimSpace(hit[1])
}
}
}
return m
}
// BuildGraphArtifacts converts a Marker into the file-meta updates
// and the EdgeGeneratedBy edge to append. Returns nothing when
// Generated is false. The fileMeta map is the caller's existing
// Node.Meta — keys are merged in place, never replacing.
//
// The edge target is, in order of preference:
// - the resolved schema source path when Marker.Source is set
// (relative to repo root; the indexer's applyRepoPrefix handles
// multi-repo namespacing downstream);
// - a `generator::<tool>` synthetic node when only Tool is known;
// - a `generator::unknown` sentinel for bare `@generated` markers.
//
// The returned edge slice is appended by the caller; the file node
// has meta.generated stamped by the caller using the returned Marker.
func BuildGraphArtifacts(filePath string, marker Marker) []*graph.Edge {
if !marker.Generated {
return nil
}
filePath = filepath.ToSlash(filePath)
target := generatorNodeID(marker)
return []*graph.Edge{{
From: filePath,
To: target,
Kind: graph.EdgeGeneratedBy,
FilePath: filePath,
Origin: graph.OriginASTResolved,
Meta: map[string]any{
"tool": marker.Tool,
"source": marker.Source,
},
}}
}
// generatorNodeID picks the most specific target available for the
// EdgeGeneratedBy edge. A real source path wins; a tool name comes
// next; the unknown sentinel is the last resort. Uses the
// `external::` synthetic-ID prefix so the exporter and other
// downstream consumers that recognise the existing synthetic
// prefixes (alongside `unresolved::` and `annotation::`) materialise
// a stub node automatically — same pattern EdgeThrows uses for
// `external::error`.
func generatorNodeID(m Marker) string {
if m.Source != "" {
// Source paths are stored verbatim — for protoc-emitted Go
// the value is typically a Go-style import path like
// `github.com/foo/bar/baz.proto`, which is not a literal
// filesystem path. The resolver can match it later via
// suffix-prefix logic if a real .proto file exists in the
// repo. Until then it's a synthetic external pointer that
// still de-duplicates correctly across files generated from
// the same source.
return "external::generator-source:" + filepath.ToSlash(m.Source)
}
if m.Tool != "" {
return "external::generator-tool:" + m.Tool
}
return "external::generator-unknown"
}
// MarkFileNode stamps generator metadata onto an existing file
// Node.Meta. Called by the indexer after a Scan returns Generated;
// keeps the file node compact while still surfacing the flag in
// brief listings.
func MarkFileNode(meta map[string]any, m Marker) {
if !m.Generated {
return
}
meta["generated"] = true
if m.Tool != "" {
meta["generated_by"] = m.Tool
}
if m.Source != "" {
meta["generated_from"] = m.Source
}
}