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
294 lines
10 KiB
Go
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
|
|
}
|