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
265 lines
9.2 KiB
Go
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)
|
|
}
|
|
}
|