chore: import upstream snapshot with attribution
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

This commit is contained in:
wehub-resource-sync
2026-07-13 12:33:42 +08:00
commit a06f331eb8
3186 changed files with 689843 additions and 0 deletions
+293
View File
@@ -0,0 +1,293 @@
package codegen
import (
"sort"
"strings"
"github.com/zzet/gortex/internal/graph"
)
// annotationGen describes the codegen tool an annotation implies and
// the member kinds it produces — members that exist after compilation
// but never appear in source, so the indexer cannot see them.
type annotationGen struct {
tool string
members []string
}
// annotationGenerators maps a generation-implying annotation (Java
// Lombok, MapStruct, Kotlin compiler plugins) to the tool plus the
// members it synthesises. Keyed by the bare annotation name — no `@`,
// no package qualifier.
var annotationGenerators = map[string]annotationGen{
// Lombok.
"Data": {"lombok", []string{"getters", "setters", "equals_hashcode", "tostring", "required_args_constructor"}},
"Value": {"lombok", []string{"getters", "equals_hashcode", "tostring", "all_args_constructor"}},
"Getter": {"lombok", []string{"getters"}},
"Setter": {"lombok", []string{"setters"}},
"Builder": {"lombok", []string{"builder"}},
"SuperBuilder": {"lombok", []string{"builder"}},
"AllArgsConstructor": {"lombok", []string{"all_args_constructor"}},
"NoArgsConstructor": {"lombok", []string{"no_args_constructor"}},
"RequiredArgsConstructor": {"lombok", []string{"required_args_constructor"}},
"EqualsAndHashCode": {"lombok", []string{"equals_hashcode"}},
"ToString": {"lombok", []string{"tostring"}},
"Slf4j": {"lombok", []string{"logger"}},
// MapStruct — generates the <Mapper>Impl implementation class.
"Mapper": {"mapstruct", []string{"mapper_impl"}},
// Kotlin compiler plugins (KAPT / kotlin-parcelize).
"Parcelize": {"kapt", []string{"parcelable"}},
// CommunityToolkit.Mvvm .NET source generators: [ObservableProperty]
// on a field generates the public property + change notification;
// [RelayCommand] on a method generates an ICommand property.
"ObservableProperty": {"mvvm_toolkit", []string{"observable_property"}},
"RelayCommand": {"mvvm_toolkit", []string{"relay_command"}},
}
// AnnotationGeneratedStats reports what MarkAnnotatedGenerated did.
type AnnotationGeneratedStats struct {
NodesMarked int
EdgesAdded int
}
// normalizeAnnotationName strips a leading `@` and any package
// qualifier, leaving the bare annotation name.
func normalizeAnnotationName(name string) string {
name = strings.TrimPrefix(strings.TrimSpace(name), "@")
if i := strings.LastIndexByte(name, '.'); i >= 0 {
name = name[i+1:]
}
return name
}
// MarkAnnotatedGenerated scans one file's extraction output for
// EdgeAnnotated edges that point at a generation-implying annotation
// (Lombok / MapStruct / Kotlin codegen) and stamps each annotated
// symbol with codegen-visibility metadata: has_generated_members,
// codegen_tool, and the generated_members list. It returns the
// EdgeGeneratedBy edges the caller must append to result.Edges.
//
// This makes generated members visible without materialising them: a
// `@Data` class is flagged as carrying Lombok-generated accessors, so
// dead-code analysis and agents know the fields are not unused and
// that a missing getName() is compiler output, not a bug.
func MarkAnnotatedGenerated(nodes []*graph.Node, edges []*graph.Edge) (extra []*graph.Edge, stats AnnotationGeneratedStats) {
if len(edges) == 0 {
return nil, stats
}
annoName := map[string]string{} // annotation node ID → bare name
byID := make(map[string]*graph.Node, len(nodes))
for _, n := range nodes {
byID[n.ID] = n
if n.Meta != nil {
if k, _ := n.Meta["kind"].(string); k == "annotation" {
annoName[n.ID] = normalizeAnnotationName(n.Name)
}
}
}
seenEdge := map[string]bool{}
for _, e := range edges {
if e.Kind != graph.EdgeAnnotated {
continue
}
gen, ok := annotationGenerators[annoName[e.To]]
if !ok {
continue
}
host := byID[e.From]
if host == nil {
continue
}
if host.Meta == nil {
host.Meta = map[string]any{}
}
host.Meta["has_generated_members"] = true
host.Meta["codegen_tool"] = gen.tool
host.Meta["generated_members"] = mergeStringSet(host.Meta["generated_members"], gen.members)
stats.NodesMarked++
key := e.From + "\x00" + gen.tool
if !seenEdge[key] {
seenEdge[key] = true
extra = append(extra, &graph.Edge{
From: e.From,
To: "external::generator-tool:" + gen.tool,
Kind: graph.EdgeGeneratedBy,
FilePath: host.FilePath,
Origin: graph.OriginASTResolved,
Meta: map[string]any{"tool": gen.tool},
})
stats.EdgesAdded++
}
}
return extra, stats
}
// lombokGeneratorTool is the external node every materialized Lombok member
// is generated by.
const lombokGeneratorTool = "external::generator-tool:lombok"
// MaterializeLombokAccessors mints the actual member nodes a Lombok class
// generates — getters / setters / a builder() factory + Builder type / an
// Slf4j log field — for every class MarkAnnotatedGenerated already flagged
// with codegen_tool="lombok". Without these nodes, `obj.getName()` cannot
// resolve to a graph node and a call chain through a Lombok accessor breaks.
// The synthetic methods carry the same Meta["receiver"] + bare Name shape the
// Java method-call resolver expects, so a `user.getName()` lands on the
// minted getName. IDs are deterministic and existence-checked, so a
// hand-written accessor wins and a reindex never duplicates.
func MaterializeLombokAccessors(nodes []*graph.Node) (newNodes []*graph.Node, newEdges []*graph.Edge) {
byID := make(map[string]*graph.Node, len(nodes))
for _, n := range nodes {
byID[n.ID] = n
}
// Group instance fields by their owning class (file + receiver). Static
// constants (KindConstant) are excluded — Lombok does not accessor them.
fieldsByClass := map[string][]*graph.Node{}
for _, n := range nodes {
if n.Kind != graph.KindField || n.Meta == nil {
continue
}
if recv, _ := n.Meta["receiver"].(string); recv != "" {
key := n.FilePath + "\x00" + recv
fieldsByClass[key] = append(fieldsByClass[key], n)
}
}
for _, host := range nodes {
if host.Kind != graph.KindType && host.Kind != graph.KindInterface {
continue
}
if host.Meta == nil {
continue
}
if tool, _ := host.Meta["codegen_tool"].(string); tool != "lombok" {
continue
}
members := metaStringSlice(host.Meta["generated_members"])
if len(members) == 0 {
continue
}
mset := map[string]bool{}
for _, m := range members {
mset[m] = true
}
class := host.Name
mint := func(id, name string, kind graph.NodeKind, member string, meta map[string]any) bool {
if byID[id] != nil {
return false // a hand-written member or an already-minted one wins
}
meta["synthesized"] = true
meta["generated"] = true
meta["codegen_tool"] = "lombok"
meta["generated_member"] = member
node := &graph.Node{
ID: id, Kind: kind, Name: name,
FilePath: host.FilePath, StartLine: host.StartLine, EndLine: host.StartLine,
Language: host.Language, Meta: meta,
}
byID[id] = node
newNodes = append(newNodes, node)
newEdges = append(newEdges,
&graph.Edge{From: host.ID, To: id, Kind: graph.EdgeDefines, FilePath: host.FilePath, Line: host.StartLine},
&graph.Edge{From: id, To: lombokGeneratorTool, Kind: graph.EdgeGeneratedBy, FilePath: host.FilePath, Origin: graph.OriginASTInferred, Meta: map[string]any{"tool": "lombok"}},
)
return true
}
for _, f := range fieldsByClass[host.FilePath+"\x00"+class] {
ftype, _ := f.Meta["field_type"].(string)
if mset["getters"] {
g := lombokGetterName(f.Name, ftype)
id := host.FilePath + "::" + class + "." + g
if mint(id, g, graph.KindMethod, "getter", map[string]any{"receiver": class, "scope_class": class, "return_type": ftype, "signature": g + "()"}) && ftype != "" {
newEdges = append(newEdges, &graph.Edge{From: id, To: "unresolved::" + simpleTypeName(ftype), Kind: graph.EdgeReferences, FilePath: host.FilePath, Origin: graph.OriginASTInferred, Meta: map[string]any{"ref_context": "return"}})
}
}
if mset["setters"] {
s := "set" + pascalCase(f.Name)
mint(host.FilePath+"::"+class+"."+s, s, graph.KindMethod, "setter", map[string]any{"receiver": class, "scope_class": class, "return_type": "void", "signature": s + "(" + ftype + ")"})
}
}
if mset["builder"] {
mint(host.FilePath+"::"+class+".builder", "builder", graph.KindMethod, "builder", map[string]any{"receiver": class, "scope_class": class, "is_static": true, "return_type": class + "Builder", "signature": "builder()"})
mint(host.FilePath+"::"+class+"Builder", class+"Builder", graph.KindType, "builder", map[string]any{})
}
if mset["logger"] {
mint(host.FilePath+"::"+class+".log", "log", graph.KindField, "logger", map[string]any{"receiver": class, "field_type": "org.slf4j.Logger"})
}
}
return newNodes, newEdges
}
// lombokGetterName returns the Lombok getter name for a field — `is<Name>` for
// a boolean (kept as-is when already `is`-prefixed), else `get<Name>`.
func lombokGetterName(field, ftype string) string {
if ftype == "boolean" {
if len(field) > 2 && field[0:2] == "is" && field[2] >= 'A' && field[2] <= 'Z' {
return field
}
return "is" + pascalCase(field)
}
return "get" + pascalCase(field)
}
// pascalCase upper-cases the first letter of s.
func pascalCase(s string) string {
if s == "" {
return s
}
return strings.ToUpper(s[:1]) + s[1:]
}
// simpleTypeName strips generic arguments and a package qualifier from a Java
// type, leaving the bare type name for a reference edge.
func simpleTypeName(t string) string {
if i := strings.IndexByte(t, '<'); i >= 0 {
t = t[:i]
}
t = strings.TrimSpace(t)
if i := strings.LastIndexByte(t, '.'); i >= 0 {
t = t[i+1:]
}
return strings.TrimSuffix(t, "[]")
}
// metaStringSlice coerces a Meta value to []string, tolerating the []any form
// a persistence round-trip can produce.
func metaStringSlice(v any) []string {
switch s := v.(type) {
case []string:
return s
case []any:
out := make([]string, 0, len(s))
for _, e := range s {
if str, ok := e.(string); ok {
out = append(out, str)
}
}
return out
}
return nil
}
// mergeStringSet merges add into an existing []string-typed meta value,
// returning a sorted, de-duplicated slice.
func mergeStringSet(existing any, add []string) []string {
set := map[string]bool{}
if cur, ok := existing.([]string); ok {
for _, s := range cur {
set[s] = true
}
}
for _, s := range add {
set[s] = true
}
out := make([]string, 0, len(set))
for s := range set {
out = append(out, s)
}
sort.Strings(out)
return out
}
+174
View File
@@ -0,0 +1,174 @@
package codegen
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/zzet/gortex/internal/graph"
)
func annoNode(id, name string) *graph.Node {
return &graph.Node{ID: id, Kind: graph.KindType, Name: name, Meta: map[string]any{"kind": "annotation"}}
}
func TestMarkAnnotatedGenerated_Lombok(t *testing.T) {
nodes := []*graph.Node{
{ID: "User.java::User", Kind: graph.KindType, Name: "User", FilePath: "User.java"},
annoNode("annotation::java::Data", "Data"),
}
edges := []*graph.Edge{
{From: "User.java::User", To: "annotation::java::Data", Kind: graph.EdgeAnnotated},
}
extra, stats := MarkAnnotatedGenerated(nodes, edges)
require.Equal(t, 1, stats.NodesMarked)
require.Len(t, extra, 1)
require.Equal(t, graph.EdgeGeneratedBy, extra[0].Kind)
require.Equal(t, "external::generator-tool:lombok", extra[0].To)
host := nodes[0]
require.Equal(t, true, host.Meta["has_generated_members"])
require.Equal(t, "lombok", host.Meta["codegen_tool"])
require.Contains(t, host.Meta["generated_members"].([]string), "getters")
}
func TestMarkAnnotatedGenerated_MapStruct(t *testing.T) {
nodes := []*graph.Node{
{ID: "M.java::M", Kind: graph.KindInterface, Name: "M", FilePath: "M.java"},
annoNode("annotation::java::Mapper", "org.mapstruct.Mapper"),
}
edges := []*graph.Edge{{From: "M.java::M", To: "annotation::java::Mapper", Kind: graph.EdgeAnnotated}}
_, stats := MarkAnnotatedGenerated(nodes, edges)
require.Equal(t, 1, stats.NodesMarked)
require.Equal(t, "mapstruct", nodes[0].Meta["codegen_tool"])
}
func TestMarkAnnotatedGenerated_IgnoresNonCodegen(t *testing.T) {
nodes := []*graph.Node{
{ID: "C.java::C", Kind: graph.KindType, Name: "C"},
annoNode("annotation::java::Override", "Override"),
}
edges := []*graph.Edge{{From: "C.java::C", To: "annotation::java::Override", Kind: graph.EdgeAnnotated}}
extra, stats := MarkAnnotatedGenerated(nodes, edges)
require.Equal(t, 0, stats.NodesMarked)
require.Empty(t, extra)
require.Nil(t, nodes[0].Meta)
}
func TestMarkAnnotatedGenerated_MergesAndDedups(t *testing.T) {
nodes := []*graph.Node{
{ID: "C::C", Kind: graph.KindType, Name: "C"},
annoNode("annotation::java::Getter", "Getter"),
annoNode("annotation::java::Setter", "Setter"),
}
edges := []*graph.Edge{
{From: "C::C", To: "annotation::java::Getter", Kind: graph.EdgeAnnotated},
{From: "C::C", To: "annotation::java::Setter", Kind: graph.EdgeAnnotated},
}
extra, stats := MarkAnnotatedGenerated(nodes, edges)
require.Equal(t, 2, stats.NodesMarked)
require.Len(t, extra, 1, "one EdgeGeneratedBy per (symbol, tool)")
members := nodes[0].Meta["generated_members"].([]string)
require.Contains(t, members, "getters")
require.Contains(t, members, "setters")
}
func TestMarkAnnotatedGenerated_MVVMSourceGenerators(t *testing.T) {
nodes := []*graph.Node{
{ID: "vm.cs::name", Kind: graph.KindField, Name: "name", FilePath: "vm.cs"},
annoNode("annotation::csharp::ObservableProperty", "ObservableProperty"),
}
edges := []*graph.Edge{
{From: "vm.cs::name", To: "annotation::csharp::ObservableProperty", Kind: graph.EdgeAnnotated},
}
_, stats := MarkAnnotatedGenerated(nodes, edges)
require.Equal(t, 1, stats.NodesMarked)
require.Equal(t, "mvvm_toolkit", nodes[0].Meta["codegen_tool"])
require.Contains(t, nodes[0].Meta["generated_members"].([]string), "observable_property")
}
func TestNormalizeAnnotationName(t *testing.T) {
require.Equal(t, "Data", normalizeAnnotationName("@Data"))
require.Equal(t, "Data", normalizeAnnotationName("lombok.Data"))
require.Equal(t, "Mapper", normalizeAnnotationName(" @org.mapstruct.Mapper "))
}
func TestMaterializeLombokAccessors_GettersSetters(t *testing.T) {
nodes := []*graph.Node{
{ID: "User.java::User", Kind: graph.KindType, Name: "User", FilePath: "User.java"},
annoNode("annotation::java::Data", "Data"),
{ID: "User.java::User.name", Kind: graph.KindField, Name: "name", FilePath: "User.java", Meta: map[string]any{"receiver": "User", "field_type": "String"}},
{ID: "User.java::User.active", Kind: graph.KindField, Name: "active", FilePath: "User.java", Meta: map[string]any{"receiver": "User", "field_type": "boolean"}},
}
edges := []*graph.Edge{{From: "User.java::User", To: "annotation::java::Data", Kind: graph.EdgeAnnotated}}
MarkAnnotatedGenerated(nodes, edges)
newNodes, newEdges := MaterializeLombokAccessors(nodes)
byID := map[string]*graph.Node{}
for _, n := range newNodes {
byID[n.ID] = n
}
getName := byID["User.java::User.getName"]
require.NotNil(t, getName, "getName accessor minted")
require.Equal(t, graph.KindMethod, getName.Kind)
require.Equal(t, "getName", getName.Name)
require.Equal(t, "User", getName.Meta["receiver"])
require.Equal(t, true, getName.Meta["synthesized"])
require.Equal(t, true, getName.Meta["generated"])
require.Equal(t, "lombok", getName.Meta["codegen_tool"])
require.NotNil(t, byID["User.java::User.setName"], "setName minted")
require.NotNil(t, byID["User.java::User.isActive"], "boolean getter is-prefixed")
require.NotNil(t, byID["User.java::User.setActive"], "boolean setter minted")
var hasDefines, hasGenBy bool
for _, e := range newEdges {
if e.Kind == graph.EdgeDefines && e.From == "User.java::User" && e.To == "User.java::User.getName" {
hasDefines = true
}
if e.Kind == graph.EdgeGeneratedBy && e.From == "User.java::User.getName" && e.To == "external::generator-tool:lombok" {
hasGenBy = true
}
}
require.True(t, hasDefines, "EdgeDefines class→getName")
require.True(t, hasGenBy, "EdgeGeneratedBy getName→lombok")
}
func TestMaterializeLombokAccessors_NoDuplicateHandWrittenWins(t *testing.T) {
nodes := []*graph.Node{
{ID: "U.java::U", Kind: graph.KindType, Name: "U", FilePath: "U.java", Meta: map[string]any{"codegen_tool": "lombok", "generated_members": []string{"getters", "setters"}}},
{ID: "U.java::U.id", Kind: graph.KindField, Name: "id", FilePath: "U.java", Meta: map[string]any{"receiver": "U", "field_type": "int"}},
{ID: "U.java::U.getId", Kind: graph.KindMethod, Name: "getId", FilePath: "U.java", Meta: map[string]any{"receiver": "U"}},
}
newNodes, _ := MaterializeLombokAccessors(nodes)
for _, n := range newNodes {
if n.ID == "U.java::U.getId" {
t.Errorf("hand-written getId must not be re-minted")
}
}
var sawSetId bool
for _, n := range newNodes {
if n.ID == "U.java::U.setId" {
sawSetId = true
}
}
require.True(t, sawSetId, "setId minted (no hand-written version)")
all := append(append([]*graph.Node{}, nodes...), newNodes...)
again, _ := MaterializeLombokAccessors(all)
require.Empty(t, again, "a reindex / re-run mints no duplicate accessors")
}
func TestMaterializeLombokAccessors_BuilderAndLogger(t *testing.T) {
nodes := []*graph.Node{
{ID: "S.java::S", Kind: graph.KindType, Name: "S", FilePath: "S.java", Meta: map[string]any{"codegen_tool": "lombok", "generated_members": []string{"builder", "logger"}}},
}
newNodes, _ := MaterializeLombokAccessors(nodes)
byID := map[string]*graph.Node{}
for _, n := range newNodes {
byID[n.ID] = n
}
require.NotNil(t, byID["S.java::S.builder"], "builder() minted")
require.NotNil(t, byID["S.java::SBuilder"], "Builder type minted")
require.NotNil(t, byID["S.java::S.log"], "log field minted")
require.Equal(t, "org.slf4j.Logger", byID["S.java::S.log"].Meta["field_type"])
}
+221
View File
@@ -0,0 +1,221 @@
// 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
}
}
+155
View File
@@ -0,0 +1,155 @@
package codegen
import (
"testing"
"github.com/zzet/gortex/internal/graph"
)
func TestScan_Variants(t *testing.T) {
cases := []struct {
name string
src string
generated bool
tool string
source string
}{
{
name: "protoc-go",
src: "// Code generated by protoc-gen-go. DO NOT EDIT.\n// source: github.com/foo/bar/baz.proto\n\npackage bar\n",
generated: true,
tool: "protoc-gen-go",
source: "github.com/foo/bar/baz.proto",
},
{
name: "stringer",
src: "// Code generated by \"stringer -type=Color\". DO NOT EDIT.\n\npackage colors\n",
generated: true,
tool: `"stringer -type=Color"`,
},
{
name: "at-generated",
src: "// @generated by graphql-codegen\n\nexport const x = 1;\n",
generated: true,
tool: "graphql-codegen",
},
{
name: "at-generated-bare",
src: "// @generated\nimport foo\n",
generated: true,
},
{
name: "no-marker",
src: "package main\n\nfunc main() {}\n",
generated: false,
},
{
name: "below-window",
src: "// 1\n// 2\n// 3\n// 4\n// 5\n// 6\n// 7\n// 8\n// 9\n// 10\n// 11\n// Code generated by foo. DO NOT EDIT.\n",
generated: false,
},
{
name: "case-sensitive",
src: "// code generated automatically\n",
generated: false,
},
{
name: "marker-in-prose-not-flagged",
src: "// Recognises the conventional Go \"Code generated by ...\" header in prose docs of this package.\n",
generated: false,
},
{
name: "tree-sitter-inline-marker",
src: "/* Automatically @generated by tree-sitter */\n\n#include\n",
// tree-sitter wraps the marker in a /* */ block with a
// short "Automatically" prefix; the maxMarkerOffset
// threshold accepts it.
generated: true,
tool: "tree-sitter",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := Scan([]byte(tc.src))
if got.Generated != tc.generated {
t.Fatalf("Generated = %v, want %v", got.Generated, tc.generated)
}
if got.Tool != tc.tool {
t.Errorf("Tool = %q, want %q", got.Tool, tc.tool)
}
if got.Source != tc.source {
t.Errorf("Source = %q, want %q", got.Source, tc.source)
}
})
}
}
func TestBuildGraphArtifacts(t *testing.T) {
t.Run("with source path", func(t *testing.T) {
edges := BuildGraphArtifacts("pkg/foo.pb.go", Marker{
Generated: true,
Tool: "protoc-gen-go",
Source: "github.com/x/y/foo.proto",
})
if len(edges) != 1 {
t.Fatalf("expected 1 edge, got %d", len(edges))
}
e := edges[0]
if e.Kind != graph.EdgeGeneratedBy {
t.Errorf("edge kind = %q", e.Kind)
}
if e.From != "pkg/foo.pb.go" {
t.Errorf("from = %q", e.From)
}
if e.To != "external::generator-source:github.com/x/y/foo.proto" {
t.Errorf("to = %q", e.To)
}
})
t.Run("tool only", func(t *testing.T) {
edges := BuildGraphArtifacts("pkg/foo.go", Marker{
Generated: true,
Tool: "stringer",
})
if edges[0].To != "external::generator-tool:stringer" {
t.Errorf("to = %q", edges[0].To)
}
})
t.Run("bare @generated", func(t *testing.T) {
edges := BuildGraphArtifacts("pkg/foo.ts", Marker{Generated: true})
if edges[0].To != "external::generator-unknown" {
t.Errorf("to = %q", edges[0].To)
}
})
t.Run("not generated", func(t *testing.T) {
edges := BuildGraphArtifacts("pkg/foo.go", Marker{})
if len(edges) != 0 {
t.Errorf("expected nil, got %+v", edges)
}
})
}
func TestMarkFileNode(t *testing.T) {
meta := map[string]any{}
MarkFileNode(meta, Marker{
Generated: true,
Tool: "stringer",
Source: "color.go",
})
if v, _ := meta["generated"].(bool); !v {
t.Errorf("generated meta missing")
}
if meta["generated_by"] != "stringer" {
t.Errorf("generated_by = %v", meta["generated_by"])
}
if meta["generated_from"] != "color.go" {
t.Errorf("generated_from = %v", meta["generated_from"])
}
}
func TestMarkFileNode_NoOpWhenNotGenerated(t *testing.T) {
meta := map[string]any{}
MarkFileNode(meta, Marker{})
if len(meta) != 0 {
t.Errorf("meta should stay empty, got %+v", meta)
}
}