Module: Shirobai::RSpec::NodeLocator
- Defined in:
- lib/shirobai/rspec/node_locator.rb
Overview
Locates parser-gem AST nodes by their source range.
Rust reports offsets on the prism AST; a few RSpec cops need the actual
Parser::AST::Node at some of those offsets (e.g. to run stock's
structural #uniq / #== over them, which byte comparison cannot
reproduce). Given a ProcessedSource and a list of [begin_char, end_char] ranges (CHARACTER offsets — convert byte offsets with
SourceOffsets first), locate returns a Hash mapping each requested
[begin, end] to the node whose expression range matches it exactly.
The SHALLOWEST node wins an exact-range tie (a wrapper node sharing its single child's range resolves to the wrapper). Ranges with no matching node are simply absent from the result. The result is identical to a plain full pre-order descent — the two phases below are only an optimization and never change which node a range resolves to.
Two phases, because two forces pull against each other:
-
Pruned descent (fast, the common case). Pre-order, but skip any child subtree whose expression range contains no remaining target: a target lives inside its ancestors' ranges, so such a subtree cannot hold one. The walk cost is then proportional to the paths down to the targets, not the whole AST. Record exact-range matches shallowest-wins and early-exit once every target is found. A wrapper sharing its child's range still has that range CONTAIN the target range, so the prune never skips it — shallowest-wins survives pruning.
-
Full-descent fallback (slow, rare). A heredoc body sits OUTSIDE the expression range of every ancestor between the heredoc node and the root (their
loc.expressionstops at the<<~marker), so a target nested in heredoc interpolation ("#{user.errors.to_h}") is pruned away in phase 1. ONLY when targets remain unfound do we pay for a full pre-order descent (no pruning) to locate the stragglers, again with the early-exit. Prune alone is unsound (drops heredoc-interior targets); full descent alone is slow on large spec files (walks the whole AST even when targets sit late); the fallback pays the full walk only when a target actually hides in a heredoc.
(This is the plugin-gem twin of Shirobai::NodeLocator; the rspec gem
keeps its own copy so it needs no dependency on that file.)
Class Method Summary collapse
Class Method Details
.locate(processed_source, ranges) ⇒ Hash{Array(Integer, Integer) => Parser::AST::Node}
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/shirobai/rspec/node_locator.rb', line 51 def locate(processed_source, ranges) result = {} ast = processed_source.ast return result if ast.nil? || ranges.empty? targets = {} ranges.each { |range| targets[range] = true } # Phase 1: pruned descent handles every non-heredoc target quickly. descend_pruned(ast, targets, result) # Phase 2: only if a target escaped phase 1 (heredoc interior), pay for # a full descent to find whatever is still missing. descend_full(ast, targets, result) if result.size < targets.size result end |