// 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 ` or `// @generated by `. Empty when the marker // was a bare `@generated`. Tool string // Source is the path or identifier extracted from a // `// source: ` 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 ." 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: ` 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::` 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 } }