Module: Hegel::DrawName
- Defined in:
- lib/hegel/draw_name.rb,
sig/hegel.rbs
Overview
Recovers a drawn value's variable name from the caller's own source (see
docs/adr/0005), so a failure report can print n = 501 instead of
draw = 501 when the caller never passed a label:. This module only
answers "what name does path:lineno assign a value to"; deciding
whether to call it, and what to fall back to when it answers nil, is
Hegel::TestCase#name_for's job, not this one's.
Constant Summary collapse
- ASSIGNMENT_NODE_TYPES =
Node types #for treats as "this line names a drawn value". An explicit list, not every Prism::Node subclass whose name ends in WriteNode, because a wrong guess here (e.g. matching a constant assignment as if it named a draw) would misname a report entry -- the one outcome this feature must never risk. Local and instance variables both expose the assigned name the same way (a Symbol, including the leading "@" for an ivar), so one code path reads both.
[Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode].freeze
Class Method Summary collapse
-
.assignment_nodes(node, matches = []) ⇒ Array[untyped]
Every ASSIGNMENT_NODE_TYPES node under
node, found by walking the whole tree: a drawn value's assignment can be nested arbitrarily deep (inside a block, a method body, a conditional), and this module has no way to know which nesting level to expect it at. - .cache ⇒ Hash[String, untyped]
-
.covers?(location, lineno) ⇒ Boolean
Range containment, not a start-line match: Ruby's own caller_locations reports the line a call's own last token is written on, and a multi-line assignment's start line can differ from that -- e.g.
-
.encloses?(outer, inner) ⇒ Boolean
Whether
outerstrictly containsinner, by byte offset rather than by line: two assignments on one line share both line numbers, and only the offsets tell them apart from a genuine nesting. -
.for(path, lineno) ⇒ String?
The name
path:+lineno+ assigns a drawn value to, or nil when that cannot be answered confidently:pathcannot be read, Prism cannot parse it, or the assignments coveringlinenodo not single one out. -
.parse(path) ⇒ Object
+path+'s parsed Prism::ProgramNode, or nil if it could not be read or parsed.
-
.read_and_parse(path) ⇒ Object
Reads and parses
path. -
.reset_cache ⇒ void
Drops every cached parse.
Class Method Details
.assignment_nodes(node, matches = []) ⇒ Array[untyped]
Every ASSIGNMENT_NODE_TYPES node under node, found by walking the
whole tree: a drawn value's assignment can be nested arbitrarily deep
(inside a block, a method body, a conditional), and this module has
no way to know which nesting level to expect it at.
84 85 86 87 88 |
# File 'lib/hegel/draw_name.rb', line 84 def assignment_nodes(node, matches = []) matches << node if ASSIGNMENT_NODE_TYPES.include?(node.class) node.compact_child_nodes.each { |child| assignment_nodes(child, matches) } matches end |
.cache ⇒ Hash[String, untyped]
64 65 66 |
# File 'lib/hegel/draw_name.rb', line 64 def cache @cache ||= {} end |
.covers?(location, lineno) ⇒ Boolean
Range containment, not a start-line match: Ruby's own caller_locations
reports the line a call's own last token is written on, and a
multi-line assignment's start line can differ from that -- e.g.
n = tc\n .draw_integer(...) reports the second line, but the
assignment node's own start_line is the first.
95 96 97 |
# File 'lib/hegel/draw_name.rb', line 95 def covers?(location, lineno) (location.start_line..location.end_line).cover?(lineno) end |
.encloses?(outer, inner) ⇒ Boolean
Whether outer strictly contains inner, by byte offset rather than
by line: two assignments on one line share both line numbers, and only
the offsets tell them apart from a genuine nesting.
102 103 104 105 106 107 |
# File 'lib/hegel/draw_name.rb', line 102 def encloses?(outer, inner) return false if outer.equal?(inner) outer.location.start_offset <= inner.location.start_offset && inner.location.end_offset <= outer.location.end_offset end |
.for(path, lineno) ⇒ String?
The name path:+lineno+ assigns a drawn value to, or nil when that
cannot be answered confidently: path cannot be read, Prism cannot
parse it, or the assignments covering lineno do not single one out.
A wrong name would misdirect a reader of the failure report more than
a missing one would, so an ambiguous line returns nil rather than
guessing between candidates.
Several assignments can cover one line by nesting rather than by
ambiguity. error = assert_raises do ... n = tc.draw_integer(...) ... end puts the draw inside both, and a reader has no doubt which one
names it. So the innermost wins: an enclosing assignment is discarded
whenever another candidate sits inside it. Two assignments written
side by side on one line contain neither the other, nothing singles
one out, and the answer is nil.
38 39 40 41 42 43 44 45 |
# File 'lib/hegel/draw_name.rb', line 38 def for(path, lineno) program = parse(path) return nil unless program matches = assignment_nodes(program).select { |node| covers?(node.location, lineno) } innermost = matches.reject { |node| matches.any? { |other| encloses?(node, other) } } innermost.one? ? innermost.first.name.to_s : nil end |
.parse(path) ⇒ Object
+path+'s parsed Prism::ProgramNode, or nil if it could not be read or parsed. Cached either way, success or nil, because a single failure report names every draw the final replay recorded against the same one file, and nothing about that file changes between those lookups.
60 61 62 |
# File 'lib/hegel/draw_name.rb', line 60 def parse(path) cache.fetch(path) { cache[path] = read_and_parse(path) } end |
.read_and_parse(path) ⇒ Object
Reads and parses path. Returns nil, not the ParseResult, for either
failure mode #for's caller cares about: path raising SystemCallError
(a nonexistent path -- eval, "-e", irb, or a file removed since the
caller was compiled) and Prism reporting a syntax error (#success?
false) for a path that did exist.
73 74 75 76 77 78 |
# File 'lib/hegel/draw_name.rb', line 73 def read_and_parse(path) result = Prism.parse(File.read(path)) result.success? ? result.value : nil rescue SystemCallError nil end |
.reset_cache ⇒ void
This method returns an undefined value.
Drops every cached parse. #for's only state; a fresh process would never need this, but a test process that calls #for many times over the life of the suite does, both to isolate one test's fixture file from another's and to prove #parse only reads a given path once (see #parse).
52 53 54 |
# File 'lib/hegel/draw_name.rb', line 52 def reset_cache @cache = {} end |