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
219 lines
6.4 KiB
Go
219 lines
6.4 KiB
Go
package languages
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
"github.com/zzet/gortex/internal/parser"
|
|
)
|
|
|
|
// fnValueCands returns the captured function-as-value placeholder edges, keyed
|
|
// by captured name → its Meta.
|
|
func fnValueCands(res *parser.ExtractionResult) map[string]map[string]any {
|
|
out := map[string]map[string]any{}
|
|
for _, e := range res.Edges {
|
|
if e.Kind != graph.EdgeReferences || e.Meta == nil {
|
|
continue
|
|
}
|
|
if v, _ := e.Meta["via"].(string); v != "callback_candidate" {
|
|
continue
|
|
}
|
|
name, _ := e.Meta["fn_value_name"].(string)
|
|
out[name] = e.Meta
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestFnRefSpecFor(t *testing.T) {
|
|
if !fnRefSpecFor("rust").ungated {
|
|
t.Error("rust spec should be ungated (qualified paths)")
|
|
}
|
|
if !fnRefSpecFor("go").matchesIDNode("selector_expression") {
|
|
t.Error("go spec should match selector_expression")
|
|
}
|
|
if !fnRefSpecFor("swift").matchesIDNode("simple_identifier") {
|
|
t.Error("swift spec should match simple_identifier")
|
|
}
|
|
if !fnRefSpecFor("python").matchesIDNode("identifier") || fnRefSpecFor("python").ungated {
|
|
t.Error("default spec is bare-identifier, gated")
|
|
}
|
|
}
|
|
|
|
// TestFnValueCapture_GoSelectorMethodValue pins capture of a Go method passed by
|
|
// value through a selector (`t.Handle`), which the bare-identifier walk misses
|
|
// (the field is a field_identifier, not an identifier).
|
|
func TestFnValueCapture_GoSelectorMethodValue(t *testing.T) {
|
|
src := []byte(`package main
|
|
|
|
type T struct{}
|
|
|
|
func (t T) Handle() {}
|
|
|
|
func register(t T) {
|
|
cb := t.Handle
|
|
_ = cb
|
|
}
|
|
`)
|
|
res, err := NewGoExtractor().Extract("main.go", src)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cands := fnValueCands(res)
|
|
meta, ok := cands["Handle"]
|
|
if !ok {
|
|
t.Fatalf("Go selector method-value not captured (got: %v)", keys(cands))
|
|
}
|
|
if meta["fn_ref_lang"] != "go" {
|
|
t.Errorf("fn_ref_lang = %v, want go", meta["fn_ref_lang"])
|
|
}
|
|
// The receiver var `t` and the called declaration must not be captured.
|
|
if _, bad := cands["t"]; bad {
|
|
t.Error("receiver var t should not be captured")
|
|
}
|
|
}
|
|
|
|
// TestFnValueCapture_RustScopedCrossModule pins capture of a qualified Rust path
|
|
// value (`other::process`) as an ungated cross-module candidate.
|
|
func TestFnValueCapture_RustScopedCrossModule(t *testing.T) {
|
|
src := []byte(`fn run() {
|
|
let cb = other::process;
|
|
let _ = cb;
|
|
}
|
|
`)
|
|
res, err := NewRustExtractor().Extract("main.rs", src)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cands := fnValueCands(res)
|
|
meta, ok := cands["process"]
|
|
if !ok {
|
|
t.Fatalf("Rust scoped path value not captured (got: %v)", keys(cands))
|
|
}
|
|
if meta["fn_value_ungated"] != true {
|
|
t.Errorf("scoped cross-module path should be ungated (got: %v)", meta)
|
|
}
|
|
}
|
|
|
|
// TestFnValueCapture_BareIdentifierStillWorks pins the pre-existing bare-name
|
|
// capture (a same-file function passed by value) is unchanged, and a local of
|
|
// the same shape is not captured.
|
|
func TestFnValueCapture_BareIdentifierStillWorks(t *testing.T) {
|
|
src := []byte(`package main
|
|
|
|
func handler() {}
|
|
|
|
func register() {
|
|
cb := handler
|
|
other := cb
|
|
_ = other
|
|
}
|
|
`)
|
|
res, err := NewGoExtractor().Extract("main.go", src)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cands := fnValueCands(res)
|
|
if _, ok := cands["handler"]; !ok {
|
|
t.Fatalf("bare same-file function value not captured (got: %v)", keys(cands))
|
|
}
|
|
if _, bad := cands["cb"]; bad {
|
|
t.Error("local variable cb should not be captured")
|
|
}
|
|
}
|
|
|
|
func keys(m map[string]map[string]any) []string {
|
|
var out []string
|
|
for k := range m {
|
|
out = append(out, k)
|
|
}
|
|
return []string{strings.Join(out, ",")}
|
|
}
|
|
|
|
// TestFnRefSpecFor_MemberValueForms pins the broadened per-language table: the
|
|
// member/attribute value forms and the astCalleeField dispatch tier.
|
|
func TestFnRefSpecFor_MemberValueForms(t *testing.T) {
|
|
js := fnRefSpecFor("javascript")
|
|
if !js.matchesIDNode("member_expression") || js.dispatch != astCalleeField {
|
|
t.Error("javascript spec should match member_expression with astCalleeField dispatch")
|
|
}
|
|
if !fnRefSpecFor("python").matchesIDNode("attribute") {
|
|
t.Error("python spec should match attribute")
|
|
}
|
|
if !fnRefSpecFor("csharp").matchesIDNode("member_access_expression") {
|
|
t.Error("csharp spec should match member_access_expression")
|
|
}
|
|
if fnRefSpecFor("java").dispatch != astCalleeField {
|
|
t.Error("java spec should use astCalleeField dispatch")
|
|
}
|
|
for _, nt := range []string{"member_expression", "attribute", "member_access_expression", "scoped_identifier", "selector_expression"} {
|
|
if !isQualifiedFnRefNode(nt) {
|
|
t.Errorf("%s should be a qualified fn-ref node", nt)
|
|
}
|
|
}
|
|
if isQualifiedFnRefNode("identifier") {
|
|
t.Error("bare identifier is not a qualified fn-ref node")
|
|
}
|
|
}
|
|
|
|
// TestFnValueCapture_JSMemberValue pins capture of a same-file function passed
|
|
// by value through a member expression (`bus.onReady`), gated on the file
|
|
// declaring `onReady`. The called member `subscribe` must not be captured.
|
|
func TestFnValueCapture_JSMemberValue(t *testing.T) {
|
|
src := []byte(`function onReady() {}
|
|
|
|
function setup(bus) {
|
|
bus.subscribe(bus.onReady);
|
|
}
|
|
`)
|
|
res, err := NewJavaScriptExtractor().Extract("app.js", src)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cands := fnValueCands(res)
|
|
if _, ok := cands["onReady"]; !ok {
|
|
t.Fatalf("JS member-expression method value not captured (got: %v)", keys(cands))
|
|
}
|
|
if _, bad := cands["subscribe"]; bad {
|
|
t.Error("the called member `subscribe` must not be captured")
|
|
}
|
|
}
|
|
|
|
// TestFnValueCapture_ASTCalleeRejectsOptionalCall pins the astCalleeField
|
|
// precision win: `tick?.()` is a call (the callee child of a call node), so the
|
|
// byte heuristic's would-be value candidate is rejected.
|
|
func TestFnValueCapture_ASTCalleeRejectsOptionalCall(t *testing.T) {
|
|
src := []byte(`function tick() {}
|
|
|
|
function run() {
|
|
tick?.();
|
|
}
|
|
`)
|
|
res, err := NewJavaScriptExtractor().Extract("app.js", src)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, bad := fnValueCands(res)["tick"]; bad {
|
|
t.Error("optional-call `tick?.()` must not be captured as a function value")
|
|
}
|
|
}
|
|
|
|
// TestFnValueCapture_PythonAttributeValue pins capture of a same-file function
|
|
// passed by value through a non-self attribute (`mod.on_change`).
|
|
func TestFnValueCapture_PythonAttributeValue(t *testing.T) {
|
|
src := []byte(`def on_change():
|
|
pass
|
|
|
|
def setup(signal, mod):
|
|
signal.connect(mod.on_change)
|
|
`)
|
|
res, err := NewPythonExtractor().Extract("app.py", src)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cands := fnValueCands(res)
|
|
if _, ok := cands["on_change"]; !ok {
|
|
t.Fatalf("Python attribute method value not captured (got: %v)", keys(cands))
|
|
}
|
|
}
|