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
222 lines
8.2 KiB
Go
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
|
|
}
|
|
}
|