Files
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

265 lines
9.2 KiB
Go

package resolver
import (
"strings"
"testing"
"github.com/zzet/gortex/internal/graph"
)
// TestImportEvidence_DisambiguatesBareJSTSCalls drives the import-closure
// disambiguation through the real extract → resolve pipeline (the same
// harness as the cross-package guard tests). Each row pins one leg of the
// precedence contract documented in import_evidence.go.
func TestImportEvidence_DisambiguatesBareJSTSCalls(t *testing.T) {
// The library-side alias-cast export (`createStoreImpl as
// CreateStore`) lands as a variable/constant node — the value-callee
// shape (zustand's `persist`-style export) whose repo-wide ambiguity
// refuses every call edge without import evidence.
vanillaCastExport := `type CreateStore = (init?: unknown) => unknown;
const createStoreImpl = (init: unknown): unknown => init;
export const createStore = createStoreImpl as CreateStore;
export const other = 1;
`
cases := []struct {
name string
files map[string]string
callerID string
callLine int
// wantTo, when set, is the node the call MUST resolve to.
wantTo string
// forbidTo, when set, is a node the call must NOT resolve to.
forbidTo string
// wantUnresolved requires the edge to stay an `unresolved::`
// placeholder (ambiguity without import evidence still refuses).
wantUnresolved bool
}{
{
// Cross-dir ambiguity that used to refuse: the caller's
// explicit import of one candidate's file resolves it.
name: "import closure wins over ambiguity refusal",
files: map[string]string{
"src/vanilla.ts": vanillaCastExport,
"tests/shadow/helper.test.ts": `export const createStore = () => ({ local: true });
`,
"tests/basic.test.ts": `import { createStore } from '../src/vanilla';
function makeCounter(): unknown {
return createStore(() => ({ count: 0 }));
}
`,
},
callerID: "tests/basic.test.ts::makeCounter",
callLine: 3,
wantTo: "src/vanilla.ts::createStore",
forbidTo: "tests/shadow/helper.test.ts::createStore",
},
{
// A same-directory neighbour defining the name is NOT ambient
// scope in the ES module system — the explicit import of
// another candidate must beat the same-dir shadow the
// locality loop would otherwise bind.
name: "same-dir shadow loses to explicit import",
files: map[string]string{
"src/vanilla.ts": `export function createStore(init: unknown): unknown { return init; }
`,
"tests/util.ts": `export function createStore(): unknown { return { helper: true }; }
`,
"tests/a.test.ts": `import { createStore } from '../src/vanilla';
function setup(): unknown {
return createStore({});
}
`,
},
callerID: "tests/a.test.ts::setup",
callLine: 3,
wantTo: "src/vanilla.ts::createStore",
forbidTo: "tests/util.ts::createStore",
},
{
// No import evidence: cross-dir value-callee ambiguity keeps
// today's refusal — no arbitrary winner.
name: "no-import ambiguity still refuses",
files: map[string]string{
"src/vanilla.ts": vanillaCastExport,
"other/helper.ts": `export const createStore = () => ({ local: true });
`,
"app/main.ts": `function run(): unknown {
return createStore();
}
`,
},
callerID: "app/main.ts::run",
callLine: 2,
wantUnresolved: true,
},
{
// Both candidates' files are imported (one for an unrelated
// binding): the import statement alone cannot arbitrate, so
// the pick refuses exactly like the no-import case.
// Alias-cast exports (KindVariable) keep today's behaviour
// deterministic: the value-callee fallback refuses on
// repo-wide ambiguity.
name: "multiple imported candidates still refuse",
files: map[string]string{
"a/one.ts": `type MakeA = () => unknown;
const implA = () => ({ a: true });
export const createStore = implA as MakeA;
`,
"b/two.ts": `type MakeB = () => unknown;
const implB = () => ({ b: true });
export const createStore = implB as MakeB;
export const helper = () => 1;
`,
"app/main.ts": `import { createStore } from '../a/one';
import { helper } from '../b/two';
function run(): unknown {
helper();
return createStore();
}
`,
},
callerID: "app/main.ts::run",
callLine: 5,
wantUnresolved: true,
},
{
// A module-local definition blocks the import pick: the call
// binds to the caller file's own symbol even though another
// candidate's file is imported (for an unrelated binding).
name: "module-local definition beats import evidence",
files: map[string]string{
"src/vanilla.ts": vanillaCastExport,
"tests/local.test.ts": `import { other } from '../src/vanilla';
export function createStore(): unknown { return { local: other }; }
function setup(): unknown {
return createStore();
}
`,
},
callerID: "tests/local.test.ts::setup",
callLine: 4,
wantTo: "tests/local.test.ts::createStore",
forbidTo: "src/vanilla.ts::createStore",
},
{
// File-local tier: with NO import, the caller's own helper
// must win over a same-named helper in a neighbouring file
// of the same directory (candidate iteration order used to
// decide this — zustand's persistSync tests bound to
// persistAsync's helper).
name: "own-file helper beats same-dir neighbour shadow",
files: map[string]string{
"tests/sync.test.ts": `export function createStore(): unknown { return { sync: true }; }
function setup(): unknown {
return createStore();
}
`,
"tests/async.test.ts": `export function createStore(): unknown { return { async: true }; }
`,
},
callerID: "tests/sync.test.ts::setup",
callLine: 3,
wantTo: "tests/sync.test.ts::createStore",
forbidTo: "tests/async.test.ts::createStore",
},
{
// Barrel hop: importing a re-exporting module makes the
// re-exported module's symbols import-evidence too.
name: "re-export barrel hop carries the evidence",
files: map[string]string{
"src/vanilla.ts": vanillaCastExport,
"src/index.ts": `export { createStore } from './vanilla.ts';
`,
"tests/shadow/helper.test.ts": `export const createStore = () => ({ local: true });
`,
"tests/barrel.test.ts": `import { createStore } from '../src/index';
function setup(): unknown {
return createStore(() => ({}));
}
`,
},
callerID: "tests/barrel.test.ts::setup",
callLine: 3,
wantTo: "src/vanilla.ts::createStore",
forbidTo: "tests/shadow/helper.test.ts::createStore",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
g := buildGraphFromSources(t, tc.files)
New(g).ResolveAll()
got := callEdgeTo(g, tc.callerID, tc.callLine)
if got == "" {
t.Fatalf("no call edge found from %s at line %d", tc.callerID, tc.callLine)
}
if tc.wantTo != "" && got != tc.wantTo {
t.Errorf("call resolved to %q, want %q", got, tc.wantTo)
}
if tc.forbidTo != "" && got == tc.forbidTo {
t.Errorf("call mis-resolved to forbidden candidate %q", tc.forbidTo)
}
if tc.wantUnresolved && !strings.HasPrefix(got, "unresolved::") {
t.Errorf("call resolved to %q, expected it to stay unresolved", got)
}
})
}
}
// TestImportEvidence_ResolvedEdgeStampsProvenance asserts the winning edge
// carries the structural-evidence tier: ast_resolved origin and the
// import_closure resolution marker, so the cross-package guard (which only
// polices text_matched / ast_inferred) never reverts it.
func TestImportEvidence_ResolvedEdgeStampsProvenance(t *testing.T) {
g := buildGraphFromSources(t, map[string]string{
"src/vanilla.ts": `export function createStore(init: unknown): unknown { return init; }
`,
"tests/util.ts": `export function createStore(): unknown { return { helper: true }; }
`,
"tests/a.test.ts": `import { createStore } from '../src/vanilla';
function setup(): unknown {
return createStore({});
}
`,
})
New(g).ResolveAll()
for _, e := range g.GetOutEdges("tests/a.test.ts::setup") {
if e.Kind != graph.EdgeCalls || e.Line != 3 {
continue
}
if e.To != "src/vanilla.ts::createStore" {
t.Fatalf("call resolved to %q, want src/vanilla.ts::createStore", e.To)
}
if e.Origin != graph.OriginASTResolved {
t.Errorf("origin = %q, want %q", e.Origin, graph.OriginASTResolved)
}
if e.Meta == nil || e.Meta["resolution"] != "import_closure" {
t.Errorf("meta resolution = %v, want import_closure", e.Meta)
}
return
}
t.Fatalf("no call edge found from tests/a.test.ts::setup at line 3")
}
// TestPickImportEvidenceCallee_LanguageGate pins the correctness gate: the
// pick never runs for non-JS/TS callers, so Go (directory-scoped packages,
// where a bare call can never name another package's symbol) and Python
// (`import x` does not bring bare names into scope) resolution is
// bit-for-bit unchanged.
func TestPickImportEvidenceCallee_LanguageGate(t *testing.T) {
g := graph.New()
r := New(g)
candidates := []*graph.Node{
{ID: "pkgA/b.go::helper", Kind: graph.KindFunction, FilePath: "pkgA/b.go", Name: "helper"},
{ID: "pkgB/c.go::helper", Kind: graph.KindFunction, FilePath: "pkgB/c.go", Name: "helper"},
}
if pick := r.pickImportEvidenceCallee("pkgA/a.go", "helper", candidates); pick != nil {
t.Errorf("go caller: pick = %v, want nil (language gate)", pick.ID)
}
if pick := r.pickImportEvidenceCallee("app/mod.py", "helper", candidates); pick != nil {
t.Errorf("python caller: pick = %v, want nil (language gate)", pick.ID)
}
}