From 1d2972b0d19bb9dde6ffcf8d681bb3cc1ae80709 Mon Sep 17 00:00:00 2001 From: Emi Date: Wed, 2 Sep 2026 16:12:56 -0700 Subject: [PATCH 1/2] chore: add test case for "SCIPIndexer.cc:122 enforced condition !baseLoc.empty()" segfault Signed-off-by: Emi --- test/scip/testdata/minitest_3.rb | 4 ++++ test/scip/testdata/minitest_3.snapshot.rb | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/test/scip/testdata/minitest_3.rb b/test/scip/testdata/minitest_3.rb index cd009cd97..b43821475 100644 --- a/test/scip/testdata/minitest_3.rb +++ b/test/scip/testdata/minitest_3.rb @@ -11,6 +11,10 @@ def self.describe(name, &blk); end test_each([[1,2], [3,4]]) do |(a,b)| describe "d" do + # `before` inside `test_each` has a synthesized method name with an empty + # source location. The indexer should skip that definition occurrence. + before do + end it "b" do T.reveal_type(a) # error: Revealed type: `Integer` end diff --git a/test/scip/testdata/minitest_3.snapshot.rb b/test/scip/testdata/minitest_3.snapshot.rb index a26e444ba..f496fb82a 100644 --- a/test/scip/testdata/minitest_3.snapshot.rb +++ b/test/scip/testdata/minitest_3.snapshot.rb @@ -31,6 +31,10 @@ def self.describe(name, &blk); end # ^ definition local 2$416088458 describe "d" do + # `before` inside `test_each` has a synthesized method name with an empty + # source location. The indexer should skip that definition occurrence. + before do + end # ⌄ enclosing_range_start [..] Test#``(). it "b" do # ^^^ definition [..] Test#``(). From 73e04a5dcfcb77a827c791472cd8b44be877110e Mon Sep 17 00:00:00 2001 From: Emi Date: Wed, 2 Sep 2026 16:14:17 -0700 Subject: [PATCH 2/2] fix: skip SCIP generation of occurrences with empty locations Sorbet can assign empty locations to synthesized AST nodes, including Minitest `before` hooks inside `test_each`. Passing these locations to `trimColonColonPrefix` triggers an assertion in debug builds and can segfault in release builds as we've seen in production. Skip definitions and references with empty locations before converting them to SCIP ranges, and add a regression test for the Minitest case. Signed-off-by: Emi --- scip_indexer/SCIPIndexer.cc | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/scip_indexer/SCIPIndexer.cc b/scip_indexer/SCIPIndexer.cc index 7d746638e..a075d3a0c 100644 --- a/scip_indexer/SCIPIndexer.cc +++ b/scip_indexer/SCIPIndexer.cc @@ -119,7 +119,12 @@ InlinedVector fromSorbetLoc(const core::GlobalState &gs, core::Loc l } core::Loc trimColonColonPrefix(const core::GlobalState &gs, core::Loc baseLoc) { - ENFORCE(!baseLoc.empty()); + // Sorbet can assign empty locations to synthesized AST nodes. There is no + // source prefix to trim in that case, and callers will skip emitting an + // occurrence because SCIP ranges must be non-empty. + if (!baseLoc.exists() || baseLoc.empty()) { + return baseLoc; + } auto source = baseLoc.source(gs); if (!source.has_value()) { return baseLoc; @@ -291,6 +296,9 @@ class SCIPState { const SmallVec &rels, optional enclosingLoc = nullopt) { ENFORCE(!symbolString.empty()); + if (!occLoc.exists() || occLoc.empty()) { + return absl::OkStatus(); + } occLoc = trimColonColonPrefix(gs, occLoc); auto range = sorbet::scip_indexer::fromSorbetLoc(gs, occLoc); if (range.size() == 4) { @@ -328,7 +336,11 @@ class SCIPState { void saveReferenceImpl(const core::GlobalState &gs, core::FileRef file, const string &symbolString, const SmallVec &overrideDocs, core::LocOffsets occLocOffsets, int32_t symbol_roles) { ENFORCE(!symbolString.empty()); - auto occLoc = trimColonColonPrefix(gs, core::Loc(file, occLocOffsets)); + auto occLoc = core::Loc(file, occLocOffsets); + if (!occLoc.exists() || occLoc.empty()) { + return; + } + occLoc = trimColonColonPrefix(gs, occLoc); scip::Occurrence occurrence; occurrence.set_symbol(symbolString); occurrence.set_symbol_roles(symbol_roles);