diff --git a/.github/workflows/regression.yml b/.github/workflows/regression.yml index f1d40ce2..71cf8345 100644 --- a/.github/workflows/regression.yml +++ b/.github/workflows/regression.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest #if: "!contains(github.event.pull_request.title, '[NO-REGRESSION-TEST]')" env: - LANGS: "go rust python typescript cxx cpp" + LANGS: "go rust python typescript cxx cpp java" # ignore package version for Go e.g. 'a.b/c@506fb8ece467f3a71c29322169bef9b0bc92d554' DIFFJSON_IGNORE: > ['id'] @@ -82,14 +82,16 @@ jobs: run: | # HACK: auto installation uses the published version, not our local version (cd ./main_repo/ts-parser && npm install && npm run build && npm install -g .) - OUTDIR=out_old ABCEXE=./abcoder_old ./pr_repo/script/run_testdata.sh first + JAVA_PARSER_JAR_PATH=$(realpath ./main_repo/lang/java/ipc/abcoder-java-analyzer-1.0-SNAPSHOT.jar) \ + OUTDIR=out_old ABCEXE=./abcoder_old ./pr_repo/script/run_testdata.sh first # avoid wasting time install a new jdtls echo "JDTLS_ROOT_PATH=$(realpath ./main_repo/lang/java/lsp/jdtls/jdt-language-server-*)" >> $GITHUB_ENV - name: Run OLD abcoder - run: + run: | # we run the old abcoder on the new data to compare the outputs - OUTDIR=out_old ABCEXE=./abcoder_old ./pr_repo/script/run_testdata.sh all + JAVA_PARSER_JAR_PATH=$(realpath ./main_repo/lang/java/ipc/abcoder-java-analyzer-1.0-SNAPSHOT.jar) \ + OUTDIR=out_old ABCEXE=./abcoder_old ./pr_repo/script/run_testdata.sh all - name: Reset dependencies run: | @@ -98,11 +100,13 @@ jobs: - name: Install dependencies for new run: | (cd ./pr_repo/ts-parser && npm install && npm run build && npm install -g .) - OUTDIR=out_new ABCEXE=./abcoder_new ./pr_repo/script/run_testdata.sh first + JAVA_PARSER_JAR_PATH=$(realpath ./pr_repo/lang/java/ipc/abcoder-java-analyzer-1.0-SNAPSHOT.jar) \ + OUTDIR=out_new ABCEXE=./abcoder_new ./pr_repo/script/run_testdata.sh first - name: Run NEW abcoder - run: - OUTDIR=out_new ABCEXE=./abcoder_new ./pr_repo/script/run_testdata.sh all + run: | + JAVA_PARSER_JAR_PATH=$(realpath ./pr_repo/lang/java/ipc/abcoder-java-analyzer-1.0-SNAPSHOT.jar) \ + OUTDIR=out_new ABCEXE=./abcoder_new ./pr_repo/script/run_testdata.sh all - name: Upload output directories uses: actions/upload-artifact@v4 diff --git a/lang/collect/export.go b/lang/collect/export.go index a8f19b2e..2f63f9af 100644 --- a/lang/collect/export.go +++ b/lang/collect/export.go @@ -354,6 +354,27 @@ func (c *Collector) exportSymbol(repo *uniast.Repository, symbol *DocumentSymbol tmp := uniast.NewIdentity(mod, path, name) id = &tmp + + // Eagerly prefix Identity.Name for methods so a cyclic visit + // (receiver Type -> receivers map -> back to this method via the + // visited cache) reads the final name, not the bare one. Without + // this, Type.Methods[k] = *mid value-copies a partially-built id + // non-deterministically. Cpp finalizes name in the SKMethod branch + // because it needs extractCppCallSig + namespace munging. + if c.Language != uniast.Cpp && (symbol.Kind == SKMethod || symbol.Kind == SKFunction) { + if mi := c.funcs[symbol].Method; mi != nil && mi.Receiver.Symbol != nil { + recvName := mi.Receiver.Symbol.Name + if mi.Interface != nil && mi.Interface.Symbol != nil { + recvName = mi.Interface.Symbol.Name + "<" + recvName + ">" + } + sep := "." + if symbol.Kind == SKFunction { + sep = "::" + } + id.Name = recvName + sep + name + } + } + // Save to visited ONLY WHEN no errors occur visited[symbol] = id diff --git a/lang/uniast/ast_test.go b/lang/uniast/ast_test.go index f38a549e..8f177f45 100644 --- a/lang/uniast/ast_test.go +++ b/lang/uniast/ast_test.go @@ -43,6 +43,36 @@ func TestRepository_BuildGraph(t *testing.T) { } } +// TestRepository_BuildGraph_Deterministic ensures BuildGraph yields a byte-stable +// JSON repeatedly. Relation slices (References, Dependencies, etc.) are filled +// via map iteration, so without an explicit canonical sort each run produced a +// different order and downstream diffs lit up spurious changes. +func TestRepository_BuildGraph_Deterministic(t *testing.T) { + astFile := testutils.GetTestAstFile("localsession") + + var prev []byte + for i := 0; i < 5; i++ { + r, err := LoadRepo(astFile) + if err != nil { + t.Fatalf("iter %d: load repo: %v", i, err) + } + if err := r.BuildGraph(); err != nil { + t.Fatalf("iter %d: build graph: %v", i, err) + } + js, err := json.Marshal(r) + if err != nil { + t.Fatalf("iter %d: marshal: %v", i, err) + } + if i == 0 { + prev = js + continue + } + if string(prev) != string(js) { + t.Fatalf("iter %d: BuildGraph output is not byte-stable across runs (len %d vs %d)", i, len(prev), len(js)) + } + } +} + func BenchmarkRepository_BuildGraph(b *testing.B) { astFile := testutils.GetTestAstFile("large_ast") r, err := LoadRepo(astFile) diff --git a/lang/uniast/node.go b/lang/uniast/node.go index 1705a9e8..981e023b 100644 --- a/lang/uniast/node.go +++ b/lang/uniast/node.go @@ -15,6 +15,7 @@ package uniast import ( + "sort" "strconv" "strings" ) @@ -257,6 +258,31 @@ func (r *Repository) BuildGraph() error { } } } + + // Canonicalize relation slice order. AddRelation is fed from map + // iterations, so insertion order varies between runs. + sortRelations := func(rs []Relation) { + if len(rs) < 2 { + return + } + sort.Slice(rs, func(i, j int) bool { + a, b := rs[i].Identity.Full(), rs[j].Identity.Full() + if a != b { + return a < b + } + if rs[i].Line != rs[j].Line { + return rs[i].Line < rs[j].Line + } + return rs[i].Kind < rs[j].Kind + }) + } + for _, node := range r.Graph { + sortRelations(node.Dependencies) + sortRelations(node.References) + sortRelations(node.Implements) + sortRelations(node.Inherits) + sortRelations(node.Groups) + } return nil }