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
192 lines
7.9 KiB
Go
192 lines
7.9 KiB
Go
package resolver
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/zzet/gortex/internal/graph"
|
|
"github.com/zzet/gortex/internal/parser/languages"
|
|
)
|
|
|
|
// loadCSource extracts a C source string with the real C extractor and
|
|
// loads its nodes + edges (plus a backing file node) into a fresh graph —
|
|
// the faithful end-to-end harness: a real extractor produces the macro
|
|
// node, its recovered body-callee edges, and the unresolved use-site call
|
|
// edge, exactly as a live index would.
|
|
func loadCSource(t *testing.T, path, src string) graph.Store {
|
|
t.Helper()
|
|
g := graph.New()
|
|
r, err := languages.NewCExtractor().Extract(path, []byte(src))
|
|
require.NoError(t, err)
|
|
g.AddNode(&graph.Node{ID: path, Kind: graph.KindFile, Name: path, FilePath: path, Language: "c"})
|
|
for _, n := range r.Nodes {
|
|
g.AddNode(n)
|
|
}
|
|
for _, e := range r.Edges {
|
|
g.AddEdge(e)
|
|
}
|
|
return g
|
|
}
|
|
|
|
// outCallEdge returns the call edge leaving fromID to a target whose
|
|
// UnresolvedName / id equals wantCallee at the given 1-based line, or nil.
|
|
func outCallEdge(g graph.Store, fromID, wantCallee string, line int) *graph.Edge {
|
|
for _, e := range g.GetOutEdges(fromID) {
|
|
if e.Kind != graph.EdgeCalls || e.Line != line {
|
|
continue
|
|
}
|
|
if e.To == wantCallee || graph.UnresolvedName(e.To) == wantCallee ||
|
|
strings.HasSuffix(e.To, "::"+wantCallee) {
|
|
return e
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// A function-like macro's body call is re-attributed to the use site: the
|
|
// `CALL_M(o);` line inside the calling function, not the `#define` line.
|
|
func TestMacroExpansion_AttributesCalleeToUseSite(t *testing.T) {
|
|
src := "#define CALL_M(o) (o)->run()\n" + // line 1: definition
|
|
"\n" +
|
|
"void use(Obj* o) {\n" + // line 3
|
|
" CALL_M(o);\n" + // line 4: use site
|
|
"}\n"
|
|
g := loadCSource(t, "u.c", src)
|
|
New(g).ResolveAll()
|
|
|
|
owned := ResolveMacroExpansionCalls(g)
|
|
assert.Equal(t, 1, owned, "one use-site callee edge synthesized")
|
|
|
|
// The minted edge: use -> run, at the USE-SITE line (4), not the
|
|
// #define line (1).
|
|
useSite := outCallEdge(g, "u.c::use", "run", 4)
|
|
require.NotNil(t, useSite, "use -> run must exist at the use-site line 4")
|
|
assert.Equal(t, macroExpansionVia, useSite.Meta["via"])
|
|
assert.Equal(t, SynthMacroExpansion, useSite.Meta[MetaSynthesizedBy])
|
|
assert.Equal(t, graph.OriginASTInferred, useSite.Origin)
|
|
assert.Equal(t, "CALL_M", useSite.Meta["macro"])
|
|
|
|
// No use -> run edge is attributed to the #define line (1) — the
|
|
// re-attribution is to the use site, not the definition.
|
|
assert.Nil(t, outCallEdge(g, "u.c::use", "run", 1),
|
|
"no use -> run edge at the #define line")
|
|
|
|
// Coexistence: the extractor's def-line edge (macro -> run, line 1)
|
|
// and the use-site placeholder (use -> CALL_M, line 4) are untouched.
|
|
assert.NotNil(t, outCallEdge(g, "u.c::CALL_M", "run", 1),
|
|
"def-line macro -> run edge survives")
|
|
assert.NotNil(t, outCallEdge(g, "u.c::use", "CALL_M", 4),
|
|
"use-site placeholder use -> CALL_M survives")
|
|
}
|
|
|
|
// Re-running the pass is idempotent: the edge key includes the line, so a
|
|
// second run lands the same owned count and adds no duplicate edge.
|
|
func TestMacroExpansion_Idempotent(t *testing.T) {
|
|
src := "#define LOG(m) write_log(m)\n" +
|
|
"void f(void) {\n" +
|
|
" LOG(1);\n" + // line 3
|
|
"}\n"
|
|
g := loadCSource(t, "m.c", src)
|
|
New(g).ResolveAll()
|
|
|
|
first := ResolveMacroExpansionCalls(g)
|
|
second := ResolveMacroExpansionCalls(g)
|
|
assert.Equal(t, first, second, "owned count stable across runs")
|
|
|
|
n := 0
|
|
for _, e := range g.GetOutEdges("m.c::f") {
|
|
if e.Kind == graph.EdgeCalls && graph.UnresolvedName(e.To) == "write_log" {
|
|
n++
|
|
}
|
|
}
|
|
assert.Equal(t, 1, n, "exactly one synthesized use-site edge, no duplicate")
|
|
}
|
|
|
|
// An object-like (non-parameterised) macro has no call use site, so the
|
|
// pass never fires for it even when its replacement names a call.
|
|
func TestMacroExpansion_SkipsObjectLikeMacro(t *testing.T) {
|
|
src := "#define WIDTH compute()\n" +
|
|
"void g(void) {\n" +
|
|
" int w = WIDTH;\n" + // line 3
|
|
"}\n"
|
|
g := loadCSource(t, "o.c", src)
|
|
New(g).ResolveAll()
|
|
|
|
assert.Equal(t, 0, ResolveMacroExpansionCalls(g),
|
|
"object-like macro use site is not a call_expression")
|
|
}
|
|
|
|
// Two function-like macros sharing a name in the same repo make any use
|
|
// of that name ambiguous; the pass refuses to guess and synthesizes
|
|
// nothing for it.
|
|
func TestMacroExpansion_AmbiguousNameSkipped(t *testing.T) {
|
|
g := graph.New()
|
|
// Two macros named DO, different bodies, both with recovered callees.
|
|
g.AddNode(&graph.Node{ID: "a.c::DO", Kind: graph.KindMacro, Name: "DO", FilePath: "a.c",
|
|
Meta: map[string]any{"macro_kind": "function"}})
|
|
g.AddEdge(&graph.Edge{From: "a.c::DO", To: "unresolved::alpha", Kind: graph.EdgeCalls,
|
|
FilePath: "a.c", Line: 1, Origin: graph.OriginASTInferred})
|
|
g.AddNode(&graph.Node{ID: "b.c::DO", Kind: graph.KindMacro, Name: "DO", FilePath: "b.c",
|
|
Meta: map[string]any{"macro_kind": "function"}})
|
|
g.AddEdge(&graph.Edge{From: "b.c::DO", To: "unresolved::beta", Kind: graph.EdgeCalls,
|
|
FilePath: "b.c", Line: 1, Origin: graph.OriginASTInferred})
|
|
// A caller invoking DO.
|
|
g.AddNode(&graph.Node{ID: "c.c::caller", Kind: graph.KindFunction, Name: "caller", FilePath: "c.c"})
|
|
g.AddEdge(&graph.Edge{From: "c.c::caller", To: "unresolved::DO", Kind: graph.EdgeCalls,
|
|
FilePath: "c.c", Line: 7})
|
|
|
|
assert.Equal(t, 0, ResolveMacroExpansionCalls(g),
|
|
"ambiguous macro name synthesizes nothing")
|
|
assert.Nil(t, outCallEdge(g, "c.c::caller", "alpha", 7))
|
|
assert.Nil(t, outCallEdge(g, "c.c::caller", "beta", 7))
|
|
}
|
|
|
|
// A use-site call edge landed directly on the macro node (rather than
|
|
// left as an `unresolved::<macro>` placeholder) is also re-attributed —
|
|
// the defensive second seam.
|
|
func TestMacroExpansion_UseSiteLandedOnMacroNode(t *testing.T) {
|
|
g := graph.New()
|
|
g.AddNode(&graph.Node{ID: "k.c::WRAP", Kind: graph.KindMacro, Name: "WRAP", FilePath: "k.c",
|
|
Meta: map[string]any{"macro_kind": "function"}})
|
|
g.AddEdge(&graph.Edge{From: "k.c::WRAP", To: "unresolved::inner", Kind: graph.EdgeCalls,
|
|
FilePath: "k.c", Line: 1, Origin: graph.OriginASTInferred})
|
|
g.AddNode(&graph.Node{ID: "k.c::caller", Kind: graph.KindFunction, Name: "caller", FilePath: "k.c"})
|
|
// Use site already bound to the macro node itself, at line 5.
|
|
g.AddEdge(&graph.Edge{From: "k.c::caller", To: "k.c::WRAP", Kind: graph.EdgeCalls,
|
|
FilePath: "k.c", Line: 5})
|
|
|
|
assert.Equal(t, 1, ResolveMacroExpansionCalls(g))
|
|
mint := outCallEdge(g, "k.c::caller", "inner", 5)
|
|
require.NotNil(t, mint, "caller -> inner minted at the use-site line 5")
|
|
assert.Equal(t, macroExpansionVia, mint.Meta["via"])
|
|
}
|
|
|
|
// A real edge already occupying the exact (from, to, file, line) identity
|
|
// slot is never overwritten or downgraded — the load-bearing invariant.
|
|
func TestMacroExpansion_DoesNotOverwriteRealEdge(t *testing.T) {
|
|
g := graph.New()
|
|
g.AddNode(&graph.Node{ID: "h.c::M", Kind: graph.KindMacro, Name: "M", FilePath: "h.c",
|
|
Meta: map[string]any{"macro_kind": "function"}})
|
|
g.AddNode(&graph.Node{ID: "h.c::run", Kind: graph.KindFunction, Name: "run", FilePath: "h.c"})
|
|
g.AddEdge(&graph.Edge{From: "h.c::M", To: "h.c::run", Kind: graph.EdgeCalls,
|
|
FilePath: "h.c", Line: 1, Origin: graph.OriginASTInferred})
|
|
g.AddNode(&graph.Node{ID: "h.c::caller", Kind: graph.KindFunction, Name: "caller", FilePath: "h.c"})
|
|
g.AddEdge(&graph.Edge{From: "h.c::caller", To: "unresolved::M", Kind: graph.EdgeCalls,
|
|
FilePath: "h.c", Line: 9})
|
|
// A pre-existing compiler-grade edge occupies the slot the pass would
|
|
// mint into (caller -> run at line 9).
|
|
g.AddEdge(&graph.Edge{From: "h.c::caller", To: "h.c::run", Kind: graph.EdgeCalls,
|
|
FilePath: "h.c", Line: 9, Origin: graph.OriginLSPResolved})
|
|
|
|
ResolveMacroExpansionCalls(g)
|
|
|
|
got := outCallEdge(g, "h.c::caller", "run", 9)
|
|
require.NotNil(t, got)
|
|
assert.Equal(t, graph.OriginLSPResolved, got.Origin,
|
|
"the real LSP edge is preserved, not downgraded")
|
|
_, stamped := got.Meta["via"]
|
|
assert.False(t, stamped, "the real edge is not stamped with macro-expansion provenance")
|
|
}
|