B&W print-friendly diagrams + tinkerpop-0001 + wave-3 proof sections. Squash of 94 local commits onto remote master.
3 KiB
| id | repo | severity | status | created |
|---|---|---|---|---|
| solargraph-0001 | Solargraph | MEDIUM | OPEN | 2026-03-26 |
Defect
File: lib/solargraph/source/chain.rb:38,225,252
Pattern: @@inference_stack = [] — global class Array used as recursion guard during type inference; linear scan per pin
Complexity: O(depth²) in the worst case per infer_from_definitions call chain
Language: Ruby
Description
Chain#infer_from_definitions is the central type-inference method in Solargraph. It is called for
every symbol the LSP server resolves. It uses a class-level global Array as a recursion guard:
# line 38 — class-level global, shared across ALL inference calls
@@inference_stack = []
# line 225 — called once per pin in `pins.each`
next if @@inference_stack.include?(pin) # O(depth) linear scan
@@inference_stack.push pin
# line 252 — called once per unresolved pin
if @@inference_stack.include?(pin.identity) # O(depth) linear scan
next
end
@@inference_stack.push(pin.identity)
Two separate objects are pushed onto the same array: pin objects (line 227) and pin.identity
strings (line 256). The mixed content means include? on line 225 will scan past identity strings
looking for pin objects and vice versa, adding unnecessary comparisons.
@@inference_depth caps recursion at 10 (line 244), which bounds maximum stack depth to ~20 entries
in normal operation. The O(depth²) overhead is therefore bounded in practice, but:
- The class-level
@@variable is shared across threads — Solargraph uses threads for parallel workspace indexing. Multiple threads modifying@@inference_stackconcurrently is a data race. - Mixed object types (Pin objects + String identities) in the same array means every
include?call unnecessarily compares against the wrong type half the time.
Fix
Split into two separate Sets (or use thread-local variables) to eliminate the mixed-type problem
and reduce lookup to O(1):
# Replace class-level Array with two thread-local Sets
def inference_pin_stack
Thread.current[:solargraph_inference_pin_stack] ||= Set.new.compare_by_identity
end
def inference_identity_stack
Thread.current[:solargraph_inference_identity_stack] ||= Set.new
end
# line 225
next if inference_pin_stack.include?(pin)
inference_pin_stack.add(pin)
# ... typify ...
inference_pin_stack.delete(pin)
# line 252
next if inference_identity_stack.include?(pin.identity)
inference_identity_stack.add(pin.identity)
# ... probe ...
inference_identity_stack.delete(pin.identity)
Using thread-local storage also eliminates the data race.
Speedup estimate
Low (depth is bounded at ~20) but fixes a thread-safety defect. In a multi-threaded workspace index with many concurrent resolutions the thread-local fix prevents incorrect inference results from concurrent stack corruption.
Work required
- Patch in
defects/solargraph/patch/ - Unit test — asserts correct behaviour under concurrent inference
- Integration test
- Benchmark
- White paper section