Files
zzet--gortex/internal/indexer/tsconfig_path_alias_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

151 lines
6.2 KiB
Go

package indexer
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"github.com/zzet/gortex/internal/graph"
)
// writeFileMk writes content to path, creating parent directories first.
func writeFileMk(t *testing.T, path, content string) {
t.Helper()
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
writeFile(t, path, content)
}
// callTargetForName returns the EdgeCalls target of the named function in
// the given file, or "" when the call edge is unresolved / absent.
func callTargetForName(t *testing.T, g graph.Store, file, name string) string {
t.Helper()
id := fnNodeID(t, g, file, name)
for _, e := range g.GetOutEdges(id) {
if e.Kind == graph.EdgeCalls {
return e.To
}
}
return ""
}
// TestTSConfigPathAlias_CallGraph reproduces issue #136: a call to a
// function imported through a tsconfig `paths` alias (`@/lib/auth`) must
// bind the same as the relative-import callers. Same-directory relative
// callers resolve trivially (same package); cross-directory relative
// callers resolve through the post-resolution import closure; the alias
// caller must resolve through the SAME closure once the alias specifier
// is expanded to its real file.
func TestTSConfigPathAlias_CallGraph(t *testing.T) {
dir := t.TempDir()
writeFileMk(t, filepath.Join(dir, "tsconfig.json"),
"{\n \"compilerOptions\": {\n \"baseUrl\": \".\",\n \"paths\": { \"@/*\": [\"src/*\"] }\n }\n}\n")
writeFileMk(t, filepath.Join(dir, "src", "lib", "auth.ts"),
"export function getAuth() { return null }\n")
writeFileMk(t, filepath.Join(dir, "src", "lib", "mid.ts"),
"import { getAuth } from './auth'\nexport function viaRelativeSameDir() { return getAuth() }\n")
writeFileMk(t, filepath.Join(dir, "src", "app", "cross.ts"),
"import { getAuth } from '../lib/auth'\nexport function viaRelativeCrossDir() { return getAuth() }\n")
writeFileMk(t, filepath.Join(dir, "src", "app", "route.ts"),
"import { getAuth } from '@/lib/auth'\nexport function viaAlias() { return getAuth() }\n")
idx, _ := newSQLiteIndexer(t)
_, err := idx.Index(dir)
require.NoError(t, err)
idx.ResolveAll()
g := idx.Graph()
const want = "src/lib/auth.ts::getAuth"
require.Equal(t, want, callTargetForName(t, g, "src/lib/mid.ts", "viaRelativeSameDir"),
"same-dir relative import must bind")
require.Equal(t, want, callTargetForName(t, g, "src/app/cross.ts", "viaRelativeCrossDir"),
"cross-dir relative import must bind through the import closure")
require.Equal(t, want, callTargetForName(t, g, "src/app/route.ts", "viaAlias"),
"tsconfig paths alias import must bind the same as a relative import (issue #136)")
// The per-binding import edges must also land on the symbol so
// find_usages / get_callers can answer "who imports getAuth". All
// three importers should now appear as incoming edges.
importerFiles := map[string]bool{}
for _, e := range g.GetInEdges(want) {
if e.Kind == graph.EdgeImports {
importerFiles[e.From] = true
}
}
for _, f := range []string{"src/lib/mid.ts", "src/app/cross.ts", "src/app/route.ts"} {
require.True(t, importerFiles[f],
"per-binding import edge from %s must resolve onto getAuth (find_usages)", f)
}
}
// TestTSConfigPathAlias_Incremental proves the alias binding survives an
// incremental single-file reindex — the resolution runs inside
// resolveImport, so the per-file ResolveFile path lands it the same as a
// cold ResolveAll.
func TestTSConfigPathAlias_Incremental(t *testing.T) {
dir := t.TempDir()
writeFileMk(t, filepath.Join(dir, "tsconfig.json"),
"{\n \"compilerOptions\": {\n \"baseUrl\": \".\",\n \"paths\": { \"@/*\": [\"src/*\"] }\n }\n}\n")
writeFileMk(t, filepath.Join(dir, "src", "lib", "auth.ts"),
"export function getAuth() { return null }\n")
routePath := filepath.Join(dir, "src", "app", "route.ts")
writeFileMk(t, routePath,
"import { getAuth } from '@/lib/auth'\nexport function viaAlias() { return getAuth() }\n")
idx, _ := newSQLiteIndexer(t)
_, err := idx.Index(dir)
require.NoError(t, err)
idx.ResolveAll()
g := idx.Graph()
const want = "src/lib/auth.ts::getAuth"
require.Equal(t, want, callTargetForName(t, g, "src/app/route.ts", "viaAlias"),
"baseline: alias import must bind after cold index")
// Re-edit route.ts (add a second alias call) and reindex just that file.
bumpMtime(t, routePath,
"import { getAuth } from '@/lib/auth'\nexport function viaAlias() { getAuth(); return getAuth() }\n")
_, err = idx.IncrementalReindexPaths(dir, []string{routePath})
require.NoError(t, err)
require.Equal(t, want, callTargetForName(t, g, "src/app/route.ts", "viaAlias"),
"alias import must stay bound after an incremental single-file reindex")
}
// TestTSConfigPathAlias_BaseURLBarrelAndExact exercises the remaining
// tsconfig shapes: a wildcard alias whose targets are rooted at a non-"."
// baseUrl and resolve to a directory barrel (`index.ts`), and a
// non-wildcard exact alias.
func TestTSConfigPathAlias_BaseURLBarrelAndExact(t *testing.T) {
dir := t.TempDir()
writeFileMk(t, filepath.Join(dir, "tsconfig.json"),
"{\n \"compilerOptions\": {\n \"baseUrl\": \"src\",\n \"paths\": {\n"+
" \"@/*\": [\"*\"],\n \"@config\": [\"lib/config/index.ts\"]\n }\n }\n}\n")
// Directory barrel reached through `@/lib/api` -> src/lib/api/index.ts.
writeFileMk(t, filepath.Join(dir, "src", "lib", "api", "index.ts"),
"export function callApi() { return 1 }\n")
// Exact (non-wildcard) alias `@config` -> src/lib/config/index.ts.
writeFileMk(t, filepath.Join(dir, "src", "lib", "config", "index.ts"),
"export function loadConfig() { return 2 }\n")
writeFileMk(t, filepath.Join(dir, "src", "app", "page.ts"),
"import { callApi } from '@/lib/api'\n"+
"import { loadConfig } from '@config'\n"+
"export function useBarrel() { return callApi() }\n"+
"export function useExact() { return loadConfig() }\n")
idx, _ := newSQLiteIndexer(t)
_, err := idx.Index(dir)
require.NoError(t, err)
idx.ResolveAll()
g := idx.Graph()
require.Equal(t, "src/lib/api/index.ts::callApi",
callTargetForName(t, g, "src/app/page.ts", "useBarrel"),
"wildcard alias rooted at baseUrl must resolve to the directory index barrel")
require.Equal(t, "src/lib/config/index.ts::loadConfig",
callTargetForName(t, g, "src/app/page.ts", "useExact"),
"non-wildcard exact alias must resolve")
}