Class: Rigor::Effects::UnitScan
- Inherits:
-
Object
- Object
- Rigor::Effects::UnitScan
- Defined in:
- lib/rigor/effects/unit_scan.rb
Overview
Scans one effect unit — a method body — into its direct Summary plus the unresolved edges the propagator later closes over (ADR-103 WD4).
Two rules shape the walk:
- Containment. A block literal's origins always join the enclosing method's summary, whether the
callee invokes the block now, later, or never; an envelope is a contract about the method's code.
So the walk simply descends into
BlockNodes and stops only at a nesteddefor at adefine_methodwith a literal name, both of which are units of their own. - Observation. Everything the walk knows about a receiver comes from what the typer already
decided at that call node (Collector::CallRecord); the scan resolves nothing, walks no callee,
and touches no
Scope.
What it cannot prove it taints, and a taint is never a finding — the summary reads "these effects,
and possibly more".
Long by construction: the walk carries one when per Ruby construct that originates an effect, and
splitting that table across classes would put the vocabulary in one file and the reasons in another.
Constant Summary collapse
- FRAME_LOCAL_GLOBALS =
$~and friends are frame-local, not global state: a read of one is notglobal.read. (Prism gives$1and$&node types of their own, so only the named specials need listing.) %w[$~ $_ $& $` $' $+ $!].to_set.freeze
- REFLECTIVE_SEND =
%i[send public_send __send__].to_set.freeze
- DEFERRED_SELECTORS =
Selectors a per-class POSTURE default must never answer for, because a more specific reading of the same site exists and would be swallowed:
sendand friends are thedynamic-sendtaint, andcallis theopaque-callableone. An explicit ROW still wins (Fiddle::Function#callisffi) — it is only the class default that steps aside. %i[send public_send __send__ call].to_set.freeze
- EVAL_SELECTORS =
evaland its family with a string argument, andbinding, hand the analyzer code it cannot read. The design note § 5.1 puts them outside the catalogue for exactly that reason: there is no upper bound to give, only the honest "and possibly more". The BLOCK forms (instance_eval { … }) are containment and must not taint, so the taint is conditioned on a positional argument being present. %i[eval instance_eval class_eval module_eval].to_set.freeze
- SELF_PATH_HEAD =
What a receiver rooted at implicit self spells as the head of a receiver path. Must agree with
Rigor::Plugin::EffectAttribution::SELF_HEAD, which is what a plugin writes; spelled again rather than required so the effects namespace does not pull the plugin contract in, and pinned by spec. "self"- GLOBAL_READ =
LabelSet.new(["global.read"])
- GLOBAL_WRITE =
LabelSet.new(["global.write"])
- MUTATE_STATIC =
LabelSet.new(["mutate.static"])
- MUTATE_SELF =
LabelSet.new(["mutate.self"])
- IO_PROCESS =
LabelSet.new(["io.process"])
Instance Attribute Summary collapse
-
#nested ⇒ Object
readonly
Units discovered inside this one — a nested
def, or adefine_methodwith a literal name whose block becomes that method's body.
Class Method Summary collapse
-
.define_method_unit(node) ⇒ Array?
define_method(:literal) { … }— the block becomesliteral's body (WD14), so the call is a unit declaration wherever it appears: in a class body it is the only way that method exists, and inside another method it is a definition the enclosing method performs (mutate.static) rather than code the enclosing method contains.
Instance Method Summary collapse
-
#delegates_upward? ⇒ Boolean
Whether this body reaches
super— an override that delegates upward still runs whatever the superclass does. -
#initialize(singleton:, parameters:, block_parameter:, owned_locals:, calls:, attribution: Attribution.empty, envelopes: EnvelopeIndex.empty, plugin_facts: PluginFacts.empty, owner_class: nil, method_name: nil) ⇒ UnitScan
constructor
A new instance of UnitScan.
-
#run(body) ⇒ Object
Walks
bodyand returns[Summary, edges].
Constructor Details
#initialize(singleton:, parameters:, block_parameter:, owned_locals:, calls:, attribution: Attribution.empty, envelopes: EnvelopeIndex.empty, plugin_facts: PluginFacts.empty, owner_class: nil, method_name: nil) ⇒ UnitScan
Returns a new instance of UnitScan.
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
# File 'lib/rigor/effects/unit_scan.rb', line 116 def initialize(singleton:, parameters:, block_parameter:, owned_locals:, calls:, # rubocop:disable Metrics/ParameterLists attribution: Attribution.empty, envelopes: EnvelopeIndex.empty, plugin_facts: PluginFacts.empty, owner_class: nil, method_name: nil) @singleton = singleton @block_parameter = block_parameter @calls = calls @attribution = attribution @envelopes = envelopes @plugin_facts = plugin_facts @owner_class = owner_class @method_name = method_name @mutation = MutationClassifier.new( singleton: singleton, parameters: parameters, owned_locals: owned_locals ) @bundles = {} @declared_bundles = {} @causes = [] @edges = [] @nested = [] @delegates_upward = false end |
Instance Attribute Details
#nested ⇒ Object (readonly)
Units discovered inside this one — a nested def, or a define_method with a literal name whose
block becomes that method's body. Each is [name, singleton, body_node, parameters_node].
140 141 142 |
# File 'lib/rigor/effects/unit_scan.rb', line 140 def nested @nested end |
Class Method Details
.define_method_unit(node) ⇒ Array?
define_method(:literal) { … } — the block becomes literal's body (WD14), so the call is a unit
declaration wherever it appears: in a class body it is the only way that method exists, and inside
another method it is a definition the enclosing method performs (mutate.static) rather than code
the enclosing method contains. A non-literal name has no key to file the block under, so it stays
contained in the enclosing method and this returns nil.
91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/rigor/effects/unit_scan.rb', line 91 def self.define_method_unit(node) return nil unless node.name == :define_method && node.receiver.nil? first = node.arguments&.arguments&.first return nil unless first.is_a?(Prism::SymbolNode) && first.unescaped block = node.block return nil unless block.is_a?(Prism::BlockNode) [first.unescaped, false, block.body, block.parameters] end |
Instance Method Details
#delegates_upward? ⇒ Boolean
Whether this body reaches super — an override that delegates upward still runs whatever the
superclass does. Nested defs are unit boundaries, so a super counted here is this unit's.
A separate reading of the same node from the edge #visit_super records: what this bit answers is whether a framework's claim about the selector survives the class having written a body for it (FrameworkUnits.replaced?, #440), which is a question about the class, not about what the parent implementation does.
149 150 151 |
# File 'lib/rigor/effects/unit_scan.rb', line 149 def delegates_upward? @delegates_upward end |
#run(body) ⇒ Object
Walks body and returns [Summary, edges].
154 155 156 157 158 159 160 161 |
# File 'lib/rigor/effects/unit_scan.rb', line 154 def run(body) walk(body) summary = Summary.new( bundles: @bundles, declared_bundles: @declared_bundles, exhaustive: @causes.empty?, causes: @causes ) [summary, @edges] end |