Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion internal/codegen/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,11 +158,13 @@ func Scan(source []byte) Marker {
//
// The returned edge slice is appended by the caller; the file node
// has meta.generated stamped by the caller using the returned Marker.
// filePath's spelling is preserved verbatim — the edge endpoint must
// match the extractor's file-node ID spelling (OS-native separators
// on Windows) or the edge dangles.
func BuildGraphArtifacts(filePath string, marker Marker) []*graph.Edge {
if !marker.Generated {
return nil
}
filePath = filepath.ToSlash(filePath)
target := generatorNodeID(marker)
return []*graph.Edge{{
From: filePath,
Expand Down
20 changes: 20 additions & 0 deletions internal/codegen/scanner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,26 @@ func TestScan_Variants(t *testing.T) {
}
}

func TestBuildGraphArtifacts_PreservesCallerPathSpelling(t *testing.T) {
// The indexer keys eviction and incremental replacement by the
// exact relPath spelling it hands the builder; a re-spelled edge
// endpoint dangles from a nonexistent file node on Windows.
// Written out, not composed with filepath.Join: on a POSIX runner
// Join yields exactly what the pre-fix ToSlash call returned, so
// the assertion would hold with or without the fix.
const rel = `src\gen\foo.pb.go`
edges := BuildGraphArtifacts(rel, Marker{Generated: true, Tool: "protoc-gen-go"})
if len(edges) != 1 {
t.Fatalf("edges = %d", len(edges))
}
if edges[0].From != rel {
t.Errorf("edge.From = %q, want %q", edges[0].From, rel)
}
if edges[0].FilePath != rel {
t.Errorf("edge file path = %q, want %q", edges[0].FilePath, rel)
}
}

func TestBuildGraphArtifacts(t *testing.T) {
t.Run("with source path", func(t *testing.T) {
edges := BuildGraphArtifacts("pkg/foo.pb.go", Marker{
Expand Down
5 changes: 3 additions & 2 deletions internal/codeowners/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,12 +142,13 @@ func LoadFromRepo(repoRoot string) (rules []Rule, sourcePath string, ok bool) {
// an email) is a person.
//
// filePath is the unprefixed path; applyRepoPrefix downstream
// handles multi-repo namespacing.
// handles multi-repo namespacing. Its spelling is preserved verbatim —
// the edge endpoint must match the extractor's file-node ID spelling
// (OS-native separators on Windows) or the edge dangles.
func BuildGraphArtifacts(filePath string, owners []string, language string) ([]*graph.Node, []*graph.Edge) {
if len(owners) == 0 {
return nil, nil
}
filePath = filepath.ToSlash(filePath)
nodes := make([]*graph.Node, 0, len(owners))
edges := make([]*graph.Edge, 0, len(owners))
for _, owner := range owners {
Expand Down
23 changes: 23 additions & 0 deletions internal/codeowners/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,26 @@ func TestBuildGraphArtifacts(t *testing.T) {
t.Errorf("edge endpoints wrong: %s -> %s", edges[0].From, edges[0].To)
}
}

func TestBuildGraphArtifacts_PreservesCallerPathSpelling(t *testing.T) {
// The indexer keys eviction and incremental replacement by the
// exact relPath spelling it hands the builder; a re-spelled edge
// endpoint dangles from a nonexistent file node on Windows.
// Written out, not composed with filepath.Join: on a POSIX runner
// Join yields exactly what the pre-fix ToSlash call returned, so
// the assertion would hold with or without the fix.
const rel = `src\data\foo.go`
nodes, edges := BuildGraphArtifacts(rel, []string{"@alice"}, "go")
if len(nodes) != 1 || len(edges) != 1 {
t.Fatalf("nodes = %d, edges = %d", len(nodes), len(edges))
}
if nodes[0].FilePath != rel {
t.Errorf("node file path = %q, want %q", nodes[0].FilePath, rel)
}
if edges[0].To != rel {
t.Errorf("edge.To = %q, want %q", edges[0].To, rel)
}
if edges[0].FilePath != rel {
t.Errorf("edge file path = %q, want %q", edges[0].FilePath, rel)
}
}
5 changes: 3 additions & 2 deletions internal/fixtures/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,13 @@ func IsFixturePath(filePath string) bool {
// the file. Keeping a single ID keeps cross-referencing simple
// (any edge that lands on the file path also lands on the fixture
// classification) and avoids the de-dup gymnastics that emitting a
// twin synthetic ID would require.
// twin synthetic ID would require. That sharing only holds when the
// spelling matches the extractor's relPath exactly (OS-native
// separators on Windows), so filePath is preserved verbatim.
func BuildGraphArtifacts(filePath, language string) []*graph.Node {
if !IsFixturePath(filePath) {
return nil
}
filePath = filepath.ToSlash(filePath)
return []*graph.Node{{
ID: filePath,
Kind: graph.KindFixture,
Expand Down
25 changes: 25 additions & 0 deletions internal/fixtures/scanner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,31 @@ func TestBuildGraphArtifacts(t *testing.T) {
})
}

func TestBuildGraphArtifacts_PreservesCallerPathSpelling(t *testing.T) {
// The standalone fixture node deliberately reuses the file path
// as its node ID so it merges with the file identity; that only
// works when the spelling matches the extractor's relPath exactly
// (OS-native separators for subdirectory files on Windows).
// The spelling is written out rather than composed with
// filepath.Join so a backslash is present on every runner. The
// qualifying `testdata/` segment keeps its forward slash because
// IsFixturePath normalizes through filepath.ToSlash, which is the
// identity on POSIX. Name is asserted by TestBuildGraphArtifacts:
// it comes from filepath.Base, whose separator set is the running
// platform's.
const rel = `testdata/sub\foo.bin`
nodes := BuildGraphArtifacts(rel, "binary")
if len(nodes) != 1 {
t.Fatalf("nodes = %d", len(nodes))
}
if nodes[0].ID != rel {
t.Errorf("node id = %q, want %q", nodes[0].ID, rel)
}
if nodes[0].FilePath != rel {
t.Errorf("node file path = %q, want %q", nodes[0].FilePath, rel)
}
}

func TestReclassifyFileToFixture(t *testing.T) {
t.Run("upgrades file to fixture", func(t *testing.T) {
n := &graph.Node{
Expand Down
122 changes: 122 additions & 0 deletions internal/graph/store_sqlite/coverage_spelling_crossplatform_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package store_sqlite

import (
"database/sql"
"path/filepath"
"testing"

"github.com/stretchr/testify/require"

"github.com/zzet/gortex/internal/graph"
)

// TestOpenKeepsLiveRowsWhenAStoreCrossesPlatforms is the case that decides
// whether the purge may delete anything at all on a path-shape argument.
//
// A store written on Windows and then carried to POSIX - a synced home
// directory, a container mounting the host's store - keeps its Windows
// rows, because eviction is spelling-exact and that is this migration's
// own premise. Re-indexing there produces LIVE forward-slash rows for the
// same logical files, while the stale backslash rows remain to vouch for
// them: they set the repository's Windows verdict and they satisfy the
// native-twin test. At that point a live POSIX row and a stale Windows
// legacy row are identical in every path field.
//
// What separates them is not the spelling but whether anything else in the
// graph still claims that path. A legacy artifact is the only thing that
// ever carried its re-spelled path; a live file's path is also carried by
// its file node and by every symbol in it.
func TestOpenKeepsLiveRowsWhenAStorePlatformChanges(t *testing.T) {
path := filepath.Join(t.TempDir(), "store.sqlite")

const (
staleWinFile = `r/a\b.go` // left over from the Windows run, never evicted
liveFile = `r/a/b.go` // the same logical file, re-indexed on POSIX
liveSymbol = `r/a/b.go::Fn`
liveTodo = `r/a/b.go::todo:3`
liveLicense = `r/license::MIT`

// A genuine legacy row in the same store: its path is claimed by
// nothing but itself, so it still heals.
deadTodo = `r/c/d.go::todo:9`
deadPath = `r/c/d.go`
liveDead = `r/c\d.go`
)

s, err := Open(path)
require.NoError(t, err)
s.AddBatch([]*graph.Node{
{ID: staleWinFile, Kind: graph.KindFile, Name: "b.go", FilePath: staleWinFile, RepoPrefix: "r"},
{ID: liveFile, Kind: graph.KindFile, Name: "b.go", FilePath: liveFile, RepoPrefix: "r"},
{ID: liveSymbol, Kind: graph.KindFunction, Name: "Fn", FilePath: liveFile, RepoPrefix: "r"},
{ID: liveTodo, Kind: graph.KindTodo, Name: "todo:3", FilePath: liveFile, RepoPrefix: "r"},
{ID: liveLicense, Kind: graph.KindLicense, Name: "MIT", FilePath: liveFile, RepoPrefix: "r"},
{ID: liveDead, Kind: graph.KindFile, Name: "d.go", FilePath: liveDead, RepoPrefix: "r"},
{ID: deadTodo, Kind: graph.KindTodo, Name: "todo:9", FilePath: deadPath, RepoPrefix: "r"},
}, []*graph.Edge{
{From: liveFile, To: liveSymbol, Kind: graph.EdgeDefines, FilePath: liveFile, Line: 1},
{From: liveFile, To: liveTodo, Kind: graph.EdgeAnnotated, FilePath: liveFile, Line: 3},
{From: liveFile, To: liveLicense, Kind: graph.EdgeLicensedAs, FilePath: liveFile},
{From: deadPath, To: deadTodo, Kind: graph.EdgeAnnotated, FilePath: deadPath, Line: 9},
})
require.NoError(t, s.Close())

withRawDB(t, path, func(db *sql.DB) {
_, err := db.Exec(`PRAGMA user_version = 12`)
require.NoError(t, err, "reset to the pre-purge version")
})

s2, err := Open(path)
require.NoError(t, err)
t.Cleanup(func() { _ = s2.Close() })

require.NotNil(t, s2.GetNode(liveTodo),
"a live POSIX todo must survive: its path is claimed by a real file node")
require.NotNil(t, s2.GetNode(liveLicense),
"and the license it points at must survive with it")
require.Len(t, s2.GetOutEdges(liveFile), 3,
"every live edge on that file must survive")

require.Nil(t, s2.GetNode(deadTodo),
"a genuine legacy row, whose path nothing else claims, still heals")
}

// TestOpenKeepsLiveRowsWhenAPosixFilenameContainsABackslash is the
// pathological but legal shape: a backslash is a valid character in a POSIX
// filename, so one such file makes its repository look Windows-written and
// doubles as the native twin of a genuinely different file.
func TestOpenKeepsLiveRowsWhenAPosixFilenameContainsABackslash(t *testing.T) {
path := filepath.Join(t.TempDir(), "store.sqlite")

const (
oddFile = `r/a\b.go` // ONE real POSIX file whose name contains a backslash
realFile = `r/a/b.go` // a different real file
realSym = `r/a/b.go::Fn`
realTodo = `r/a/b.go::todo:7`
)

s, err := Open(path)
require.NoError(t, err)
s.AddBatch([]*graph.Node{
{ID: oddFile, Kind: graph.KindFile, Name: `a\b.go`, FilePath: oddFile, RepoPrefix: "r"},
{ID: realFile, Kind: graph.KindFile, Name: "b.go", FilePath: realFile, RepoPrefix: "r"},
{ID: realSym, Kind: graph.KindFunction, Name: "Fn", FilePath: realFile, RepoPrefix: "r"},
{ID: realTodo, Kind: graph.KindTodo, Name: "todo:7", FilePath: realFile, RepoPrefix: "r"},
}, []*graph.Edge{
{From: realFile, To: realSym, Kind: graph.EdgeDefines, FilePath: realFile, Line: 1},
{From: realFile, To: realTodo, Kind: graph.EdgeAnnotated, FilePath: realFile, Line: 7},
})
require.NoError(t, s.Close())

withRawDB(t, path, func(db *sql.DB) {
_, err := db.Exec(`PRAGMA user_version = 12`)
require.NoError(t, err, "reset to the pre-purge version")
})

s2, err := Open(path)
require.NoError(t, err)
t.Cleanup(func() { _ = s2.Close() })

require.NotNil(t, s2.GetNode(realTodo), "a live todo must survive")
require.Len(t, s2.GetOutEdges(realFile), 2, "its live edges must survive")
}
Loading
Loading