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
255 lines
10 KiB
Go
255 lines
10 KiB
Go
package languages
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// cValueRefCandidates returns the function-as-value candidates the C extractor
|
|
// captured for src — the placeholder edges (via=callback_candidate) awaiting the
|
|
// resolver gate — as a name → source-line map. It is the capture-side view a
|
|
// generated command table exercises before any resolution runs.
|
|
func cValueRefCandidates(t *testing.T, path, src string) map[string]int {
|
|
t.Helper()
|
|
r, err := NewCExtractor().Extract(path, []byte(src))
|
|
require.NoError(t, err)
|
|
out := map[string]int{}
|
|
for _, e := range r.Edges {
|
|
if e.Meta == nil {
|
|
continue
|
|
}
|
|
if v, _ := e.Meta["via"].(string); v != "callback_candidate" {
|
|
continue
|
|
}
|
|
if name, _ := e.Meta["fn_value_name"].(string); name != "" {
|
|
out[name] = e.Line
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// TestCExtractor_CommandTableMacroCandidates pins the redis command-table shape:
|
|
// a `MAKE_CMD(..., handler, ...)` macro row in a generated `.def` fragment
|
|
// captures the handler as a function-value candidate, while the string summary,
|
|
// the ALL_CAPS flag macro, the short arity counts, and the MAKE_CMD callee
|
|
// itself do not become references.
|
|
func TestCExtractor_CommandTableMacroCandidates(t *testing.T) {
|
|
src := "" +
|
|
"struct redisCommand redisCommandTable[] = {\n" + // line 1
|
|
"{MAKE_CMD(\"get\", \"Get the value\", -2, CMD_READONLY, getCommand, 1, 2)},\n" + // line 2
|
|
"{MAKE_CMD(\"strlen\", \"String length\", 2, CMD_READONLY, strlenCommand, 1, 2)},\n" + // line 3
|
|
"};\n"
|
|
cands := cValueRefCandidates(t, "commands.def", src)
|
|
|
|
assert.Equal(t, 2, cands["getCommand"], "handler in a macro arg list is captured")
|
|
assert.Equal(t, 3, cands["strlenCommand"], "handler in a macro arg list is captured")
|
|
|
|
assert.NotContains(t, cands, "CMD_READONLY", "an ALL_CAPS flag macro is not a function reference")
|
|
assert.NotContains(t, cands, "MAKE_CMD", "the macro callee is a call, not a value reference")
|
|
assert.NotContains(t, cands, "get", "a short string-literal table key is not an identifier reference")
|
|
assert.NotContains(t, cands, "Get the value", "a string literal is never captured")
|
|
}
|
|
|
|
// TestCExtractor_DispatchTableInitListCandidates covers the classic positional
|
|
// initializer-list dispatch table `{ "name", fnPtr, arity }` — the handler in
|
|
// the second slot is captured as a function-value candidate.
|
|
func TestCExtractor_DispatchTableInitListCandidates(t *testing.T) {
|
|
src := "" +
|
|
"struct cmd table[] = {\n" + // line 1
|
|
"{ \"ping\", pingCommand, 2 },\n" + // line 2
|
|
"{ \"echo\", echoCommand, 2 },\n" + // line 3
|
|
"};\n"
|
|
cands := cValueRefCandidates(t, "table.c", src)
|
|
|
|
assert.Equal(t, 2, cands["pingCommand"], "handler in an initializer-list slot is captured")
|
|
assert.Equal(t, 3, cands["echoCommand"], "handler in an initializer-list slot is captured")
|
|
assert.NotContains(t, cands, "ping", "a string key is not an identifier reference")
|
|
}
|
|
|
|
// TestCExtractor_CommandTableNoiseNotCaptured is the negative precision pin: the
|
|
// three noise classes the guard exists for — ALL_CAPS macros / enum constants,
|
|
// sub-4-character identifiers, and string literals — never become candidates,
|
|
// while the one mixed-case, long-enough handler in the same row does.
|
|
func TestCExtractor_CommandTableNoiseNotCaptured(t *testing.T) {
|
|
src := "" +
|
|
"struct redisCommand t[] = {\n" + // line 1
|
|
"{MAKE_CMD(\"noise\", ARG_TYPE_KEY, CMD_WRITE, run, xy, realCommand)},\n" + // line 2
|
|
"};\n"
|
|
cands := cValueRefCandidates(t, "n.def", src)
|
|
|
|
assert.Contains(t, cands, "realCommand", "the mixed-case, long-enough handler is captured")
|
|
assert.NotContains(t, cands, "ARG_TYPE_KEY", "an ALL_CAPS enum constant is filtered")
|
|
assert.NotContains(t, cands, "CMD_WRITE", "an ALL_CAPS flag macro is filtered")
|
|
assert.NotContains(t, cands, "run", "a sub-4-character identifier is filtered")
|
|
assert.NotContains(t, cands, "xy", "a two-character identifier is filtered")
|
|
}
|
|
|
|
// TestCExtractor_CommandTableErrorRecovery pins that a malformed generated
|
|
// fragment (a missing close paren plus a line of garbage — the shape a `.def`
|
|
// degrades to when its surrounding translation-unit context is absent) does not
|
|
// suppress extraction: the extractor recovers the handlers whose argument lists
|
|
// still parse rather than emitting nothing.
|
|
func TestCExtractor_CommandTableErrorRecovery(t *testing.T) {
|
|
src := "" +
|
|
"MAKE_CMD(\"get\", getCommand, 2)\n" + // line 1: clean
|
|
"MAKE_CMD(\"strlen\", strlenCommand, 2\n" + // line 2: missing close paren
|
|
"%%% garbage !!! {{{ nonsense\n" + // line 3: junk
|
|
"MAKE_CMD(\"append\", appendCommand, 3)\n" // line 4
|
|
|
|
r, err := NewCExtractor().Extract("broken.def", []byte(src))
|
|
require.NoError(t, err, "extraction must not fail on an ERROR-recovered tree")
|
|
|
|
cands := map[string]bool{}
|
|
for _, e := range r.Edges {
|
|
if e.Meta == nil {
|
|
continue
|
|
}
|
|
if v, _ := e.Meta["via"].(string); v != "callback_candidate" {
|
|
continue
|
|
}
|
|
if name, _ := e.Meta["fn_value_name"].(string); name != "" {
|
|
cands[name] = true
|
|
}
|
|
}
|
|
assert.True(t, cands["getCommand"], "the cleanly-parsed row before the error survives")
|
|
assert.True(t, cands["strlenCommand"], "the handler still in an arg list after the missing paren survives recovery")
|
|
}
|
|
|
|
// TestCExtractor_LexerLookaheadFilteredAndDeduped pins the generated-lexer flood
|
|
// shape. An uninitialised local (`int32_t lookahead;`) compared on ==/!= across
|
|
// many lines is a local, never a function address, so it yields no candidate; a
|
|
// genuinely-undeclared free name compared the same way collapses to exactly one
|
|
// candidate regardless of how many lines mention it (the per-name dedup).
|
|
func TestCExtractor_LexerLookaheadFilteredAndDeduped(t *testing.T) {
|
|
src := "" +
|
|
"void lexScan(void) {\n" + // line 1
|
|
" int32_t lookahead;\n" + // line 2: uninitialised local
|
|
" if (lookahead == 65) return;\n" + // 3
|
|
" if (lookahead != 66) return;\n" + // 4
|
|
" if (lookahead == 67) return;\n" + // 5
|
|
" if (externScanHook == 68) return;\n" + // 6: undeclared free name
|
|
" if (externScanHook != 69) return;\n" + // 7
|
|
" if (externScanHook == 70) return;\n" + // 8
|
|
"}\n"
|
|
r, err := NewCExtractor().Extract("lexer.c", []byte(src))
|
|
require.NoError(t, err)
|
|
|
|
counts := map[string]int{}
|
|
for _, e := range r.Edges {
|
|
if e.Meta == nil {
|
|
continue
|
|
}
|
|
if v, _ := e.Meta["via"].(string); v != "callback_candidate" {
|
|
continue
|
|
}
|
|
if name, _ := e.Meta["fn_value_name"].(string); name != "" {
|
|
counts[name]++
|
|
}
|
|
}
|
|
assert.Zero(t, counts["lookahead"], "an uninitialised local compared on ==/!= is never a function-address candidate")
|
|
assert.Equal(t, 1, counts["externScanHook"], "an undeclared free name on N comparison lines collapses to one candidate")
|
|
}
|
|
|
|
// TestCExtractor_EnumMembersNotCaptured pins the generated-parser enum flood: an
|
|
// in-file anonymous enum's members, referenced from a designated-initializer
|
|
// action table, are compile-time constants — never function addresses — so none
|
|
// becomes a candidate, while a genuine cross-TU handler in the same table does.
|
|
func TestCExtractor_EnumMembersNotCaptured(t *testing.T) {
|
|
src := "" +
|
|
"enum { sym_alpha, sym_beta, sym_gamma };\n" + // line 1
|
|
"void *actionTable[] = {\n" + // line 2
|
|
" [0] = sym_alpha,\n" + // 3
|
|
" [1] = sym_beta,\n" + // 4
|
|
" [2] = sym_gamma,\n" + // 5
|
|
" [3] = reduceAction,\n" + // 6: a real cross-TU handler
|
|
"};\n"
|
|
cands := cValueRefCandidates(t, "parser.c", src)
|
|
|
|
assert.NotContains(t, cands, "sym_alpha", "an in-file enum member is not a function reference")
|
|
assert.NotContains(t, cands, "sym_beta", "an in-file enum member is not a function reference")
|
|
assert.NotContains(t, cands, "sym_gamma", "an in-file enum member is not a function reference")
|
|
assert.Contains(t, cands, "reduceAction", "a genuine handler in the same table is still captured")
|
|
}
|
|
|
|
// TestCExtractor_PerFileCandidateFuse pins the per-file fuse: a pathological
|
|
// generated file with more distinct value-position handlers than the cap emits
|
|
// exactly the cap — the bound that stops a generated dispatch table from
|
|
// exploding the placeholder set.
|
|
func TestCExtractor_PerFileCandidateFuse(t *testing.T) {
|
|
var b strings.Builder
|
|
b.WriteString("void *bigDispatch[] = {\n")
|
|
for i := 0; i < cFnAddressMaxPerFile+200; i++ {
|
|
fmt.Fprintf(&b, " handlerFn%05d,\n", i)
|
|
}
|
|
b.WriteString("};\n")
|
|
|
|
r, err := NewCExtractor().Extract("generated.c", []byte(b.String()))
|
|
require.NoError(t, err)
|
|
|
|
n := 0
|
|
for _, e := range r.Edges {
|
|
if e.Meta == nil {
|
|
continue
|
|
}
|
|
if v, _ := e.Meta["via"].(string); v == "callback_candidate" {
|
|
n++
|
|
}
|
|
}
|
|
assert.Equal(t, cFnAddressMaxPerFile, n, "the per-file fuse caps emitted candidates at the const bound")
|
|
}
|
|
|
|
// TestCExtractor_DispatchTableCandidateFields pins the candidate metadata a
|
|
// realistic cross-TU dispatch table produces: each handler is captured exactly
|
|
// once (dedup), marked ungated so the resolver binds it cross-translation-unit,
|
|
// tagged with the capturing grammar, and carries no address-of form (a plain
|
|
// value slot).
|
|
func TestCExtractor_DispatchTableCandidateFields(t *testing.T) {
|
|
src := "" +
|
|
"struct cmd table[] = {\n" + // line 1
|
|
"{ \"get\", getCommand, 2 },\n" + // 2
|
|
"{ \"set\", setCommand, 3 },\n" + // 3
|
|
"};\n"
|
|
r, err := NewCExtractor().Extract("dispatch.c", []byte(src))
|
|
require.NoError(t, err)
|
|
|
|
type capture struct {
|
|
count int
|
|
ungated bool
|
|
form string
|
|
lang string
|
|
}
|
|
got := map[string]capture{}
|
|
for _, e := range r.Edges {
|
|
if e.Meta == nil {
|
|
continue
|
|
}
|
|
if v, _ := e.Meta["via"].(string); v != "callback_candidate" {
|
|
continue
|
|
}
|
|
name, _ := e.Meta["fn_value_name"].(string)
|
|
if name == "" {
|
|
continue
|
|
}
|
|
c := got[name]
|
|
c.count++
|
|
if u, _ := e.Meta["fn_value_ungated"].(bool); u {
|
|
c.ungated = true
|
|
}
|
|
c.form, _ = e.Meta["fn_ref_form"].(string)
|
|
c.lang, _ = e.Meta["fn_ref_lang"].(string)
|
|
got[name] = c
|
|
}
|
|
|
|
for _, name := range []string{"getCommand", "setCommand"} {
|
|
c := got[name]
|
|
assert.Equal(t, 1, c.count, "%s is captured exactly once", name)
|
|
assert.True(t, c.ungated, "%s is ungated for cross-TU binding", name)
|
|
assert.Equal(t, "", c.form, "%s sits in a plain value position (no address-of form)", name)
|
|
assert.Equal(t, "c", c.lang, "%s is tagged with the capturing grammar", name)
|
|
}
|
|
}
|