Files
zzet--gortex/internal/parser/languages/go_dataflow_qualifier_test.go
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

162 lines
5.1 KiB
Go

package languages
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/zzet/gortex/internal/graph"
)
// TestGoDataflow_SelectorCallPreservesPackageQualifier is the
// regression for the dataflow walker dropping the package qualifier
// on selector calls (`fmt.Sprintf`, `strings.Join`, `assert.True`)
// and leaking `unresolved::*.<method>` instead of the proper
// `unresolved::extern::<importPath>::<method>` shape the call
// extractor uses. The resolver's resolveExtern pass then lands
// these on stdlib::/dep::/external::, so without preserving the
// qualifier here every package-qualified call inside a dataflow
// context (argument source, return target, value flow) stays as
// an unresolved phantom.
func TestGoDataflow_SelectorCallPreservesPackageQualifier(t *testing.T) {
src := `package foo
import (
"fmt"
"strings"
)
func Handler(input string) string {
cleaned := strings.TrimSpace(input)
return fmt.Sprintf("got: %s", cleaned)
}
`
fix := runGoExtract(t, src)
// Every `unresolved::extern::<path>::<method>` target the
// dataflow walker emits must use the canonical import path,
// not the `*.method` collapsed form.
var hasStringsTrimSpace, hasFmtSprintf bool
for _, edges := range fix.edgesByKind {
for _, e := range edges {
switch e.To {
case "unresolved::extern::strings::TrimSpace":
hasStringsTrimSpace = true
case "unresolved::extern::fmt::Sprintf":
hasFmtSprintf = true
}
}
}
assert.True(t, hasStringsTrimSpace,
"dataflow walker must preserve the `strings` qualifier on TrimSpace(...) calls — got: %s",
dumpDataflowSelectorTargets(fix))
assert.True(t, hasFmtSprintf,
"dataflow walker must preserve the `fmt` qualifier on Sprintf(...) calls — got: %s",
dumpDataflowSelectorTargets(fix))
// And the collapsed `*.TrimSpace`/`*.Sprintf` shape must NOT
// appear for these calls.
for _, edges := range fix.edgesByKind {
for _, e := range edges {
assert.NotEqual(t, "unresolved::*.TrimSpace", e.To,
"package-qualified Trim should never land as `unresolved::*.TrimSpace`")
assert.NotEqual(t, "unresolved::*.Sprintf", e.To,
"package-qualified Sprintf should never land as `unresolved::*.Sprintf`")
}
}
}
// TestGoDataflow_NonImportedReceiverFallsBack ensures the pass
// doesn't false-positive: when the receiver is NOT a package alias
// (a local variable, a struct field), it must keep emitting the
// `unresolved::*.<method>` form so other passes can apply their
// own heuristics.
func TestGoDataflow_NonImportedReceiverFallsBack(t *testing.T) {
src := `package foo
type Buffer struct{}
func (b *Buffer) Write(p []byte) {}
func Run(buf *Buffer, data []byte) {
buf.Write(data)
}
`
fix := runGoExtract(t, src)
// `buf.Write(data)` — buf is a parameter, NOT an import; the
// walker's fallback path must keep `*.` (the call extractor's
// own path already records receiver_type on the call edge).
var seen bool
for _, edges := range fix.edgesByKind {
for _, e := range edges {
if e.To == "unresolved::*.Write" {
seen = true
}
assert.NotEqual(t, "unresolved::extern::buf::Write", e.To,
"`buf` is a parameter — must not be classified as a package alias")
}
}
assert.True(t, seen, "the walker must still emit `unresolved::*.Write` for non-import receivers; "+
"got: %s", dumpDataflowSelectorTargets(fix))
}
func dumpDataflowSelectorTargets(fix *extractedFixture) string {
var b strings.Builder
for _, edges := range fix.edgesByKind {
for _, e := range edges {
if strings.Contains(e.To, "Sprintf") || strings.Contains(e.To, "TrimSpace") || strings.Contains(e.To, "Write") {
b.WriteString("\n [" + string(e.Kind) + "] " + e.From + " -> " + e.To)
}
}
}
return b.String()
}
// guard: also verifies the same fix applies in exprSources (not just
// calleeRef) — a selector accessed as a value (not invoked) should
// also preserve its qualifier. Uses a real stdlib import so the
// extractor's emitImport handler matches its production code path.
func TestGoDataflow_SelectorValuePreservesQualifier(t *testing.T) {
src := `package foo
import "os"
func DefaultPerm() any {
return os.ModePerm
}
`
fix := runGoExtract(t, src)
_ = graph.KindFunction
var foundProperShape bool
for _, edges := range fix.edgesByKind {
for _, e := range edges {
// handleReturn emits `From: src, To: owner` — flow goes
// FROM the value source TO the function's owner. So the
// qualified target lives on e.From, not e.To.
if strings.HasPrefix(e.From, "unresolved::extern::os::") ||
strings.HasPrefix(e.To, "unresolved::extern::os::") {
foundProperShape = true
}
}
}
assert.True(t, foundProperShape,
"selector-value access (os.ModePerm) must emit the extern:: shape; got:\n%s",
dumpAllSelectorish(fix))
}
func dumpAllSelectorish(fix *extractedFixture) string {
var b strings.Builder
for _, edges := range fix.edgesByKind {
for _, e := range edges {
if strings.Contains(e.To, "ModePerm") || strings.Contains(e.To, "::os::") || strings.HasPrefix(e.To, "unresolved::*.") {
b.WriteString(" [" + string(e.Kind) + "] " + e.From + " -> " + e.To + "\n")
}
}
}
return b.String()
}