rigortype 0.4.3 → 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/data/core_overlay/ostruct.rbs +40 -0
- data/docs/handbook/11-sig-gen.md +104 -15
- data/docs/manual/02-cli-reference.md +43 -12
- data/docs/manual/03-configuration.md +20 -7
- data/docs/manual/04-diagnostics.md +56 -3
- data/docs/manual/05-inspecting-types.md +2 -2
- data/docs/manual/06-baseline.md +1 -1
- data/docs/manual/07-plugins.md +3 -2
- data/docs/manual/08-skills.md +7 -0
- data/docs/manual/10-mcp-server.md +40 -4
- data/docs/manual/18-removing-dead-code.md +23 -2
- data/docs/manual/plugins/rigor-rails-routes.md +2 -2
- data/docs/manual/plugins/rigor-rspec.md +101 -1
- data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
- data/exe/rigor +12 -17
- 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 +60 -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 +274 -51
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +61 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +270 -28
- data/lib/rigor/analysis/path_expansion.rb +3 -1
- data/lib/rigor/analysis/reachability/graph.rb +111 -14
- data/lib/rigor/analysis/reachability/scan.rb +305 -23
- data/lib/rigor/analysis/rule_catalog.rb +22 -4
- 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 +179 -31
- data/lib/rigor/analysis/runner/project_pre_passes.rb +29 -4
- data/lib/rigor/analysis/runner.rb +102 -72
- data/lib/rigor/analysis/severity_stamp.rb +45 -1
- data/lib/rigor/analysis/worker_session.rb +69 -2
- data/lib/rigor/cache/descriptor.rb +79 -12
- data/lib/rigor/cache/engine_source.rb +3 -2
- data/lib/rigor/cache/incremental_snapshot.rb +39 -5
- 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/mcp_command.rb +1 -1
- 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 +163 -22
- data/lib/rigor/configuration/dependencies.rb +19 -11
- 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 +67 -11
- data/lib/rigor/inference/block_call_timing.rb +12 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/captured_locals.rb +67 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/definer_resolution.rb +101 -25
- data/lib/rigor/inference/expression_typer.rb +94 -9
- data/lib/rigor/inference/guard_rebinding.rb +7 -2
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
- 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 +213 -51
- data/lib/rigor/inference/narrowing.rb +235 -75
- 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 +116 -0
- data/lib/rigor/inference/repeated_or_writes.rb +414 -1
- data/lib/rigor/inference/return_barrier.rb +16 -0
- data/lib/rigor/inference/scope_indexer.rb +1061 -122
- data/lib/rigor/inference/self_call_ivar_writes.rb +346 -0
- 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 +512 -150
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- data/lib/rigor/lens/anchor.rb +3 -3
- data/lib/rigor/lens/contract_reader.rb +60 -2
- data/lib/rigor/lens/document.rb +4 -3
- data/lib/rigor/lens/file_reader.rb +452 -73
- data/lib/rigor/lens/members.rb +81 -17
- data/lib/rigor/lens/run.rb +73 -4
- data/lib/rigor/lens.rb +2 -1
- data/lib/rigor/mcp/server.rb +63 -22
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/box_probe.rb +154 -7
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -3
- data/lib/rigor/project_environment.rb +6 -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 +56 -8
- 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 +40 -1
- data/lib/rigor/sig_gen/method_candidate.rb +8 -0
- data/lib/rigor/sig_gen/observation_collector.rb +137 -11
- data/lib/rigor/sig_gen/path_mapper.rb +18 -2
- 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 +50 -35
- 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-schema/lib/rigor/plugin/dry_schema.rb +5 -1
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +57 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
- 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-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-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +155 -12
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
- 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 +16 -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,13 +510,30 @@ 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)
|
|
514
|
+
exempt_class_body_writes!(init_writes, accumulator.delete(DEFINE_METHOD_WRITES) || {})
|
|
510
515
|
merge_held_ivar_writes!(accumulator)
|
|
511
516
|
record_aliased_ivar_mutations!(root, mutated_ivars)
|
|
512
517
|
widen_mutated_ivar_entries!(accumulator, mutated_ivars)
|
|
513
518
|
contribute_read_before_write_nil!(accumulator, read_before_write, init_writes)
|
|
519
|
+
make_reflective_classes_gradual!(accumulator, reflective_opaque)
|
|
514
520
|
accumulator.transform_values(&:freeze).freeze
|
|
515
521
|
end
|
|
516
522
|
|
|
523
|
+
# Folds the class-body writes {#collect_class_body_ivar_writes} held under {CLASS_BODY_WRITES} into the
|
|
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.
|
|
530
|
+
def exempt_class_body_writes!(init_writes, define_method_writes)
|
|
531
|
+
(init_writes.delete(CLASS_BODY_WRITES) || {}).each do |class_name, ivars|
|
|
532
|
+
kept = ivars - (define_method_writes[class_name] || EMPTY_GUARDED_IVARS)
|
|
533
|
+
(init_writes[class_name] ||= Set.new).merge(kept)
|
|
534
|
+
end
|
|
535
|
+
end
|
|
536
|
+
|
|
517
537
|
# B2.3 — finalize the read-before-write nil contribution. For each class, for each ivar where SOME method body
|
|
518
538
|
# observed a read-before-write AND no `initialize` write exists for that ivar, contribute `Constant[nil]` to the
|
|
519
539
|
# class-wide accumulator.
|
|
@@ -853,7 +873,8 @@ module Rigor
|
|
|
853
873
|
|
|
854
874
|
owner = def_owner || qualified_prefix
|
|
855
875
|
collect_initializer_block_ivars(node, owner, default_scope, accumulator, mutated_ivars, init_writes)
|
|
856
|
-
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))
|
|
877
|
+
collect_define_method_block_ivars(node, owner, default_scope, accumulator, mutated_ivars)
|
|
857
878
|
false
|
|
858
879
|
else
|
|
859
880
|
walk_ivars_meta_new?(node, qualified_prefix, default_scope, accumulator,
|
|
@@ -1065,6 +1086,51 @@ module Rigor
|
|
|
1065
1086
|
end
|
|
1066
1087
|
end
|
|
1067
1088
|
|
|
1089
|
+
# The `define_method` arm of {#walk_class_ivars} (#1768). `define_method(:m) { … }` on the implicit
|
|
1090
|
+
# or `self` receiver installs its block as an instance method of `owner`, and Ruby runs the body on
|
|
1091
|
+
# the instance, so its `@x = …` writes are the instance's ivars exactly as a `def m` body's are —
|
|
1092
|
+
# `DefineMethodBlockSelf` narrows the block's `self` the same way when it is evaluated. Without this
|
|
1093
|
+
# arm an ivar `initialize` sets to `nil` and a `define_method` block sets to a String read as `nil`
|
|
1094
|
+
# in every sibling `def`. The block is typed under the instance `self`, as a def body is. Only the
|
|
1095
|
+
# literal-block form counts: a `define_method(:m, callable)` or `&callable` argument is not walked.
|
|
1096
|
+
# `owner` is empty in a `class << self` body (the call defines a class method there) and in an
|
|
1097
|
+
# anonymous factory body, and the block contributes nothing there. A `define_method` inside a `def`
|
|
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.
|
|
1103
|
+
def collect_define_method_block_ivars(node, owner, default_scope, accumulator, mutated_ivars)
|
|
1104
|
+
return if owner.empty?
|
|
1105
|
+
|
|
1106
|
+
class_name = owner.join("::")
|
|
1107
|
+
return unless method_body_block_call?(node, class_name, default_scope)
|
|
1108
|
+
|
|
1109
|
+
block = node.block
|
|
1110
|
+
return unless block.body
|
|
1111
|
+
|
|
1112
|
+
body_scope = census_body_scope(default_scope, owner, Type::Combinator.nominal_of(class_name))
|
|
1113
|
+
gather_ivar_writes(block.body, body_scope, class_name, accumulator, EMPTY_GUARDED_IVARS, mutated_ivars)
|
|
1114
|
+
written = (accumulator[DEFINE_METHOD_WRITES] ||= {})[class_name] ||= Set.new
|
|
1115
|
+
define_method_written_ivars(block.body, written)
|
|
1116
|
+
end
|
|
1117
|
+
|
|
1118
|
+
# The ivars a `define_method` block body writes, in any write form, outside a nested `def` / `class` / `module`.
|
|
1119
|
+
def define_method_written_ivars(node, acc)
|
|
1120
|
+
return unless node.is_a?(Prism::Node)
|
|
1121
|
+
return if IVAR_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
|
|
1122
|
+
|
|
1123
|
+
case node
|
|
1124
|
+
when Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode,
|
|
1125
|
+
Prism::InstanceVariableAndWriteNode, Prism::InstanceVariableOperatorWriteNode,
|
|
1126
|
+
Prism::InstanceVariableTargetNode
|
|
1127
|
+
acc << node.name
|
|
1128
|
+
end
|
|
1129
|
+
reflective = ReflectiveIvarWrite.literal_name(node)
|
|
1130
|
+
acc << reflective if reflective
|
|
1131
|
+
node.rigor_each_child { |child| define_method_written_ivars(child, acc) }
|
|
1132
|
+
end
|
|
1133
|
+
|
|
1068
1134
|
# ADR-38 block-form gate: true when a loaded plugin declares `method_name` a block-form initializer for
|
|
1069
1135
|
# `class_name` (or an ancestor). Mirrors `additional_initializer?` but queries `covers_block_method?` instead of
|
|
1070
1136
|
# `covers_method?`.
|
|
@@ -1156,15 +1222,21 @@ module Rigor
|
|
|
1156
1222
|
# every `@x = …` write target as a class-body init. Consumed by `contribute_read_before_write_nil!` to exempt
|
|
1157
1223
|
# ivars the author already knows might be nil (the `@x = nil` at class-body level is the canonical nullability
|
|
1158
1224
|
# acknowledgement; the instance @x is technically a separate store, but the pragmatic intent is unambiguous).
|
|
1225
|
+
#
|
|
1226
|
+
# A literal `define_method` block is a method body, not class-body code (#1768): its writes reach the census as
|
|
1227
|
+
# a `def` body's do, so they must not exempt the ivar from the read-before-write `nil` either. Only its receiver
|
|
1228
|
+
# and arguments are walked.
|
|
1159
1229
|
def collect_class_body_ivar_writes(node, class_name, init_writes)
|
|
1160
1230
|
return unless node.is_a?(Prism::Node)
|
|
1161
1231
|
return if IVAR_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
|
|
1162
1232
|
|
|
1233
|
+
return collect_define_method_argument_writes(node, class_name, init_writes) if define_method_block_call?(node)
|
|
1234
|
+
|
|
1163
1235
|
if node.is_a?(Prism::InstanceVariableWriteNode) ||
|
|
1164
1236
|
node.is_a?(Prism::InstanceVariableOrWriteNode) ||
|
|
1165
1237
|
node.is_a?(Prism::InstanceVariableAndWriteNode) ||
|
|
1166
1238
|
node.is_a?(Prism::InstanceVariableOperatorWriteNode)
|
|
1167
|
-
(init_writes[class_name] ||= Set.new) << node.name
|
|
1239
|
+
((init_writes[CLASS_BODY_WRITES] ||= {})[class_name] ||= Set.new) << node.name
|
|
1168
1240
|
end
|
|
1169
1241
|
|
|
1170
1242
|
node.rigor_each_child do |child|
|
|
@@ -1172,6 +1244,37 @@ module Rigor
|
|
|
1172
1244
|
end
|
|
1173
1245
|
end
|
|
1174
1246
|
|
|
1247
|
+
# The arguments of a `define_method` block call, walked by {#collect_class_body_ivar_writes} in place of the
|
|
1248
|
+
# call's children so the block body stays out of the class-body init writes.
|
|
1249
|
+
def collect_define_method_argument_writes(node, class_name, init_writes)
|
|
1250
|
+
node.arguments&.arguments&.each { |arg| collect_class_body_ivar_writes(arg, class_name, init_writes) }
|
|
1251
|
+
end
|
|
1252
|
+
|
|
1253
|
+
# A `define_method` call with a literal block on the implicit or `self` receiver: the block is an instance
|
|
1254
|
+
# method body of the class the walk is in (#1768).
|
|
1255
|
+
def define_method_block_call?(node)
|
|
1256
|
+
DefineMethodBlockSelf.define_method_on_lexical_self?(node) && node.block.is_a?(Prism::BlockNode)
|
|
1257
|
+
end
|
|
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
|
+
|
|
1175
1278
|
def detect_read_before_write(node, seen_writes, read_first)
|
|
1176
1279
|
return unless node.is_a?(Prism::Node)
|
|
1177
1280
|
return if IVAR_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
|
|
@@ -1199,6 +1302,9 @@ module Rigor
|
|
|
1199
1302
|
end
|
|
1200
1303
|
|
|
1201
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
|
|
1202
1308
|
end
|
|
1203
1309
|
|
|
1204
1310
|
# Records each ivar target of a `MultiWriteNode` / nested `MultiTargetNode` into `seen_writes`, and descends into
|
|
@@ -1246,6 +1352,7 @@ module Rigor
|
|
|
1246
1352
|
end
|
|
1247
1353
|
else
|
|
1248
1354
|
record_compound_ivar_write(node, scope, class_name, accumulator)
|
|
1355
|
+
record_reflective_ivar_write(node, scope, class_name, accumulator)
|
|
1249
1356
|
end
|
|
1250
1357
|
|
|
1251
1358
|
# N1 — parallel / multiple assignment (`old, @cb = @cb, block`, `@i, @o, @e, @thr = Open3.popen3(cmd)`). A
|
|
@@ -1273,6 +1380,32 @@ module Rigor
|
|
|
1273
1380
|
end
|
|
1274
1381
|
end
|
|
1275
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
|
+
|
|
1276
1409
|
# Records `@ivar.<method>(...)` calls whose method is in `MutationWidening::SHAPE_MUTATORS`.
|
|
1277
1410
|
# The class-ivar pre-pass uses the resulting set to widen the post-collected accumulator entries (see
|
|
1278
1411
|
# {.widen_mutated_ivar_entries!}). Always-safe to over- collect: any name that the widening primitive declines is
|
|
@@ -2011,6 +2144,15 @@ module Rigor
|
|
|
2011
2144
|
OPERATOR_WRITE_CONTRIBUTIONS = :operator_write_contributions
|
|
2012
2145
|
private_constant :AND_WRITE_CONTRIBUTIONS, :OPERATOR_WRITE_CONTRIBUTIONS
|
|
2013
2146
|
|
|
2147
|
+
# The same Symbol-key convention for two side tables the walk fills and {#build_class_ivar_index} removes
|
|
2148
|
+
# before anything reads class names: the ivars each class's `define_method` blocks write (in the
|
|
2149
|
+
# accumulator), and the class-body writes held out of `init_writes` until {#exempt_class_body_writes!}.
|
|
2150
|
+
DEFINE_METHOD_WRITES = :define_method_writes
|
|
2151
|
+
CLASS_BODY_WRITES = :class_body_writes
|
|
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
|
|
2155
|
+
|
|
2014
2156
|
# Unions `type` into the class-ivar accumulator for `(class_name, ivar_name)`. Shared by the single-write and
|
|
2015
2157
|
# multi-write (parallel-assignment) collectors.
|
|
2016
2158
|
def accumulate_ivar_type(accumulator, class_name, ivar_name, type)
|
|
@@ -4010,6 +4152,10 @@ module Rigor
|
|
|
4010
4152
|
(singleton_cref && owner_prefix.empty?)
|
|
4011
4153
|
record_alias_or_undef(node, owner_prefix, in_singleton_class || defs_singleton,
|
|
4012
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
|
|
4013
4159
|
end
|
|
4014
4160
|
return
|
|
4015
4161
|
when Prism::SymbolNode
|
|
@@ -4024,6 +4170,9 @@ module Rigor
|
|
|
4024
4170
|
end
|
|
4025
4171
|
anonymous = record_call_node_methods(node, owner_prefix, in_singleton_class, methods_acc.at(node),
|
|
4026
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
|
|
4027
4176
|
tables = methods_acc.at(node)
|
|
4028
4177
|
record_singleton_prepend(node, qualified_prefix, tables,
|
|
4029
4178
|
self_ok: node.name == :prepend && tables.in_block?(node))
|
|
@@ -4093,11 +4242,40 @@ module Rigor
|
|
|
4093
4242
|
def record_declaration_facts(node, child_prefix, tables)
|
|
4094
4243
|
if node.is_a?(Prism::ClassNode)
|
|
4095
4244
|
record_meta_superclass_members(node, child_prefix, tables)
|
|
4245
|
+
record_constructed_superclass(node.superclass, child_prefix.join("::"), tables)
|
|
4096
4246
|
else
|
|
4097
4247
|
record_surface_mark(tables, child_prefix.join("::"), Scope::DiscoveryIndex::ENVELOPE_MODULE_MARK)
|
|
4098
4248
|
end
|
|
4099
4249
|
end
|
|
4100
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
|
+
|
|
4101
4279
|
# The block form of a meta-new constant write: its members, and the module mark `Module.new` earns.
|
|
4102
4280
|
# Issue #1820 — `K = Class.new(Parent)`, with or without a block, names a class whose superclass the superclass
|
|
4103
4281
|
# table leaves out; it takes {Scope::DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK}.
|
|
@@ -4136,7 +4314,7 @@ module Rigor
|
|
|
4136
4314
|
# descent.
|
|
4137
4315
|
def record_call_node_methods(node, qualified_prefix, in_singleton_class, methods_acc, source_path)
|
|
4138
4316
|
record_define_method(node, qualified_prefix, in_singleton_class, methods_acc) if node.name == :define_method
|
|
4139
|
-
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)
|
|
4140
4318
|
record_module_attr_methods(node, qualified_prefix, methods_acc) if MODULE_ATTR_MACROS.key?(node.name)
|
|
4141
4319
|
record_alias_method_call(node, qualified_prefix, in_singleton_class, methods_acc)
|
|
4142
4320
|
record_surface_evidence(node, qualified_prefix, methods_acc)
|
|
@@ -4649,6 +4827,49 @@ module Rigor
|
|
|
4649
4827
|
end
|
|
4650
4828
|
end
|
|
4651
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
|
+
|
|
4652
4873
|
# #319 — a `def` inside an anonymous `Class.new` / `Module.new` body ALSO stays in the `<toplevel>` table.
|
|
4653
4874
|
# Before the anonymous class had a name the body was walked with an empty prefix, so every such `def`
|
|
4654
4875
|
# landed there; that is the same leniency this key already grants a `def` nested in any other DSL block
|
|
@@ -4694,6 +4915,18 @@ module Rigor
|
|
|
4694
4915
|
# The singleton walk's accumulator: the `{class => {name => DefNode}}` table, plus the contested slots, which
|
|
4695
4916
|
# follow each slot's writer ({#follow_slot_writer}'s rule).
|
|
4696
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
|
+
|
|
4697
4930
|
def track(certainty)
|
|
4698
4931
|
@certainty = certainty
|
|
4699
4932
|
@contested = nil
|
|
@@ -4701,9 +4934,11 @@ module Rigor
|
|
|
4701
4934
|
end
|
|
4702
4935
|
|
|
4703
4936
|
# Writes the slot; `node` and `other` are the contributions the write rests on, possible when either is.
|
|
4704
|
-
|
|
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)
|
|
4705
4940
|
(self[class_name] ||= {})[name] = def_node
|
|
4706
|
-
if Certainty.possible?(@certainty, node) || (other && Certainty.possible?(@certainty, other))
|
|
4941
|
+
if possible || Certainty.possible?(@certainty, node) || (other && Certainty.possible?(@certainty, other))
|
|
4707
4942
|
(@contested ||= Set.new) << [class_name, name]
|
|
4708
4943
|
else
|
|
4709
4944
|
@contested&.delete([class_name, name])
|
|
@@ -4746,6 +4981,12 @@ module Rigor
|
|
|
4746
4981
|
# `Const`'s singleton class (a `remove_method`, a `def`) without declaring `Const`, so it stays out of
|
|
4747
4982
|
# `class_sources` (whose readers take a name there as one the project declares) and joins only the per-file
|
|
4748
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.
|
|
4749
4990
|
def record_singleton_class_sources(sources, path, root)
|
|
4750
4991
|
collect_singleton_class_targets(root, []) { |name| (sources[name] ||= Set.new) << path }
|
|
4751
4992
|
end
|
|
@@ -4761,6 +5002,8 @@ module Rigor
|
|
|
4761
5002
|
yield eval_constant_receiver_prefix(node, node.expression, rendered, lexical_prefix).join("::") if rendered
|
|
4762
5003
|
when Prism::CallNode
|
|
4763
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, &)
|
|
4764
5007
|
end
|
|
4765
5008
|
node.rigor_each_child { |child| collect_singleton_class_targets(child, lexical_prefix, &) }
|
|
4766
5009
|
end
|
|
@@ -4768,8 +5011,7 @@ module Rigor
|
|
|
4768
5011
|
# A call on a constant that may rewrite its singleton side (`Const.singleton_class`,
|
|
4769
5012
|
# `Const.define_singleton_method`, `Const.extend(M)`, `Const.instance_eval { … }`) records facts about `Const`'s
|
|
4770
5013
|
# singleton methods the refinement walk reads from a file that need not declare `Const`, so the file joins the
|
|
4771
|
-
# constant's set too. Written inside `Const`'s own body, the name is in the file's set already
|
|
4772
|
-
# 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.
|
|
4773
5015
|
def singleton_side_call?(node)
|
|
4774
5016
|
node.name == :singleton_class || surface_rewriting_call?(node)
|
|
4775
5017
|
end
|
|
@@ -4984,7 +5226,7 @@ module Rigor
|
|
|
4984
5226
|
# own `def` leaves additionally split on {INSTANCE_EVAL_CALLS}, whose `def`s bind on the
|
|
4985
5227
|
# receiver's singleton rather than the receiver.
|
|
4986
5228
|
def receiver_eval_call?(node)
|
|
4987
|
-
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)
|
|
4988
5230
|
end
|
|
4989
5231
|
|
|
4990
5232
|
# A `class_eval` / `module_eval` / `class_exec` / `module_exec` block is NOT deferred: it runs
|
|
@@ -5367,7 +5609,10 @@ module Rigor
|
|
|
5367
5609
|
def_owner_prefix = nil, singleton_cref: false,
|
|
5368
5610
|
defs_singleton: false)
|
|
5369
5611
|
return unless node.is_a?(Prism::Node)
|
|
5370
|
-
|
|
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)
|
|
5371
5616
|
return walk_eval_singleton_defs(node, qualified_prefix, in_singleton_class, accumulator,
|
|
5372
5617
|
def_owner_prefix, singleton_cref: singleton_cref,
|
|
5373
5618
|
defs_singleton: defs_singleton)
|
|
@@ -5658,7 +5903,8 @@ module Rigor
|
|
|
5658
5903
|
defs_singleton: false)
|
|
5659
5904
|
owner_prefix = def_owner_prefix || qualified_prefix
|
|
5660
5905
|
statements = statements_of(body)
|
|
5661
|
-
ModuleFunctionState.
|
|
5906
|
+
conditional = ModuleFunctionState.conditional_clause_statements(body)
|
|
5907
|
+
ModuleFunctionState.each_singleton_sibling(statements, conditional) do |stmt, module_function_on, named_call|
|
|
5662
5908
|
if named_call
|
|
5663
5909
|
# `:unnameable` — the named defs the call copies live on the metaclass,
|
|
5664
5910
|
# which this table cannot name either.
|
|
@@ -5708,19 +5954,96 @@ module Rigor
|
|
|
5708
5954
|
statements
|
|
5709
5955
|
end
|
|
5710
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).
|
|
5711
5960
|
def record_singleton_def_node(def_node, qualified_prefix, in_singleton_class, module_function_on, accumulator)
|
|
5712
|
-
|
|
5713
|
-
return unless
|
|
5961
|
+
declared = def_singleton?(def_node, qualified_prefix, in_singleton_class)
|
|
5962
|
+
return unless declared || module_function_on
|
|
5714
5963
|
return if qualified_prefix.empty?
|
|
5715
5964
|
|
|
5716
5965
|
class_name = qualified_prefix.join("::")
|
|
5717
|
-
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
|
|
5718
6041
|
end
|
|
5719
6042
|
|
|
5720
6043
|
# One singleton slot write, through {SingletonDefTable#write} when the walk tracks certainty.
|
|
5721
|
-
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)
|
|
5722
6045
|
if accumulator.is_a?(SingletonDefTable)
|
|
5723
|
-
accumulator.write(class_name, name, def_node, node, other)
|
|
6046
|
+
accumulator.write(class_name, name, def_node, node, other, possible: possible)
|
|
5724
6047
|
else
|
|
5725
6048
|
(accumulator[class_name] ||= {})[name] = def_node
|
|
5726
6049
|
end
|
|
@@ -6617,32 +6940,34 @@ module Rigor
|
|
|
6617
6940
|
# differently for the two spellings of one edge.
|
|
6618
6941
|
def record_mixin_call(node, qualified_prefix, current_class, accumulator)
|
|
6619
6942
|
taint_opaque_eval(node, current_class, accumulator)
|
|
6620
|
-
|
|
6621
|
-
return if
|
|
6943
|
+
view = mixin_call_view(node)
|
|
6944
|
+
return if view.nil? || view.first.nil?
|
|
6622
6945
|
|
|
6946
|
+
kind = view.first
|
|
6623
6947
|
current_class ||= toplevel_include_owner(node, qualified_prefix, current_class, accumulator)
|
|
6624
6948
|
effects = mixin_effects(node, kind, qualified_prefix, current_class, accumulator, in_singleton: false)
|
|
6625
6949
|
.select { |_owner, side| side == :include }
|
|
6626
6950
|
return if effects.empty?
|
|
6627
|
-
return if
|
|
6951
|
+
return if instance_mixin_recorded?(node, view, qualified_prefix, current_class, accumulator)
|
|
6628
6952
|
|
|
6629
6953
|
# A mixin call the tables cannot hold still reshapes the ancestry: taint the whole owner side.
|
|
6630
6954
|
effects.each { |owner, side| accumulator.taint(owner, side) }
|
|
6631
6955
|
end
|
|
6632
6956
|
|
|
6633
6957
|
# Records the instance-side edges of a call {#mixin_call_recorded?} accepts. False when it recorded
|
|
6634
|
-
# nothing — the caller then taints the owner instead.
|
|
6635
|
-
|
|
6636
|
-
|
|
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)
|
|
6637
6963
|
|
|
6638
|
-
arguments = node.arguments&.arguments || []
|
|
6639
6964
|
targets = arguments.filter_map { |arg| Source::ConstantPath.qualified_name(arg) }
|
|
6640
6965
|
return false if targets.empty?
|
|
6641
6966
|
|
|
6642
6967
|
owner = node.receiver.nil? ? current_class : prepend_call_receiver(node, qualified_prefix)
|
|
6643
6968
|
return false if owner.nil?
|
|
6644
6969
|
|
|
6645
|
-
write_mixin_targets(accumulator, owner, targets, prepend:
|
|
6970
|
+
write_mixin_targets(accumulator, owner, targets, prepend: kind == :prepend)
|
|
6646
6971
|
accumulator.note(node, owner, :include, targets, complete: targets.size == arguments.size)
|
|
6647
6972
|
true
|
|
6648
6973
|
end
|
|
@@ -6790,15 +7115,17 @@ module Rigor
|
|
|
6790
7115
|
end
|
|
6791
7116
|
|
|
6792
7117
|
# Whether a mixin call contributes to the tables at all: a receiverless `include` / `prepend` needs an
|
|
6793
|
-
# enclosing declaration to file the edge under, and the RECEIVER form
|
|
6794
|
-
#
|
|
6795
|
-
#
|
|
6796
|
-
#
|
|
6797
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
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?
|
|
6800
7127
|
|
|
6801
|
-
|
|
7128
|
+
true
|
|
6802
7129
|
end
|
|
6803
7130
|
|
|
6804
7131
|
# Issue #1697 — a receiverless `include M` written as a statement of the file's own top level is
|
|
@@ -7004,7 +7331,9 @@ module Rigor
|
|
|
7004
7331
|
def extends_block_handled?(node, qualified_prefix, current_class, accumulator, in_singleton, singleton_self,
|
|
7005
7332
|
singleton_cref)
|
|
7006
7333
|
case extends_block_plan(node, accumulator)
|
|
7007
|
-
when :skip
|
|
7334
|
+
when :skip
|
|
7335
|
+
taint_skipped_singleton_rewrites(node.block, qualified_prefix, current_class, accumulator)
|
|
7336
|
+
true
|
|
7008
7337
|
when :rebound
|
|
7009
7338
|
walk_extends_block_call(node, qualified_prefix, current_class, accumulator,
|
|
7010
7339
|
in_singleton, singleton_self, singleton_cref)
|
|
@@ -7013,6 +7342,31 @@ module Rigor
|
|
|
7013
7342
|
end
|
|
7014
7343
|
end
|
|
7015
7344
|
|
|
7345
|
+
# Issue #1906 — a block the extends walk skips may still run, and a `singleton_class` call in it, or in a `def`
|
|
7346
|
+
# it holds (`[1].each { D.singleton_class.alias_method :own, :other }` in a method body,
|
|
7347
|
+
# `to_prepare do def helper = D.singleton_class.alias_method(...) end`), reaches no mark the methods walk records
|
|
7348
|
+
# there. Every such call taints the side it may rewrite, as {#singleton_class_rewrite_tainted?} does for one the
|
|
7349
|
+
# walk visits; a constant receiver taints every name it can denote from here.
|
|
7350
|
+
def taint_skipped_singleton_rewrites(node, qualified_prefix, current_class, accumulator)
|
|
7351
|
+
return unless node.is_a?(Prism::Node)
|
|
7352
|
+
|
|
7353
|
+
if node.is_a?(Prism::CallNode) && singleton_class_of_receiver?(node.receiver) && surface_rewriting_call?(node)
|
|
7354
|
+
skipped_rewrite_owners(node.receiver.receiver, qualified_prefix, current_class).each do |owner|
|
|
7355
|
+
accumulator.taint(owner, :extend)
|
|
7356
|
+
end
|
|
7357
|
+
end
|
|
7358
|
+
node.rigor_each_child do |child|
|
|
7359
|
+
taint_skipped_singleton_rewrites(child, qualified_prefix, current_class, accumulator)
|
|
7360
|
+
end
|
|
7361
|
+
end
|
|
7362
|
+
|
|
7363
|
+
def skipped_rewrite_owners(object, qualified_prefix, current_class)
|
|
7364
|
+
return [current_class].compact if object.nil? || object.is_a?(Prism::SelfNode)
|
|
7365
|
+
return [] unless object.is_a?(Prism::ConstantReadNode) || object.is_a?(Prism::ConstantPathNode)
|
|
7366
|
+
|
|
7367
|
+
constant_receiver_candidates(object, qualified_prefix)
|
|
7368
|
+
end
|
|
7369
|
+
|
|
7016
7370
|
ARRAY_ITERATORS = %i[each each_with_index map].freeze
|
|
7017
7371
|
HASH_ITERATORS = %i[each each_pair each_key each_value map].freeze
|
|
7018
7372
|
private_constant :ARRAY_ITERATORS, :HASH_ITERATORS
|
|
@@ -7228,10 +7582,15 @@ module Rigor
|
|
|
7228
7582
|
# `Const.singleton_class.send(:include, M)`) puts methods or modules on that singleton class that no edge
|
|
7229
7583
|
# records, so its singleton side is unpositioned: the owner is the enclosing class for a bare or `self.`
|
|
7230
7584
|
# receiver, else the constant. True when it tainted.
|
|
7585
|
+
#
|
|
7586
|
+
# Issue #1906 — a naming call with literal names ({#singleton_class_named_call?}) marks only its names instead,
|
|
7587
|
+
# but only where it is a direct statement (a top-level statement, or one of a direct declaration body or of a
|
|
7588
|
+
# block that runs there once): there the methods walk records those marks under the same owner. In a method
|
|
7589
|
+
# 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
7590
|
def singleton_class_rewrite_tainted?(node, qualified_prefix, current_class, accumulator)
|
|
7232
7591
|
receiver = node.receiver
|
|
7233
7592
|
return false unless singleton_class_of_receiver?(receiver) && surface_rewriting_call?(node)
|
|
7234
|
-
return false if singleton_class_named_call?(node)
|
|
7593
|
+
return false if singleton_class_named_call?(node) && accumulator.direct?(node)
|
|
7235
7594
|
|
|
7236
7595
|
owner = singleton_class_owner(node, receiver.receiver, qualified_prefix, current_class)
|
|
7237
7596
|
return false if owner.nil?
|
|
@@ -7245,15 +7604,37 @@ module Rigor
|
|
|
7245
7604
|
receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class && receiver.arguments.nil?
|
|
7246
7605
|
end
|
|
7247
7606
|
|
|
7248
|
-
# An `attr_
|
|
7249
|
-
#
|
|
7250
|
-
#
|
|
7251
|
-
#
|
|
7252
|
-
#
|
|
7607
|
+
# An `attr_*`, `alias_method`, `define_method` or `remove_method` on a singleton class
|
|
7608
|
+
# (`singleton_class.attr_accessor :conf`, `singleton_class.alias_method :o2, :own`) rewrites only the names it
|
|
7609
|
+
# spells, so it makes those names' singleton envelopes opaque and contested ({#record_singleton_class_names}) and
|
|
7610
|
+
# leaves the rest of the singleton side readable. One whose rewritten name is not a literal Symbol or String
|
|
7611
|
+
# (`attr_accessor(*KEYS)`, `alias_method name, :own`, `define_method(sym)`), and any other surface call there
|
|
7612
|
+
# (`class_eval`, `send`, `prepend`), still taints the side: the own-definer read would otherwise answer the body
|
|
7613
|
+
# Ruby replaced (`definer_resolution_witness_spec.rb`).
|
|
7253
7614
|
def singleton_class_named_call?(node)
|
|
7615
|
+
!singleton_class_rewritten_names(node).nil?
|
|
7616
|
+
end
|
|
7617
|
+
|
|
7618
|
+
# Issue #1906 — the names a singleton-class call rewrites, or nil where it is not one of the naming forms or
|
|
7619
|
+
# does not spell every rewritten name as a literal. `alias_method` rewrites its new name (the aliased one keeps
|
|
7620
|
+
# its body), `define_method` its first argument, `remove_method` and `attr_*` every argument.
|
|
7621
|
+
def singleton_class_rewritten_names(node)
|
|
7254
7622
|
arguments = node.arguments&.arguments || []
|
|
7255
|
-
|
|
7256
|
-
|
|
7623
|
+
rewritten = case node.name
|
|
7624
|
+
when :alias_method, :define_method then arguments.first(1)
|
|
7625
|
+
when :remove_method then arguments
|
|
7626
|
+
else ATTR_MACROS.include?(node.name) && arguments
|
|
7627
|
+
end
|
|
7628
|
+
return nil if !rewritten || arguments.empty? || !arguments.all? { |argument| literal_argument?(node, argument) }
|
|
7629
|
+
|
|
7630
|
+
rewritten.map { |argument| literal_method_name(argument) }
|
|
7631
|
+
end
|
|
7632
|
+
|
|
7633
|
+
# `define_method(:x, body)` takes a body that is not a name; every other argument must be a literal name.
|
|
7634
|
+
def literal_argument?(node, argument)
|
|
7635
|
+
return true if node.name == :define_method && !argument.equal?(node.arguments.arguments.first)
|
|
7636
|
+
|
|
7637
|
+
!literal_method_name(argument).nil?
|
|
7257
7638
|
end
|
|
7258
7639
|
|
|
7259
7640
|
def singleton_class_owner(node, object, qualified_prefix, current_class)
|
|
@@ -7447,19 +7828,17 @@ module Rigor
|
|
|
7447
7828
|
|
|
7448
7829
|
# v0.1.2 — per-class method-visibility table for the `def.method-visibility-mismatch` CheckRule.
|
|
7449
7830
|
#
|
|
7450
|
-
# Tracks
|
|
7831
|
+
# Tracks three visibility-changing forms ({#apply_visibility_call}):
|
|
7832
|
+
#
|
|
7833
|
+
# - **Modifier blocks**: a bare `private` / `protected` / `public` call inside a class body (also written
|
|
7834
|
+
# `self.private`, `send(:private)` or with a block) switches the "current default" visibility for every
|
|
7835
|
+
# subsequent `def` and `attr_*` member until another modifier flips it again.
|
|
7836
|
+
# - **Named-argument form**: `private :foo, :bar` or `private %i[foo bar]` (or the same with `protected` /
|
|
7837
|
+
# `public`) marks specific names already recorded under the class.
|
|
7838
|
+
# - **Wrap-around form** (#1911, #1819, #1633): `private def foo` and `private attr_reader :foo` record the
|
|
7839
|
+
# members with the call's visibility and leave the default alone.
|
|
7451
7840
|
#
|
|
7452
|
-
#
|
|
7453
|
-
# `public` call inside a class body switches the
|
|
7454
|
-
# "current default" visibility for every subsequent
|
|
7455
|
-
# `def` until another modifier flips it again.
|
|
7456
|
-
# - **Named-argument form**: `private :foo, :bar` (or
|
|
7457
|
-
# the same with `protected` / `public`) marks specific
|
|
7458
|
-
# names already-recorded under the class. Symbol-only
|
|
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.
|
|
7841
|
+
# A `module_function` call takes each form as `private` does (#1569): Ruby makes the instance copy private.
|
|
7463
7842
|
#
|
|
7464
7843
|
# Top-level (no surrounding class) defs do not contribute — Ruby's top-level visibility nuances (private at
|
|
7465
7844
|
# top-level marks the method on `Object`) are out of scope for v0.1.2.
|
|
@@ -7477,8 +7856,8 @@ module Rigor
|
|
|
7477
7856
|
end
|
|
7478
7857
|
|
|
7479
7858
|
# The bare calls that set the running default visibility of the body they run in: the modifiers and
|
|
7480
|
-
# `module_function`, which {#walk_method_visibilities}
|
|
7481
|
-
#
|
|
7859
|
+
# `module_function`, which {#walk_method_visibilities} applies as `private` on the instance side
|
|
7860
|
+
# ({ModuleFunctionState.instance_visibility}, #1569).
|
|
7482
7861
|
VISIBILITY_TOGGLES = (VISIBILITY_MODIFIERS + %i[module_function]).freeze
|
|
7483
7862
|
|
|
7484
7863
|
# The `{class => {name => visibility}}` accumulator of {#walk_method_visibilities}, which also carries the
|
|
@@ -7516,6 +7895,39 @@ module Rigor
|
|
|
7516
7895
|
end
|
|
7517
7896
|
end
|
|
7518
7897
|
|
|
7898
|
+
# Contests the `[class_name, name]` slot, recorded or not: a visibility call changed it in a way the walk
|
|
7899
|
+
# cannot read (`private def self.x` reaching an instance `x`).
|
|
7900
|
+
def contest_slot(class_name, name)
|
|
7901
|
+
(@contested ||= Set.new) << [class_name, name].freeze
|
|
7902
|
+
end
|
|
7903
|
+
|
|
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)
|
|
7920
|
+
end
|
|
7921
|
+
|
|
7922
|
+
# Runs the block with the running default uncertain, so a `def` it records is contested.
|
|
7923
|
+
def uncertainly
|
|
7924
|
+
saved = @uncertain
|
|
7925
|
+
@uncertain = true
|
|
7926
|
+
yield
|
|
7927
|
+
ensure
|
|
7928
|
+
@uncertain = saved
|
|
7929
|
+
end
|
|
7930
|
+
|
|
7519
7931
|
# Runs the block as a fresh body (a class or module, `class << self`, a meta-new or eval block), whose
|
|
7520
7932
|
# default starts certain and whose toggles do not reach the enclosing body.
|
|
7521
7933
|
def body
|
|
@@ -7537,10 +7949,37 @@ module Rigor
|
|
|
7537
7949
|
end
|
|
7538
7950
|
end
|
|
7539
7951
|
|
|
7540
|
-
# A bare toggle ({VISIBILITY_TOGGLES}):
|
|
7952
|
+
# A bare toggle ({VISIBILITY_TOGGLES}): a visibility call ({#visibility_call_parts}) with no arguments.
|
|
7541
7953
|
def bare_visibility_toggle?(node)
|
|
7542
|
-
|
|
7543
|
-
|
|
7954
|
+
parts = visibility_call_parts(node)
|
|
7955
|
+
!parts.nil? && parts.last.empty?
|
|
7956
|
+
end
|
|
7957
|
+
|
|
7958
|
+
# Whether a visibility call with these arguments may set the running default: none at all, or a splat that
|
|
7959
|
+
# may expand to none (`private(*[])` is a bare `private`).
|
|
7960
|
+
def visibility_toggle_arguments?(arguments)
|
|
7961
|
+
arguments.empty? || arguments.any?(Prism::SplatNode)
|
|
7962
|
+
end
|
|
7963
|
+
|
|
7964
|
+
VISIBILITY_SEND_CALLS = %i[send __send__].freeze
|
|
7965
|
+
EMPTY_VISIBILITY_ARGUMENTS = [].freeze
|
|
7966
|
+
private_constant :VISIBILITY_SEND_CALLS, :EMPTY_VISIBILITY_ARGUMENTS
|
|
7967
|
+
|
|
7968
|
+
# Issue #1633 — `[name, arguments]` for a call Ruby runs as one of {VISIBILITY_TOGGLES} on the body's self:
|
|
7969
|
+
# written receiverless or on `self` (`self.private`), or sent with a literal name (`send(:private, :x)`,
|
|
7970
|
+
# `__send__(:private)`). A block is ignored, as `Module#private` ignores it (`private {}` is a bare
|
|
7971
|
+
# `private`). nil for any other call.
|
|
7972
|
+
def visibility_call_parts(node)
|
|
7973
|
+
return nil unless node.is_a?(Prism::CallNode)
|
|
7974
|
+
return nil unless node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
|
7975
|
+
|
|
7976
|
+
name = node.name
|
|
7977
|
+
arguments = node.arguments&.arguments || EMPTY_VISIBILITY_ARGUMENTS
|
|
7978
|
+
if VISIBILITY_SEND_CALLS.include?(name)
|
|
7979
|
+
name = Source::Literals.symbol_or_string(arguments.first)
|
|
7980
|
+
arguments = arguments.drop(1)
|
|
7981
|
+
end
|
|
7982
|
+
VISIBILITY_TOGGLES.include?(name) ? [name, arguments] : nil
|
|
7544
7983
|
end
|
|
7545
7984
|
|
|
7546
7985
|
# rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/AbcSize
|
|
@@ -7594,26 +8033,70 @@ module Rigor
|
|
|
7594
8033
|
return current_visibility
|
|
7595
8034
|
end
|
|
7596
8035
|
when Prism::DefNode
|
|
7597
|
-
|
|
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),
|
|
7598
8038
|
current_visibility, accumulator)
|
|
7599
8039
|
return current_visibility
|
|
7600
8040
|
when Prism::CallNode
|
|
8041
|
+
record_constant_class_method_calls(node, qualified_prefix, accumulator)
|
|
7601
8042
|
if receiver_eval_call?(node)
|
|
7602
8043
|
walk_eval_visibilities(node, qualified_prefix, in_singleton_class, current_visibility,
|
|
7603
8044
|
accumulator, def_owner_prefix, singleton_cref: singleton_cref,
|
|
7604
8045
|
defs_singleton: defs_singleton)
|
|
7605
8046
|
return current_visibility
|
|
7606
8047
|
end
|
|
7607
|
-
|
|
7608
|
-
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
|
|
7614
|
-
|
|
7615
|
-
|
|
7616
|
-
|
|
8048
|
+
if AnonymousMetaClass.block_form_receiver(node)
|
|
8049
|
+
walk_anonymous_meta_visibilities(node, qualified_prefix, in_singleton_class, current_visibility,
|
|
8050
|
+
accumulator, def_owner_prefix, singleton_cref: singleton_cref,
|
|
8051
|
+
defs_singleton: defs_singleton)
|
|
8052
|
+
return current_visibility
|
|
8053
|
+
end
|
|
8054
|
+
if (parts = visibility_call_parts(node))
|
|
8055
|
+
accumulator.toggles += 1 if visibility_toggle_arguments?(parts.last)
|
|
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|
|
|
8070
|
+
walk_method_visibilities(argument, qualified_prefix, in_singleton_class, current_visibility,
|
|
8071
|
+
accumulator, def_owner_prefix, singleton_cref: singleton_cref,
|
|
8072
|
+
defs_singleton: defs_singleton)
|
|
8073
|
+
end
|
|
8074
|
+
end
|
|
8075
|
+
end
|
|
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)
|
|
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
|
|
7617
8100
|
end
|
|
7618
8101
|
|
|
7619
8102
|
# Statement-position StatementsNode preserves left-to-right visibility flow; everything else recurses with the
|
|
@@ -7667,6 +8150,23 @@ module Rigor
|
|
|
7667
8150
|
end
|
|
7668
8151
|
end
|
|
7669
8152
|
|
|
8153
|
+
# A `Class.new` / `Module.new` / `Struct.new` / `Data.define` block no constant write names
|
|
8154
|
+
# ({#meta_new_block_split} takes the named ones): `klass = Class.new do … end`, `@k = Struct.new(:a) do … end`,
|
|
8155
|
+
# `register(Class.new do … end)` or a bare `Data.define(:a) do … end`. Its body is the anonymous class's, so
|
|
8156
|
+
# it walks as a fresh body with no owner and records nothing for the enclosing class; the receiver and the
|
|
8157
|
+
# arguments are walked where they stand.
|
|
8158
|
+
def walk_anonymous_meta_visibilities(node, qualified_prefix, in_singleton_class, current_visibility,
|
|
8159
|
+
accumulator, def_owner_prefix, singleton_cref:, defs_singleton:) # rubocop:disable Metrics/ParameterLists
|
|
8160
|
+
[node.receiver, *node.arguments&.arguments].each do |part|
|
|
8161
|
+
walk_method_visibilities(part, qualified_prefix, in_singleton_class, current_visibility, accumulator,
|
|
8162
|
+
def_owner_prefix, singleton_cref: singleton_cref, defs_singleton: defs_singleton)
|
|
8163
|
+
end
|
|
8164
|
+
accumulator.body do
|
|
8165
|
+
walk_method_visibilities(node.block.body, qualified_prefix, false, :public, accumulator,
|
|
8166
|
+
EMPTY_PREFIX, singleton_cref: singleton_cref)
|
|
8167
|
+
end
|
|
8168
|
+
end
|
|
8169
|
+
|
|
7670
8170
|
# The eval-block arm of {#walk_method_visibilities}: the block is a fresh class body under
|
|
7671
8171
|
# the receiver's def-owner — visibility starts at `:public`, and nothing inside it leaks
|
|
7672
8172
|
# back to this body's modifier state. An unnameable receiver walks the body ownerless;
|
|
@@ -7680,6 +8180,9 @@ module Rigor
|
|
|
7680
8180
|
singleton_cref: singleton_cref,
|
|
7681
8181
|
defs_singleton: defs_singleton)
|
|
7682
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
|
+
|
|
7683
8186
|
node.arguments&.arguments&.each do |arg|
|
|
7684
8187
|
walk_method_visibilities(arg, qualified_prefix, in_singleton_class, current_visibility,
|
|
7685
8188
|
accumulator, def_owner_prefix, singleton_cref: singleton_cref,
|
|
@@ -7704,48 +8207,201 @@ module Rigor
|
|
|
7704
8207
|
end
|
|
7705
8208
|
end
|
|
7706
8209
|
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
|
|
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?
|
|
7710
8312
|
|
|
7711
8313
|
class_name = qualified_prefix.join("::")
|
|
7712
|
-
|
|
7713
|
-
|
|
7714
|
-
|
|
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
|
|
7715
8321
|
end
|
|
7716
8322
|
|
|
7717
|
-
|
|
7718
|
-
|
|
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
|
|
8338
|
+
end
|
|
8339
|
+
|
|
8340
|
+
# Applies one `private` / `protected` / `public` call ({#visibility_call_parts}) written on the body's self
|
|
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.
|
|
7719
8343
|
#
|
|
7720
|
-
# -
|
|
7721
|
-
#
|
|
7722
|
-
#
|
|
7723
|
-
#
|
|
7724
|
-
#
|
|
7725
|
-
#
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
|
|
8344
|
+
# - no arguments — the call's visibility becomes the default for subsequent defs.
|
|
8345
|
+
# - `private :foo, :bar`, `private %i[foo bar]` — back-patch the named methods in the accumulator; the
|
|
8346
|
+
# default does NOT change for this form.
|
|
8347
|
+
# - Issues #1911, #1819, #1633 — `private def foo` (endless or not) and `private attr_reader :foo` define
|
|
8348
|
+
# the members with the call's visibility, whatever the running default is (`private; public def foo` is
|
|
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
|
|
8353
|
+
# ({#visibility_toggle_arguments?}).
|
|
8354
|
+
# - any other argument (`private memoize def foo`, `private delegate(...)`) is walked with the default
|
|
8355
|
+
# uncertain, so a `def` under it is contested: the call's visibility reaches it through a return value
|
|
8356
|
+
# the walk does not read.
|
|
8357
|
+
#
|
|
8358
|
+
# `walk` walks an argument the call does not consume itself.
|
|
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)
|
|
8361
|
+
return name if args.empty?
|
|
7730
8362
|
|
|
7731
|
-
|
|
7732
|
-
if args.
|
|
7733
|
-
|
|
8363
|
+
class_name = qualified_prefix.join("::")
|
|
8364
|
+
accumulator.contest_class(class_name, side) if args.any?(Prism::SplatNode)
|
|
8365
|
+
args.each do |arg|
|
|
8366
|
+
apply_visibility_argument(arg, name, call_node, qualified_prefix, accumulator,
|
|
8367
|
+
side: side, defs_singleton: defs_singleton, &walk)
|
|
8368
|
+
end
|
|
8369
|
+
current_visibility
|
|
8370
|
+
end
|
|
8371
|
+
|
|
8372
|
+
def apply_visibility_argument(arg, name, call_node, qualified_prefix, accumulator, side:, defs_singleton:, &walk)
|
|
8373
|
+
case arg
|
|
8374
|
+
when Prism::DefNode
|
|
8375
|
+
if arg.receiver.nil?
|
|
8376
|
+
def_side = side == :instance && defs_singleton ? :eval : side
|
|
8377
|
+
record_def_visibility(arg, qualified_prefix, def_side, name, accumulator, toggled: false)
|
|
8378
|
+
else
|
|
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)
|
|
8382
|
+
end
|
|
8383
|
+
when Prism::SymbolNode, Prism::StringNode, Prism::ArrayNode
|
|
8384
|
+
apply_named_visibility(arg, name, qualified_prefix, call_node, accumulator, side: side, &walk)
|
|
7734
8385
|
else
|
|
7735
|
-
|
|
7736
|
-
|
|
8386
|
+
# An `instance_eval` body's wrapped attr is walked like any other argument and records nothing.
|
|
8387
|
+
if !defs_singleton && attr_visibility_call?(arg)
|
|
8388
|
+
record_attr_visibility(arg, qualified_prefix, name, accumulator, toggled: false, side: side)
|
|
8389
|
+
else
|
|
8390
|
+
accumulator.uncertainly { walk.call(arg) }
|
|
8391
|
+
end
|
|
7737
8392
|
end
|
|
7738
8393
|
end
|
|
7739
8394
|
|
|
7740
|
-
|
|
8395
|
+
# `private :foo` / `private "foo"` / `private %i[foo bar]`: each literal name the call's visibility. An array
|
|
8396
|
+
# element that is not a literal name is walked like any other argument.
|
|
8397
|
+
def apply_named_visibility(arg, visibility, qualified_prefix, call_node, accumulator, side: :instance, &walk)
|
|
8398
|
+
elements = arg.is_a?(Prism::ArrayNode) ? arg.elements : [arg]
|
|
7741
8399
|
class_name = qualified_prefix.join("::")
|
|
7742
|
-
|
|
7743
|
-
name = visibility_target_name(
|
|
7744
|
-
next if name.nil?
|
|
8400
|
+
elements.each do |element|
|
|
8401
|
+
name = visibility_target_name(element)
|
|
8402
|
+
next accumulator.uncertainly { walk.call(element) } if name.nil?
|
|
7745
8403
|
|
|
7746
|
-
accumulator
|
|
7747
|
-
accumulator[class_name][name] = call_node.name
|
|
7748
|
-
accumulator.wrote(class_name, name, call_node, toggled: false)
|
|
8404
|
+
accumulator.write(class_name, name, side, visibility, call_node, toggled: false)
|
|
7749
8405
|
end
|
|
7750
8406
|
end
|
|
7751
8407
|
|
|
@@ -7753,6 +8409,135 @@ module Rigor
|
|
|
7753
8409
|
Source::Literals.symbol_or_string(arg)
|
|
7754
8410
|
end
|
|
7755
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
|
+
|
|
8492
|
+
# Issue #1911 — an `attr_reader` / `attr_writer` / `attr_accessor` call written on the body's self records
|
|
8493
|
+
# each member it defines (`x`, `x=`) with `visibility`: the running default for a statement (`toggled`), the
|
|
8494
|
+
# wrapping call's for `private attr_accessor :x`, which Ruby applies to the `[:x, :x=]` the macro returns.
|
|
8495
|
+
# A non-literal name (`attr_reader(*FIELDS)`) records nothing; a call that is no such macro records nothing.
|
|
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?
|
|
8523
|
+
|
|
8524
|
+
class_name = qualified_prefix.join("::")
|
|
8525
|
+
each_attr_macro_name(call_node) do |base|
|
|
8526
|
+
members = []
|
|
8527
|
+
members << base unless macro == :attr_writer
|
|
8528
|
+
members << :"#{base}=" unless macro == :attr_reader
|
|
8529
|
+
members.each do |member|
|
|
8530
|
+
accumulator.write(class_name, member, side, visibility, call_node, toggled: toggled)
|
|
8531
|
+
end
|
|
8532
|
+
end
|
|
8533
|
+
end
|
|
8534
|
+
|
|
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.
|
|
8537
|
+
def attr_visibility_call?(node)
|
|
8538
|
+
!attr_macro(node).nil?
|
|
8539
|
+
end
|
|
8540
|
+
|
|
7756
8541
|
# Registers the alias name in the `discovered_methods` table so `undefined-method` diagnostics are not emitted for
|
|
7757
8542
|
# calls to the aliased name. The kind mirrors the surrounding class context (instance inside a regular class body,
|
|
7758
8543
|
# singleton inside `class << self`).
|
|
@@ -7858,21 +8643,21 @@ module Rigor
|
|
|
7858
8643
|
end
|
|
7859
8644
|
|
|
7860
8645
|
# 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.
|
|
8646
|
+
# singleton envelope opaque and contested: the evidence the refinement typer reads as a rewritten singleton name.
|
|
7862
8647
|
def record_singleton_class_names(node, object, qualified_prefix, tables)
|
|
7863
8648
|
current_class = qualified_prefix.empty? ? nil : qualified_prefix.join("::")
|
|
7864
8649
|
owner = singleton_class_owner(node, object, qualified_prefix, current_class)
|
|
7865
8650
|
return if owner.nil?
|
|
7866
8651
|
|
|
7867
|
-
(node
|
|
7868
|
-
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
names = [method_name]
|
|
8652
|
+
singleton_class_rewritten_names(node).each do |method_name|
|
|
8653
|
+
# `attr_writer :x` defines `x=` alone (#1936), so `x` keeps its own `def`.
|
|
8654
|
+
names = node.name == :attr_writer ? [] : [method_name]
|
|
7872
8655
|
names << :"#{method_name}=" if %i[attr_writer attr_accessor].include?(node.name)
|
|
7873
8656
|
names.each do |name|
|
|
7874
8657
|
record_envelope(tables.envelopes, owner, [:singleton, name], Source::ParameterEnvelope::OPAQUE)
|
|
7875
|
-
|
|
8658
|
+
# Contested even where the call is certain: the slot's `def` is not the body Ruby runs, so the
|
|
8659
|
+
# own-definer read declines on the name rather than answering it.
|
|
8660
|
+
contest_envelope(tables, owner, [:singleton, name])
|
|
7876
8661
|
end
|
|
7877
8662
|
end
|
|
7878
8663
|
end
|
|
@@ -8209,8 +8994,9 @@ module Rigor
|
|
|
8209
8994
|
return if qualified_prefix.empty?
|
|
8210
8995
|
|
|
8211
8996
|
kind = in_singleton_class ? :singleton : :instance
|
|
8212
|
-
|
|
8213
|
-
|
|
8997
|
+
macro = attr_macro(call_node)
|
|
8998
|
+
reader = macro != :attr_writer
|
|
8999
|
+
writer = macro != :attr_reader
|
|
8214
9000
|
class_name = qualified_prefix.join("::")
|
|
8215
9001
|
each_attr_macro_name(call_node) do |base|
|
|
8216
9002
|
record_method(accumulator, class_name, base, kind) if reader
|
|
@@ -8218,18 +9004,34 @@ module Rigor
|
|
|
8218
9004
|
end
|
|
8219
9005
|
end
|
|
8220
9006
|
|
|
8221
|
-
# Yields each name an `attr_*` call declares: a Symbol or String literal argument. A
|
|
8222
|
-
# (`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.
|
|
8223
9009
|
def each_attr_macro_name(call_node)
|
|
8224
|
-
return
|
|
8225
|
-
return if call_node.arguments.nil?
|
|
9010
|
+
return if attr_macro(call_node).nil? || call_node.arguments.nil?
|
|
8226
9011
|
|
|
8227
|
-
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|
|
|
8228
9015
|
base = literal_method_name(arg)
|
|
8229
9016
|
yield base unless base.nil?
|
|
8230
9017
|
end
|
|
8231
9018
|
end
|
|
8232
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
|
+
|
|
8233
9035
|
# Issue #736 — the module-level accessor macros, which introduce methods on BOTH sides of the class.
|
|
8234
9036
|
# ActiveSupport defines them on `Module`, so every class and module body can use them, and Rails codebases
|
|
8235
9037
|
# do: `mattr_accessor` / `cattr_accessor` (an alias of it) and `class_attribute` account for 92 false
|
|
@@ -8351,12 +9153,14 @@ module Rigor
|
|
|
8351
9153
|
# {#discovered_project_index_incremental}'s changed-file branch) yields the live def nodes the signature
|
|
8352
9154
|
# reads their parameter structure from. Issue #1120 — and each file's own refinement table, which the merged
|
|
8353
9155
|
# def-index cannot attribute back to a file, so the session can diff it against the file's seed bundle.
|
|
8354
|
-
#
|
|
9156
|
+
# Issue #1459 — and each file's {#defined_method_names}, diffed the same way.
|
|
9157
|
+
# @return `{ def_index:, code_fingerprints:, declaration_signatures:, refinements:, defined_method_names: }`.
|
|
8355
9158
|
def scan_summary_for_paths(paths, buffer: nil)
|
|
8356
9159
|
acc = new_def_index_accumulator
|
|
8357
9160
|
code_fingerprints = {}
|
|
8358
9161
|
declaration_signatures = {}
|
|
8359
9162
|
refinements = {}
|
|
9163
|
+
defined_method_names = {}
|
|
8360
9164
|
paths.each do |path|
|
|
8361
9165
|
physical = buffer ? buffer.resolve(path) : path
|
|
8362
9166
|
source = File.read(physical)
|
|
@@ -8366,13 +9170,29 @@ module Rigor
|
|
|
8366
9170
|
code_fingerprints[path] = code_fingerprint(source, parsed.comments)
|
|
8367
9171
|
declaration_signatures[path] = declaration_signature(file_index)
|
|
8368
9172
|
refinements[path] = file_index[:refinements] if file_index[:refinements]
|
|
9173
|
+
defined_method_names[path] = defined_method_names(file_index)
|
|
8369
9174
|
rescue DeclarationWalk::ContractError
|
|
8370
9175
|
raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
|
|
8371
9176
|
rescue StandardError
|
|
8372
9177
|
next
|
|
8373
9178
|
end
|
|
8374
9179
|
{ def_index: finalize_def_index(acc), code_fingerprints: code_fingerprints,
|
|
8375
|
-
declaration_signatures: declaration_signatures, refinements: refinements
|
|
9180
|
+
declaration_signatures: declaration_signatures, refinements: refinements,
|
|
9181
|
+
defined_method_names: defined_method_names }
|
|
9182
|
+
end
|
|
9183
|
+
|
|
9184
|
+
# Issue #1459 — every method name one file's index defines, on any class or module and either side: the keys of
|
|
9185
|
+
# `methods` (which also carries an `attr_*`, `alias` or `define_method` name) and of the instance and singleton
|
|
9186
|
+
# def-node tables. A single file's `methods` already holds its `def` names, since `subtract_def_methods` runs only
|
|
9187
|
+
# on the merged accumulator; the def-node tables are read as well so the census reads exactly the tables
|
|
9188
|
+
# `BlockCallTiming.project_defines_anywhere?` reads. Takes a live file index or a seed bundle, which hold the
|
|
9189
|
+
# same three keys.
|
|
9190
|
+
def defined_method_names(file_index)
|
|
9191
|
+
names = Set.new
|
|
9192
|
+
%i[methods def_nodes singleton_def_nodes].each do |key|
|
|
9193
|
+
file_index[key]&.each_value { |table| names.merge(table.keys) }
|
|
9194
|
+
end
|
|
9195
|
+
names.freeze
|
|
8376
9196
|
end
|
|
8377
9197
|
|
|
8378
9198
|
# ADR-89 WD1 — a per-file digest of every cross-file DECLARATION surface an ancestry / file-level
|
|
@@ -8748,7 +9568,7 @@ module Rigor
|
|
|
8748
9568
|
when :contested_discovered_singleton_def_nodes
|
|
8749
9569
|
fold_slot_contests(target.fetch(name), singleton_def_nodes, table)
|
|
8750
9570
|
when :contested_discovered_method_visibilities
|
|
8751
|
-
|
|
9571
|
+
fold_visibility_contests(target.fetch(name), acc[:method_visibilities], method_visibilities, table)
|
|
8752
9572
|
else union_sibling_value(target.fetch(name), table)
|
|
8753
9573
|
end
|
|
8754
9574
|
end
|
|
@@ -8821,6 +9641,30 @@ module Rigor
|
|
|
8821
9641
|
result.merge(file_contested)
|
|
8822
9642
|
end
|
|
8823
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
|
+
|
|
8824
9668
|
def union_paths(sitting, arriving)
|
|
8825
9669
|
return sitting if arriving.empty?
|
|
8826
9670
|
return arriving if sitting.empty?
|
|
@@ -9324,8 +10168,9 @@ module Rigor
|
|
|
9324
10168
|
def merge_class_keyed_index_tables(acc, root, file_methods, certainty)
|
|
9325
10169
|
file_visibilities, file_contested = build_method_visibility_tables(root, certainty: certainty)
|
|
9326
10170
|
siblings = acc[:siblings]
|
|
9327
|
-
siblings[:contested_discovered_method_visibilities] =
|
|
9328
|
-
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
|
|
9329
10174
|
)
|
|
9330
10175
|
file_visibilities.each do |class_name, table|
|
|
9331
10176
|
(acc[:method_visibilities][class_name] ||= {}).merge!(table)
|
|
@@ -10589,8 +11434,15 @@ module Rigor
|
|
|
10589
11434
|
|
|
10590
11435
|
# Issue #1717 — a `def` / `class` / `module` body opens its own `self`, so an enclosing unentered block's
|
|
10591
11436
|
# unknown-`self` mark ({Scope#block_self_unknown?}) stops at it, as the evaluator's fresh body scope does.
|
|
10592
|
-
#
|
|
11437
|
+
# A `case` narrows its clauses ({#propagate_subjectless_case}, {#propagate_subject_case}). Every other node
|
|
11438
|
+
# hands its children the scope as is.
|
|
10593
11439
|
def propagate_children(node, table, current_scope)
|
|
11440
|
+
if node.is_a?(Prism::CaseNode) || node.is_a?(Prism::CaseMatchNode)
|
|
11441
|
+
return propagate_subjectless_case(node, table, current_scope) if node.predicate.nil?
|
|
11442
|
+
|
|
11443
|
+
return propagate_subject_case(node, table, current_scope)
|
|
11444
|
+
end
|
|
11445
|
+
|
|
10594
11446
|
child_scope =
|
|
10595
11447
|
SELF_OPENING_BODIES.include?(node.class) ? current_scope.with_block_self_unknown(false) : current_scope
|
|
10596
11448
|
node.rigor_each_child { |child| propagate(child, table, child_scope) }
|
|
@@ -10783,19 +11635,106 @@ module Rigor
|
|
|
10783
11635
|
scope.local(name).nil? ? scope : scope.with_local(name, Type::Combinator.untyped)
|
|
10784
11636
|
end
|
|
10785
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.
|
|
10786
11642
|
def propagate_if_branches(node, table, current_scope)
|
|
10787
|
-
truthy_scope, falsey_scope =
|
|
11643
|
+
truthy_scope, falsey_scope = conditional_edges(node.predicate, current_scope)
|
|
10788
11644
|
propagate(node.predicate, table, current_scope) if node.predicate
|
|
10789
11645
|
propagate(node.statements, table, truthy_scope) if node.statements
|
|
10790
11646
|
propagate(node.subsequent, table, falsey_scope) if node.subsequent
|
|
10791
11647
|
end
|
|
10792
11648
|
|
|
10793
11649
|
def propagate_unless_branches(node, table, current_scope)
|
|
10794
|
-
truthy_scope, falsey_scope =
|
|
11650
|
+
truthy_scope, falsey_scope = conditional_edges(node.predicate, current_scope)
|
|
10795
11651
|
propagate(node.predicate, table, current_scope) if node.predicate
|
|
10796
11652
|
propagate(node.statements, table, falsey_scope) if node.statements
|
|
10797
11653
|
propagate(node.else_clause, table, truthy_scope) if node.else_clause
|
|
10798
11654
|
end
|
|
11655
|
+
|
|
11656
|
+
def conditional_edges(predicate, scope)
|
|
11657
|
+
Narrowing.predicate_scopes(predicate, widen_condition_writes(predicate, scope))
|
|
11658
|
+
end
|
|
11659
|
+
|
|
11660
|
+
# Issue #1495 — a `case` without a subject the evaluator did not enter (`[case; when x.nil? then 0; else x.size
|
|
11661
|
+
# end]`) narrows by its conditions' edges, as {StatementEvaluator#subjectless_when_scopes} does: each condition
|
|
11662
|
+
# under the falsey edges of the conditions before it, each clause under the join of its conditions' truthy edges,
|
|
11663
|
+
# and the `else` under the last falsey edge. Like the walk of an `if`, it applies no write a condition makes, so
|
|
11664
|
+
# the edges are taken from a scope where every bound local the condition writes reads `Dynamic[top]`
|
|
11665
|
+
# ({#widen_condition_writes}): neither the local's old type nor an earlier condition's narrowing of it reaches
|
|
11666
|
+
# them, as `when (x = s ? String(s) : nil) then x.upcase` after `x = 1` would.
|
|
11667
|
+
def propagate_subjectless_case(node, table, current_scope)
|
|
11668
|
+
falsey = current_scope
|
|
11669
|
+
node.conditions.each do |when_node|
|
|
11670
|
+
truthy_edges = when_node.conditions.map do |condition|
|
|
11671
|
+
propagate(condition, table, falsey)
|
|
11672
|
+
truthy, falsey = Narrowing.predicate_scopes(condition, widen_condition_writes(condition, falsey))
|
|
11673
|
+
truthy
|
|
11674
|
+
end
|
|
11675
|
+
propagate(when_node, table, truthy_edges.reduce(:join))
|
|
11676
|
+
end
|
|
11677
|
+
propagate(node.else_clause, table, falsey) if node.else_clause
|
|
11678
|
+
end
|
|
11679
|
+
|
|
11680
|
+
# Issue #1484 — a `case` with a subject the evaluator did not enter (`[case io when StringIO then io.string
|
|
11681
|
+
# end]`, `[case io in StringIO then io.string else nil end]`) narrows each clause as the evaluator does
|
|
11682
|
+
# ({Narrowing.case_clause_scopes}): the body under the clause's narrowed edge, the next clause and the `else`
|
|
11683
|
+
# under its falsey edge. The walk applies no write the subject, a `when` condition or an `in` pattern makes, so
|
|
11684
|
+
# every bound local one of them writes reads `Dynamic[top]` from that point on ({#widen_condition_writes}).
|
|
11685
|
+
# Nor does it run the evaluator's per-clause `LastLine.forget_if_set`: the statement holding the `case`
|
|
11686
|
+
# already forgets a `$_` any of its tests may set. A clause's tests are recorded under the scope the earlier
|
|
11687
|
+
# clauses' falsey edges left, where the evaluator's `when` tests read the post-subject scope: a test reading
|
|
11688
|
+
# the subject sees what the earlier clauses ruled out (`when NilClass then 0 when v.succ`).
|
|
11689
|
+
#
|
|
11690
|
+
# A test that calls a method may rebind an instance-variable or global subject before the body reads it
|
|
11691
|
+
# (`when String then 1 when reset! then @x.upcase`, where `reset!` writes `@x`), and the walk cannot tell. From
|
|
11692
|
+
# the first such test on, the subject reads `Dynamic[top]` on both edges ({#forget_called_subject}). The
|
|
11693
|
+
# evaluator keeps the narrowing there (a separate defect); the walk declines it rather than add a report.
|
|
11694
|
+
def propagate_subject_case(node, table, current_scope)
|
|
11695
|
+
subject = node.predicate
|
|
11696
|
+
propagate(subject, table, current_scope)
|
|
11697
|
+
falsey = widen_condition_writes(subject, current_scope)
|
|
11698
|
+
called = false
|
|
11699
|
+
node.conditions.each do |clause|
|
|
11700
|
+
tests = clause.is_a?(Prism::WhenNode) ? clause.conditions : [clause.pattern]
|
|
11701
|
+
tests.each { |test| propagate(test, table, falsey) }
|
|
11702
|
+
falsey = tests.reduce(falsey) { |acc, test| widen_condition_writes(test, acc) }
|
|
11703
|
+
called ||= tests.any? { |test| runs_call?(test) }
|
|
11704
|
+
body, falsey = Narrowing.case_clause_scopes(subject, clause, falsey)
|
|
11705
|
+
body, falsey = [body, falsey].map { |scope| forget_called_subject(subject, scope) } if called
|
|
11706
|
+
propagate(clause, table, body)
|
|
11707
|
+
end
|
|
11708
|
+
propagate(node.else_clause, table, falsey) if node.else_clause
|
|
11709
|
+
end
|
|
11710
|
+
|
|
11711
|
+
CODE_RUNNING_NODES = [Prism::CallNode, Prism::SuperNode, Prism::ForwardingSuperNode, Prism::YieldNode].freeze
|
|
11712
|
+
private_constant :CODE_RUNNING_NODES
|
|
11713
|
+
|
|
11714
|
+
# Whether `node` holds a method call, `super` or `yield` anywhere, a lambda body's included.
|
|
11715
|
+
def runs_call?(node)
|
|
11716
|
+
return false unless node.is_a?(Prism::Node)
|
|
11717
|
+
return true if CODE_RUNNING_NODES.include?(node.class)
|
|
11718
|
+
|
|
11719
|
+
node.rigor_each_child { |child| return true if runs_call?(child) }
|
|
11720
|
+
false
|
|
11721
|
+
end
|
|
11722
|
+
|
|
11723
|
+
# `scope` with an instance-variable or global `subject` rebound to `Dynamic[top]`; any other subject as is.
|
|
11724
|
+
def forget_called_subject(subject, scope)
|
|
11725
|
+
case subject
|
|
11726
|
+
when Prism::InstanceVariableReadNode then scope.with_ivar(subject.name, Type::Combinator.untyped)
|
|
11727
|
+
when Prism::GlobalVariableReadNode then scope.with_global(subject.name, Type::Combinator.untyped)
|
|
11728
|
+
else scope
|
|
11729
|
+
end
|
|
11730
|
+
end
|
|
11731
|
+
|
|
11732
|
+
# `scope` with every bound local `condition` writes rebound to `Dynamic[top]`.
|
|
11733
|
+
def widen_condition_writes(condition, scope)
|
|
11734
|
+
OperandEffects.written_variables(condition).reduce(scope) do |acc, name|
|
|
11735
|
+
acc.local(name) ? acc.with_local(name, Type::Combinator.untyped) : acc
|
|
11736
|
+
end
|
|
11737
|
+
end
|
|
10799
11738
|
end
|
|
10800
11739
|
end
|
|
10801
11740
|
end
|