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 the value a draw call there
produced, when that call is the whole assigned value"; deciding whether
to call it, what counts as a draw call, 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, call_names) ⇒ 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, the assignments coveringlinenodo not single one out, or the one they single out does not assign a draw call directly. -
.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.
98 99 100 101 102 |
# File 'lib/hegel/draw_name.rb', line 98 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]
78 79 80 |
# File 'lib/hegel/draw_name.rb', line 78 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.
109 110 111 |
# File 'lib/hegel/draw_name.rb', line 109 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.
116 117 118 119 120 121 |
# File 'lib/hegel/draw_name.rb', line 116 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, call_names) ⇒ 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, the assignments covering lineno do not single one out, or
the one they single out does not assign a draw call directly.
call_names is the set of method names that count as a draw call
(Hegel::TestCase::DRAW_METHOD_NAMES); this module has no opinion of
its own about which methods draw.
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. That is also why a singled-out
assignment still answers nil when its value is built from a draw
rather than being the draw itself, as in xs = [tc.draw(integers)]
or n = tc.draw(integers).abs: the recovered name would describe the
Array or the Integer#abs result, not the drawn value the report
actually prints next to it.
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.
49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/hegel/draw_name.rb', line 49 def for(path, lineno, call_names) 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) } } return nil unless innermost.one? node = innermost.first (node.value.is_a?(Prism::CallNode) && call_names.include?(node.value.name)) ? node.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.
74 75 76 |
# File 'lib/hegel/draw_name.rb', line 74 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.
87 88 89 90 91 92 |
# File 'lib/hegel/draw_name.rb', line 87 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).
66 67 68 |
# File 'lib/hegel/draw_name.rb', line 66 def reset_cache @cache = {} end |