rigortype 0.4.3 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/data/core_overlay/ostruct.rbs +40 -0
- data/docs/handbook/11-sig-gen.md +16 -0
- data/docs/manual/02-cli-reference.md +4 -3
- data/docs/manual/03-configuration.md +16 -5
- data/docs/manual/04-diagnostics.md +44 -2
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/10-mcp-server.md +32 -3
- data/docs/manual/18-removing-dead-code.md +11 -1
- data/docs/manual/plugins/rigor-rails-routes.md +2 -2
- data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
- data/exe/rigor +12 -17
- data/lib/rigor/analysis/check_rules/rule_ids.rb +57 -0
- data/lib/rigor/analysis/check_rules.rb +46 -36
- data/lib/rigor/analysis/incremental.rb +12 -0
- data/lib/rigor/analysis/incremental_session.rb +121 -21
- data/lib/rigor/analysis/reachability/graph.rb +23 -3
- data/lib/rigor/analysis/reachability/scan.rb +51 -9
- data/lib/rigor/analysis/rule_catalog.rb +6 -1
- data/lib/rigor/analysis/run_cache_key.rb +31 -7
- data/lib/rigor/analysis/run_cache_probe.rb +5 -3
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +48 -12
- data/lib/rigor/analysis/runner/pool_coordinator.rb +110 -15
- data/lib/rigor/analysis/runner/project_pre_passes.rb +29 -4
- data/lib/rigor/analysis/runner.rb +35 -9
- data/lib/rigor/analysis/severity_stamp.rb +34 -1
- data/lib/rigor/analysis/worker_session.rb +57 -1
- data/lib/rigor/cache/descriptor.rb +40 -6
- data/lib/rigor/cache/incremental_snapshot.rb +20 -4
- data/lib/rigor/cli/mcp_command.rb +1 -1
- data/lib/rigor/config_audit.rb +111 -15
- data/lib/rigor/configuration/dependencies.rb +19 -11
- data/lib/rigor/environment/rbs_loader.rb +46 -10
- data/lib/rigor/inference/block_call_timing.rb +5 -0
- data/lib/rigor/inference/captured_locals.rb +26 -3
- data/lib/rigor/inference/definer_resolution.rb +16 -7
- data/lib/rigor/inference/expression_typer.rb +59 -4
- data/lib/rigor/inference/guard_rebinding.rb +7 -2
- data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
- data/lib/rigor/inference/module_function_state.rb +80 -13
- data/lib/rigor/inference/narrowing.rb +184 -68
- data/lib/rigor/inference/reflective_ivar_write.rb +116 -0
- data/lib/rigor/inference/repeated_or_writes.rb +414 -1
- data/lib/rigor/inference/scope_indexer.rb +439 -70
- data/lib/rigor/inference/self_call_ivar_writes.rb +341 -0
- data/lib/rigor/inference/statement_evaluator.rb +84 -29
- data/lib/rigor/lens/anchor.rb +3 -3
- data/lib/rigor/lens/contract_reader.rb +15 -0
- data/lib/rigor/lens/document.rb +2 -2
- data/lib/rigor/lens/file_reader.rb +442 -73
- data/lib/rigor/lens/members.rb +65 -15
- data/lib/rigor/lens/run.rb +44 -2
- data/lib/rigor/lens.rb +2 -1
- data/lib/rigor/mcp/server.rb +61 -20
- data/lib/rigor/plugin/box_probe.rb +154 -7
- data/lib/rigor/project_environment.rb +6 -3
- data/lib/rigor/scope.rb +11 -6
- data/lib/rigor/sig_gen/layout_index.rb +36 -0
- data/lib/rigor/sig_gen/method_candidate.rb +8 -0
- data/lib/rigor/sig_gen/path_mapper.rb +18 -2
- data/lib/rigor/sig_gen/writer.rb +47 -34
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +28 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +1 -1
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +5 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +155 -12
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
- metadata +4 -1
|
@@ -507,6 +507,7 @@ module Rigor
|
|
|
507
507
|
method_assign_effects = build_method_assign_effects(root)
|
|
508
508
|
walk_class_ivars(root, [], default_scope, accumulator, mutated_ivars,
|
|
509
509
|
read_before_write, init_writes, method_assign_effects)
|
|
510
|
+
exempt_class_body_writes!(init_writes, accumulator.delete(DEFINE_METHOD_WRITES) || {})
|
|
510
511
|
merge_held_ivar_writes!(accumulator)
|
|
511
512
|
record_aliased_ivar_mutations!(root, mutated_ivars)
|
|
512
513
|
widen_mutated_ivar_entries!(accumulator, mutated_ivars)
|
|
@@ -514,6 +515,19 @@ module Rigor
|
|
|
514
515
|
accumulator.transform_values(&:freeze).freeze
|
|
515
516
|
end
|
|
516
517
|
|
|
518
|
+
# Folds the class-body writes {#collect_class_body_ivar_writes} held under {CLASS_BODY_WRITES} into the
|
|
519
|
+
# `init_writes` exemption, except for an ivar a `define_method` block of the class writes (#1768). Such an ivar
|
|
520
|
+
# had no census entry before the block's writes were counted, so the exemption never applied to it; a
|
|
521
|
+
# class-body `@x = nil` writes the class object's ivar and says nothing about whether an instance's `@x` is set,
|
|
522
|
+
# and exempting it would seed `@x` non-nil from the block's write alone (`if @x` folding always-truthy on an
|
|
523
|
+
# instance whose `@x` is unset). It keeps the read-before-write `nil` instead, which only widens.
|
|
524
|
+
def exempt_class_body_writes!(init_writes, define_method_writes)
|
|
525
|
+
(init_writes.delete(CLASS_BODY_WRITES) || {}).each do |class_name, ivars|
|
|
526
|
+
kept = ivars - (define_method_writes[class_name] || EMPTY_GUARDED_IVARS)
|
|
527
|
+
(init_writes[class_name] ||= Set.new).merge(kept)
|
|
528
|
+
end
|
|
529
|
+
end
|
|
530
|
+
|
|
517
531
|
# B2.3 — finalize the read-before-write nil contribution. For each class, for each ivar where SOME method body
|
|
518
532
|
# observed a read-before-write AND no `initialize` write exists for that ivar, contribute `Constant[nil]` to the
|
|
519
533
|
# class-wide accumulator.
|
|
@@ -854,6 +868,7 @@ module Rigor
|
|
|
854
868
|
owner = def_owner || qualified_prefix
|
|
855
869
|
collect_initializer_block_ivars(node, owner, default_scope, accumulator, mutated_ivars, init_writes)
|
|
856
870
|
record_attr_writer_ivars(node, owner, accumulator) if ATTR_WRITER_MACROS.include?(node.name)
|
|
871
|
+
collect_define_method_block_ivars(node, owner, default_scope, accumulator, mutated_ivars)
|
|
857
872
|
false
|
|
858
873
|
else
|
|
859
874
|
walk_ivars_meta_new?(node, qualified_prefix, default_scope, accumulator,
|
|
@@ -1065,6 +1080,44 @@ module Rigor
|
|
|
1065
1080
|
end
|
|
1066
1081
|
end
|
|
1067
1082
|
|
|
1083
|
+
# The `define_method` arm of {#walk_class_ivars} (#1768). `define_method(:m) { … }` on the implicit
|
|
1084
|
+
# or `self` receiver installs its block as an instance method of `owner`, and Ruby runs the body on
|
|
1085
|
+
# the instance, so its `@x = …` writes are the instance's ivars exactly as a `def m` body's are —
|
|
1086
|
+
# `DefineMethodBlockSelf` narrows the block's `self` the same way when it is evaluated. Without this
|
|
1087
|
+
# arm an ivar `initialize` sets to `nil` and a `define_method` block sets to a String read as `nil`
|
|
1088
|
+
# in every sibling `def`. The block is typed under the instance `self`, as a def body is. Only the
|
|
1089
|
+
# literal-block form counts: a `define_method(:m, callable)` or `&callable` argument is not walked.
|
|
1090
|
+
# `owner` is empty in a `class << self` body (the call defines a class method there) and in an
|
|
1091
|
+
# anonymous factory body, and the block contributes nothing there. A `define_method` inside a `def`
|
|
1092
|
+
# body is already reached by that body's walk ({#gather_ivar_writes} enters blocks).
|
|
1093
|
+
def collect_define_method_block_ivars(node, owner, default_scope, accumulator, mutated_ivars)
|
|
1094
|
+
return if owner.empty?
|
|
1095
|
+
return unless define_method_block_call?(node)
|
|
1096
|
+
|
|
1097
|
+
block = node.block
|
|
1098
|
+
return unless block.body
|
|
1099
|
+
|
|
1100
|
+
class_name = owner.join("::")
|
|
1101
|
+
body_scope = census_body_scope(default_scope, owner, Type::Combinator.nominal_of(class_name))
|
|
1102
|
+
gather_ivar_writes(block.body, body_scope, class_name, accumulator, EMPTY_GUARDED_IVARS, mutated_ivars)
|
|
1103
|
+
written = (accumulator[DEFINE_METHOD_WRITES] ||= {})[class_name] ||= Set.new
|
|
1104
|
+
define_method_written_ivars(block.body, written)
|
|
1105
|
+
end
|
|
1106
|
+
|
|
1107
|
+
# The ivars a `define_method` block body writes, in any write form, outside a nested `def` / `class` / `module`.
|
|
1108
|
+
def define_method_written_ivars(node, acc)
|
|
1109
|
+
return unless node.is_a?(Prism::Node)
|
|
1110
|
+
return if IVAR_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
|
|
1111
|
+
|
|
1112
|
+
case node
|
|
1113
|
+
when Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode,
|
|
1114
|
+
Prism::InstanceVariableAndWriteNode, Prism::InstanceVariableOperatorWriteNode,
|
|
1115
|
+
Prism::InstanceVariableTargetNode
|
|
1116
|
+
acc << node.name
|
|
1117
|
+
end
|
|
1118
|
+
node.rigor_each_child { |child| define_method_written_ivars(child, acc) }
|
|
1119
|
+
end
|
|
1120
|
+
|
|
1068
1121
|
# ADR-38 block-form gate: true when a loaded plugin declares `method_name` a block-form initializer for
|
|
1069
1122
|
# `class_name` (or an ancestor). Mirrors `additional_initializer?` but queries `covers_block_method?` instead of
|
|
1070
1123
|
# `covers_method?`.
|
|
@@ -1156,15 +1209,21 @@ module Rigor
|
|
|
1156
1209
|
# every `@x = …` write target as a class-body init. Consumed by `contribute_read_before_write_nil!` to exempt
|
|
1157
1210
|
# ivars the author already knows might be nil (the `@x = nil` at class-body level is the canonical nullability
|
|
1158
1211
|
# acknowledgement; the instance @x is technically a separate store, but the pragmatic intent is unambiguous).
|
|
1212
|
+
#
|
|
1213
|
+
# A literal `define_method` block is a method body, not class-body code (#1768): its writes reach the census as
|
|
1214
|
+
# a `def` body's do, so they must not exempt the ivar from the read-before-write `nil` either. Only its receiver
|
|
1215
|
+
# and arguments are walked.
|
|
1159
1216
|
def collect_class_body_ivar_writes(node, class_name, init_writes)
|
|
1160
1217
|
return unless node.is_a?(Prism::Node)
|
|
1161
1218
|
return if IVAR_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
|
|
1162
1219
|
|
|
1220
|
+
return collect_define_method_argument_writes(node, class_name, init_writes) if define_method_block_call?(node)
|
|
1221
|
+
|
|
1163
1222
|
if node.is_a?(Prism::InstanceVariableWriteNode) ||
|
|
1164
1223
|
node.is_a?(Prism::InstanceVariableOrWriteNode) ||
|
|
1165
1224
|
node.is_a?(Prism::InstanceVariableAndWriteNode) ||
|
|
1166
1225
|
node.is_a?(Prism::InstanceVariableOperatorWriteNode)
|
|
1167
|
-
(init_writes[class_name] ||= Set.new) << node.name
|
|
1226
|
+
((init_writes[CLASS_BODY_WRITES] ||= {})[class_name] ||= Set.new) << node.name
|
|
1168
1227
|
end
|
|
1169
1228
|
|
|
1170
1229
|
node.rigor_each_child do |child|
|
|
@@ -1172,6 +1231,18 @@ module Rigor
|
|
|
1172
1231
|
end
|
|
1173
1232
|
end
|
|
1174
1233
|
|
|
1234
|
+
# The arguments of a `define_method` block call, walked by {#collect_class_body_ivar_writes} in place of the
|
|
1235
|
+
# call's children so the block body stays out of the class-body init writes.
|
|
1236
|
+
def collect_define_method_argument_writes(node, class_name, init_writes)
|
|
1237
|
+
node.arguments&.arguments&.each { |arg| collect_class_body_ivar_writes(arg, class_name, init_writes) }
|
|
1238
|
+
end
|
|
1239
|
+
|
|
1240
|
+
# A `define_method` call with a literal block on the implicit or `self` receiver: the block is an instance
|
|
1241
|
+
# method body of the class the walk is in (#1768).
|
|
1242
|
+
def define_method_block_call?(node)
|
|
1243
|
+
DefineMethodBlockSelf.define_method_on_lexical_self?(node) && node.block.is_a?(Prism::BlockNode)
|
|
1244
|
+
end
|
|
1245
|
+
|
|
1175
1246
|
def detect_read_before_write(node, seen_writes, read_first)
|
|
1176
1247
|
return unless node.is_a?(Prism::Node)
|
|
1177
1248
|
return if IVAR_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
|
|
@@ -2011,6 +2082,13 @@ module Rigor
|
|
|
2011
2082
|
OPERATOR_WRITE_CONTRIBUTIONS = :operator_write_contributions
|
|
2012
2083
|
private_constant :AND_WRITE_CONTRIBUTIONS, :OPERATOR_WRITE_CONTRIBUTIONS
|
|
2013
2084
|
|
|
2085
|
+
# The same Symbol-key convention for two side tables the walk fills and {#build_class_ivar_index} removes
|
|
2086
|
+
# before anything reads class names: the ivars each class's `define_method` blocks write (in the
|
|
2087
|
+
# accumulator), and the class-body writes held out of `init_writes` until {#exempt_class_body_writes!}.
|
|
2088
|
+
DEFINE_METHOD_WRITES = :define_method_writes
|
|
2089
|
+
CLASS_BODY_WRITES = :class_body_writes
|
|
2090
|
+
private_constant :DEFINE_METHOD_WRITES, :CLASS_BODY_WRITES
|
|
2091
|
+
|
|
2014
2092
|
# Unions `type` into the class-ivar accumulator for `(class_name, ivar_name)`. Shared by the single-write and
|
|
2015
2093
|
# multi-write (parallel-assignment) collectors.
|
|
2016
2094
|
def accumulate_ivar_type(accumulator, class_name, ivar_name, type)
|
|
@@ -7004,7 +7082,9 @@ module Rigor
|
|
|
7004
7082
|
def extends_block_handled?(node, qualified_prefix, current_class, accumulator, in_singleton, singleton_self,
|
|
7005
7083
|
singleton_cref)
|
|
7006
7084
|
case extends_block_plan(node, accumulator)
|
|
7007
|
-
when :skip
|
|
7085
|
+
when :skip
|
|
7086
|
+
taint_skipped_singleton_rewrites(node.block, qualified_prefix, current_class, accumulator)
|
|
7087
|
+
true
|
|
7008
7088
|
when :rebound
|
|
7009
7089
|
walk_extends_block_call(node, qualified_prefix, current_class, accumulator,
|
|
7010
7090
|
in_singleton, singleton_self, singleton_cref)
|
|
@@ -7013,6 +7093,31 @@ module Rigor
|
|
|
7013
7093
|
end
|
|
7014
7094
|
end
|
|
7015
7095
|
|
|
7096
|
+
# Issue #1906 — a block the extends walk skips may still run, and a `singleton_class` call in it, or in a `def`
|
|
7097
|
+
# it holds (`[1].each { D.singleton_class.alias_method :own, :other }` in a method body,
|
|
7098
|
+
# `to_prepare do def helper = D.singleton_class.alias_method(...) end`), reaches no mark the methods walk records
|
|
7099
|
+
# there. Every such call taints the side it may rewrite, as {#singleton_class_rewrite_tainted?} does for one the
|
|
7100
|
+
# walk visits; a constant receiver taints every name it can denote from here.
|
|
7101
|
+
def taint_skipped_singleton_rewrites(node, qualified_prefix, current_class, accumulator)
|
|
7102
|
+
return unless node.is_a?(Prism::Node)
|
|
7103
|
+
|
|
7104
|
+
if node.is_a?(Prism::CallNode) && singleton_class_of_receiver?(node.receiver) && surface_rewriting_call?(node)
|
|
7105
|
+
skipped_rewrite_owners(node.receiver.receiver, qualified_prefix, current_class).each do |owner|
|
|
7106
|
+
accumulator.taint(owner, :extend)
|
|
7107
|
+
end
|
|
7108
|
+
end
|
|
7109
|
+
node.rigor_each_child do |child|
|
|
7110
|
+
taint_skipped_singleton_rewrites(child, qualified_prefix, current_class, accumulator)
|
|
7111
|
+
end
|
|
7112
|
+
end
|
|
7113
|
+
|
|
7114
|
+
def skipped_rewrite_owners(object, qualified_prefix, current_class)
|
|
7115
|
+
return [current_class].compact if object.nil? || object.is_a?(Prism::SelfNode)
|
|
7116
|
+
return [] unless object.is_a?(Prism::ConstantReadNode) || object.is_a?(Prism::ConstantPathNode)
|
|
7117
|
+
|
|
7118
|
+
constant_receiver_candidates(object, qualified_prefix)
|
|
7119
|
+
end
|
|
7120
|
+
|
|
7016
7121
|
ARRAY_ITERATORS = %i[each each_with_index map].freeze
|
|
7017
7122
|
HASH_ITERATORS = %i[each each_pair each_key each_value map].freeze
|
|
7018
7123
|
private_constant :ARRAY_ITERATORS, :HASH_ITERATORS
|
|
@@ -7228,10 +7333,15 @@ module Rigor
|
|
|
7228
7333
|
# `Const.singleton_class.send(:include, M)`) puts methods or modules on that singleton class that no edge
|
|
7229
7334
|
# records, so its singleton side is unpositioned: the owner is the enclosing class for a bare or `self.`
|
|
7230
7335
|
# receiver, else the constant. True when it tainted.
|
|
7336
|
+
#
|
|
7337
|
+
# Issue #1906 — a naming call with literal names ({#singleton_class_named_call?}) marks only its names instead,
|
|
7338
|
+
# but only where it is a direct statement (a top-level statement, or one of a direct declaration body or of a
|
|
7339
|
+
# block that runs there once): there the methods walk records those marks under the same owner. In a method
|
|
7340
|
+
# body, a hook or a conditional it may run on a receiver or at a time the marks cannot place, so it still taints.
|
|
7231
7341
|
def singleton_class_rewrite_tainted?(node, qualified_prefix, current_class, accumulator)
|
|
7232
7342
|
receiver = node.receiver
|
|
7233
7343
|
return false unless singleton_class_of_receiver?(receiver) && surface_rewriting_call?(node)
|
|
7234
|
-
return false if singleton_class_named_call?(node)
|
|
7344
|
+
return false if singleton_class_named_call?(node) && accumulator.direct?(node)
|
|
7235
7345
|
|
|
7236
7346
|
owner = singleton_class_owner(node, receiver.receiver, qualified_prefix, current_class)
|
|
7237
7347
|
return false if owner.nil?
|
|
@@ -7245,15 +7355,37 @@ module Rigor
|
|
|
7245
7355
|
receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class && receiver.arguments.nil?
|
|
7246
7356
|
end
|
|
7247
7357
|
|
|
7248
|
-
# An `attr_
|
|
7249
|
-
#
|
|
7250
|
-
#
|
|
7251
|
-
#
|
|
7252
|
-
#
|
|
7358
|
+
# An `attr_*`, `alias_method`, `define_method` or `remove_method` on a singleton class
|
|
7359
|
+
# (`singleton_class.attr_accessor :conf`, `singleton_class.alias_method :o2, :own`) rewrites only the names it
|
|
7360
|
+
# spells, so it makes those names' singleton envelopes opaque and contested ({#record_singleton_class_names}) and
|
|
7361
|
+
# leaves the rest of the singleton side readable. One whose rewritten name is not a literal Symbol or String
|
|
7362
|
+
# (`attr_accessor(*KEYS)`, `alias_method name, :own`, `define_method(sym)`), and any other surface call there
|
|
7363
|
+
# (`class_eval`, `send`, `prepend`), still taints the side: the own-definer read would otherwise answer the body
|
|
7364
|
+
# Ruby replaced (`definer_resolution_witness_spec.rb`).
|
|
7253
7365
|
def singleton_class_named_call?(node)
|
|
7366
|
+
!singleton_class_rewritten_names(node).nil?
|
|
7367
|
+
end
|
|
7368
|
+
|
|
7369
|
+
# Issue #1906 — the names a singleton-class call rewrites, or nil where it is not one of the naming forms or
|
|
7370
|
+
# does not spell every rewritten name as a literal. `alias_method` rewrites its new name (the aliased one keeps
|
|
7371
|
+
# its body), `define_method` its first argument, `remove_method` and `attr_*` every argument.
|
|
7372
|
+
def singleton_class_rewritten_names(node)
|
|
7254
7373
|
arguments = node.arguments&.arguments || []
|
|
7255
|
-
|
|
7256
|
-
|
|
7374
|
+
rewritten = case node.name
|
|
7375
|
+
when :alias_method, :define_method then arguments.first(1)
|
|
7376
|
+
when :remove_method then arguments
|
|
7377
|
+
else ATTR_MACROS.include?(node.name) && arguments
|
|
7378
|
+
end
|
|
7379
|
+
return nil if !rewritten || arguments.empty? || !arguments.all? { |argument| literal_argument?(node, argument) }
|
|
7380
|
+
|
|
7381
|
+
rewritten.map { |argument| literal_method_name(argument) }
|
|
7382
|
+
end
|
|
7383
|
+
|
|
7384
|
+
# `define_method(:x, body)` takes a body that is not a name; every other argument must be a literal name.
|
|
7385
|
+
def literal_argument?(node, argument)
|
|
7386
|
+
return true if node.name == :define_method && !argument.equal?(node.arguments.arguments.first)
|
|
7387
|
+
|
|
7388
|
+
!literal_method_name(argument).nil?
|
|
7257
7389
|
end
|
|
7258
7390
|
|
|
7259
7391
|
def singleton_class_owner(node, object, qualified_prefix, current_class)
|
|
@@ -7447,19 +7579,15 @@ module Rigor
|
|
|
7447
7579
|
|
|
7448
7580
|
# v0.1.2 — per-class method-visibility table for the `def.method-visibility-mismatch` CheckRule.
|
|
7449
7581
|
#
|
|
7450
|
-
# Tracks
|
|
7582
|
+
# Tracks three visibility-changing forms ({#apply_visibility_call}):
|
|
7451
7583
|
#
|
|
7452
|
-
# - **Modifier blocks**: a bare `private` / `protected` /
|
|
7453
|
-
# `
|
|
7454
|
-
#
|
|
7455
|
-
#
|
|
7456
|
-
#
|
|
7457
|
-
#
|
|
7458
|
-
#
|
|
7459
|
-
# args are recognised; `private def foo; end` (the
|
|
7460
|
-
# wrap-around form) is not yet — it would need
|
|
7461
|
-
# tracking the def-call's return-value visibility,
|
|
7462
|
-
# which is a separate slice.
|
|
7584
|
+
# - **Modifier blocks**: a bare `private` / `protected` / `public` call inside a class body (also written
|
|
7585
|
+
# `self.private`, `send(:private)` or with a block) switches the "current default" visibility for every
|
|
7586
|
+
# subsequent `def` and `attr_*` member until another modifier flips it again.
|
|
7587
|
+
# - **Named-argument form**: `private :foo, :bar` or `private %i[foo bar]` (or the same with `protected` /
|
|
7588
|
+
# `public`) marks specific names already recorded under the class.
|
|
7589
|
+
# - **Wrap-around form** (#1911, #1819, #1633): `private def foo` and `private attr_reader :foo` record the
|
|
7590
|
+
# members with the call's visibility and leave the default alone.
|
|
7463
7591
|
#
|
|
7464
7592
|
# Top-level (no surrounding class) defs do not contribute — Ruby's top-level visibility nuances (private at
|
|
7465
7593
|
# top-level marks the method on `Object`) are out of scope for v0.1.2.
|
|
@@ -7516,6 +7644,26 @@ module Rigor
|
|
|
7516
7644
|
end
|
|
7517
7645
|
end
|
|
7518
7646
|
|
|
7647
|
+
# Contests the `[class_name, name]` slot, recorded or not: a visibility call changed it in a way the walk
|
|
7648
|
+
# cannot read (`private def self.x` reaching an instance `x`).
|
|
7649
|
+
def contest_slot(class_name, name)
|
|
7650
|
+
(@contested ||= Set.new) << [class_name, name].freeze
|
|
7651
|
+
end
|
|
7652
|
+
|
|
7653
|
+
# Contests every slot of `class_name` recorded so far (`private(*names)` may name any of them).
|
|
7654
|
+
def contest_class(class_name)
|
|
7655
|
+
self[class_name]&.each_key { |name| contest_slot(class_name, name) }
|
|
7656
|
+
end
|
|
7657
|
+
|
|
7658
|
+
# Runs the block with the running default uncertain, so a `def` it records is contested.
|
|
7659
|
+
def uncertainly
|
|
7660
|
+
saved = @uncertain
|
|
7661
|
+
@uncertain = true
|
|
7662
|
+
yield
|
|
7663
|
+
ensure
|
|
7664
|
+
@uncertain = saved
|
|
7665
|
+
end
|
|
7666
|
+
|
|
7519
7667
|
# Runs the block as a fresh body (a class or module, `class << self`, a meta-new or eval block), whose
|
|
7520
7668
|
# default starts certain and whose toggles do not reach the enclosing body.
|
|
7521
7669
|
def body
|
|
@@ -7537,10 +7685,37 @@ module Rigor
|
|
|
7537
7685
|
end
|
|
7538
7686
|
end
|
|
7539
7687
|
|
|
7540
|
-
# A bare toggle ({VISIBILITY_TOGGLES}):
|
|
7688
|
+
# A bare toggle ({VISIBILITY_TOGGLES}): a visibility call ({#visibility_call_parts}) with no arguments.
|
|
7541
7689
|
def bare_visibility_toggle?(node)
|
|
7542
|
-
|
|
7543
|
-
|
|
7690
|
+
parts = visibility_call_parts(node)
|
|
7691
|
+
!parts.nil? && parts.last.empty?
|
|
7692
|
+
end
|
|
7693
|
+
|
|
7694
|
+
# Whether a visibility call with these arguments may set the running default: none at all, or a splat that
|
|
7695
|
+
# may expand to none (`private(*[])` is a bare `private`).
|
|
7696
|
+
def visibility_toggle_arguments?(arguments)
|
|
7697
|
+
arguments.empty? || arguments.any?(Prism::SplatNode)
|
|
7698
|
+
end
|
|
7699
|
+
|
|
7700
|
+
VISIBILITY_SEND_CALLS = %i[send __send__].freeze
|
|
7701
|
+
EMPTY_VISIBILITY_ARGUMENTS = [].freeze
|
|
7702
|
+
private_constant :VISIBILITY_SEND_CALLS, :EMPTY_VISIBILITY_ARGUMENTS
|
|
7703
|
+
|
|
7704
|
+
# Issue #1633 — `[name, arguments]` for a call Ruby runs as one of {VISIBILITY_TOGGLES} on the body's self:
|
|
7705
|
+
# written receiverless or on `self` (`self.private`), or sent with a literal name (`send(:private, :x)`,
|
|
7706
|
+
# `__send__(:private)`). A block is ignored, as `Module#private` ignores it (`private {}` is a bare
|
|
7707
|
+
# `private`). nil for any other call.
|
|
7708
|
+
def visibility_call_parts(node)
|
|
7709
|
+
return nil unless node.is_a?(Prism::CallNode)
|
|
7710
|
+
return nil unless node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
|
7711
|
+
|
|
7712
|
+
name = node.name
|
|
7713
|
+
arguments = node.arguments&.arguments || EMPTY_VISIBILITY_ARGUMENTS
|
|
7714
|
+
if VISIBILITY_SEND_CALLS.include?(name)
|
|
7715
|
+
name = Source::Literals.symbol_or_string(arguments.first)
|
|
7716
|
+
arguments = arguments.drop(1)
|
|
7717
|
+
end
|
|
7718
|
+
VISIBILITY_TOGGLES.include?(name) ? [name, arguments] : nil
|
|
7544
7719
|
end
|
|
7545
7720
|
|
|
7546
7721
|
# rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/AbcSize
|
|
@@ -7604,16 +7779,31 @@ module Rigor
|
|
|
7604
7779
|
defs_singleton: defs_singleton)
|
|
7605
7780
|
return current_visibility
|
|
7606
7781
|
end
|
|
7607
|
-
|
|
7608
|
-
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
|
|
7614
|
-
|
|
7615
|
-
|
|
7616
|
-
|
|
7782
|
+
if AnonymousMetaClass.block_form_receiver(node)
|
|
7783
|
+
walk_anonymous_meta_visibilities(node, qualified_prefix, in_singleton_class, current_visibility,
|
|
7784
|
+
accumulator, def_owner_prefix, singleton_cref: singleton_cref,
|
|
7785
|
+
defs_singleton: defs_singleton)
|
|
7786
|
+
return current_visibility
|
|
7787
|
+
end
|
|
7788
|
+
if (parts = visibility_call_parts(node))
|
|
7789
|
+
accumulator.toggles += 1 if visibility_toggle_arguments?(parts.last)
|
|
7790
|
+
# The visibility table is instance-side only — `private :x` inside `class <<`
|
|
7791
|
+
# (or a receiver-eval body that re-evaluates a singleton self) marks the
|
|
7792
|
+
# SINGLETON method, which this table cannot express. The `owner_prefix` check is a
|
|
7793
|
+
# guard: an ownerless body (the top level, an anonymous factory block) records nothing,
|
|
7794
|
+
# and its calls walk as any other call.
|
|
7795
|
+
if !in_singleton_class && !owner_prefix.empty? && VISIBILITY_MODIFIERS.include?(parts.first)
|
|
7796
|
+
return apply_visibility_call(node, *parts, owner_prefix, current_visibility, accumulator,
|
|
7797
|
+
defs_singleton: defs_singleton) do |argument|
|
|
7798
|
+
walk_method_visibilities(argument, qualified_prefix, in_singleton_class, current_visibility,
|
|
7799
|
+
accumulator, def_owner_prefix, singleton_cref: singleton_cref,
|
|
7800
|
+
defs_singleton: defs_singleton)
|
|
7801
|
+
end
|
|
7802
|
+
end
|
|
7803
|
+
end
|
|
7804
|
+
unless in_singleton_class || defs_singleton
|
|
7805
|
+
record_attr_visibility(node, owner_prefix, current_visibility, accumulator, toggled: true)
|
|
7806
|
+
end
|
|
7617
7807
|
end
|
|
7618
7808
|
|
|
7619
7809
|
# Statement-position StatementsNode preserves left-to-right visibility flow; everything else recurses with the
|
|
@@ -7667,6 +7857,23 @@ module Rigor
|
|
|
7667
7857
|
end
|
|
7668
7858
|
end
|
|
7669
7859
|
|
|
7860
|
+
# A `Class.new` / `Module.new` / `Struct.new` / `Data.define` block no constant write names
|
|
7861
|
+
# ({#meta_new_block_split} takes the named ones): `klass = Class.new do … end`, `@k = Struct.new(:a) do … end`,
|
|
7862
|
+
# `register(Class.new do … end)` or a bare `Data.define(:a) do … end`. Its body is the anonymous class's, so
|
|
7863
|
+
# it walks as a fresh body with no owner and records nothing for the enclosing class; the receiver and the
|
|
7864
|
+
# arguments are walked where they stand.
|
|
7865
|
+
def walk_anonymous_meta_visibilities(node, qualified_prefix, in_singleton_class, current_visibility,
|
|
7866
|
+
accumulator, def_owner_prefix, singleton_cref:, defs_singleton:) # rubocop:disable Metrics/ParameterLists
|
|
7867
|
+
[node.receiver, *node.arguments&.arguments].each do |part|
|
|
7868
|
+
walk_method_visibilities(part, qualified_prefix, in_singleton_class, current_visibility, accumulator,
|
|
7869
|
+
def_owner_prefix, singleton_cref: singleton_cref, defs_singleton: defs_singleton)
|
|
7870
|
+
end
|
|
7871
|
+
accumulator.body do
|
|
7872
|
+
walk_method_visibilities(node.block.body, qualified_prefix, false, :public, accumulator,
|
|
7873
|
+
EMPTY_PREFIX, singleton_cref: singleton_cref)
|
|
7874
|
+
end
|
|
7875
|
+
end
|
|
7876
|
+
|
|
7670
7877
|
# The eval-block arm of {#walk_method_visibilities}: the block is a fresh class body under
|
|
7671
7878
|
# the receiver's def-owner — visibility starts at `:public`, and nothing inside it leaks
|
|
7672
7879
|
# back to this body's modifier state. An unnameable receiver walks the body ownerless;
|
|
@@ -7704,47 +7911,80 @@ module Rigor
|
|
|
7704
7911
|
end
|
|
7705
7912
|
end
|
|
7706
7913
|
|
|
7707
|
-
def
|
|
7914
|
+
# `toggled` when the def reads the running default (a statement), false for one a visibility call wraps.
|
|
7915
|
+
def record_def_visibility(def_node, qualified_prefix, in_singleton_class, current_visibility, accumulator,
|
|
7916
|
+
toggled: true)
|
|
7708
7917
|
return if def_node.receiver.is_a?(Prism::SelfNode) || in_singleton_class
|
|
7709
7918
|
return if qualified_prefix.empty?
|
|
7710
7919
|
|
|
7711
7920
|
class_name = qualified_prefix.join("::")
|
|
7712
7921
|
accumulator[class_name] ||= {}
|
|
7713
7922
|
accumulator[class_name][def_node.name] = current_visibility
|
|
7714
|
-
accumulator.wrote(class_name, def_node.name, def_node, toggled:
|
|
7715
|
-
end
|
|
7923
|
+
accumulator.wrote(class_name, def_node.name, def_node, toggled: toggled)
|
|
7924
|
+
end
|
|
7925
|
+
|
|
7926
|
+
# Applies one `private` / `protected` / `public` call ({#visibility_call_parts}) written on the body's self
|
|
7927
|
+
# and returns the running default after it:
|
|
7928
|
+
#
|
|
7929
|
+
# - no arguments — the call's visibility becomes the default for subsequent defs.
|
|
7930
|
+
# - `private :foo, :bar`, `private %i[foo bar]` — back-patch the named methods in the accumulator; the
|
|
7931
|
+
# default does NOT change for this form.
|
|
7932
|
+
# - Issues #1911, #1819, #1633 — `private def foo` (endless or not) and `private attr_reader :foo` define
|
|
7933
|
+
# the members with the call's visibility, whatever the running default is (`private; public def foo` is
|
|
7934
|
+
# public); `private def self.foo` changes the instance method `foo`, if one exists, so its slot is
|
|
7935
|
+
# contested.
|
|
7936
|
+
# - a splat (`private(*names)`) may name any member or none (a bare toggle), so the class's slots this file
|
|
7937
|
+
# wrote so far are contested, and the statement loop reads the default after it as uncertain
|
|
7938
|
+
# ({#visibility_toggle_arguments?}).
|
|
7939
|
+
# - any other argument (`private memoize def foo`, `private delegate(...)`) is walked with the default
|
|
7940
|
+
# uncertain, so a `def` under it is contested: the call's visibility reaches it through a return value
|
|
7941
|
+
# the walk does not read.
|
|
7942
|
+
#
|
|
7943
|
+
# `walk` walks an argument the call does not consume itself.
|
|
7944
|
+
def apply_visibility_call(call_node, name, args, qualified_prefix, current_visibility, accumulator,
|
|
7945
|
+
defs_singleton: false, &walk)
|
|
7946
|
+
return name if args.empty?
|
|
7716
7947
|
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
# change for this form.
|
|
7726
|
-
def apply_visibility_call(call_node, qualified_prefix, current_visibility, accumulator)
|
|
7727
|
-
return current_visibility unless call_node.receiver.nil?
|
|
7728
|
-
return current_visibility unless VISIBILITY_MODIFIERS.include?(call_node.name)
|
|
7729
|
-
return current_visibility if qualified_prefix.empty?
|
|
7948
|
+
class_name = qualified_prefix.join("::")
|
|
7949
|
+
accumulator.contest_class(class_name) if args.any?(Prism::SplatNode)
|
|
7950
|
+
args.each do |arg|
|
|
7951
|
+
apply_visibility_argument(arg, name, call_node, qualified_prefix, accumulator,
|
|
7952
|
+
defs_singleton: defs_singleton, &walk)
|
|
7953
|
+
end
|
|
7954
|
+
current_visibility
|
|
7955
|
+
end
|
|
7730
7956
|
|
|
7731
|
-
|
|
7732
|
-
|
|
7733
|
-
|
|
7957
|
+
def apply_visibility_argument(arg, name, call_node, qualified_prefix, accumulator, defs_singleton:, &walk)
|
|
7958
|
+
case arg
|
|
7959
|
+
when Prism::DefNode
|
|
7960
|
+
if arg.receiver.nil?
|
|
7961
|
+
record_def_visibility(arg, qualified_prefix, defs_singleton, name, accumulator, toggled: false)
|
|
7962
|
+
else
|
|
7963
|
+
accumulator.contest_slot(qualified_prefix.join("::"), arg.name)
|
|
7964
|
+
end
|
|
7965
|
+
when Prism::SymbolNode, Prism::StringNode, Prism::ArrayNode
|
|
7966
|
+
apply_named_visibility(arg, name, qualified_prefix, call_node, accumulator, &walk)
|
|
7734
7967
|
else
|
|
7735
|
-
|
|
7736
|
-
|
|
7968
|
+
# An `instance_eval` body's wrapped attr is walked like any other argument and records nothing.
|
|
7969
|
+
if !defs_singleton && attr_visibility_call?(arg)
|
|
7970
|
+
record_attr_visibility(arg, qualified_prefix, name, accumulator, toggled: false)
|
|
7971
|
+
else
|
|
7972
|
+
accumulator.uncertainly { walk.call(arg) }
|
|
7973
|
+
end
|
|
7737
7974
|
end
|
|
7738
7975
|
end
|
|
7739
7976
|
|
|
7740
|
-
|
|
7977
|
+
# `private :foo` / `private "foo"` / `private %i[foo bar]`: each literal name the call's visibility. An array
|
|
7978
|
+
# element that is not a literal name is walked like any other argument.
|
|
7979
|
+
def apply_named_visibility(arg, visibility, qualified_prefix, call_node, accumulator, &walk)
|
|
7980
|
+
elements = arg.is_a?(Prism::ArrayNode) ? arg.elements : [arg]
|
|
7741
7981
|
class_name = qualified_prefix.join("::")
|
|
7742
|
-
|
|
7743
|
-
name = visibility_target_name(
|
|
7744
|
-
next if name.nil?
|
|
7982
|
+
elements.each do |element|
|
|
7983
|
+
name = visibility_target_name(element)
|
|
7984
|
+
next accumulator.uncertainly { walk.call(element) } if name.nil?
|
|
7745
7985
|
|
|
7746
7986
|
accumulator[class_name] ||= {}
|
|
7747
|
-
accumulator[class_name][name] =
|
|
7987
|
+
accumulator[class_name][name] = visibility
|
|
7748
7988
|
accumulator.wrote(class_name, name, call_node, toggled: false)
|
|
7749
7989
|
end
|
|
7750
7990
|
end
|
|
@@ -7753,6 +7993,32 @@ module Rigor
|
|
|
7753
7993
|
Source::Literals.symbol_or_string(arg)
|
|
7754
7994
|
end
|
|
7755
7995
|
|
|
7996
|
+
# Issue #1911 — an `attr_reader` / `attr_writer` / `attr_accessor` call written on the body's self records
|
|
7997
|
+
# each member it defines (`x`, `x=`) with `visibility`: the running default for a statement (`toggled`), the
|
|
7998
|
+
# wrapping call's for `private attr_accessor :x`, which Ruby applies to the `[:x, :x=]` the macro returns.
|
|
7999
|
+
# A non-literal name (`attr_reader(*FIELDS)`) records nothing; a call that is no such macro records nothing.
|
|
8000
|
+
def record_attr_visibility(call_node, qualified_prefix, visibility, accumulator, toggled:)
|
|
8001
|
+
return unless attr_visibility_call?(call_node)
|
|
8002
|
+
return if qualified_prefix.empty?
|
|
8003
|
+
|
|
8004
|
+
class_name = qualified_prefix.join("::")
|
|
8005
|
+
each_attr_macro_name(call_node) do |base|
|
|
8006
|
+
members = []
|
|
8007
|
+
members << base unless call_node.name == :attr_writer
|
|
8008
|
+
members << :"#{base}=" unless call_node.name == :attr_reader
|
|
8009
|
+
members.each do |member|
|
|
8010
|
+
accumulator[class_name] ||= {}
|
|
8011
|
+
accumulator[class_name][member] = visibility
|
|
8012
|
+
accumulator.wrote(class_name, member, call_node, toggled: toggled)
|
|
8013
|
+
end
|
|
8014
|
+
end
|
|
8015
|
+
end
|
|
8016
|
+
|
|
8017
|
+
# A receiverless `attr_reader` / `attr_writer` / `attr_accessor` call ({#record_attr_visibility}).
|
|
8018
|
+
def attr_visibility_call?(node)
|
|
8019
|
+
node.is_a?(Prism::CallNode) && node.receiver.nil? && ATTR_MACROS.include?(node.name)
|
|
8020
|
+
end
|
|
8021
|
+
|
|
7756
8022
|
# Registers the alias name in the `discovered_methods` table so `undefined-method` diagnostics are not emitted for
|
|
7757
8023
|
# calls to the aliased name. The kind mirrors the surrounding class context (instance inside a regular class body,
|
|
7758
8024
|
# singleton inside `class << self`).
|
|
@@ -7858,21 +8124,20 @@ module Rigor
|
|
|
7858
8124
|
end
|
|
7859
8125
|
|
|
7860
8126
|
# Issue #1752 — the names a {#singleton_class_named_call?} rewrites on its singleton class's owner, each
|
|
7861
|
-
# singleton envelope opaque: the evidence the refinement typer reads as a rewritten singleton name.
|
|
8127
|
+
# singleton envelope opaque and contested: the evidence the refinement typer reads as a rewritten singleton name.
|
|
7862
8128
|
def record_singleton_class_names(node, object, qualified_prefix, tables)
|
|
7863
8129
|
current_class = qualified_prefix.empty? ? nil : qualified_prefix.join("::")
|
|
7864
8130
|
owner = singleton_class_owner(node, object, qualified_prefix, current_class)
|
|
7865
8131
|
return if owner.nil?
|
|
7866
8132
|
|
|
7867
|
-
(node
|
|
7868
|
-
method_name = literal_method_name(argument)
|
|
7869
|
-
next unless method_name
|
|
7870
|
-
|
|
8133
|
+
singleton_class_rewritten_names(node).each do |method_name|
|
|
7871
8134
|
names = [method_name]
|
|
7872
8135
|
names << :"#{method_name}=" if %i[attr_writer attr_accessor].include?(node.name)
|
|
7873
8136
|
names.each do |name|
|
|
7874
8137
|
record_envelope(tables.envelopes, owner, [:singleton, name], Source::ParameterEnvelope::OPAQUE)
|
|
7875
|
-
|
|
8138
|
+
# Contested even where the call is certain: the slot's `def` is not the body Ruby runs, so the
|
|
8139
|
+
# own-definer read declines on the name rather than answering it.
|
|
8140
|
+
contest_envelope(tables, owner, [:singleton, name])
|
|
7876
8141
|
end
|
|
7877
8142
|
end
|
|
7878
8143
|
end
|
|
@@ -8351,12 +8616,14 @@ module Rigor
|
|
|
8351
8616
|
# {#discovered_project_index_incremental}'s changed-file branch) yields the live def nodes the signature
|
|
8352
8617
|
# reads their parameter structure from. Issue #1120 — and each file's own refinement table, which the merged
|
|
8353
8618
|
# def-index cannot attribute back to a file, so the session can diff it against the file's seed bundle.
|
|
8354
|
-
#
|
|
8619
|
+
# Issue #1459 — and each file's {#defined_method_names}, diffed the same way.
|
|
8620
|
+
# @return `{ def_index:, code_fingerprints:, declaration_signatures:, refinements:, defined_method_names: }`.
|
|
8355
8621
|
def scan_summary_for_paths(paths, buffer: nil)
|
|
8356
8622
|
acc = new_def_index_accumulator
|
|
8357
8623
|
code_fingerprints = {}
|
|
8358
8624
|
declaration_signatures = {}
|
|
8359
8625
|
refinements = {}
|
|
8626
|
+
defined_method_names = {}
|
|
8360
8627
|
paths.each do |path|
|
|
8361
8628
|
physical = buffer ? buffer.resolve(path) : path
|
|
8362
8629
|
source = File.read(physical)
|
|
@@ -8366,13 +8633,29 @@ module Rigor
|
|
|
8366
8633
|
code_fingerprints[path] = code_fingerprint(source, parsed.comments)
|
|
8367
8634
|
declaration_signatures[path] = declaration_signature(file_index)
|
|
8368
8635
|
refinements[path] = file_index[:refinements] if file_index[:refinements]
|
|
8636
|
+
defined_method_names[path] = defined_method_names(file_index)
|
|
8369
8637
|
rescue DeclarationWalk::ContractError
|
|
8370
8638
|
raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
|
|
8371
8639
|
rescue StandardError
|
|
8372
8640
|
next
|
|
8373
8641
|
end
|
|
8374
8642
|
{ def_index: finalize_def_index(acc), code_fingerprints: code_fingerprints,
|
|
8375
|
-
declaration_signatures: declaration_signatures, refinements: refinements
|
|
8643
|
+
declaration_signatures: declaration_signatures, refinements: refinements,
|
|
8644
|
+
defined_method_names: defined_method_names }
|
|
8645
|
+
end
|
|
8646
|
+
|
|
8647
|
+
# Issue #1459 — every method name one file's index defines, on any class or module and either side: the keys of
|
|
8648
|
+
# `methods` (which also carries an `attr_*`, `alias` or `define_method` name) and of the instance and singleton
|
|
8649
|
+
# def-node tables. A single file's `methods` already holds its `def` names, since `subtract_def_methods` runs only
|
|
8650
|
+
# on the merged accumulator; the def-node tables are read as well so the census reads exactly the tables
|
|
8651
|
+
# `BlockCallTiming.project_defines_anywhere?` reads. Takes a live file index or a seed bundle, which hold the
|
|
8652
|
+
# same three keys.
|
|
8653
|
+
def defined_method_names(file_index)
|
|
8654
|
+
names = Set.new
|
|
8655
|
+
%i[methods def_nodes singleton_def_nodes].each do |key|
|
|
8656
|
+
file_index[key]&.each_value { |table| names.merge(table.keys) }
|
|
8657
|
+
end
|
|
8658
|
+
names.freeze
|
|
8376
8659
|
end
|
|
8377
8660
|
|
|
8378
8661
|
# ADR-89 WD1 — a per-file digest of every cross-file DECLARATION surface an ancestry / file-level
|
|
@@ -10589,8 +10872,15 @@ module Rigor
|
|
|
10589
10872
|
|
|
10590
10873
|
# Issue #1717 — a `def` / `class` / `module` body opens its own `self`, so an enclosing unentered block's
|
|
10591
10874
|
# unknown-`self` mark ({Scope#block_self_unknown?}) stops at it, as the evaluator's fresh body scope does.
|
|
10592
|
-
#
|
|
10875
|
+
# A `case` narrows its clauses ({#propagate_subjectless_case}, {#propagate_subject_case}). Every other node
|
|
10876
|
+
# hands its children the scope as is.
|
|
10593
10877
|
def propagate_children(node, table, current_scope)
|
|
10878
|
+
if node.is_a?(Prism::CaseNode) || node.is_a?(Prism::CaseMatchNode)
|
|
10879
|
+
return propagate_subjectless_case(node, table, current_scope) if node.predicate.nil?
|
|
10880
|
+
|
|
10881
|
+
return propagate_subject_case(node, table, current_scope)
|
|
10882
|
+
end
|
|
10883
|
+
|
|
10594
10884
|
child_scope =
|
|
10595
10885
|
SELF_OPENING_BODIES.include?(node.class) ? current_scope.with_block_self_unknown(false) : current_scope
|
|
10596
10886
|
node.rigor_each_child { |child| propagate(child, table, child_scope) }
|
|
@@ -10796,6 +11086,85 @@ module Rigor
|
|
|
10796
11086
|
propagate(node.statements, table, falsey_scope) if node.statements
|
|
10797
11087
|
propagate(node.else_clause, table, truthy_scope) if node.else_clause
|
|
10798
11088
|
end
|
|
11089
|
+
|
|
11090
|
+
# Issue #1495 — a `case` without a subject the evaluator did not enter (`[case; when x.nil? then 0; else x.size
|
|
11091
|
+
# end]`) narrows by its conditions' edges, as {StatementEvaluator#subjectless_when_scopes} does: each condition
|
|
11092
|
+
# under the falsey edges of the conditions before it, each clause under the join of its conditions' truthy edges,
|
|
11093
|
+
# and the `else` under the last falsey edge. Like the walk of an `if`, it applies no write a condition makes, so
|
|
11094
|
+
# the edges are taken from a scope where every bound local the condition writes reads `Dynamic[top]`
|
|
11095
|
+
# ({#widen_condition_writes}): neither the local's old type nor an earlier condition's narrowing of it reaches
|
|
11096
|
+
# them, as `when (x = s ? String(s) : nil) then x.upcase` after `x = 1` would.
|
|
11097
|
+
def propagate_subjectless_case(node, table, current_scope)
|
|
11098
|
+
falsey = current_scope
|
|
11099
|
+
node.conditions.each do |when_node|
|
|
11100
|
+
truthy_edges = when_node.conditions.map do |condition|
|
|
11101
|
+
propagate(condition, table, falsey)
|
|
11102
|
+
truthy, falsey = Narrowing.predicate_scopes(condition, widen_condition_writes(condition, falsey))
|
|
11103
|
+
truthy
|
|
11104
|
+
end
|
|
11105
|
+
propagate(when_node, table, truthy_edges.reduce(:join))
|
|
11106
|
+
end
|
|
11107
|
+
propagate(node.else_clause, table, falsey) if node.else_clause
|
|
11108
|
+
end
|
|
11109
|
+
|
|
11110
|
+
# Issue #1484 — a `case` with a subject the evaluator did not enter (`[case io when StringIO then io.string
|
|
11111
|
+
# end]`, `[case io in StringIO then io.string else nil end]`) narrows each clause as the evaluator does
|
|
11112
|
+
# ({Narrowing.case_clause_scopes}): the body under the clause's narrowed edge, the next clause and the `else`
|
|
11113
|
+
# under its falsey edge. The walk applies no write the subject, a `when` condition or an `in` pattern makes, so
|
|
11114
|
+
# every bound local one of them writes reads `Dynamic[top]` from that point on ({#widen_condition_writes}).
|
|
11115
|
+
# Nor does it run the evaluator's per-clause `LastLine.forget_if_set`: the statement holding the `case`
|
|
11116
|
+
# already forgets a `$_` any of its tests may set. A clause's tests are recorded under the scope the earlier
|
|
11117
|
+
# clauses' falsey edges left, where the evaluator's `when` tests read the post-subject scope: a test reading
|
|
11118
|
+
# the subject sees what the earlier clauses ruled out (`when NilClass then 0 when v.succ`).
|
|
11119
|
+
#
|
|
11120
|
+
# A test that calls a method may rebind an instance-variable or global subject before the body reads it
|
|
11121
|
+
# (`when String then 1 when reset! then @x.upcase`, where `reset!` writes `@x`), and the walk cannot tell. From
|
|
11122
|
+
# the first such test on, the subject reads `Dynamic[top]` on both edges ({#forget_called_subject}). The
|
|
11123
|
+
# evaluator keeps the narrowing there (a separate defect); the walk declines it rather than add a report.
|
|
11124
|
+
def propagate_subject_case(node, table, current_scope)
|
|
11125
|
+
subject = node.predicate
|
|
11126
|
+
propagate(subject, table, current_scope)
|
|
11127
|
+
falsey = widen_condition_writes(subject, current_scope)
|
|
11128
|
+
called = false
|
|
11129
|
+
node.conditions.each do |clause|
|
|
11130
|
+
tests = clause.is_a?(Prism::WhenNode) ? clause.conditions : [clause.pattern]
|
|
11131
|
+
tests.each { |test| propagate(test, table, falsey) }
|
|
11132
|
+
falsey = tests.reduce(falsey) { |acc, test| widen_condition_writes(test, acc) }
|
|
11133
|
+
called ||= tests.any? { |test| runs_call?(test) }
|
|
11134
|
+
body, falsey = Narrowing.case_clause_scopes(subject, clause, falsey)
|
|
11135
|
+
body, falsey = [body, falsey].map { |scope| forget_called_subject(subject, scope) } if called
|
|
11136
|
+
propagate(clause, table, body)
|
|
11137
|
+
end
|
|
11138
|
+
propagate(node.else_clause, table, falsey) if node.else_clause
|
|
11139
|
+
end
|
|
11140
|
+
|
|
11141
|
+
CODE_RUNNING_NODES = [Prism::CallNode, Prism::SuperNode, Prism::ForwardingSuperNode, Prism::YieldNode].freeze
|
|
11142
|
+
private_constant :CODE_RUNNING_NODES
|
|
11143
|
+
|
|
11144
|
+
# Whether `node` holds a method call, `super` or `yield` anywhere, a lambda body's included.
|
|
11145
|
+
def runs_call?(node)
|
|
11146
|
+
return false unless node.is_a?(Prism::Node)
|
|
11147
|
+
return true if CODE_RUNNING_NODES.include?(node.class)
|
|
11148
|
+
|
|
11149
|
+
node.rigor_each_child { |child| return true if runs_call?(child) }
|
|
11150
|
+
false
|
|
11151
|
+
end
|
|
11152
|
+
|
|
11153
|
+
# `scope` with an instance-variable or global `subject` rebound to `Dynamic[top]`; any other subject as is.
|
|
11154
|
+
def forget_called_subject(subject, scope)
|
|
11155
|
+
case subject
|
|
11156
|
+
when Prism::InstanceVariableReadNode then scope.with_ivar(subject.name, Type::Combinator.untyped)
|
|
11157
|
+
when Prism::GlobalVariableReadNode then scope.with_global(subject.name, Type::Combinator.untyped)
|
|
11158
|
+
else scope
|
|
11159
|
+
end
|
|
11160
|
+
end
|
|
11161
|
+
|
|
11162
|
+
# `scope` with every bound local `condition` writes rebound to `Dynamic[top]`.
|
|
11163
|
+
def widen_condition_writes(condition, scope)
|
|
11164
|
+
OperandEffects.written_variables(condition).reduce(scope) do |acc, name|
|
|
11165
|
+
acc.local(name) ? acc.with_local(name, Type::Combinator.untyped) : acc
|
|
11166
|
+
end
|
|
11167
|
+
end
|
|
10799
11168
|
end
|
|
10800
11169
|
end
|
|
10801
11170
|
end
|