Files
zzet--gortex/internal/codegen/annotations.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

294 lines
10 KiB
Go

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
}