rigortype 0.4.4 → 0.4.5
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 +5 -5
- data/docs/handbook/11-sig-gen.md +90 -17
- data/docs/manual/02-cli-reference.md +39 -9
- data/docs/manual/03-configuration.md +10 -8
- data/docs/manual/04-diagnostics.md +16 -5
- data/docs/manual/05-inspecting-types.md +2 -2
- data/docs/manual/06-baseline.md +1 -1
- data/docs/manual/07-plugins.md +2 -1
- data/docs/manual/08-skills.md +7 -0
- data/docs/manual/10-mcp-server.md +8 -1
- data/docs/manual/18-removing-dead-code.md +18 -7
- data/docs/manual/plugins/rigor-rspec.md +101 -1
- data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +111 -7
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +190 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +3 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +13 -4
- data/lib/rigor/analysis/check_rules.rb +228 -15
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +49 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +166 -24
- data/lib/rigor/analysis/path_expansion.rb +3 -1
- data/lib/rigor/analysis/reachability/graph.rb +90 -13
- data/lib/rigor/analysis/reachability/scan.rb +279 -39
- data/lib/rigor/analysis/rule_catalog.rb +17 -4
- data/lib/rigor/analysis/runner/pool_coordinator.rb +69 -16
- data/lib/rigor/analysis/runner.rb +69 -65
- data/lib/rigor/analysis/severity_stamp.rb +11 -0
- data/lib/rigor/analysis/worker_session.rb +12 -1
- data/lib/rigor/cache/descriptor.rb +40 -7
- data/lib/rigor/cache/engine_source.rb +3 -2
- data/lib/rigor/cache/incremental_snapshot.rb +26 -8
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +4 -3
- data/lib/rigor/cli/command.rb +3 -1
- data/lib/rigor/cli/docs_command.rb +3 -2
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/explain_command.rb +3 -2
- data/lib/rigor/cli/plugin_command.rb +2 -1
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +32 -11
- data/lib/rigor/cli/skill_command.rb +2 -1
- data/lib/rigor/cli/skill_describe.rb +3 -2
- data/lib/rigor/cli/unused_command.rb +10 -7
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +60 -15
- data/lib/rigor/configuration/severity_profile.rb +20 -2
- data/lib/rigor/configuration.rb +29 -3
- data/lib/rigor/effects/signature_sources.rb +3 -1
- data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
- data/lib/rigor/environment/gated_signature_guard.rb +2 -1
- data/lib/rigor/environment/gated_signature_plan.rb +2 -1
- data/lib/rigor/environment/installed_gem_set.rb +2 -1
- data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
- data/lib/rigor/environment/rbs_loader.rb +21 -1
- data/lib/rigor/inference/block_call_timing.rb +7 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/captured_locals.rb +44 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/definer_resolution.rb +87 -20
- data/lib/rigor/inference/expression_typer.rb +35 -5
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- data/lib/rigor/inference/macro_block_self_type.rb +20 -6
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +170 -35
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
- data/lib/rigor/inference/method_dispatcher.rb +67 -3
- data/lib/rigor/inference/module_function_state.rb +137 -42
- data/lib/rigor/inference/narrowing.rb +54 -10
- data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
- data/lib/rigor/inference/project_patched_methods.rb +168 -4
- data/lib/rigor/inference/project_patched_mixins.rb +162 -0
- data/lib/rigor/inference/project_patched_scanner.rb +51 -12
- data/lib/rigor/inference/project_patched_visibility.rb +214 -0
- data/lib/rigor/inference/project_seed_tables.rb +120 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
- data/lib/rigor/inference/reflective_ivar_write.rb +6 -6
- data/lib/rigor/inference/return_barrier.rb +16 -0
- data/lib/rigor/inference/scope_indexer.rb +675 -105
- data/lib/rigor/inference/self_call_ivar_writes.rb +11 -6
- data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
- data/lib/rigor/inference/statement_evaluator.rb +446 -139
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- data/lib/rigor/lens/contract_reader.rb +45 -2
- data/lib/rigor/lens/document.rb +2 -1
- data/lib/rigor/lens/file_reader.rb +12 -2
- data/lib/rigor/lens/members.rb +16 -2
- data/lib/rigor/lens/run.rb +34 -7
- data/lib/rigor/mcp/server.rb +2 -2
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -3
- data/lib/rigor/protection/discovery_seed.rb +9 -36
- data/lib/rigor/protection/mutation_cache.rb +8 -2
- data/lib/rigor/rooted_glob.rb +27 -0
- data/lib/rigor/scope/discovery_index.rb +5 -0
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
- data/lib/rigor/scope/resolution_chain.rb +40 -5
- data/lib/rigor/scope.rb +45 -2
- data/lib/rigor/sig_gen/alias_index.rb +2 -1
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +167 -22
- data/lib/rigor/sig_gen/layout_index.rb +5 -2
- data/lib/rigor/sig_gen/observation_collector.rb +137 -11
- data/lib/rigor/sig_gen/renderer.rb +13 -155
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
- data/lib/rigor/sig_gen/write_reporter.rb +203 -0
- data/lib/rigor/sig_gen/write_result.rb +3 -1
- data/lib/rigor/sig_gen/writer.rb +5 -3
- data/lib/rigor/signature_path_audit.rb +4 -2
- data/lib/rigor/type/constant.rb +10 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +38 -9
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
- data/sig/rigor/plugin/io_boundary.rbs +5 -2
- data/sig/rigor/rooted_glob.rbs +7 -0
- data/sig/rigor/scope.rbs +4 -3
- data/skills/rigor-doctor/references/01-checks.md +23 -11
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
- metadata +13 -1
|
@@ -25,6 +25,7 @@ require_relative "hash_lookup_mutation"
|
|
|
25
25
|
require_relative "in_effect_refinements"
|
|
26
26
|
require_relative "refine_census"
|
|
27
27
|
require_relative "refine_self"
|
|
28
|
+
require_relative "reflective_ivar_write"
|
|
28
29
|
require_relative "index_write_widening"
|
|
29
30
|
require_relative "module_function_state"
|
|
30
31
|
require_relative "multi_target_binder"
|
|
@@ -332,8 +333,10 @@ module Rigor
|
|
|
332
333
|
# possible methods were folded already ({#seed_discovered_methods}).
|
|
333
334
|
def per_file_siblings(siblings, file_def_nodes, file_siblings, file_singleton, file_visibilities)
|
|
334
335
|
file_singleton_def_nodes, file_singleton_contested = file_singleton
|
|
335
|
-
|
|
336
|
-
|
|
336
|
+
# The seed's singleton contests stay ({#fold_visibility_contests}); it already holds a disagreement between
|
|
337
|
+
# this file and another.
|
|
338
|
+
siblings[:contested_discovered_method_visibilities] = fold_visibility_contests(
|
|
339
|
+
siblings[:contested_discovered_method_visibilities], {}, *file_visibilities
|
|
337
340
|
)
|
|
338
341
|
siblings[:contested_discovered_def_nodes] = fold_slot_contests(
|
|
339
342
|
siblings[:contested_discovered_def_nodes], file_def_nodes, file_siblings[:contested_discovered_def_nodes]
|
|
@@ -507,20 +510,23 @@ module Rigor
|
|
|
507
510
|
method_assign_effects = build_method_assign_effects(root)
|
|
508
511
|
walk_class_ivars(root, [], default_scope, accumulator, mutated_ivars,
|
|
509
512
|
read_before_write, init_writes, method_assign_effects)
|
|
513
|
+
reflective_opaque = accumulator.delete(REFLECTIVE_OPAQUE_CLASSES)
|
|
510
514
|
exempt_class_body_writes!(init_writes, accumulator.delete(DEFINE_METHOD_WRITES) || {})
|
|
511
515
|
merge_held_ivar_writes!(accumulator)
|
|
512
516
|
record_aliased_ivar_mutations!(root, mutated_ivars)
|
|
513
517
|
widen_mutated_ivar_entries!(accumulator, mutated_ivars)
|
|
514
518
|
contribute_read_before_write_nil!(accumulator, read_before_write, init_writes)
|
|
519
|
+
make_reflective_classes_gradual!(accumulator, reflective_opaque)
|
|
515
520
|
accumulator.transform_values(&:freeze).freeze
|
|
516
521
|
end
|
|
517
522
|
|
|
518
523
|
# 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
|
|
520
|
-
#
|
|
521
|
-
#
|
|
522
|
-
#
|
|
523
|
-
#
|
|
524
|
+
# `init_writes` exemption, except for an ivar a `define_method` block, or a `block_as_methods:` block bound to the
|
|
525
|
+
# receiver's instance ({#method_body_block_call?}), of the class writes (#1768). Such an ivar had no census entry
|
|
526
|
+
# before the block's writes were counted, so the exemption never applied to it; a class-body `@x = nil` writes
|
|
527
|
+
# the class object's ivar and says nothing about whether an instance's `@x` is set, and exempting it would seed
|
|
528
|
+
# `@x` non-nil from the block's write alone (`if @x` folding always-truthy on an instance whose `@x` is unset).
|
|
529
|
+
# It keeps the read-before-write `nil` instead, which only widens.
|
|
524
530
|
def exempt_class_body_writes!(init_writes, define_method_writes)
|
|
525
531
|
(init_writes.delete(CLASS_BODY_WRITES) || {}).each do |class_name, ivars|
|
|
526
532
|
kept = ivars - (define_method_writes[class_name] || EMPTY_GUARDED_IVARS)
|
|
@@ -867,7 +873,7 @@ module Rigor
|
|
|
867
873
|
|
|
868
874
|
owner = def_owner || qualified_prefix
|
|
869
875
|
collect_initializer_block_ivars(node, owner, default_scope, accumulator, mutated_ivars, init_writes)
|
|
870
|
-
record_attr_writer_ivars(node, owner, accumulator) if ATTR_WRITER_MACROS.include?(node
|
|
876
|
+
record_attr_writer_ivars(node, owner, accumulator) if ATTR_WRITER_MACROS.include?(attr_macro(node))
|
|
871
877
|
collect_define_method_block_ivars(node, owner, default_scope, accumulator, mutated_ivars)
|
|
872
878
|
false
|
|
873
879
|
else
|
|
@@ -1090,14 +1096,19 @@ module Rigor
|
|
|
1090
1096
|
# `owner` is empty in a `class << self` body (the call defines a class method there) and in an
|
|
1091
1097
|
# anonymous factory body, and the block contributes nothing there. A `define_method` inside a `def`
|
|
1092
1098
|
# body is already reached by that body's walk ({#gather_ivar_writes} enters blocks).
|
|
1099
|
+
#
|
|
1100
|
+
# An ADR-16 `block_as_methods:` call whose entry binds the receiver's instance (`get "/" do … end` in a
|
|
1101
|
+
# `Sinatra::Base` subclass under rigor-sinatra) takes the same arm: the plugin declares that its block runs as an
|
|
1102
|
+
# instance method of the receiver class, and `MacroBlockSelfType` narrows the block's `self` to that instance.
|
|
1093
1103
|
def collect_define_method_block_ivars(node, owner, default_scope, accumulator, mutated_ivars)
|
|
1094
1104
|
return if owner.empty?
|
|
1095
|
-
|
|
1105
|
+
|
|
1106
|
+
class_name = owner.join("::")
|
|
1107
|
+
return unless method_body_block_call?(node, class_name, default_scope)
|
|
1096
1108
|
|
|
1097
1109
|
block = node.block
|
|
1098
1110
|
return unless block.body
|
|
1099
1111
|
|
|
1100
|
-
class_name = owner.join("::")
|
|
1101
1112
|
body_scope = census_body_scope(default_scope, owner, Type::Combinator.nominal_of(class_name))
|
|
1102
1113
|
gather_ivar_writes(block.body, body_scope, class_name, accumulator, EMPTY_GUARDED_IVARS, mutated_ivars)
|
|
1103
1114
|
written = (accumulator[DEFINE_METHOD_WRITES] ||= {})[class_name] ||= Set.new
|
|
@@ -1115,6 +1126,8 @@ module Rigor
|
|
|
1115
1126
|
Prism::InstanceVariableTargetNode
|
|
1116
1127
|
acc << node.name
|
|
1117
1128
|
end
|
|
1129
|
+
reflective = ReflectiveIvarWrite.literal_name(node)
|
|
1130
|
+
acc << reflective if reflective
|
|
1118
1131
|
node.rigor_each_child { |child| define_method_written_ivars(child, acc) }
|
|
1119
1132
|
end
|
|
1120
1133
|
|
|
@@ -1243,6 +1256,25 @@ module Rigor
|
|
|
1243
1256
|
DefineMethodBlockSelf.define_method_on_lexical_self?(node) && node.block.is_a?(Prism::BlockNode)
|
|
1244
1257
|
end
|
|
1245
1258
|
|
|
1259
|
+
# A call with a literal block on the implicit or `self` receiver whose block is an instance method body of
|
|
1260
|
+
# `class_name`, the class the walk is in (#1768): `define_method`, or a call an ADR-16 `block_as_methods:` entry
|
|
1261
|
+
# declares runs its block as an instance method of the receiver class ({MacroBlockSelfType}). The entry is
|
|
1262
|
+
# matched on `Singleton[class_name]`, the receiver such a call has in the class body, through the same
|
|
1263
|
+
# receiver-ancestry match the evaluator narrows the block's `self` with, so the census counts exactly the blocks
|
|
1264
|
+
# whose `self` is narrowed to the instance.
|
|
1265
|
+
#
|
|
1266
|
+
# The class-body walk ({#collect_class_body_ivar_writes}) still records a `block_as_methods:` block's writes as
|
|
1267
|
+
# class-body writes, and {#exempt_class_body_writes!} takes back every ivar such a block writes.
|
|
1268
|
+
def method_body_block_call?(node, class_name, scope)
|
|
1269
|
+
return true if define_method_block_call?(node)
|
|
1270
|
+
return false unless node.block.is_a?(Prism::BlockNode)
|
|
1271
|
+
return false unless scope && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
|
|
1272
|
+
|
|
1273
|
+
MacroBlockSelfType.runs_as_receiver_instance_method?(
|
|
1274
|
+
scope: scope, call_node: node, receiver_type: Type::Combinator.singleton_of(class_name)
|
|
1275
|
+
)
|
|
1276
|
+
end
|
|
1277
|
+
|
|
1246
1278
|
def detect_read_before_write(node, seen_writes, read_first)
|
|
1247
1279
|
return unless node.is_a?(Prism::Node)
|
|
1248
1280
|
return if IVAR_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
|
|
@@ -1270,6 +1302,9 @@ module Rigor
|
|
|
1270
1302
|
end
|
|
1271
1303
|
|
|
1272
1304
|
seen_writes << node.name if IVAR_WRITE_NODES.any? { |klass| node.is_a?(klass) }
|
|
1305
|
+
# #1931 — a literal `instance_variable_set` is a write too; its value argument, read above, runs first.
|
|
1306
|
+
reflective = ReflectiveIvarWrite.literal_name(node)
|
|
1307
|
+
seen_writes << reflective if reflective
|
|
1273
1308
|
end
|
|
1274
1309
|
|
|
1275
1310
|
# Records each ivar target of a `MultiWriteNode` / nested `MultiTargetNode` into `seen_writes`, and descends into
|
|
@@ -1317,6 +1352,7 @@ module Rigor
|
|
|
1317
1352
|
end
|
|
1318
1353
|
else
|
|
1319
1354
|
record_compound_ivar_write(node, scope, class_name, accumulator)
|
|
1355
|
+
record_reflective_ivar_write(node, scope, class_name, accumulator)
|
|
1320
1356
|
end
|
|
1321
1357
|
|
|
1322
1358
|
# N1 — parallel / multiple assignment (`old, @cb = @cb, block`, `@i, @o, @e, @thr = Open3.popen3(cmd)`). A
|
|
@@ -1344,6 +1380,32 @@ module Rigor
|
|
|
1344
1380
|
end
|
|
1345
1381
|
end
|
|
1346
1382
|
|
|
1383
|
+
# #1931 — `instance_variable_set` on `self` writes an ivar no write node shows ({ReflectiveIvarWrite}). A literal
|
|
1384
|
+
# name (`instance_variable_set(:@k, v)`) contributes the value's type to that ivar's entry, as `@k = v` would. A
|
|
1385
|
+
# name the analysis cannot read (`instance_variable_set(:"@#{k}", v)`, the options-hash constructor idiom) may
|
|
1386
|
+
# write any ivar of the class, so every entry of the class turns gradual once the walk ends
|
|
1387
|
+
# ({#make_reflective_classes_gradual!}): a reader in another method otherwise kept a write's literal (`@timeout =
|
|
1388
|
+
# 5` read as `5`) and reported a call on it.
|
|
1389
|
+
def record_reflective_ivar_write(node, scope, class_name, accumulator)
|
|
1390
|
+
return unless ReflectiveIvarWrite.self_write?(node)
|
|
1391
|
+
|
|
1392
|
+
name = ReflectiveIvarWrite.literal_name(node)
|
|
1393
|
+
if name
|
|
1394
|
+
accumulate_ivar_type(accumulator, class_name, name, scope.type_of(ReflectiveIvarWrite.value_node(node)))
|
|
1395
|
+
else
|
|
1396
|
+
(accumulator[REFLECTIVE_OPAQUE_CLASSES] ||= Set.new) << class_name
|
|
1397
|
+
end
|
|
1398
|
+
end
|
|
1399
|
+
|
|
1400
|
+
def make_reflective_classes_gradual!(accumulator, classes)
|
|
1401
|
+
classes&.each do |class_name|
|
|
1402
|
+
entries = accumulator[class_name]
|
|
1403
|
+
next if entries.nil?
|
|
1404
|
+
|
|
1405
|
+
accumulator[class_name] = entries.transform_values { Type::Combinator.untyped }
|
|
1406
|
+
end
|
|
1407
|
+
end
|
|
1408
|
+
|
|
1347
1409
|
# Records `@ivar.<method>(...)` calls whose method is in `MutationWidening::SHAPE_MUTATORS`.
|
|
1348
1410
|
# The class-ivar pre-pass uses the resulting set to widen the post-collected accumulator entries (see
|
|
1349
1411
|
# {.widen_mutated_ivar_entries!}). Always-safe to over- collect: any name that the widening primitive declines is
|
|
@@ -2087,7 +2149,9 @@ module Rigor
|
|
|
2087
2149
|
# accumulator), and the class-body writes held out of `init_writes` until {#exempt_class_body_writes!}.
|
|
2088
2150
|
DEFINE_METHOD_WRITES = :define_method_writes
|
|
2089
2151
|
CLASS_BODY_WRITES = :class_body_writes
|
|
2090
|
-
|
|
2152
|
+
# The classes a computed-name `instance_variable_set` may write any ivar of ({#record_reflective_ivar_write}).
|
|
2153
|
+
REFLECTIVE_OPAQUE_CLASSES = :reflective_opaque_classes
|
|
2154
|
+
private_constant :DEFINE_METHOD_WRITES, :CLASS_BODY_WRITES, :REFLECTIVE_OPAQUE_CLASSES
|
|
2091
2155
|
|
|
2092
2156
|
# Unions `type` into the class-ivar accumulator for `(class_name, ivar_name)`. Shared by the single-write and
|
|
2093
2157
|
# multi-write (parallel-assignment) collectors.
|
|
@@ -4088,6 +4152,10 @@ module Rigor
|
|
|
4088
4152
|
(singleton_cref && owner_prefix.empty?)
|
|
4089
4153
|
record_alias_or_undef(node, owner_prefix, in_singleton_class || defs_singleton,
|
|
4090
4154
|
methods_acc.at(node))
|
|
4155
|
+
if node.is_a?(Prism::AliasMethodNode) && !(in_singleton_class || defs_singleton)
|
|
4156
|
+
supersede_def_nodes(Array(Source::AliasNames.keyword_names(node)&.first), owner_prefix,
|
|
4157
|
+
def_nodes_acc, methods_acc.at(node))
|
|
4158
|
+
end
|
|
4091
4159
|
end
|
|
4092
4160
|
return
|
|
4093
4161
|
when Prism::SymbolNode
|
|
@@ -4102,6 +4170,9 @@ module Rigor
|
|
|
4102
4170
|
end
|
|
4103
4171
|
anonymous = record_call_node_methods(node, owner_prefix, in_singleton_class, methods_acc.at(node),
|
|
4104
4172
|
source_path)
|
|
4173
|
+
unless in_singleton_class
|
|
4174
|
+
supersede_def_nodes(call_superseded_names(node), owner_prefix, def_nodes_acc, methods_acc.at(node))
|
|
4175
|
+
end
|
|
4105
4176
|
tables = methods_acc.at(node)
|
|
4106
4177
|
record_singleton_prepend(node, qualified_prefix, tables,
|
|
4107
4178
|
self_ok: node.name == :prepend && tables.in_block?(node))
|
|
@@ -4171,11 +4242,40 @@ module Rigor
|
|
|
4171
4242
|
def record_declaration_facts(node, child_prefix, tables)
|
|
4172
4243
|
if node.is_a?(Prism::ClassNode)
|
|
4173
4244
|
record_meta_superclass_members(node, child_prefix, tables)
|
|
4245
|
+
record_constructed_superclass(node.superclass, child_prefix.join("::"), tables)
|
|
4174
4246
|
else
|
|
4175
4247
|
record_surface_mark(tables, child_prefix.join("::"), Scope::DiscoveryIndex::ENVELOPE_MODULE_MARK)
|
|
4176
4248
|
end
|
|
4177
4249
|
end
|
|
4178
4250
|
|
|
4251
|
+
# Issue #1979 — `class K < Data.define(:a)`, `< Struct.new(:a)` or `< Class.new(Parent)`: the superclass table
|
|
4252
|
+
# records `nil` for K, so a subclass walk enumerates it under every class. Each constant the expression names
|
|
4253
|
+
# (the factory's receiver, and `Class.new`'s argument, or `Object` for none) takes a
|
|
4254
|
+
# `[ENVELOPE_CONSTRUCTED_KIND, name]` entry; an expression of any other shape records none.
|
|
4255
|
+
def record_constructed_superclass(superclass, class_name, tables)
|
|
4256
|
+
names = constructed_superclass_names(superclass)
|
|
4257
|
+
names&.each do |name|
|
|
4258
|
+
record_envelope(tables.envelopes, class_name, [Scope::DiscoveryIndex::ENVELOPE_CONSTRUCTED_KIND, name],
|
|
4259
|
+
Source::ParameterEnvelope::OPAQUE)
|
|
4260
|
+
end
|
|
4261
|
+
end
|
|
4262
|
+
|
|
4263
|
+
def constructed_superclass_names(node)
|
|
4264
|
+
return ["Data"] if meta_call_with_name?(node, :Data, :define)
|
|
4265
|
+
return ["Struct"] if meta_call_with_name?(node, :Struct, :new)
|
|
4266
|
+
return nil unless class_new_call?(node)
|
|
4267
|
+
|
|
4268
|
+
arguments = node.arguments&.arguments || []
|
|
4269
|
+
return %w[Class Object] if arguments.empty?
|
|
4270
|
+
return nil unless arguments.one?
|
|
4271
|
+
|
|
4272
|
+
argument = arguments.first
|
|
4273
|
+
inner = if argument.is_a?(Prism::ConstantReadNode) then [argument.name.to_s]
|
|
4274
|
+
else constructed_superclass_names(argument)
|
|
4275
|
+
end
|
|
4276
|
+
inner && ["Class", *inner]
|
|
4277
|
+
end
|
|
4278
|
+
|
|
4179
4279
|
# The block form of a meta-new constant write: its members, and the module mark `Module.new` earns.
|
|
4180
4280
|
# Issue #1820 — `K = Class.new(Parent)`, with or without a block, names a class whose superclass the superclass
|
|
4181
4281
|
# table leaves out; it takes {Scope::DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK}.
|
|
@@ -4214,7 +4314,7 @@ module Rigor
|
|
|
4214
4314
|
# descent.
|
|
4215
4315
|
def record_call_node_methods(node, qualified_prefix, in_singleton_class, methods_acc, source_path)
|
|
4216
4316
|
record_define_method(node, qualified_prefix, in_singleton_class, methods_acc) if node.name == :define_method
|
|
4217
|
-
record_attr_methods(node, qualified_prefix, in_singleton_class, methods_acc) if
|
|
4317
|
+
record_attr_methods(node, qualified_prefix, in_singleton_class, methods_acc) if attr_macro(node)
|
|
4218
4318
|
record_module_attr_methods(node, qualified_prefix, methods_acc) if MODULE_ATTR_MACROS.key?(node.name)
|
|
4219
4319
|
record_alias_method_call(node, qualified_prefix, in_singleton_class, methods_acc)
|
|
4220
4320
|
record_surface_evidence(node, qualified_prefix, methods_acc)
|
|
@@ -4727,6 +4827,49 @@ module Rigor
|
|
|
4727
4827
|
end
|
|
4728
4828
|
end
|
|
4729
4829
|
|
|
4830
|
+
# Issue #1989 — the instance method names a non-`def` definer written on the class body's self replaces:
|
|
4831
|
+
# each reader (and `x=` writer) an `attr_*` call declares, a literal `define_method(:x)`, and the new name of
|
|
4832
|
+
# an `alias_method :x, :y` call. A non-literal name replaces nothing Rigor can name.
|
|
4833
|
+
def call_superseded_names(call_node)
|
|
4834
|
+
if (macro = attr_macro(call_node))
|
|
4835
|
+
names = []
|
|
4836
|
+
each_attr_macro_name(call_node) do |base|
|
|
4837
|
+
names << base unless macro == :attr_writer
|
|
4838
|
+
names << :"#{base}=" unless macro == :attr_reader
|
|
4839
|
+
end
|
|
4840
|
+
names
|
|
4841
|
+
elsif call_node.name == :define_method
|
|
4842
|
+
define_method_superseded_name(call_node)
|
|
4843
|
+
else
|
|
4844
|
+
Source::AliasNames.alias_method_call_names(call_node)&.first(1) || []
|
|
4845
|
+
end
|
|
4846
|
+
end
|
|
4847
|
+
|
|
4848
|
+
def define_method_superseded_name(call_node)
|
|
4849
|
+
return [] unless call_node.receiver.nil? || call_node.receiver.is_a?(Prism::SelfNode)
|
|
4850
|
+
|
|
4851
|
+
name = literal_method_name(call_node.arguments&.arguments&.first)
|
|
4852
|
+
name.nil? ? [] : [name.to_sym]
|
|
4853
|
+
end
|
|
4854
|
+
|
|
4855
|
+
# Issue #1989 — a definer that is not a `def` replaces the method an earlier `def` of the same name in this walk
|
|
4856
|
+
# installed: `def x = 1; attr_reader :x` answers the attr reader, so the `def` body must not type the call. The
|
|
4857
|
+
# slot keeps the `def` and is contested, certain definer or possible, so the readers that re-type a body
|
|
4858
|
+
# decline. The slot is not emptied: the file still writes it, so a sibling file's `def` of the name (which the
|
|
4859
|
+
# cross-file seed and the project fold would otherwise put there) cannot fill it. A `def` written after the
|
|
4860
|
+
# definer writes the slot again and clears the contest.
|
|
4861
|
+
def supersede_def_nodes(names, qualified_prefix, accumulator, tables)
|
|
4862
|
+
return if names.empty?
|
|
4863
|
+
|
|
4864
|
+
class_name = qualified_prefix.join("::")
|
|
4865
|
+
slot = accumulator[class_name]
|
|
4866
|
+
return if slot.nil?
|
|
4867
|
+
|
|
4868
|
+
names.each do |name|
|
|
4869
|
+
follow_slot_writer(tables, :contested_def_nodes, [class_name, name], false) if slot.key?(name)
|
|
4870
|
+
end
|
|
4871
|
+
end
|
|
4872
|
+
|
|
4730
4873
|
# #319 — a `def` inside an anonymous `Class.new` / `Module.new` body ALSO stays in the `<toplevel>` table.
|
|
4731
4874
|
# Before the anonymous class had a name the body was walked with an empty prefix, so every such `def`
|
|
4732
4875
|
# landed there; that is the same leniency this key already grants a `def` nested in any other DSL block
|
|
@@ -4772,6 +4915,18 @@ module Rigor
|
|
|
4772
4915
|
# The singleton walk's accumulator: the `{class => {name => DefNode}}` table, plus the contested slots, which
|
|
4773
4916
|
# follow each slot's writer ({#follow_slot_writer}'s rule).
|
|
4774
4917
|
class SingletonDefTable < Hash
|
|
4918
|
+
# Issue #1936 — contests the slots of `names` (every slot of `class_name` for `:all`) that an earlier write
|
|
4919
|
+
# filled: a later non-`def` definer replaced the `def` they hold. The slot keeps the `def`, as
|
|
4920
|
+
# {ScopeIndexer#supersede_def_nodes} keeps the instance one, and a later write clears the contest.
|
|
4921
|
+
def supersede(class_name, names)
|
|
4922
|
+
slot = self[class_name]
|
|
4923
|
+
return if slot.nil?
|
|
4924
|
+
|
|
4925
|
+
(names == :all ? slot.keys : names).each do |name|
|
|
4926
|
+
(@contested ||= Set.new) << [class_name, name] if slot.key?(name)
|
|
4927
|
+
end
|
|
4928
|
+
end
|
|
4929
|
+
|
|
4775
4930
|
def track(certainty)
|
|
4776
4931
|
@certainty = certainty
|
|
4777
4932
|
@contested = nil
|
|
@@ -4779,9 +4934,11 @@ module Rigor
|
|
|
4779
4934
|
end
|
|
4780
4935
|
|
|
4781
4936
|
# Writes the slot; `node` and `other` are the contributions the write rests on, possible when either is.
|
|
4782
|
-
|
|
4937
|
+
# `possible` says the write rests on a fact the contributions do not carry (a module_function copy after a
|
|
4938
|
+
# reset that may not run, #1968).
|
|
4939
|
+
def write(class_name, name, def_node, node, other = nil, possible: false)
|
|
4783
4940
|
(self[class_name] ||= {})[name] = def_node
|
|
4784
|
-
if Certainty.possible?(@certainty, node) || (other && Certainty.possible?(@certainty, other))
|
|
4941
|
+
if possible || Certainty.possible?(@certainty, node) || (other && Certainty.possible?(@certainty, other))
|
|
4785
4942
|
(@contested ||= Set.new) << [class_name, name]
|
|
4786
4943
|
else
|
|
4787
4944
|
@contested&.delete([class_name, name])
|
|
@@ -4824,6 +4981,12 @@ module Rigor
|
|
|
4824
4981
|
# `Const`'s singleton class (a `remove_method`, a `def`) without declaring `Const`, so it stays out of
|
|
4825
4982
|
# `class_sources` (whose readers take a name there as one the project declares) and joins only the per-file
|
|
4826
4983
|
# class-declaration sets the incremental `class:` edges diff (`Runner#class_declarations`).
|
|
4984
|
+
#
|
|
4985
|
+
# Issue #1941 — so does a `def Const.x` (`def D.own`, or `def E.x` inside `module M`, resolved as a `class << E`
|
|
4986
|
+
# receiver is): it adds to `Const`'s singleton side from a file the reader's class edge need not name. In the set,
|
|
4987
|
+
# the declaration diff fires `class:` when the file gains or loses one, and
|
|
4988
|
+
# `IncrementalSession#singleton_rewrite_affected` fires it when one is renamed. `def (M::E).x` records no
|
|
4989
|
+
# singleton `def` at all, so it joins nothing.
|
|
4827
4990
|
def record_singleton_class_sources(sources, path, root)
|
|
4828
4991
|
collect_singleton_class_targets(root, []) { |name| (sources[name] ||= Set.new) << path }
|
|
4829
4992
|
end
|
|
@@ -4839,6 +5002,8 @@ module Rigor
|
|
|
4839
5002
|
yield eval_constant_receiver_prefix(node, node.expression, rendered, lexical_prefix).join("::") if rendered
|
|
4840
5003
|
when Prism::CallNode
|
|
4841
5004
|
yield_singleton_side_constant(node, node.receiver, lexical_prefix, &) if singleton_side_call?(node)
|
|
5005
|
+
when Prism::DefNode
|
|
5006
|
+
yield_singleton_side_constant(node, node.receiver, lexical_prefix, &)
|
|
4842
5007
|
end
|
|
4843
5008
|
node.rigor_each_child { |child| collect_singleton_class_targets(child, lexical_prefix, &) }
|
|
4844
5009
|
end
|
|
@@ -4846,8 +5011,7 @@ module Rigor
|
|
|
4846
5011
|
# A call on a constant that may rewrite its singleton side (`Const.singleton_class`,
|
|
4847
5012
|
# `Const.define_singleton_method`, `Const.extend(M)`, `Const.instance_eval { … }`) records facts about `Const`'s
|
|
4848
5013
|
# singleton methods the refinement walk reads from a file that need not declare `Const`, so the file joins the
|
|
4849
|
-
# constant's set too. Written inside `Const`'s own body, the name is in the file's set already
|
|
4850
|
-
# files its own `method:` key.
|
|
5014
|
+
# constant's set too. Written inside `Const`'s own body, the name is in the file's set already.
|
|
4851
5015
|
def singleton_side_call?(node)
|
|
4852
5016
|
node.name == :singleton_class || surface_rewriting_call?(node)
|
|
4853
5017
|
end
|
|
@@ -5062,7 +5226,7 @@ module Rigor
|
|
|
5062
5226
|
# own `def` leaves additionally split on {INSTANCE_EVAL_CALLS}, whose `def`s bind on the
|
|
5063
5227
|
# receiver's singleton rather than the receiver.
|
|
5064
5228
|
def receiver_eval_call?(node)
|
|
5065
|
-
node.block.is_a?(Prism::BlockNode) && RECEIVER_EVAL_CALLS.include?(node.name)
|
|
5229
|
+
node.is_a?(Prism::CallNode) && node.block.is_a?(Prism::BlockNode) && RECEIVER_EVAL_CALLS.include?(node.name)
|
|
5066
5230
|
end
|
|
5067
5231
|
|
|
5068
5232
|
# A `class_eval` / `module_eval` / `class_exec` / `module_exec` block is NOT deferred: it runs
|
|
@@ -5445,7 +5609,10 @@ module Rigor
|
|
|
5445
5609
|
def_owner_prefix = nil, singleton_cref: false,
|
|
5446
5610
|
defs_singleton: false)
|
|
5447
5611
|
return unless node.is_a?(Prism::Node)
|
|
5448
|
-
|
|
5612
|
+
|
|
5613
|
+
supersede_singleton_def_nodes(node, qualified_prefix, in_singleton_class, defs_singleton, accumulator,
|
|
5614
|
+
def_owner_prefix)
|
|
5615
|
+
if receiver_eval_call?(node)
|
|
5449
5616
|
return walk_eval_singleton_defs(node, qualified_prefix, in_singleton_class, accumulator,
|
|
5450
5617
|
def_owner_prefix, singleton_cref: singleton_cref,
|
|
5451
5618
|
defs_singleton: defs_singleton)
|
|
@@ -5736,7 +5903,8 @@ module Rigor
|
|
|
5736
5903
|
defs_singleton: false)
|
|
5737
5904
|
owner_prefix = def_owner_prefix || qualified_prefix
|
|
5738
5905
|
statements = statements_of(body)
|
|
5739
|
-
ModuleFunctionState.
|
|
5906
|
+
conditional = ModuleFunctionState.conditional_clause_statements(body)
|
|
5907
|
+
ModuleFunctionState.each_singleton_sibling(statements, conditional) do |stmt, module_function_on, named_call|
|
|
5740
5908
|
if named_call
|
|
5741
5909
|
# `:unnameable` — the named defs the call copies live on the metaclass,
|
|
5742
5910
|
# which this table cannot name either.
|
|
@@ -5786,19 +5954,96 @@ module Rigor
|
|
|
5786
5954
|
statements
|
|
5787
5955
|
end
|
|
5788
5956
|
|
|
5957
|
+
# `module_function_on` is {ModuleFunctionState.each_singleton_sibling}'s: `:uncertain` writes the module
|
|
5958
|
+
# function's copy as possible, so the slot holds the later def, contested, rather than the def the
|
|
5959
|
+
# slot held before (#1968).
|
|
5789
5960
|
def record_singleton_def_node(def_node, qualified_prefix, in_singleton_class, module_function_on, accumulator)
|
|
5790
|
-
|
|
5791
|
-
return unless
|
|
5961
|
+
declared = def_singleton?(def_node, qualified_prefix, in_singleton_class)
|
|
5962
|
+
return unless declared || module_function_on
|
|
5792
5963
|
return if qualified_prefix.empty?
|
|
5793
5964
|
|
|
5794
5965
|
class_name = qualified_prefix.join("::")
|
|
5795
|
-
write_singleton_slot(accumulator, class_name, def_node.name, def_node, def_node
|
|
5966
|
+
write_singleton_slot(accumulator, class_name, def_node.name, def_node, def_node,
|
|
5967
|
+
possible: !declared && module_function_on == :uncertain)
|
|
5968
|
+
end
|
|
5969
|
+
|
|
5970
|
+
# Issue #1936 — the singleton twin of {#supersede_def_nodes}: a definer that is not a `def` replaces the
|
|
5971
|
+
# singleton method an earlier `def self.x` of the walk installed, so the slot is contested and the typing read
|
|
5972
|
+
# declines instead of re-typing the replaced body. The definers, each written on the class's own singleton
|
|
5973
|
+
# class: `singleton_class.alias_method` / `define_method` / `remove_method` / `undef_method` / `attr_*` and its
|
|
5974
|
+
# `class_eval` family and `send` (bare, `self.` or a constant's `singleton_class`), `define_singleton_method`,
|
|
5975
|
+
# and in a `class << self` body the receiverless calls of the same names with `alias` and `undef`. Certain or
|
|
5976
|
+
# possible alike; a call whose rewritten names are not all literal contests every slot the owner holds so far.
|
|
5977
|
+
# Under an `instance_eval` split (`defs_singleton`) only `alias` and `undef` bind on the singleton; its calls
|
|
5978
|
+
# stay instance-side.
|
|
5979
|
+
def supersede_singleton_def_nodes(node, qualified_prefix, in_singleton_class, defs_singleton, accumulator,
|
|
5980
|
+
def_owner_prefix)
|
|
5981
|
+
return unless accumulator.is_a?(SingletonDefTable) && defs_singleton != :unnameable
|
|
5982
|
+
|
|
5983
|
+
owner_prefix = def_owner_prefix || qualified_prefix
|
|
5984
|
+
current = owner_prefix.empty? ? nil : owner_prefix.join("::")
|
|
5985
|
+
owner, names =
|
|
5986
|
+
case node
|
|
5987
|
+
when Prism::AliasMethodNode, Prism::UndefNode
|
|
5988
|
+
[current, singleton_keyword_names(node)] if in_singleton_class || defs_singleton == true
|
|
5989
|
+
when Prism::CallNode
|
|
5990
|
+
singleton_call_target(node, qualified_prefix, current, in_singleton_class)
|
|
5991
|
+
end
|
|
5992
|
+
accumulator.supersede(owner, names) if owner && names
|
|
5993
|
+
end
|
|
5994
|
+
|
|
5995
|
+
def singleton_keyword_names(node)
|
|
5996
|
+
return literal_names_or_all(node.names) if node.is_a?(Prism::UndefNode)
|
|
5997
|
+
|
|
5998
|
+
names = Source::AliasNames.keyword_names(node)
|
|
5999
|
+
names ? names.first(1) : :all
|
|
6000
|
+
end
|
|
6001
|
+
|
|
6002
|
+
# `[owner, names]` for a singleton definer call {#supersede_singleton_def_nodes} reads, or nil.
|
|
6003
|
+
def singleton_call_target(node, qualified_prefix, current, in_singleton_class)
|
|
6004
|
+
receiver = node.receiver
|
|
6005
|
+
if !in_singleton_class && singleton_class_of_receiver?(receiver)
|
|
6006
|
+
[singleton_class_owner(node, receiver.receiver, qualified_prefix, current), singleton_naming_call_names(node)]
|
|
6007
|
+
elsif receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
6008
|
+
[current, in_singleton_class ? singleton_naming_call_names(node) : define_singleton_method_names(node)]
|
|
6009
|
+
end
|
|
6010
|
+
end
|
|
6011
|
+
|
|
6012
|
+
# The singleton names a naming call on the singleton class replaces, `:all` where they are not all literal,
|
|
6013
|
+
# or nil for another call. `attr_writer` replaces only `x=`, `attr_reader` only `x`. A `class_eval`-family or
|
|
6014
|
+
# `send` call may replace any name, so it is `:all`.
|
|
6015
|
+
def singleton_naming_call_names(node)
|
|
6016
|
+
arguments = node.arguments&.arguments || []
|
|
6017
|
+
case node.name
|
|
6018
|
+
when *SURFACE_EVAL_CALLS, *SURFACE_SEND_CALLS then :all
|
|
6019
|
+
when :alias_method then literal_names_or_all(arguments.first(1))
|
|
6020
|
+
when :define_method then define_singleton_method_names(node, :define_method)
|
|
6021
|
+
when :remove_method, :undef_method then literal_names_or_all(arguments)
|
|
6022
|
+
when *ATTR_MACROS
|
|
6023
|
+
names = literal_names_or_all(arguments)
|
|
6024
|
+
return names if names == :all
|
|
6025
|
+
|
|
6026
|
+
names.flat_map do |base|
|
|
6027
|
+
[(base unless node.name == :attr_writer), (:"#{base}=" unless node.name == :attr_reader)].compact
|
|
6028
|
+
end
|
|
6029
|
+
end
|
|
6030
|
+
end
|
|
6031
|
+
|
|
6032
|
+
def define_singleton_method_names(node, call = :define_singleton_method)
|
|
6033
|
+
return nil unless node.name == call
|
|
6034
|
+
|
|
6035
|
+
literal_names_or_all(node.arguments&.arguments&.first(1) || [])
|
|
6036
|
+
end
|
|
6037
|
+
|
|
6038
|
+
def literal_names_or_all(arguments)
|
|
6039
|
+
names = arguments.map { |argument| literal_method_name(argument) }
|
|
6040
|
+
names.empty? || names.any?(&:nil?) ? :all : names
|
|
5796
6041
|
end
|
|
5797
6042
|
|
|
5798
6043
|
# One singleton slot write, through {SingletonDefTable#write} when the walk tracks certainty.
|
|
5799
|
-
def write_singleton_slot(accumulator, class_name, name, def_node, node, other = nil)
|
|
6044
|
+
def write_singleton_slot(accumulator, class_name, name, def_node, node, other = nil, possible: false)
|
|
5800
6045
|
if accumulator.is_a?(SingletonDefTable)
|
|
5801
|
-
accumulator.write(class_name, name, def_node, node, other)
|
|
6046
|
+
accumulator.write(class_name, name, def_node, node, other, possible: possible)
|
|
5802
6047
|
else
|
|
5803
6048
|
(accumulator[class_name] ||= {})[name] = def_node
|
|
5804
6049
|
end
|
|
@@ -6695,32 +6940,34 @@ module Rigor
|
|
|
6695
6940
|
# differently for the two spellings of one edge.
|
|
6696
6941
|
def record_mixin_call(node, qualified_prefix, current_class, accumulator)
|
|
6697
6942
|
taint_opaque_eval(node, current_class, accumulator)
|
|
6698
|
-
|
|
6699
|
-
return if
|
|
6943
|
+
view = mixin_call_view(node)
|
|
6944
|
+
return if view.nil? || view.first.nil?
|
|
6700
6945
|
|
|
6946
|
+
kind = view.first
|
|
6701
6947
|
current_class ||= toplevel_include_owner(node, qualified_prefix, current_class, accumulator)
|
|
6702
6948
|
effects = mixin_effects(node, kind, qualified_prefix, current_class, accumulator, in_singleton: false)
|
|
6703
6949
|
.select { |_owner, side| side == :include }
|
|
6704
6950
|
return if effects.empty?
|
|
6705
|
-
return if
|
|
6951
|
+
return if instance_mixin_recorded?(node, view, qualified_prefix, current_class, accumulator)
|
|
6706
6952
|
|
|
6707
6953
|
# A mixin call the tables cannot hold still reshapes the ancestry: taint the whole owner side.
|
|
6708
6954
|
effects.each { |owner, side| accumulator.taint(owner, side) }
|
|
6709
6955
|
end
|
|
6710
6956
|
|
|
6711
6957
|
# Records the instance-side edges of a call {#mixin_call_recorded?} accepts. False when it recorded
|
|
6712
|
-
# nothing — the caller then taints the owner instead.
|
|
6713
|
-
|
|
6714
|
-
|
|
6958
|
+
# nothing — the caller then taints the owner instead. `view` is {#mixin_call_view}'s, so
|
|
6959
|
+
# `Integer.send(:include, Ext)` records `Ext` as `Integer.include(Ext)` does (issue #1722).
|
|
6960
|
+
def instance_mixin_recorded?(node, view, qualified_prefix, current_class, accumulator)
|
|
6961
|
+
kind, arguments, via_send = view
|
|
6962
|
+
return false unless mixin_call_recorded?(node, kind, via_send, current_class)
|
|
6715
6963
|
|
|
6716
|
-
arguments = node.arguments&.arguments || []
|
|
6717
6964
|
targets = arguments.filter_map { |arg| Source::ConstantPath.qualified_name(arg) }
|
|
6718
6965
|
return false if targets.empty?
|
|
6719
6966
|
|
|
6720
6967
|
owner = node.receiver.nil? ? current_class : prepend_call_receiver(node, qualified_prefix)
|
|
6721
6968
|
return false if owner.nil?
|
|
6722
6969
|
|
|
6723
|
-
write_mixin_targets(accumulator, owner, targets, prepend:
|
|
6970
|
+
write_mixin_targets(accumulator, owner, targets, prepend: kind == :prepend)
|
|
6724
6971
|
accumulator.note(node, owner, :include, targets, complete: targets.size == arguments.size)
|
|
6725
6972
|
true
|
|
6726
6973
|
end
|
|
@@ -6868,15 +7115,17 @@ module Rigor
|
|
|
6868
7115
|
end
|
|
6869
7116
|
|
|
6870
7117
|
# Whether a mixin call contributes to the tables at all: a receiverless `include` / `prepend` needs an
|
|
6871
|
-
# enclosing declaration to file the edge under, and the RECEIVER form
|
|
6872
|
-
#
|
|
6873
|
-
#
|
|
6874
|
-
#
|
|
6875
|
-
|
|
6876
|
-
|
|
6877
|
-
|
|
7118
|
+
# enclosing declaration to file the edge under, and the RECEIVER form (`Base.prepend(Loud)`, issue #1123;
|
|
7119
|
+
# `Integer.include(Ext)` and `Integer.send(:include, Ext)`, issue #1722) files it under the class the
|
|
7120
|
+
# receiver names. A receiver-form edge is unpositioned wherever it is written ({MixinAccumulator#note}), so
|
|
7121
|
+
# a reader that depends on ORDER declines on it, while the set-shaped readers (undefined-method
|
|
7122
|
+
# suppression, arity, visibility) see the module as they see an in-body `include`. A receiverless `send`
|
|
7123
|
+
# stays a taint: the walk does not resolve its `self`.
|
|
7124
|
+
def mixin_call_recorded?(node, kind, via_send, current_class)
|
|
7125
|
+
return false unless MIXIN_CALL_NAMES.include?(kind)
|
|
7126
|
+
return !via_send && !current_class.nil? if node.receiver.nil?
|
|
6878
7127
|
|
|
6879
|
-
|
|
7128
|
+
true
|
|
6880
7129
|
end
|
|
6881
7130
|
|
|
6882
7131
|
# Issue #1697 — a receiverless `include M` written as a statement of the file's own top level is
|
|
@@ -7589,6 +7838,8 @@ module Rigor
|
|
|
7589
7838
|
# - **Wrap-around form** (#1911, #1819, #1633): `private def foo` and `private attr_reader :foo` record the
|
|
7590
7839
|
# members with the call's visibility and leave the default alone.
|
|
7591
7840
|
#
|
|
7841
|
+
# A `module_function` call takes each form as `private` does (#1569): Ruby makes the instance copy private.
|
|
7842
|
+
#
|
|
7592
7843
|
# Top-level (no surrounding class) defs do not contribute — Ruby's top-level visibility nuances (private at
|
|
7593
7844
|
# top-level marks the method on `Object`) are out of scope for v0.1.2.
|
|
7594
7845
|
def build_discovered_method_visibilities(root)
|
|
@@ -7605,8 +7856,8 @@ module Rigor
|
|
|
7605
7856
|
end
|
|
7606
7857
|
|
|
7607
7858
|
# The bare calls that set the running default visibility of the body they run in: the modifiers and
|
|
7608
|
-
# `module_function`, which {#walk_method_visibilities}
|
|
7609
|
-
#
|
|
7859
|
+
# `module_function`, which {#walk_method_visibilities} applies as `private` on the instance side
|
|
7860
|
+
# ({ModuleFunctionState.instance_visibility}, #1569).
|
|
7610
7861
|
VISIBILITY_TOGGLES = (VISIBILITY_MODIFIERS + %i[module_function]).freeze
|
|
7611
7862
|
|
|
7612
7863
|
# The `{class => {name => visibility}}` accumulator of {#walk_method_visibilities}, which also carries the
|
|
@@ -7650,9 +7901,22 @@ module Rigor
|
|
|
7650
7901
|
(@contested ||= Set.new) << [class_name, name].freeze
|
|
7651
7902
|
end
|
|
7652
7903
|
|
|
7653
|
-
# Contests every slot of `class_name` recorded so far (`private(*names)` may name any of them).
|
|
7654
|
-
|
|
7655
|
-
|
|
7904
|
+
# Contests every slot of `class_name` on `side` recorded so far (`private(*names)` may name any of them).
|
|
7905
|
+
# Issue #1984 — on the singleton side it also contests the class's wildcard slot
|
|
7906
|
+
# ({ScopeIndexer::SINGLETON_WILDCARD_SLOT}): the names may be ones another file records.
|
|
7907
|
+
def contest_class(class_name, side = :instance)
|
|
7908
|
+
self[class_name]&.each_key do |slot|
|
|
7909
|
+
contest_slot(class_name, slot) if slot.is_a?(Array) == (side == :singleton)
|
|
7910
|
+
end
|
|
7911
|
+
contest_slot(class_name, SINGLETON_WILDCARD_SLOT) if side == :singleton
|
|
7912
|
+
end
|
|
7913
|
+
|
|
7914
|
+
# Issue #1984 — records `visibility` in the slot of `name` on `side` ({ScopeIndexer.visibility_slot}) and
|
|
7915
|
+
# settles its contest as {#wrote} does.
|
|
7916
|
+
def write(class_name, name, side, visibility, node, toggled:)
|
|
7917
|
+
slot = ScopeIndexer.visibility_slot(name, side)
|
|
7918
|
+
(self[class_name] ||= {})[slot] = visibility
|
|
7919
|
+
wrote(class_name, slot, node, toggled: toggled)
|
|
7656
7920
|
end
|
|
7657
7921
|
|
|
7658
7922
|
# Runs the block with the running default uncertain, so a `def` it records is contested.
|
|
@@ -7769,10 +8033,12 @@ module Rigor
|
|
|
7769
8033
|
return current_visibility
|
|
7770
8034
|
end
|
|
7771
8035
|
when Prism::DefNode
|
|
7772
|
-
|
|
8036
|
+
contest_foreign_singleton_visibility(node, qualified_prefix, owner_prefix, in_singleton_class, accumulator)
|
|
8037
|
+
record_def_visibility(node, owner_prefix, visibility_side(in_singleton_class, defs_singleton),
|
|
7773
8038
|
current_visibility, accumulator)
|
|
7774
8039
|
return current_visibility
|
|
7775
8040
|
when Prism::CallNode
|
|
8041
|
+
record_constant_class_method_calls(node, qualified_prefix, accumulator)
|
|
7776
8042
|
if receiver_eval_call?(node)
|
|
7777
8043
|
walk_eval_visibilities(node, qualified_prefix, in_singleton_class, current_visibility,
|
|
7778
8044
|
accumulator, def_owner_prefix, singleton_cref: singleton_cref,
|
|
@@ -7787,23 +8053,50 @@ module Rigor
|
|
|
7787
8053
|
end
|
|
7788
8054
|
if (parts = visibility_call_parts(node))
|
|
7789
8055
|
accumulator.toggles += 1 if visibility_toggle_arguments?(parts.last)
|
|
7790
|
-
#
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
#
|
|
7794
|
-
#
|
|
7795
|
-
|
|
7796
|
-
|
|
7797
|
-
|
|
8056
|
+
# A `module_function` call is applied as a `private` one (#1569): it makes the instance copy private.
|
|
8057
|
+
visibility = parts.first
|
|
8058
|
+
visibility = ModuleFunctionState.instance_visibility unless VISIBILITY_MODIFIERS.include?(visibility)
|
|
8059
|
+
# Issue #1984 — inside `class << self` (or a `class_eval` on the singleton class) the call marks the
|
|
8060
|
+
# SINGLETON methods, recorded under their `[:singleton, name]` slots. `module_function` is no method of
|
|
8061
|
+
# a singleton class, so it is walked as any other call there. The `owner_prefix` check is a guard: an
|
|
8062
|
+
# ownerless body (the top level, an anonymous factory block) records nothing, and its calls walk as
|
|
8063
|
+
# any other call.
|
|
8064
|
+
# An `instance_eval` body's call (`:eval`) runs on the receiver as a module: the instance side.
|
|
8065
|
+
side = visibility_side(in_singleton_class, defs_singleton)
|
|
8066
|
+
side = :instance if side == :eval
|
|
8067
|
+
if side && !owner_prefix.empty? && (side == :instance || VISIBILITY_MODIFIERS.include?(parts.first))
|
|
8068
|
+
return apply_visibility_call(node, visibility, parts.last, owner_prefix, current_visibility, accumulator,
|
|
8069
|
+
side: side, defs_singleton: defs_singleton) do |argument|
|
|
7798
8070
|
walk_method_visibilities(argument, qualified_prefix, in_singleton_class, current_visibility,
|
|
7799
8071
|
accumulator, def_owner_prefix, singleton_cref: singleton_cref,
|
|
7800
8072
|
defs_singleton: defs_singleton)
|
|
7801
8073
|
end
|
|
7802
8074
|
end
|
|
7803
8075
|
end
|
|
7804
|
-
|
|
7805
|
-
|
|
8076
|
+
if !in_singleton_class && !owner_prefix.empty?
|
|
8077
|
+
class_method_parts = class_method_visibility_parts(node)
|
|
8078
|
+
if class_method_parts
|
|
8079
|
+
apply_class_method_visibility(node, *class_method_parts, owner_prefix, accumulator) do |argument|
|
|
8080
|
+
walk_method_visibilities(argument, qualified_prefix, in_singleton_class, current_visibility,
|
|
8081
|
+
accumulator, def_owner_prefix, singleton_cref: singleton_cref,
|
|
8082
|
+
defs_singleton: defs_singleton)
|
|
8083
|
+
end
|
|
8084
|
+
return current_visibility
|
|
8085
|
+
end
|
|
8086
|
+
contest_singleton_rewrites(node, owner_prefix, accumulator)
|
|
8087
|
+
end
|
|
8088
|
+
side = visibility_side(in_singleton_class, defs_singleton)
|
|
8089
|
+
if %i[instance singleton].include?(side)
|
|
8090
|
+
record_attr_visibility(node, owner_prefix, current_visibility, accumulator, toggled: true, side: side)
|
|
8091
|
+
contest_replaced_visibility(replaced_by_call(node), owner_prefix, accumulator, side: side)
|
|
7806
8092
|
end
|
|
8093
|
+
when Prism::AliasMethodNode
|
|
8094
|
+
side = visibility_side(in_singleton_class, defs_singleton)
|
|
8095
|
+
if %i[instance singleton].include?(side)
|
|
8096
|
+
contest_replaced_visibility(Array(Source::AliasNames.keyword_names(node)&.first), owner_prefix,
|
|
8097
|
+
accumulator, side: side)
|
|
8098
|
+
end
|
|
8099
|
+
return current_visibility
|
|
7807
8100
|
end
|
|
7808
8101
|
|
|
7809
8102
|
# Statement-position StatementsNode preserves left-to-right visibility flow; everything else recurses with the
|
|
@@ -7887,6 +8180,9 @@ module Rigor
|
|
|
7887
8180
|
singleton_cref: singleton_cref,
|
|
7888
8181
|
defs_singleton: defs_singleton)
|
|
7889
8182
|
end
|
|
8183
|
+
return if singleton_class_eval_walked?(node, qualified_prefix, in_singleton_class, accumulator,
|
|
8184
|
+
def_owner_prefix, singleton_cref, defs_singleton)
|
|
8185
|
+
|
|
7890
8186
|
node.arguments&.arguments&.each do |arg|
|
|
7891
8187
|
walk_method_visibilities(arg, qualified_prefix, in_singleton_class, current_visibility,
|
|
7892
8188
|
accumulator, def_owner_prefix, singleton_cref: singleton_cref,
|
|
@@ -7911,63 +8207,185 @@ module Rigor
|
|
|
7911
8207
|
end
|
|
7912
8208
|
end
|
|
7913
8209
|
|
|
7914
|
-
#
|
|
7915
|
-
def
|
|
7916
|
-
|
|
7917
|
-
|
|
7918
|
-
|
|
8210
|
+
# Issue #1984 — `singleton_class.class_eval do … end` in a class body (or `module_eval` / `class_exec` /
|
|
8211
|
+
# `module_exec`) runs its block as a `class << self` body does: its `def`s and visibility calls land on the
|
|
8212
|
+
# class's singleton slots. True when it walked the block so.
|
|
8213
|
+
def singleton_class_eval_walked?(node, qualified_prefix, in_singleton_class, accumulator,
|
|
8214
|
+
def_owner_prefix, singleton_cref, defs_singleton)
|
|
8215
|
+
return false if in_singleton_class || defs_singleton || INSTANCE_EVAL_CALLS.include?(node.name)
|
|
8216
|
+
return false unless own_singleton_class?(node.receiver) && node.arguments.nil?
|
|
8217
|
+
|
|
8218
|
+
owner_prefix = def_owner_prefix || qualified_prefix
|
|
8219
|
+
return false if owner_prefix.empty?
|
|
8220
|
+
|
|
8221
|
+
if node.block.body
|
|
8222
|
+
accumulator.body do
|
|
8223
|
+
walk_method_visibilities(node.block.body, qualified_prefix, true, :public, accumulator, owner_prefix,
|
|
8224
|
+
singleton_cref: singleton_cref)
|
|
8225
|
+
end
|
|
8226
|
+
end
|
|
8227
|
+
true
|
|
8228
|
+
end
|
|
8229
|
+
|
|
8230
|
+
# Issue #1984 — the slot a contest stands on for every singleton name of a class: a call that may make any of
|
|
8231
|
+
# them public (`K.public_class_method(*names)`, `K.singleton_class.class_eval { … }`).
|
|
8232
|
+
SINGLETON_WILDCARD_SLOT = %i[singleton *].freeze
|
|
8233
|
+
|
|
8234
|
+
# Issue #1984 — a class method visibility call written on a constant (`K.public_class_method(:x)`,
|
|
8235
|
+
# `K.send(:public_class_method, :x)`), anywhere: each literal name is recorded public on every class the
|
|
8236
|
+
# constant may name, so a reopening's or another file's `private_class_method` disagrees with it and is
|
|
8237
|
+
# contested. A `K.private_class_method` is not read (the rule only declines more where it is missing). Any call
|
|
8238
|
+
# on `K.singleton_class` (`send(:public, :x)`, `class_eval { … }`), or a `public_class_method` whose names are
|
|
8239
|
+
# not literal, contests every singleton name of the class.
|
|
8240
|
+
def record_constant_class_method_calls(node, qualified_prefix, accumulator)
|
|
8241
|
+
receiver = node.receiver
|
|
8242
|
+
if singleton_class_of_receiver?(receiver) && constant_receiver?(receiver.receiver)
|
|
8243
|
+
each_constant_class(receiver.receiver, qualified_prefix) do |class_name|
|
|
8244
|
+
accumulator.contest_class(class_name, :singleton)
|
|
8245
|
+
end
|
|
8246
|
+
return
|
|
8247
|
+
end
|
|
8248
|
+
names = constant_public_class_method_names(node)
|
|
8249
|
+
return if names.nil?
|
|
8250
|
+
|
|
8251
|
+
each_constant_class(receiver, qualified_prefix) do |class_name|
|
|
8252
|
+
next accumulator.contest_class(class_name, :singleton) if names.any?(&:nil?)
|
|
8253
|
+
|
|
8254
|
+
names.each do |method_name|
|
|
8255
|
+
accumulator.write(class_name, method_name, :singleton, :public, node, toggled: false)
|
|
8256
|
+
end
|
|
8257
|
+
end
|
|
8258
|
+
end
|
|
8259
|
+
|
|
8260
|
+
# The names a `K.public_class_method(…)` (or `K.send(:public_class_method, …)`) call on a constant names, a nil
|
|
8261
|
+
# for each one that is not a literal; nil for any other call.
|
|
8262
|
+
def constant_public_class_method_names(node)
|
|
8263
|
+
return nil unless constant_receiver?(node.receiver)
|
|
8264
|
+
|
|
8265
|
+
name = node.name
|
|
8266
|
+
arguments = node.arguments&.arguments || EMPTY_VISIBILITY_ARGUMENTS
|
|
8267
|
+
if VISIBILITY_SEND_CALLS.include?(name) || name == :public_send
|
|
8268
|
+
name = Source::Literals.symbol_or_string(arguments.first)
|
|
8269
|
+
arguments = arguments.drop(1)
|
|
8270
|
+
end
|
|
8271
|
+
return nil unless name == :public_class_method
|
|
8272
|
+
|
|
8273
|
+
arguments.flat_map { |arg| arg.is_a?(Prism::ArrayNode) ? arg.elements : [arg] }
|
|
8274
|
+
.map { |arg| Source::Literals.symbol_or_string(arg) }
|
|
8275
|
+
end
|
|
8276
|
+
|
|
8277
|
+
def constant_receiver?(node)
|
|
8278
|
+
node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
|
|
8279
|
+
end
|
|
8280
|
+
|
|
8281
|
+
def each_constant_class(receiver, qualified_prefix, &)
|
|
8282
|
+
constant_receiver_candidates(receiver, qualified_prefix).each(&)
|
|
8283
|
+
end
|
|
8284
|
+
|
|
8285
|
+
# Issue #1984 — the side a body's members land on: `:singleton` in a `class << self` body or a `class_eval` on
|
|
8286
|
+
# the singleton class, `:eval` in an `instance_eval` body on a class (its `def`s are singleton methods, its
|
|
8287
|
+
# visibility calls instance-side), `:instance` in a class body, and nil where the singleton cannot be named.
|
|
8288
|
+
def visibility_side(in_singleton_class, defs_singleton)
|
|
8289
|
+
return defs_singleton == :unnameable ? nil : :singleton if in_singleton_class
|
|
8290
|
+
return :eval if defs_singleton
|
|
8291
|
+
|
|
8292
|
+
:instance
|
|
8293
|
+
end
|
|
8294
|
+
|
|
8295
|
+
# Issue #1984 — the key of `name`'s slot in a class's visibility table: the name on the instance side,
|
|
8296
|
+
# `[:singleton, name]` on the singleton side (the envelope table's spelling).
|
|
8297
|
+
def visibility_slot(name, side)
|
|
8298
|
+
side == :singleton ? [:singleton, name].freeze : name
|
|
8299
|
+
end
|
|
8300
|
+
|
|
8301
|
+
# Records the visibility a `def` gets ({#visibility_side} `side`):
|
|
8302
|
+
#
|
|
8303
|
+
# - a receiverless `def` takes `current_visibility` on its side; `toggled` when that is the running default
|
|
8304
|
+
# (a statement), false for one a visibility call wraps;
|
|
8305
|
+
# - issue #1984 — `def self.x` (or `def K.x` in `K`'s body) is a public singleton method whatever the running
|
|
8306
|
+
# default is (`private; def self.x` is public), and writes nothing in a singleton body (it defines a method
|
|
8307
|
+
# of the singleton's singleton);
|
|
8308
|
+
# - an `instance_eval` body's `def` (`:eval`) replaces a singleton method with a visibility the walk does not
|
|
8309
|
+
# read, so its singleton slot is contested.
|
|
8310
|
+
def record_def_visibility(def_node, qualified_prefix, side, current_visibility, accumulator, toggled: true)
|
|
8311
|
+
return if side.nil? || qualified_prefix.empty?
|
|
7919
8312
|
|
|
7920
8313
|
class_name = qualified_prefix.join("::")
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
|
|
8314
|
+
if side == :eval
|
|
8315
|
+
accumulator.contest_slot(class_name, visibility_slot(def_node.name, :singleton))
|
|
8316
|
+
elsif def_node.receiver.nil?
|
|
8317
|
+
accumulator.write(class_name, def_node.name, side, current_visibility, def_node, toggled: toggled)
|
|
8318
|
+
elsif side == :instance && self_singleton_def?(def_node, qualified_prefix)
|
|
8319
|
+
accumulator.write(class_name, def_node.name, :singleton, :public, def_node, toggled: false)
|
|
8320
|
+
end
|
|
8321
|
+
end
|
|
8322
|
+
|
|
8323
|
+
def self_singleton_def?(def_node, qualified_prefix)
|
|
8324
|
+
def_node.receiver.is_a?(Prism::SelfNode) || def_receiver_targets_lexical_self?(def_node.receiver,
|
|
8325
|
+
qualified_prefix)
|
|
8326
|
+
end
|
|
8327
|
+
|
|
8328
|
+
# Issue #1984 — `def Other.x` whose receiver is not the lexical self (`def_owner`) makes `Other.x` public in
|
|
8329
|
+
# whatever order the files load, so each class the constant may name has its singleton slot contested.
|
|
8330
|
+
def contest_foreign_singleton_visibility(def_node, qualified_prefix, def_owner, in_singleton_class, accumulator)
|
|
8331
|
+
receiver = def_node.receiver
|
|
8332
|
+
return unless receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)
|
|
8333
|
+
return if !in_singleton_class && def_receiver_targets_lexical_self?(receiver, def_owner)
|
|
8334
|
+
|
|
8335
|
+
constant_receiver_candidates(receiver, qualified_prefix).each do |name|
|
|
8336
|
+
accumulator.contest_slot(name, visibility_slot(def_node.name, :singleton))
|
|
8337
|
+
end
|
|
7924
8338
|
end
|
|
7925
8339
|
|
|
7926
8340
|
# Applies one `private` / `protected` / `public` call ({#visibility_call_parts}) written on the body's self
|
|
7927
|
-
# and returns the running default after it:
|
|
8341
|
+
# and returns the running default after it. `side` is the side its names land on: `:singleton` in a
|
|
8342
|
+
# `class << self` body (issue #1984), `:instance` elsewhere.
|
|
7928
8343
|
#
|
|
7929
8344
|
# - no arguments — the call's visibility becomes the default for subsequent defs.
|
|
7930
8345
|
# - `private :foo, :bar`, `private %i[foo bar]` — back-patch the named methods in the accumulator; the
|
|
7931
8346
|
# default does NOT change for this form.
|
|
7932
8347
|
# - Issues #1911, #1819, #1633 — `private def foo` (endless or not) and `private attr_reader :foo` define
|
|
7933
8348
|
# 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
|
|
7935
|
-
# contested.
|
|
7936
|
-
# - a splat (`private(*names)`) may name any member or none (a bare toggle), so the class's slots
|
|
7937
|
-
# wrote so far are contested, and the statement loop reads the default after it as uncertain
|
|
8349
|
+
# public); `private def self.foo` changes the method `foo` of the side, if one exists, so its slot is
|
|
8350
|
+
# contested, and defines a public singleton `foo` (a singleton-singleton one in a `class << self` body).
|
|
8351
|
+
# - a splat (`private(*names)`) may name any member or none (a bare toggle), so the class's slots on the side
|
|
8352
|
+
# this file wrote so far are contested, and the statement loop reads the default after it as uncertain
|
|
7938
8353
|
# ({#visibility_toggle_arguments?}).
|
|
7939
8354
|
# - any other argument (`private memoize def foo`, `private delegate(...)`) is walked with the default
|
|
7940
8355
|
# uncertain, so a `def` under it is contested: the call's visibility reaches it through a return value
|
|
7941
8356
|
# the walk does not read.
|
|
7942
8357
|
#
|
|
7943
8358
|
# `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)
|
|
8359
|
+
def apply_visibility_call(call_node, name, args, qualified_prefix, current_visibility, accumulator, # rubocop:disable Metrics/ParameterLists
|
|
8360
|
+
side: :instance, defs_singleton: false, &walk)
|
|
7946
8361
|
return name if args.empty?
|
|
7947
8362
|
|
|
7948
8363
|
class_name = qualified_prefix.join("::")
|
|
7949
|
-
accumulator.contest_class(class_name) if args.any?(Prism::SplatNode)
|
|
8364
|
+
accumulator.contest_class(class_name, side) if args.any?(Prism::SplatNode)
|
|
7950
8365
|
args.each do |arg|
|
|
7951
8366
|
apply_visibility_argument(arg, name, call_node, qualified_prefix, accumulator,
|
|
7952
|
-
defs_singleton: defs_singleton, &walk)
|
|
8367
|
+
side: side, defs_singleton: defs_singleton, &walk)
|
|
7953
8368
|
end
|
|
7954
8369
|
current_visibility
|
|
7955
8370
|
end
|
|
7956
8371
|
|
|
7957
|
-
def apply_visibility_argument(arg, name, call_node, qualified_prefix, accumulator, defs_singleton:, &walk)
|
|
8372
|
+
def apply_visibility_argument(arg, name, call_node, qualified_prefix, accumulator, side:, defs_singleton:, &walk)
|
|
7958
8373
|
case arg
|
|
7959
8374
|
when Prism::DefNode
|
|
7960
8375
|
if arg.receiver.nil?
|
|
7961
|
-
|
|
8376
|
+
def_side = side == :instance && defs_singleton ? :eval : side
|
|
8377
|
+
record_def_visibility(arg, qualified_prefix, def_side, name, accumulator, toggled: false)
|
|
7962
8378
|
else
|
|
7963
|
-
accumulator.contest_slot(qualified_prefix.join("::"), arg.name)
|
|
8379
|
+
accumulator.contest_slot(qualified_prefix.join("::"), visibility_slot(arg.name, side))
|
|
8380
|
+
record_def_visibility(arg, qualified_prefix, defs_singleton ? :eval : side, name, accumulator,
|
|
8381
|
+
toggled: false)
|
|
7964
8382
|
end
|
|
7965
8383
|
when Prism::SymbolNode, Prism::StringNode, Prism::ArrayNode
|
|
7966
|
-
apply_named_visibility(arg, name, qualified_prefix, call_node, accumulator, &walk)
|
|
8384
|
+
apply_named_visibility(arg, name, qualified_prefix, call_node, accumulator, side: side, &walk)
|
|
7967
8385
|
else
|
|
7968
8386
|
# An `instance_eval` body's wrapped attr is walked like any other argument and records nothing.
|
|
7969
8387
|
if !defs_singleton && attr_visibility_call?(arg)
|
|
7970
|
-
record_attr_visibility(arg, qualified_prefix, name, accumulator, toggled: false)
|
|
8388
|
+
record_attr_visibility(arg, qualified_prefix, name, accumulator, toggled: false, side: side)
|
|
7971
8389
|
else
|
|
7972
8390
|
accumulator.uncertainly { walk.call(arg) }
|
|
7973
8391
|
end
|
|
@@ -7976,16 +8394,14 @@ module Rigor
|
|
|
7976
8394
|
|
|
7977
8395
|
# `private :foo` / `private "foo"` / `private %i[foo bar]`: each literal name the call's visibility. An array
|
|
7978
8396
|
# 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)
|
|
8397
|
+
def apply_named_visibility(arg, visibility, qualified_prefix, call_node, accumulator, side: :instance, &walk)
|
|
7980
8398
|
elements = arg.is_a?(Prism::ArrayNode) ? arg.elements : [arg]
|
|
7981
8399
|
class_name = qualified_prefix.join("::")
|
|
7982
8400
|
elements.each do |element|
|
|
7983
8401
|
name = visibility_target_name(element)
|
|
7984
8402
|
next accumulator.uncertainly { walk.call(element) } if name.nil?
|
|
7985
8403
|
|
|
7986
|
-
accumulator
|
|
7987
|
-
accumulator[class_name][name] = visibility
|
|
7988
|
-
accumulator.wrote(class_name, name, call_node, toggled: false)
|
|
8404
|
+
accumulator.write(class_name, name, side, visibility, call_node, toggled: false)
|
|
7989
8405
|
end
|
|
7990
8406
|
end
|
|
7991
8407
|
|
|
@@ -7993,30 +8409,133 @@ module Rigor
|
|
|
7993
8409
|
Source::Literals.symbol_or_string(arg)
|
|
7994
8410
|
end
|
|
7995
8411
|
|
|
8412
|
+
CLASS_METHOD_VISIBILITIES = { private_class_method: :private, public_class_method: :public }.freeze
|
|
8413
|
+
private_constant :CLASS_METHOD_VISIBILITIES
|
|
8414
|
+
|
|
8415
|
+
# Issue #1984 — `[visibility, arguments]` for a `private_class_method` / `public_class_method` call written on
|
|
8416
|
+
# the body's self, in the spellings {#visibility_call_parts} reads (receiverless, `self.`, `send` with a
|
|
8417
|
+
# literal name); nil for any other call.
|
|
8418
|
+
def class_method_visibility_parts(node)
|
|
8419
|
+
return nil unless node.is_a?(Prism::CallNode)
|
|
8420
|
+
return nil unless node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
|
8421
|
+
|
|
8422
|
+
name = node.name
|
|
8423
|
+
arguments = node.arguments&.arguments || EMPTY_VISIBILITY_ARGUMENTS
|
|
8424
|
+
if VISIBILITY_SEND_CALLS.include?(name)
|
|
8425
|
+
name = Source::Literals.symbol_or_string(arguments.first)
|
|
8426
|
+
arguments = arguments.drop(1)
|
|
8427
|
+
end
|
|
8428
|
+
visibility = CLASS_METHOD_VISIBILITIES[name]
|
|
8429
|
+
visibility && [visibility, arguments]
|
|
8430
|
+
end
|
|
8431
|
+
|
|
8432
|
+
# Issue #1984 — applies `private_class_method` / `public_class_method` to the class's singleton slots. It sets
|
|
8433
|
+
# no running default (with no arguments it does nothing). A literal name (`:x`, `"x"`, `[:x]`) and a wrapped
|
|
8434
|
+
# `def self.x` take the call's visibility; a wrapped receiverless `def x` defines an instance method (walked as
|
|
8435
|
+
# a statement) and names the singleton `x` the walk does not see, as does any other receiver, so that slot is
|
|
8436
|
+
# contested; a splat or any other argument may name any singleton method, so every singleton slot the file
|
|
8437
|
+
# wrote so far for the class is contested, and the argument is walked.
|
|
8438
|
+
def apply_class_method_visibility(call_node, visibility, args, qualified_prefix, accumulator, &walk)
|
|
8439
|
+
class_name = qualified_prefix.join("::")
|
|
8440
|
+
args.each do |arg|
|
|
8441
|
+
case arg
|
|
8442
|
+
when Prism::DefNode
|
|
8443
|
+
if arg.receiver && self_singleton_def?(arg, qualified_prefix)
|
|
8444
|
+
accumulator.write(class_name, arg.name, :singleton, visibility, call_node, toggled: false)
|
|
8445
|
+
else
|
|
8446
|
+
walk.call(arg)
|
|
8447
|
+
accumulator.contest_slot(class_name, visibility_slot(arg.name, :singleton))
|
|
8448
|
+
end
|
|
8449
|
+
when Prism::SymbolNode, Prism::StringNode, Prism::ArrayNode
|
|
8450
|
+
apply_named_visibility(arg, visibility, qualified_prefix, call_node, accumulator, side: :singleton) do |el|
|
|
8451
|
+
accumulator.contest_class(class_name, :singleton)
|
|
8452
|
+
walk.call(el)
|
|
8453
|
+
end
|
|
8454
|
+
else
|
|
8455
|
+
accumulator.contest_class(class_name, :singleton)
|
|
8456
|
+
walk.call(arg)
|
|
8457
|
+
end
|
|
8458
|
+
end
|
|
8459
|
+
end
|
|
8460
|
+
|
|
8461
|
+
SINGLETON_DEFINING_CALLS = %i[define_singleton_method].freeze
|
|
8462
|
+
private_constant :SINGLETON_DEFINING_CALLS
|
|
8463
|
+
|
|
8464
|
+
# Issue #1984 — calls in a class body that change a singleton method's visibility in a way the walk does not
|
|
8465
|
+
# read, so the singleton slots they may reach are contested: `define_singleton_method(:x)` (a fresh public
|
|
8466
|
+
# method; a computed name contests every singleton slot of the class), and any call on `singleton_class`
|
|
8467
|
+
# (`singleton_class.send(:public, :x)`, `singleton_class.define_method(:x)`), whose literal arguments name
|
|
8468
|
+
# the slots and whose other arguments may name any.
|
|
8469
|
+
def contest_singleton_rewrites(node, qualified_prefix, accumulator)
|
|
8470
|
+
on_singleton_class = own_singleton_class?(node.receiver)
|
|
8471
|
+
return unless on_singleton_class || node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
|
8472
|
+
return unless on_singleton_class || SINGLETON_DEFINING_CALLS.include?(node.name)
|
|
8473
|
+
|
|
8474
|
+
class_name = qualified_prefix.join("::")
|
|
8475
|
+
arguments = node.arguments&.arguments || EMPTY_VISIBILITY_ARGUMENTS
|
|
8476
|
+
arguments = arguments.first(1) if SINGLETON_DEFINING_CALLS.include?(node.name)
|
|
8477
|
+
arguments.each do |argument|
|
|
8478
|
+
name = Source::Literals.symbol_or_string(argument)
|
|
8479
|
+
if name
|
|
8480
|
+
accumulator.contest_slot(class_name, visibility_slot(name, :singleton))
|
|
8481
|
+
else
|
|
8482
|
+
accumulator.contest_class(class_name, :singleton)
|
|
8483
|
+
end
|
|
8484
|
+
end
|
|
8485
|
+
end
|
|
8486
|
+
|
|
8487
|
+
# `singleton_class` or `self.singleton_class`: the body's own singleton class.
|
|
8488
|
+
def own_singleton_class?(receiver)
|
|
8489
|
+
singleton_class_of_receiver?(receiver) && (receiver.receiver.nil? || receiver.receiver.is_a?(Prism::SelfNode))
|
|
8490
|
+
end
|
|
8491
|
+
|
|
7996
8492
|
# Issue #1911 — an `attr_reader` / `attr_writer` / `attr_accessor` call written on the body's self records
|
|
7997
8493
|
# each member it defines (`x`, `x=`) with `visibility`: the running default for a statement (`toggled`), the
|
|
7998
8494
|
# wrapping call's for `private attr_accessor :x`, which Ruby applies to the `[:x, :x=]` the macro returns.
|
|
7999
8495
|
# A non-literal name (`attr_reader(*FIELDS)`) records nothing; a call that is no such macro records nothing.
|
|
8000
|
-
|
|
8001
|
-
|
|
8002
|
-
|
|
8496
|
+
# Issue #1989 — the names a `define_method(:x)` or an `alias_method :x, :y` call replaces. An `attr_*` call is
|
|
8497
|
+
# not among them: it writes its members' slots itself ({#record_attr_visibility}).
|
|
8498
|
+
def replaced_by_call(call_node)
|
|
8499
|
+
return define_method_superseded_name(call_node) if call_node.name == :define_method
|
|
8500
|
+
|
|
8501
|
+
Source::AliasNames.alias_method_call_names(call_node)&.first(1) || []
|
|
8502
|
+
end
|
|
8503
|
+
|
|
8504
|
+
# Issue #1989 — a `define_method` or an alias that replaces a method whose visibility slot this walk recorded
|
|
8505
|
+
# gives the name a visibility the walk does not read (the running default for `define_method`, the old
|
|
8506
|
+
# name's for an alias), so the slot is contested rather than left asserting the replaced `def`'s. Issue #1984
|
|
8507
|
+
# — on the singleton side the slot is contested whether or not this file recorded it, since another file may
|
|
8508
|
+
# have.
|
|
8509
|
+
def contest_replaced_visibility(names, qualified_prefix, accumulator, side: :instance)
|
|
8510
|
+
return if names.empty? || qualified_prefix.empty?
|
|
8511
|
+
|
|
8512
|
+
class_name = qualified_prefix.join("::")
|
|
8513
|
+
recorded = accumulator[class_name]
|
|
8514
|
+
names.each do |name|
|
|
8515
|
+
slot = visibility_slot(name, side)
|
|
8516
|
+
accumulator.contest_slot(class_name, slot) if side == :singleton || recorded&.key?(slot)
|
|
8517
|
+
end
|
|
8518
|
+
end
|
|
8519
|
+
|
|
8520
|
+
def record_attr_visibility(call_node, qualified_prefix, visibility, accumulator, toggled:, side: :instance)
|
|
8521
|
+
macro = attr_macro(call_node)
|
|
8522
|
+
return if macro.nil? || qualified_prefix.empty?
|
|
8003
8523
|
|
|
8004
8524
|
class_name = qualified_prefix.join("::")
|
|
8005
8525
|
each_attr_macro_name(call_node) do |base|
|
|
8006
8526
|
members = []
|
|
8007
|
-
members << base unless
|
|
8008
|
-
members << :"#{base}=" unless
|
|
8527
|
+
members << base unless macro == :attr_writer
|
|
8528
|
+
members << :"#{base}=" unless macro == :attr_reader
|
|
8009
8529
|
members.each do |member|
|
|
8010
|
-
accumulator
|
|
8011
|
-
accumulator[class_name][member] = visibility
|
|
8012
|
-
accumulator.wrote(class_name, member, call_node, toggled: toggled)
|
|
8530
|
+
accumulator.write(class_name, member, side, visibility, call_node, toggled: toggled)
|
|
8013
8531
|
end
|
|
8014
8532
|
end
|
|
8015
8533
|
end
|
|
8016
8534
|
|
|
8017
|
-
#
|
|
8535
|
+
# An `attr_reader` / `attr_writer` / `attr_accessor` call on the body's self ({#attr_macro},
|
|
8536
|
+
# {#record_attr_visibility}). Ruby 4.0.5 gives every spelling the running default visibility.
|
|
8018
8537
|
def attr_visibility_call?(node)
|
|
8019
|
-
node
|
|
8538
|
+
!attr_macro(node).nil?
|
|
8020
8539
|
end
|
|
8021
8540
|
|
|
8022
8541
|
# Registers the alias name in the `discovered_methods` table so `undefined-method` diagnostics are not emitted for
|
|
@@ -8131,7 +8650,8 @@ module Rigor
|
|
|
8131
8650
|
return if owner.nil?
|
|
8132
8651
|
|
|
8133
8652
|
singleton_class_rewritten_names(node).each do |method_name|
|
|
8134
|
-
|
|
8653
|
+
# `attr_writer :x` defines `x=` alone (#1936), so `x` keeps its own `def`.
|
|
8654
|
+
names = node.name == :attr_writer ? [] : [method_name]
|
|
8135
8655
|
names << :"#{method_name}=" if %i[attr_writer attr_accessor].include?(node.name)
|
|
8136
8656
|
names.each do |name|
|
|
8137
8657
|
record_envelope(tables.envelopes, owner, [:singleton, name], Source::ParameterEnvelope::OPAQUE)
|
|
@@ -8474,8 +8994,9 @@ module Rigor
|
|
|
8474
8994
|
return if qualified_prefix.empty?
|
|
8475
8995
|
|
|
8476
8996
|
kind = in_singleton_class ? :singleton : :instance
|
|
8477
|
-
|
|
8478
|
-
|
|
8997
|
+
macro = attr_macro(call_node)
|
|
8998
|
+
reader = macro != :attr_writer
|
|
8999
|
+
writer = macro != :attr_reader
|
|
8479
9000
|
class_name = qualified_prefix.join("::")
|
|
8480
9001
|
each_attr_macro_name(call_node) do |base|
|
|
8481
9002
|
record_method(accumulator, class_name, base, kind) if reader
|
|
@@ -8483,18 +9004,34 @@ module Rigor
|
|
|
8483
9004
|
end
|
|
8484
9005
|
end
|
|
8485
9006
|
|
|
8486
|
-
# Yields each name an `attr_*` call declares: a Symbol or String literal argument. A
|
|
8487
|
-
# (`attr_writer(*names)`) names nothing Rigor can read.
|
|
9007
|
+
# Yields each name an `attr_*` call declares ({#attr_macro}): a Symbol or String literal argument. A
|
|
9008
|
+
# non-literal one (`attr_writer(*names)`, `send(:attr_writer, name)`) names nothing Rigor can read.
|
|
8488
9009
|
def each_attr_macro_name(call_node)
|
|
8489
|
-
return
|
|
8490
|
-
return if call_node.arguments.nil?
|
|
9010
|
+
return if attr_macro(call_node).nil? || call_node.arguments.nil?
|
|
8491
9011
|
|
|
8492
|
-
call_node.arguments.arguments
|
|
9012
|
+
arguments = call_node.arguments.arguments
|
|
9013
|
+
arguments = arguments.drop(1) if SURFACE_SEND_CALLS.include?(call_node.name)
|
|
9014
|
+
arguments.each do |arg|
|
|
8493
9015
|
base = literal_method_name(arg)
|
|
8494
9016
|
yield base unless base.nil?
|
|
8495
9017
|
end
|
|
8496
9018
|
end
|
|
8497
9019
|
|
|
9020
|
+
# Issue #2097 — the {ATTR_MACROS} entry a call declares members with on the lexical class, or nil. The
|
|
9021
|
+
# call is on the body's self, implicitly or as `self.` (`self.attr_accessor :x`), and names the macro
|
|
9022
|
+
# directly or as the literal first argument of `send` / `public_send` / `__send__`
|
|
9023
|
+
# (`send(:attr_accessor, :x)`); the same spellings `SigGen::Generator#attr_writer_names` reads. Any
|
|
9024
|
+
# other receiver defines on another object.
|
|
9025
|
+
def attr_macro(call_node)
|
|
9026
|
+
return nil unless call_node.is_a?(Prism::CallNode)
|
|
9027
|
+
return nil unless call_node.receiver.nil? || call_node.receiver.is_a?(Prism::SelfNode)
|
|
9028
|
+
return call_node.name if ATTR_MACROS.include?(call_node.name)
|
|
9029
|
+
return nil unless SURFACE_SEND_CALLS.include?(call_node.name)
|
|
9030
|
+
|
|
9031
|
+
macro = literal_method_name(call_node.arguments&.arguments&.first)
|
|
9032
|
+
macro if ATTR_MACROS.include?(macro)
|
|
9033
|
+
end
|
|
9034
|
+
|
|
8498
9035
|
# Issue #736 — the module-level accessor macros, which introduce methods on BOTH sides of the class.
|
|
8499
9036
|
# ActiveSupport defines them on `Module`, so every class and module body can use them, and Rails codebases
|
|
8500
9037
|
# do: `mattr_accessor` / `cattr_accessor` (an alias of it) and `class_attribute` account for 92 false
|
|
@@ -9031,7 +9568,7 @@ module Rigor
|
|
|
9031
9568
|
when :contested_discovered_singleton_def_nodes
|
|
9032
9569
|
fold_slot_contests(target.fetch(name), singleton_def_nodes, table)
|
|
9033
9570
|
when :contested_discovered_method_visibilities
|
|
9034
|
-
|
|
9571
|
+
fold_visibility_contests(target.fetch(name), acc[:method_visibilities], method_visibilities, table)
|
|
9035
9572
|
else union_sibling_value(target.fetch(name), table)
|
|
9036
9573
|
end
|
|
9037
9574
|
end
|
|
@@ -9104,6 +9641,30 @@ module Rigor
|
|
|
9104
9641
|
result.merge(file_contested)
|
|
9105
9642
|
end
|
|
9106
9643
|
|
|
9644
|
+
# The visibility table's {#fold_slot_contests}, where `recorded` is the table the file folds into as it stood.
|
|
9645
|
+
# Issue #1984 — a singleton slot (`[:singleton, name]`) does not follow its writer: file order is not load
|
|
9646
|
+
# order, so two files that give it different visibilities (`private_class_method :x` here,
|
|
9647
|
+
# `class << self; public :x; end` in a reopening) leave it contested, and a contest on it stays whichever
|
|
9648
|
+
# file folds later. The instance slots fold as before.
|
|
9649
|
+
def fold_visibility_contests(sitting, recorded, file_slots, file_contested)
|
|
9650
|
+
stale = sitting.select { |class_name, name| !name.is_a?(Array) && file_slots[class_name]&.key?(name) }
|
|
9651
|
+
disagreeing = singleton_visibility_disagreements(recorded, file_slots)
|
|
9652
|
+
return sitting if stale.empty? && file_contested.empty? && disagreeing.empty?
|
|
9653
|
+
|
|
9654
|
+
(sitting - stale).merge(file_contested).merge(disagreeing)
|
|
9655
|
+
end
|
|
9656
|
+
|
|
9657
|
+
def singleton_visibility_disagreements(recorded, file_slots)
|
|
9658
|
+
file_slots.flat_map do |class_name, table|
|
|
9659
|
+
sitting = recorded[class_name]
|
|
9660
|
+
next [] if sitting.nil?
|
|
9661
|
+
|
|
9662
|
+
table.filter_map do |slot, visibility|
|
|
9663
|
+
[class_name, slot] if slot.is_a?(Array) && sitting.key?(slot) && sitting[slot] != visibility
|
|
9664
|
+
end
|
|
9665
|
+
end
|
|
9666
|
+
end
|
|
9667
|
+
|
|
9107
9668
|
def union_paths(sitting, arriving)
|
|
9108
9669
|
return sitting if arriving.empty?
|
|
9109
9670
|
return arriving if sitting.empty?
|
|
@@ -9607,8 +10168,9 @@ module Rigor
|
|
|
9607
10168
|
def merge_class_keyed_index_tables(acc, root, file_methods, certainty)
|
|
9608
10169
|
file_visibilities, file_contested = build_method_visibility_tables(root, certainty: certainty)
|
|
9609
10170
|
siblings = acc[:siblings]
|
|
9610
|
-
siblings[:contested_discovered_method_visibilities] =
|
|
9611
|
-
siblings[:contested_discovered_method_visibilities], file_visibilities,
|
|
10171
|
+
siblings[:contested_discovered_method_visibilities] = fold_visibility_contests(
|
|
10172
|
+
siblings[:contested_discovered_method_visibilities], acc[:method_visibilities], file_visibilities,
|
|
10173
|
+
file_contested
|
|
9612
10174
|
)
|
|
9613
10175
|
file_visibilities.each do |class_name, table|
|
|
9614
10176
|
(acc[:method_visibilities][class_name] ||= {}).merge!(table)
|
|
@@ -11073,20 +11635,28 @@ module Rigor
|
|
|
11073
11635
|
scope.local(name).nil? ? scope : scope.with_local(name, Type::Combinator.untyped)
|
|
11074
11636
|
end
|
|
11075
11637
|
|
|
11638
|
+
# Issue #1924 — an `if` / `elsif` / `unless` / ternary the evaluator did not enter (`[if (x ||= "d").empty?
|
|
11639
|
+
# then 0 else x.upcase end]`) applies no write its condition makes, so its edges are taken from a scope where
|
|
11640
|
+
# every bound local the condition writes reads `Dynamic[top]` ({#widen_condition_writes}), as
|
|
11641
|
+
# {#propagate_subjectless_case} takes a `when` condition's. The condition itself reads the scope before it.
|
|
11076
11642
|
def propagate_if_branches(node, table, current_scope)
|
|
11077
|
-
truthy_scope, falsey_scope =
|
|
11643
|
+
truthy_scope, falsey_scope = conditional_edges(node.predicate, current_scope)
|
|
11078
11644
|
propagate(node.predicate, table, current_scope) if node.predicate
|
|
11079
11645
|
propagate(node.statements, table, truthy_scope) if node.statements
|
|
11080
11646
|
propagate(node.subsequent, table, falsey_scope) if node.subsequent
|
|
11081
11647
|
end
|
|
11082
11648
|
|
|
11083
11649
|
def propagate_unless_branches(node, table, current_scope)
|
|
11084
|
-
truthy_scope, falsey_scope =
|
|
11650
|
+
truthy_scope, falsey_scope = conditional_edges(node.predicate, current_scope)
|
|
11085
11651
|
propagate(node.predicate, table, current_scope) if node.predicate
|
|
11086
11652
|
propagate(node.statements, table, falsey_scope) if node.statements
|
|
11087
11653
|
propagate(node.else_clause, table, truthy_scope) if node.else_clause
|
|
11088
11654
|
end
|
|
11089
11655
|
|
|
11656
|
+
def conditional_edges(predicate, scope)
|
|
11657
|
+
Narrowing.predicate_scopes(predicate, widen_condition_writes(predicate, scope))
|
|
11658
|
+
end
|
|
11659
|
+
|
|
11090
11660
|
# Issue #1495 — a `case` without a subject the evaluator did not enter (`[case; when x.nil? then 0; else x.size
|
|
11091
11661
|
# end]`) narrows by its conditions' edges, as {StatementEvaluator#subjectless_when_scopes} does: each condition
|
|
11092
11662
|
# under the falsey edges of the conditions before it, each clause under the join of its conditions' truthy edges,
|