rigortype 0.4.7 → 0.4.8
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 +3 -1
- data/data/core_overlay/bigdecimal.rbs +33 -0
- data/data/core_overlay/resolv.rbs +30 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +29 -0
- data/data/gem_overlay/addressable/addressable.rbs +40 -0
- data/docs/handbook/01-getting-started.md +1 -1
- data/docs/handbook/11-sig-gen.md +35 -0
- data/docs/install.md +20 -0
- data/docs/manual/01-installation.md +67 -26
- data/docs/manual/02-cli-reference.md +307 -91
- data/docs/manual/03-configuration.md +41 -3
- data/docs/manual/04-diagnostics.md +5 -4
- data/docs/manual/08-skills.md +11 -0
- data/docs/manual/09-editor-integration.md +8 -0
- data/docs/manual/14-rails-quickstart.md +1 -1
- data/docs/manual/19-effect-labels.md +3 -4
- data/docs/manual/plugins/rigor-actionpack.md +15 -6
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +3 -0
- data/docs/manual/plugins/rigor-sorbet.md +17 -3
- data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
- data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +8 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +13 -2
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -0
- data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +40 -11
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
- data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +8 -1
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules.rb +598 -99
- data/lib/rigor/analysis/crash_signature.rb +43 -1
- data/lib/rigor/analysis/dependency_recorder.rb +233 -76
- data/lib/rigor/analysis/incremental.rb +11 -1
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -1
- data/lib/rigor/analysis/incremental_session.rb +279 -37
- data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
- data/lib/rigor/analysis/rule_catalog.rb +37 -1
- data/lib/rigor/analysis/run_cache_key.rb +27 -3
- data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +4 -0
- data/lib/rigor/analysis/runner/declaration_position.rb +7 -7
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +72 -20
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +3 -8
- data/lib/rigor/analysis/runner/pool_coordinator.rb +26 -5
- data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
- data/lib/rigor/analysis/runner.rb +222 -39
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +61 -22
- data/lib/rigor/analysis/worker_session.rb +42 -41
- data/lib/rigor/bleeding_edge.rb +19 -4
- data/lib/rigor/cache/file_digest.rb +39 -31
- data/lib/rigor/cache/incremental_snapshot.rb +66 -7
- data/lib/rigor/cache/rbs_descriptor.rb +37 -2
- data/lib/rigor/cache/rbs_environment.rb +5 -1
- data/lib/rigor/cli/baseline_command.rb +15 -2
- data/lib/rigor/cli/check_command.rb +4 -2
- data/lib/rigor/cli/effects_snapshot_command.rb +8 -1
- data/lib/rigor/cli/lens_command.rb +17 -2
- data/lib/rigor/cli/sig_gen_command.rb +8 -0
- data/lib/rigor/cli/skill_describe.rb +1 -0
- data/lib/rigor/cli.rb +5 -2
- data/lib/rigor/config_audit.rb +32 -18
- data/lib/rigor/configuration/severity_profile.rb +9 -0
- data/lib/rigor/configuration.rb +32 -5
- data/lib/rigor/effects/collector.rb +53 -21
- data/lib/rigor/environment/bundle_sig_discovery.rb +63 -28
- data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
- data/lib/rigor/environment/default_libraries.rb +17 -0
- data/lib/rigor/environment/deferred_signature_plan.rb +104 -6
- data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
- data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
- data/lib/rigor/environment/rbs_hierarchy.rb +6 -0
- data/lib/rigor/environment/rbs_loader.rb +312 -63
- data/lib/rigor/environment.rb +131 -35
- data/lib/rigor/inference/acceptance.rb +50 -15
- data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
- data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
- data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
- data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
- data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
- data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
- data/lib/rigor/inference/block_auto_splat.rb +1 -1
- data/lib/rigor/inference/block_run_count.rb +9 -2
- data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
- data/lib/rigor/inference/callee_mutation_chain.rb +1 -1
- data/lib/rigor/inference/captured_locals.rb +2 -0
- data/lib/rigor/inference/census_foreign_block.rb +354 -0
- data/lib/rigor/inference/clause_widening.rb +1 -1
- data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
- data/lib/rigor/inference/construction_escape.rb +56 -15
- data/lib/rigor/inference/declaration_walk.rb +4 -0
- data/lib/rigor/inference/def_node_resolver.rb +32 -14
- data/lib/rigor/inference/def_return_typer.rb +1 -1
- data/lib/rigor/inference/definer_resolution.rb +15 -7
- data/lib/rigor/inference/definers_index.rb +72 -16
- data/lib/rigor/inference/error_info.rb +16 -0
- data/lib/rigor/inference/expression_typer.rb +1459 -287
- data/lib/rigor/inference/flow_tracer.rb +31 -14
- data/lib/rigor/inference/fork_map.rb +3 -0
- data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
- data/lib/rigor/inference/global_write_census.rb +8 -6
- data/lib/rigor/inference/in_effect_refinements.rb +7 -0
- data/lib/rigor/inference/indexed_narrowing.rb +57 -8
- data/lib/rigor/inference/ivar_writer_census.rb +106 -11
- data/lib/rigor/inference/key_presence_guard.rb +51 -25
- data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
- data/lib/rigor/inference/last_line.rb +5 -2
- data/lib/rigor/inference/loop_value_record.rb +63 -15
- data/lib/rigor/inference/match_rebinding/frame.rb +11 -0
- data/lib/rigor/inference/match_rebinding/yielding_match.rb +74 -18
- data/lib/rigor/inference/match_rebinding.rb +21 -8
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +43 -29
- data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
- data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
- data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
- data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +12 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +409 -67
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +384 -64
- data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
- data/lib/rigor/inference/method_dispatcher.rb +66 -18
- data/lib/rigor/inference/method_parameter_binder.rb +136 -56
- data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
- data/lib/rigor/inference/module_function_state.rb +13 -2
- data/lib/rigor/inference/multi_target_binder.rb +1 -1
- data/lib/rigor/inference/mutation_widening.rb +44 -4
- data/lib/rigor/inference/narrowing.rb +201 -27
- data/lib/rigor/inference/operand_effects.rb +43 -2
- data/lib/rigor/inference/optimistic_origin.rb +133 -8
- data/lib/rigor/inference/origin_lookup.rb +1 -1
- data/lib/rigor/inference/precision_scanner.rb +1 -1
- data/lib/rigor/inference/project_method_ownership.rb +28 -10
- data/lib/rigor/inference/project_patched_methods.rb +65 -13
- data/lib/rigor/inference/project_patched_scanner.rb +120 -25
- data/lib/rigor/inference/project_patched_visibility.rb +39 -3
- data/lib/rigor/inference/rbs_type_translator.rb +68 -15
- data/lib/rigor/inference/receiver_writer_call.rb +67 -0
- data/lib/rigor/inference/recursion_rebinding.rb +157 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +56 -18
- data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -17
- data/lib/rigor/inference/refined_dispatch.rb +13 -0
- data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
- data/lib/rigor/inference/remover_holder_scan.rb +76 -22
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +12 -11
- data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +188 -28
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
- data/lib/rigor/inference/scope_indexer.rb +2079 -183
- data/lib/rigor/inference/self_call_ivar_writes.rb +26 -7
- data/lib/rigor/inference/self_preserving_block.rb +87 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +41 -6
- data/lib/rigor/inference/singleton_object_constant.rb +32 -2
- data/lib/rigor/inference/statement_evaluator.rb +970 -250
- data/lib/rigor/inference/string_mutation.rb +14 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +35 -11
- data/lib/rigor/inference/toplevel_def_census.rb +14 -6
- data/lib/rigor/inference/unknown_store_widening.rb +1 -1
- data/lib/rigor/inference/untyped_rejoin.rb +1 -1
- data/lib/rigor/inference/void_tail_summary.rb +29 -10
- data/lib/rigor/lens/claims/comparison.rb +78 -0
- data/lib/rigor/lens/claims/request.rb +131 -0
- data/lib/rigor/lens/claims/type_reader.rb +216 -0
- data/lib/rigor/lens/claims.rb +208 -0
- data/lib/rigor/lens/contract_reader.rb +5 -0
- data/lib/rigor/lens/document.rb +1 -1
- data/lib/rigor/lens/file_reader.rb +21 -8
- data/lib/rigor/lens/run.rb +8 -0
- data/lib/rigor/lens.rb +1 -1
- data/lib/rigor/library_probe.rb +68 -0
- data/lib/rigor/memo_audit.rb +282 -0
- data/lib/rigor/plugin/base.rb +15 -2
- data/lib/rigor/plugin/io_boundary.rb +25 -3
- data/lib/rigor/plugin/manifest.rb +7 -3
- data/lib/rigor/plugin/registry.rb +13 -2
- data/lib/rigor/plugin/trust_policy.rb +46 -0
- data/lib/rigor/protection/mutation_cache.rb +4 -1
- data/lib/rigor/reflection/constant_ancestors.rb +79 -22
- data/lib/rigor/reflection/declaration_collectors.rb +55 -0
- data/lib/rigor/reflection/declarations.rb +58 -0
- data/lib/rigor/reflection/project_method.rb +177 -0
- data/lib/rigor/reflection.rb +2 -0
- data/lib/rigor/rooted_glob.rb +18 -1
- data/lib/rigor/run_scoped_slots.rb +20 -20
- data/lib/rigor/scope/discovery_index.rb +149 -2
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
- data/lib/rigor/scope/resolution_chain.rb +900 -55
- data/lib/rigor/scope.rb +307 -34
- data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
- data/lib/rigor/sig_gen/classification.rb +14 -1
- data/lib/rigor/sig_gen/generator.rb +222 -13
- data/lib/rigor/sig_gen/method_candidate.rb +14 -0
- data/lib/rigor/sig_gen/observation_collector.rb +11 -2
- data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +47 -0
- data/lib/rigor/sig_gen/writer.rb +21 -0
- data/lib/rigor/sig_gen.rb +1 -0
- data/lib/rigor/source/forwardable_delegators.rb +122 -0
- data/lib/rigor/source/node_children.rb +13 -0
- data/lib/rigor/type/combinator.rb +164 -8
- data/lib/rigor/type/interface.rb +60 -0
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_of_memo/serving.rb +99 -0
- data/lib/rigor/type_of_memo/stats.rb +43 -0
- data/lib/rigor/type_of_memo.rb +337 -0
- data/lib/rigor/typing_state.rb +1094 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +2 -2
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +57 -25
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +130 -30
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -5
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +49 -13
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +4 -2
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +13 -5
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +29 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -2
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +6 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +199 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +3 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +4 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +209 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +80 -3
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +1 -6
- data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
- data/sig/rigor/analysis/fact_store.rbs +2 -3
- data/sig/rigor/cache/descriptor.rbs +14 -0
- data/sig/rigor/environment.rbs +4 -1
- data/sig/rigor/plugin/base.rbs +53 -19
- data/sig/rigor/plugin/interfaces.rbs +51 -0
- data/sig/rigor/plugin/io_boundary.rbs +2 -3
- data/sig/rigor/plugin/node_context.rbs +21 -0
- data/sig/rigor/reflection.rbs +37 -0
- data/sig/rigor/scope.rbs +22 -3
- data/sig/rigor/trinary.rbs +1 -0
- data/sig/rigor/type.rbs +9 -3
- data/sig/rigor.rbs +3 -2
- data/skills/rigor-ask/SKILL.md +1 -0
- data/skills/rigor-project-init/references/01-detect.md +1 -0
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +5 -1
- data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
- data/skills/rigor-rbs-contract/SKILL.md +147 -0
- data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
- data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
- data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
- data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
- data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
- data/skills/rigor-rbs-setup/SKILL.md +3 -1
- data/skills/rigor-type-oracle/SKILL.md +26 -1
- data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
- metadata +48 -2
- data/lib/rigor/inference/memo_audit.rb +0 -55
|
@@ -5,6 +5,7 @@ require "prism"
|
|
|
5
5
|
require_relative "../scope"
|
|
6
6
|
require_relative "../type"
|
|
7
7
|
require_relative "../source/alias_names"
|
|
8
|
+
require_relative "../source/forwardable_delegators"
|
|
8
9
|
require_relative "../source/literals"
|
|
9
10
|
require_relative "../source/constant_path"
|
|
10
11
|
require_relative "object_mixins"
|
|
@@ -12,8 +13,11 @@ require_relative "../source/node_children"
|
|
|
12
13
|
require_relative "../source/node_walker"
|
|
13
14
|
require_relative "../source/parameter_envelope"
|
|
14
15
|
require_relative "../cache/file_digest"
|
|
16
|
+
require_relative "../typing_state"
|
|
17
|
+
require_relative "../memo_audit"
|
|
15
18
|
require_relative "../analysis/check_rules/published_constant_guard"
|
|
16
19
|
require_relative "anonymous_meta_class"
|
|
20
|
+
require_relative "census_foreign_block"
|
|
17
21
|
require_relative "def_handle"
|
|
18
22
|
require_relative "definers_index"
|
|
19
23
|
require_relative "fresh_frame_blocks"
|
|
@@ -26,6 +30,7 @@ require_relative "hash_lookup_mutation"
|
|
|
26
30
|
require_relative "in_effect_refinements"
|
|
27
31
|
require_relative "refine_census"
|
|
28
32
|
require_relative "refine_self"
|
|
33
|
+
require_relative "receiver_writer_call"
|
|
29
34
|
require_relative "reflective_ivar_write"
|
|
30
35
|
require_relative "index_write_widening"
|
|
31
36
|
require_relative "module_function_state"
|
|
@@ -228,13 +233,14 @@ module Rigor
|
|
|
228
233
|
# nodes in `excluded`: the file's own pass, and each construct {#evaluate_unentered_flow} evaluates in the walk
|
|
229
234
|
# after it.
|
|
230
235
|
def recording_evaluator(table, converged_loop_recording, widen_clauses)
|
|
231
|
-
lambda do |node, scope, excluded = nil|
|
|
236
|
+
lambda do |node, scope, excluded = nil, block_only: false|
|
|
232
237
|
on_enter =
|
|
233
238
|
if excluded.nil? || excluded.empty? then ->(visited, at) { table[visited] = at }
|
|
234
239
|
else ->(visited, at) { table[visited] = at unless excluded.include?(visited) }
|
|
235
240
|
end
|
|
236
|
-
StatementEvaluator.new(scope: scope, on_enter: on_enter,
|
|
237
|
-
|
|
241
|
+
evaluator = StatementEvaluator.new(scope: scope, on_enter: on_enter, widen_clauses: widen_clauses,
|
|
242
|
+
converged_loop_recording: converged_loop_recording)
|
|
243
|
+
block_only ? evaluator.evaluate_block_of(node) : evaluator.evaluate(node)
|
|
238
244
|
end
|
|
239
245
|
end
|
|
240
246
|
|
|
@@ -393,8 +399,9 @@ module Rigor
|
|
|
393
399
|
default_scope.discovered_includes.merge(file[:includes]) do |_class, cross_file, per_file|
|
|
394
400
|
(per_file + cross_file).uniq
|
|
395
401
|
end,
|
|
402
|
+
# ADR-129 G4 — each merged list is frozen as the per-file walk's lists are ({#freeze_mixin_lists}).
|
|
396
403
|
default_scope.discovered_prepends.merge(file[:prepends]) do |_class, cross_file, per_file|
|
|
397
|
-
(per_file + cross_file).uniq
|
|
404
|
+
(per_file + cross_file).uniq.freeze
|
|
398
405
|
end,
|
|
399
406
|
file[:unpositioned]
|
|
400
407
|
]
|
|
@@ -569,12 +576,13 @@ module Rigor
|
|
|
569
576
|
read_before_write, init_writes, method_assign_effects)
|
|
570
577
|
accumulator.delete(CONSTANT_IVAR_VALUES)
|
|
571
578
|
reflective_opaque = accumulator.delete(REFLECTIVE_OPAQUE_CLASSES)
|
|
579
|
+
singleton_writes = accumulator.delete(SINGLETON_IVAR_WRITES)
|
|
572
580
|
exempt_class_body_writes!(init_writes, accumulator.delete(DEFINE_METHOD_WRITES) || {})
|
|
573
581
|
merge_held_ivar_writes!(accumulator)
|
|
574
582
|
record_aliased_ivar_mutations!(root, mutated_ivars)
|
|
575
583
|
widen_mutated_ivar_entries!(accumulator, mutated_ivars)
|
|
576
584
|
# #2170 — an ancestor's writes as this file's census types them, before its read-before-write `nil`.
|
|
577
|
-
writes = accumulator
|
|
585
|
+
writes = instance_side_writes(accumulator, singleton_writes)
|
|
578
586
|
contribute_read_before_write_nil!(accumulator, read_before_write, init_writes)
|
|
579
587
|
make_reflective_classes_gradual!(accumulator, reflective_opaque)
|
|
580
588
|
# #2079 / #2170 — another file's writes, and the chain's modules' and superclasses', join existing entries.
|
|
@@ -586,6 +594,17 @@ module Rigor
|
|
|
586
594
|
accumulator.transform_values(&:freeze).freeze
|
|
587
595
|
end
|
|
588
596
|
|
|
597
|
+
# #2170 — a copy of the census entries for {IvarContributions} to join as an ancestor's own. #2931 — an ivar a
|
|
598
|
+
# singleton-side `def` of the class writes is left out: its entry joins that write, which runs on the module or
|
|
599
|
+
# class object and never on an includer's instance, so the join reads the file's instance-side census entries
|
|
600
|
+
# for it, as another file's reader does.
|
|
601
|
+
def instance_side_writes(accumulator, singleton_writes)
|
|
602
|
+
accumulator.to_h do |class_name, entries|
|
|
603
|
+
skipped = singleton_writes&.[](class_name)
|
|
604
|
+
[class_name, skipped ? entries.except(*skipped) : entries.dup]
|
|
605
|
+
end
|
|
606
|
+
end
|
|
607
|
+
|
|
589
608
|
# Folds the class-body writes {#collect_class_body_ivar_writes} held under {CLASS_BODY_WRITES} into the
|
|
590
609
|
# `init_writes` exemption, except for an ivar a `define_method` block, or a `block_as_methods:` block bound to the
|
|
591
610
|
# receiver's instance ({#method_body_block_call?}), of the class writes (#1768). Such an ivar had no census entry
|
|
@@ -949,6 +968,7 @@ module Rigor
|
|
|
949
968
|
collect_initializer_block_ivars(node, owner, default_scope, accumulator, mutated_ivars, init_writes)
|
|
950
969
|
record_attr_writer_ivars(node, owner, accumulator) if ATTR_WRITER_MACROS.include?(attr_macro(node))
|
|
951
970
|
collect_define_method_block_ivars(node, owner, default_scope, accumulator, mutated_ivars)
|
|
971
|
+
collect_receiver_writer_ivars(node, owner, qualified_prefix, default_scope, accumulator, mutated_ivars)
|
|
952
972
|
false
|
|
953
973
|
else
|
|
954
974
|
walk_ivars_meta_new?(node, qualified_prefix, default_scope, accumulator,
|
|
@@ -1077,6 +1097,8 @@ module Rigor
|
|
|
1077
1097
|
read_before_write = nil, init_writes = nil, method_assign_effects = nil,
|
|
1078
1098
|
singleton_self: false)
|
|
1079
1099
|
return if def_node.body.nil? || qualified_prefix.empty?
|
|
1100
|
+
# Issue #3155 — `def String.q` writes the ivars of another object.
|
|
1101
|
+
return if foreign_def?(def_node, qualified_prefix, false)
|
|
1080
1102
|
|
|
1081
1103
|
class_name = qualified_prefix.join("::")
|
|
1082
1104
|
singleton = singleton_self || def_node.receiver.is_a?(Prism::SelfNode) ||
|
|
@@ -1103,6 +1125,10 @@ module Rigor
|
|
|
1103
1125
|
declared = DeclaredParamWrites.collect(def_node, class_name, body_scope) unless singleton
|
|
1104
1126
|
gather_ivar_writes(def_node.body, body_scope, class_name, accumulator,
|
|
1105
1127
|
EMPTY_GUARDED_IVARS, mutated_ivars, dead_writes, declared)
|
|
1128
|
+
if singleton
|
|
1129
|
+
singleton_written = (accumulator[SINGLETON_IVAR_WRITES] ||= {})[class_name] ||= Set.new
|
|
1130
|
+
define_method_written_ivars(def_node.body, singleton_written)
|
|
1131
|
+
end
|
|
1106
1132
|
|
|
1107
1133
|
# B2.3 — collect per-method evidence for the read-before- write nil contribution. The accumulator-level decision
|
|
1108
1134
|
# ("is this ivar truly read-before-write across the class lifetime?") is finalised at
|
|
@@ -1129,7 +1155,7 @@ module Rigor
|
|
|
1129
1155
|
|
|
1130
1156
|
seen_writes = Set.new
|
|
1131
1157
|
read_first = Set.new
|
|
1132
|
-
detect_read_before_write(block_node.body, seen_writes, read_first)
|
|
1158
|
+
detect_read_before_write(block_node.body, seen_writes, read_first, class_name)
|
|
1133
1159
|
init_set = (init_writes[class_name] ||= Set.new)
|
|
1134
1160
|
seen_writes.each { |name| init_set << name }
|
|
1135
1161
|
end
|
|
@@ -1192,6 +1218,29 @@ module Rigor
|
|
|
1192
1218
|
define_method_written_ivars(block.body, written)
|
|
1193
1219
|
end
|
|
1194
1220
|
|
|
1221
|
+
# Issue #2488 — the explicit-receiver arm of {#walk_class_ivars}: `E.attr_writer :w` (or through `send`) records
|
|
1222
|
+
# `@w` as `Dynamic[top]` on `E`, as an `attr_writer` in `E`'s body does ({#record_attr_writer_ivars}), and an
|
|
1223
|
+
# `E.define_method(:m) { … }` block is walked as an instance body of `E`, as one in `E`'s body is
|
|
1224
|
+
# ({#collect_define_method_block_ivars}). `E` resolves as a `class_eval` receiver does; an unnameable one
|
|
1225
|
+
# contributes nothing. A call inside a `def` is that body's to walk.
|
|
1226
|
+
def collect_receiver_writer_ivars(node, owner, qualified_prefix, default_scope, accumulator, mutated_ivars)
|
|
1227
|
+
return if ReceiverWriterCall.receiver(node).nil?
|
|
1228
|
+
|
|
1229
|
+
target = eval_receiver_prefix(node, owner, qualified_prefix, unnameable_self: owner.empty?)
|
|
1230
|
+
return if target.nil? || target.empty?
|
|
1231
|
+
|
|
1232
|
+
class_name = target.join("::")
|
|
1233
|
+
ReceiverWriterCall.writer_ivars(node).each do |ivar|
|
|
1234
|
+
accumulate_ivar_type(accumulator, class_name, ivar, Type::Combinator.untyped)
|
|
1235
|
+
end
|
|
1236
|
+
body = ReceiverWriterCall.define_method_block(node)&.body
|
|
1237
|
+
return if body.nil?
|
|
1238
|
+
|
|
1239
|
+
body_scope = census_body_scope(default_scope, target, Type::Combinator.nominal_of(class_name))
|
|
1240
|
+
gather_ivar_writes(body, body_scope, class_name, accumulator, EMPTY_GUARDED_IVARS, mutated_ivars)
|
|
1241
|
+
define_method_written_ivars(body, (accumulator[DEFINE_METHOD_WRITES] ||= {})[class_name] ||= Set.new)
|
|
1242
|
+
end
|
|
1243
|
+
|
|
1195
1244
|
# The ivars a `define_method` block body writes, in any write form, outside a nested `def` / `class` / `module`.
|
|
1196
1245
|
def define_method_written_ivars(node, acc)
|
|
1197
1246
|
return unless node.is_a?(Prism::Node)
|
|
@@ -1237,7 +1286,7 @@ module Rigor
|
|
|
1237
1286
|
|
|
1238
1287
|
seen_writes = Set.new
|
|
1239
1288
|
read_first = Set.new
|
|
1240
|
-
detect_read_before_write(def_node.body, seen_writes, read_first)
|
|
1289
|
+
detect_read_before_write(def_node.body, seen_writes, read_first, class_name)
|
|
1241
1290
|
|
|
1242
1291
|
# ADR-38 — `initialize` is the built-in initializer gate; a plugin may declare additional `def`-form initializer
|
|
1243
1292
|
# methods (minitest `setup`, Rails `after_initialize`, DI setters) on a constrained class. Both fold their
|
|
@@ -1352,7 +1401,7 @@ module Rigor
|
|
|
1352
1401
|
)
|
|
1353
1402
|
end
|
|
1354
1403
|
|
|
1355
|
-
def detect_read_before_write(node, seen_writes, read_first)
|
|
1404
|
+
def detect_read_before_write(node, seen_writes, read_first, class_name = nil)
|
|
1356
1405
|
return unless node.is_a?(Prism::Node)
|
|
1357
1406
|
return if IVAR_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
|
|
1358
1407
|
|
|
@@ -1366,16 +1415,17 @@ module Rigor
|
|
|
1366
1415
|
# target is committed, so descend into `value` FIRST (an ivar read there is read-before-write), then mark every
|
|
1367
1416
|
# ivar target as written.
|
|
1368
1417
|
if node.is_a?(Prism::MultiWriteNode)
|
|
1369
|
-
detect_read_before_write(node.value, seen_writes, read_first) if node.value
|
|
1370
|
-
detect_multi_write_target_writes(node, seen_writes, read_first)
|
|
1418
|
+
detect_read_before_write(node.value, seen_writes, read_first, class_name) if node.value
|
|
1419
|
+
detect_multi_write_target_writes(node, seen_writes, read_first, class_name)
|
|
1371
1420
|
return
|
|
1372
1421
|
end
|
|
1373
1422
|
|
|
1374
1423
|
# Descend BEFORE recording a write — `@x = @x + 1`'s RHS is an `InstanceVariableReadNode` that runs before the
|
|
1375
1424
|
# write is committed; the read is therefore read-before-write semantically. `each_child` yields the value
|
|
1376
1425
|
# child before the lvalue target (`compact_child_nodes` field order), matching this order.
|
|
1377
|
-
|
|
1378
|
-
|
|
1426
|
+
# #2490 — what a block whose `self` is another object reads or writes is that object's.
|
|
1427
|
+
each_census_child(node, class_name) do |c|
|
|
1428
|
+
detect_read_before_write(c, seen_writes, read_first, class_name)
|
|
1379
1429
|
end
|
|
1380
1430
|
|
|
1381
1431
|
seen_writes << node.name if IVAR_WRITE_NODES.any? { |klass| node.is_a?(klass) }
|
|
@@ -1387,19 +1437,19 @@ module Rigor
|
|
|
1387
1437
|
# Records each ivar target of a `MultiWriteNode` / nested `MultiTargetNode` into `seen_writes`, and descends into
|
|
1388
1438
|
# any non-ivar target (a `CallTargetNode` / `IndexTargetNode` receiver such as `@obj.x, @y = …`) so an ivar read
|
|
1389
1439
|
# inside a target receiver still counts as read-before-write.
|
|
1390
|
-
def detect_multi_write_target_writes(node, seen_writes, read_first)
|
|
1440
|
+
def detect_multi_write_target_writes(node, seen_writes, read_first, class_name = nil)
|
|
1391
1441
|
targets = (node.lefts || []) + [node.rest].compact + (node.rights || [])
|
|
1392
1442
|
targets.each do |target|
|
|
1393
1443
|
case target
|
|
1394
1444
|
when Prism::InstanceVariableTargetNode
|
|
1395
1445
|
seen_writes << target.name
|
|
1396
1446
|
when Prism::MultiTargetNode
|
|
1397
|
-
detect_multi_write_target_writes(target, seen_writes, read_first)
|
|
1447
|
+
detect_multi_write_target_writes(target, seen_writes, read_first, class_name)
|
|
1398
1448
|
when Prism::SplatNode
|
|
1399
1449
|
inner = target.expression
|
|
1400
1450
|
seen_writes << inner.name if inner.is_a?(Prism::InstanceVariableTargetNode)
|
|
1401
1451
|
else
|
|
1402
|
-
detect_read_before_write(target, seen_writes, read_first)
|
|
1452
|
+
detect_read_before_write(target, seen_writes, read_first, class_name)
|
|
1403
1453
|
end
|
|
1404
1454
|
end
|
|
1405
1455
|
end
|
|
@@ -1407,6 +1457,21 @@ module Rigor
|
|
|
1407
1457
|
IVAR_BARRIER_NODES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode].freeze
|
|
1408
1458
|
private_constant :IVAR_BARRIER_NODES
|
|
1409
1459
|
|
|
1460
|
+
# Issue #2490 — yields the children of `node` a census walk for `class_name` descends into: all of them, or only
|
|
1461
|
+
# those {CensusForeignBlock.census_children} keeps when `node` runs code whose `self` is another object. A nil
|
|
1462
|
+
# `class_name` keeps them all. A reentered `def` (#2941) is yielded as itself, so a walk that stops at a `def`
|
|
1463
|
+
# skips it; `def_bodies:` yields its body instead, for a walk that counts its writes. `reentry: false` (a
|
|
1464
|
+
# singleton-side `def`) keeps none of the reentered code.
|
|
1465
|
+
def each_census_child(node, class_name, def_bodies: false, reentry: true, &)
|
|
1466
|
+
outside = class_name && CensusForeignBlock.census_children(node, class_name, reentry: reentry)
|
|
1467
|
+
return node.rigor_each_child(&) unless outside
|
|
1468
|
+
|
|
1469
|
+
outside.each do |child|
|
|
1470
|
+
child = child.body if def_bodies && child.is_a?(Prism::DefNode)
|
|
1471
|
+
yield child if child
|
|
1472
|
+
end
|
|
1473
|
+
end
|
|
1474
|
+
|
|
1410
1475
|
EMPTY_GUARDED_IVARS = Set.new.freeze
|
|
1411
1476
|
|
|
1412
1477
|
# Shared empty result for a def that returns no ivar — the overwhelmingly common case.
|
|
@@ -1440,6 +1505,7 @@ module Rigor
|
|
|
1440
1505
|
# array/tuple-shaped, else the unanalyzable floor (the same `Dynamic[top]` a single write to an unknown RHS
|
|
1441
1506
|
# records — an unanalyzable multi-write means unknown, not nil).
|
|
1442
1507
|
record_multi_write_ivars(node, scope, class_name, accumulator)
|
|
1508
|
+
record_binding_target_ivars(node, scope, class_name, accumulator)
|
|
1443
1509
|
|
|
1444
1510
|
record_ivar_mutator_call(node, class_name, mutated_ivars) if mutated_ivars
|
|
1445
1511
|
|
|
@@ -1452,7 +1518,10 @@ module Rigor
|
|
|
1452
1518
|
return
|
|
1453
1519
|
end
|
|
1454
1520
|
|
|
1455
|
-
|
|
1521
|
+
# #2490 — a block or `class <<` body whose `self` is another object writes that object's ivars. #2941 — a `def`
|
|
1522
|
+
# in it that runs on a `class_name` instance again counts its writes.
|
|
1523
|
+
# The reentry is for code that runs on an instance again, so a singleton-side `def` takes none.
|
|
1524
|
+
each_census_child(node, class_name, def_bodies: true, reentry: !scope.self_type.is_a?(Type::Singleton)) do |c|
|
|
1456
1525
|
gather_ivar_writes(c, scope, class_name, accumulator, guarded_ivars, mutated_ivars, dead_writes, declared)
|
|
1457
1526
|
end
|
|
1458
1527
|
end
|
|
@@ -2240,7 +2309,11 @@ module Rigor
|
|
|
2240
2309
|
REFLECTIVE_OPAQUE_CLASSES = :reflective_opaque_classes
|
|
2241
2310
|
# The census's {ConstantIvarValue}, which {#record_ivar_write} reads.
|
|
2242
2311
|
CONSTANT_IVAR_VALUES = :constant_ivar_values
|
|
2243
|
-
|
|
2312
|
+
# Issue #2931 — per class, the ivars a singleton-side `def` (`def self.x`) of it writes, which the census files
|
|
2313
|
+
# under the class too (#1958).
|
|
2314
|
+
SINGLETON_IVAR_WRITES = :singleton_ivar_writes
|
|
2315
|
+
private_constant :DEFINE_METHOD_WRITES, :CLASS_BODY_WRITES, :REFLECTIVE_OPAQUE_CLASSES, :CONSTANT_IVAR_VALUES,
|
|
2316
|
+
:SINGLETON_IVAR_WRITES
|
|
2244
2317
|
|
|
2245
2318
|
# Unions `type` into the class-ivar accumulator for `(class_name, ivar_name)`. Shared by the single-write and
|
|
2246
2319
|
# multi-write (parallel-assignment) collectors.
|
|
@@ -2280,6 +2353,58 @@ module Rigor
|
|
|
2280
2353
|
end
|
|
2281
2354
|
end
|
|
2282
2355
|
|
|
2356
|
+
# Issue #1217 — `rescue => @err` and `for @x in xs` (with `for *@x in`, `for @x, y in`) write the ivar through a
|
|
2357
|
+
# target node, not an `InstanceVariableWriteNode`, so the census missed them: a sibling method read the
|
|
2358
|
+
# constructor's `@err = nil` alone and folded `if @err` always-falsey. A rescue reference contributes the
|
|
2359
|
+
# exception instance type the clause names; a `for` index the `Dynamic[top]` floor, since the census does not
|
|
2360
|
+
# type the element, and an unknown value is honest where a missing one is not.
|
|
2361
|
+
def record_binding_target_ivars(node, scope, class_name, accumulator)
|
|
2362
|
+
names = BindingTargets.names(node, Prism::InstanceVariableTargetNode)
|
|
2363
|
+
return if names.empty?
|
|
2364
|
+
|
|
2365
|
+
type = BindingTargets.value_type(node, scope)
|
|
2366
|
+
names.each { |name| accumulate_ivar_type(accumulator, class_name, name, type) }
|
|
2367
|
+
end
|
|
2368
|
+
|
|
2369
|
+
# The names a `rescue =>` reference or a `for` index binds through a target of `target_class`, looking through
|
|
2370
|
+
# a splat and a multi-target index.
|
|
2371
|
+
module BindingTargets
|
|
2372
|
+
EMPTY = [].freeze
|
|
2373
|
+
private_constant :EMPTY
|
|
2374
|
+
|
|
2375
|
+
module_function
|
|
2376
|
+
|
|
2377
|
+
def names(node, target_class)
|
|
2378
|
+
root =
|
|
2379
|
+
case node
|
|
2380
|
+
when Prism::RescueNode then node.reference
|
|
2381
|
+
when Prism::ForNode then node.index
|
|
2382
|
+
end
|
|
2383
|
+
return EMPTY if root.nil?
|
|
2384
|
+
|
|
2385
|
+
collect(root, target_class, [])
|
|
2386
|
+
end
|
|
2387
|
+
|
|
2388
|
+
# What an ivar target is written with: a rescue reference's exception instance, as the in-method binding
|
|
2389
|
+
# types it ({ErrorInfo.rescue_exception_type}), a `for` index's `Dynamic[top]` floor.
|
|
2390
|
+
def value_type(node, scope)
|
|
2391
|
+
return Type::Combinator.untyped unless node.is_a?(Prism::RescueNode)
|
|
2392
|
+
|
|
2393
|
+
ErrorInfo.rescue_exception_type(node) { |exception_node| scope.type_of(exception_node) }
|
|
2394
|
+
end
|
|
2395
|
+
|
|
2396
|
+
def collect(target, target_class, acc)
|
|
2397
|
+
case target
|
|
2398
|
+
when target_class then acc << target.name
|
|
2399
|
+
when Prism::SplatNode then collect(target.expression, target_class, acc) if target.expression
|
|
2400
|
+
when Prism::MultiTargetNode
|
|
2401
|
+
[*target.lefts, target.rest, *target.rights].compact.each { |t| collect(t, target_class, acc) }
|
|
2402
|
+
end
|
|
2403
|
+
acc
|
|
2404
|
+
end
|
|
2405
|
+
end
|
|
2406
|
+
private_constant :BindingTargets
|
|
2407
|
+
|
|
2283
2408
|
def falsey_constant?(type)
|
|
2284
2409
|
type.is_a?(Type::Constant) && (type.value.nil? || type.value == false)
|
|
2285
2410
|
end
|
|
@@ -2304,7 +2429,7 @@ module Rigor
|
|
|
2304
2429
|
def legacy_class_cvar_index(root, default_scope)
|
|
2305
2430
|
accumulator = {}
|
|
2306
2431
|
walk_class_cvars(root, [], default_scope, accumulator)
|
|
2307
|
-
accumulator
|
|
2432
|
+
finished_cvar_census(accumulator)
|
|
2308
2433
|
end
|
|
2309
2434
|
|
|
2310
2435
|
# `def_owner` names the class a `def` leaf's cvar writes belong to when `self` is
|
|
@@ -2382,6 +2507,8 @@ module Rigor
|
|
|
2382
2507
|
_self_decl, child_prefix, child_cref = ctx
|
|
2383
2508
|
return true unless node.body
|
|
2384
2509
|
|
|
2510
|
+
collect_body_cvar_writes(node.body, child_cref ? [] : child_prefix,
|
|
2511
|
+
scope_entering_declaration(default_scope, node.constant_path), accumulator)
|
|
2385
2512
|
walk_class_cvars(node.body, child_cref ? [] : child_prefix,
|
|
2386
2513
|
scope_entering_declaration(default_scope, node.constant_path), accumulator,
|
|
2387
2514
|
singleton_cref: child_cref)
|
|
@@ -2431,24 +2558,89 @@ module Rigor
|
|
|
2431
2558
|
class_name = qualified_prefix.join("::")
|
|
2432
2559
|
body_scope = census_body_scope(default_scope, qualified_prefix,
|
|
2433
2560
|
Type::Combinator.nominal_of(class_name))
|
|
2434
|
-
gather_cvar_writes(def_node.body, body_scope, class_name, accumulator)
|
|
2561
|
+
gather_cvar_writes(def_node.body, body_scope, class_name, accumulator, join_only: false)
|
|
2435
2562
|
end
|
|
2436
2563
|
|
|
2437
|
-
def
|
|
2564
|
+
# Issue #3204 — the writes a `class` / `module` body makes outside its `def`s (`@@x = nil` at the top of
|
|
2565
|
+
# the body, in a block or a `class << self` there): Ruby resolves each through the body's lexical cref,
|
|
2566
|
+
# so it is the same cvar a sibling `def` reads. Such a write only joins an entry a `def` body's write of
|
|
2567
|
+
# the same class creates: the census misses most of the other ways a cvar is written
|
|
2568
|
+
# (`||=`, `+=`, an including class, a subclass, another file, `class_variable_set` from outside), so a
|
|
2569
|
+
# body-only seed would be precise where Ruby's value is not, and stays unbound. The rvalue is typed with
|
|
2570
|
+
# the class itself as `self`. The gather stops at `def`, `class` and `module`, which their own events enter.
|
|
2571
|
+
def collect_body_cvar_writes(body, qualified_prefix, default_scope, accumulator)
|
|
2572
|
+
return if body.nil? || qualified_prefix.empty?
|
|
2573
|
+
|
|
2574
|
+
class_name = qualified_prefix.join("::")
|
|
2575
|
+
body_scope = census_body_scope(default_scope, qualified_prefix,
|
|
2576
|
+
Type::Combinator.singleton_of(class_name))
|
|
2577
|
+
gather_cvar_writes(body, body_scope, class_name, accumulator, join_only: true)
|
|
2578
|
+
end
|
|
2579
|
+
|
|
2580
|
+
def gather_cvar_writes(node, scope, class_name, accumulator, join_only:)
|
|
2438
2581
|
return unless node.is_a?(Prism::Node)
|
|
2439
2582
|
|
|
2440
|
-
|
|
2583
|
+
if node.is_a?(Prism::ClassVariableWriteNode)
|
|
2584
|
+
record_cvar_write(accumulator, class_name, node.name, EmptySeed.erase(node.value, scope.type_of(node.value)),
|
|
2585
|
+
join_only: join_only)
|
|
2586
|
+
end
|
|
2587
|
+
record_reflective_cvar_write(node, scope, class_name, accumulator) if node.is_a?(Prism::CallNode)
|
|
2588
|
+
# Issue #1217 — `rescue => @@c` / `for @@c in xs` write through a target node. Each enters as the
|
|
2589
|
+
# `Dynamic[top]` floor, the rescue reference too: the census keys each class alone, so a superclass's
|
|
2590
|
+
# `@@c = nil` is not in a subclass's entry, and a typed exception entry would read as non-nil there.
|
|
2591
|
+
BindingTargets.names(node, Prism::ClassVariableTargetNode).each do |name|
|
|
2592
|
+
record_cvar_write(accumulator, class_name, name, Type::Combinator.untyped, join_only: join_only)
|
|
2593
|
+
end
|
|
2441
2594
|
return if IVAR_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
|
|
2442
2595
|
|
|
2443
|
-
node.rigor_each_child { |c| gather_cvar_writes(c, scope, class_name, accumulator) }
|
|
2596
|
+
node.rigor_each_child { |c| gather_cvar_writes(c, scope, class_name, accumulator, join_only: join_only) }
|
|
2444
2597
|
end
|
|
2445
2598
|
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2599
|
+
# Issue #3204 — `class_variable_set(:@@x, v)` on an implicit or `self` receiver writes the cvar a sibling
|
|
2600
|
+
# `def` reads, as `@@x = v` does. It only joins, like a class-body write: a dynamic name, a `send`, an
|
|
2601
|
+
# explicit receiver or another file may write the same cvar unseen.
|
|
2602
|
+
def record_reflective_cvar_write(node, scope, class_name, accumulator)
|
|
2603
|
+
return unless node.name == :class_variable_set && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
|
|
2604
|
+
|
|
2605
|
+
args = node.arguments&.arguments
|
|
2606
|
+
return unless args&.size == 2
|
|
2607
|
+
|
|
2608
|
+
name_node, value = args
|
|
2609
|
+
# A name that is not a cvar's never meets a `def` write's entry, so the join drops it.
|
|
2610
|
+
name = name_node.unescaped if name_node.is_a?(Prism::SymbolNode) || name_node.is_a?(Prism::StringNode)
|
|
2611
|
+
return unless name
|
|
2612
|
+
|
|
2613
|
+
record_cvar_write(accumulator, class_name, name.to_sym, EmptySeed.erase(value, scope.type_of(value)),
|
|
2614
|
+
join_only: true)
|
|
2615
|
+
end
|
|
2616
|
+
|
|
2617
|
+
# A class-body or `class_variable_set` write no `def` body's write has met yet (issue #3204): it joins an
|
|
2618
|
+
# entry a creating write makes later, and {#finished_cvar_census} drops it when none does.
|
|
2619
|
+
JoinOnlyCvarWrite = Data.define(:type)
|
|
2620
|
+
private_constant :JoinOnlyCvarWrite
|
|
2621
|
+
|
|
2622
|
+
# Every census write unions with what the entry already holds, so the seed does not depend on the order of
|
|
2623
|
+
# the writes. A `join_only` write (a class or module body's, or `class_variable_set`) never creates an entry
|
|
2624
|
+
# on its own: it is held as a {JoinOnlyCvarWrite} until a `def` body's write of the same cvar arrives.
|
|
2625
|
+
def record_cvar_write(accumulator, class_name, name, type, join_only:)
|
|
2626
|
+
entries = (accumulator[class_name] ||= {})
|
|
2627
|
+
existing = entries[name]
|
|
2628
|
+
entries[name] =
|
|
2629
|
+
if existing.nil? then join_only ? JoinOnlyCvarWrite.new(type) : type
|
|
2630
|
+
elsif existing.is_a?(JoinOnlyCvarWrite)
|
|
2631
|
+
joined = Type::Combinator.union(existing.type, type)
|
|
2632
|
+
join_only ? JoinOnlyCvarWrite.new(joined) : joined
|
|
2633
|
+
else Type::Combinator.union(existing, type)
|
|
2634
|
+
end
|
|
2635
|
+
end
|
|
2636
|
+
|
|
2637
|
+
# The census as the discovery index holds it: each cvar only joining writes reached dropped, each class's
|
|
2638
|
+
# entry frozen, then the whole.
|
|
2639
|
+
def finished_cvar_census(accumulator)
|
|
2640
|
+
accumulator.filter_map do |class_name, entries|
|
|
2641
|
+
kept = entries.reject { |_, type| type.is_a?(JoinOnlyCvarWrite) }
|
|
2642
|
+
[class_name, kept.freeze] unless kept.empty?
|
|
2643
|
+
end.to_h.freeze
|
|
2452
2644
|
end
|
|
2453
2645
|
|
|
2454
2646
|
# The program-global pre-pass's tables on the seeded scope's discovery index, and each global materialised into
|
|
@@ -3505,6 +3697,31 @@ module Rigor
|
|
|
3505
3697
|
tables.siblings]
|
|
3506
3698
|
end
|
|
3507
3699
|
|
|
3700
|
+
# Issue #3015 — the bodies of a concern's `included` / `prepended` / `class_methods` blocks, each with the prefix
|
|
3701
|
+
# of the module whose body writes it ({ScopeIndexer#concern_hook_blocks}). Ruby runs such a block on the includer
|
|
3702
|
+
# (or on `Owner::ClassMethods`), so a `def self.x` or `class << self` def in it is not the module's own singleton
|
|
3703
|
+
# method, and the walks that file one ask {#hook_block?} first. Mixed into an accumulator with a
|
|
3704
|
+
# `hook_block_spans` slot.
|
|
3705
|
+
module HookBlockSpans
|
|
3706
|
+
def mark_hook_blocks(blocks, owner)
|
|
3707
|
+
blocks.each do |block|
|
|
3708
|
+
(self.hook_block_spans ||= []) << [block.location.start_offset, block.location.end_offset, owner]
|
|
3709
|
+
end
|
|
3710
|
+
end
|
|
3711
|
+
|
|
3712
|
+
# Whether `node` sits in a block marked for the module `owner` (a prefix).
|
|
3713
|
+
def hook_block?(node, owner)
|
|
3714
|
+
offset = node.location.start_offset
|
|
3715
|
+
!hook_block_spans.nil? &&
|
|
3716
|
+
hook_block_spans.any? { |start, stop, marked| offset >= start && offset < stop && marked == owner }
|
|
3717
|
+
end
|
|
3718
|
+
|
|
3719
|
+
# Whether `def_node` is a `def self.x` in a block marked for the module `owner`.
|
|
3720
|
+
def hook_block_self_def?(def_node, owner)
|
|
3721
|
+
def_node.receiver.is_a?(Prism::SelfNode) && hook_block?(def_node, owner)
|
|
3722
|
+
end
|
|
3723
|
+
end
|
|
3724
|
+
|
|
3508
3725
|
# The accumulator {#walk_methods_and_def_nodes} threads: the existence table and its issue #992
|
|
3509
3726
|
# envelope twin, which only {#record_method} and {#record_surface_mark} write, and the issue #1120
|
|
3510
3727
|
# refinement table, which only {#record_refinement_defs} writes. That last one stays nil until a
|
|
@@ -3527,7 +3744,9 @@ module Rigor
|
|
|
3527
3744
|
MethodTables = Struct.new(:existence, :envelopes, :refinements, :certainty, :certain, :possible,
|
|
3528
3745
|
:contested_envelopes, :contested_def_nodes, :root, :in_effect, :census,
|
|
3529
3746
|
:refine_text, :literal_seen, :source_bytes, :loose_constants, :block_spans,
|
|
3530
|
-
:definer_history) do
|
|
3747
|
+
:definer_history, :forwardable_extends, :hook_block_spans) do
|
|
3748
|
+
include HookBlockSpans
|
|
3749
|
+
|
|
3531
3750
|
# Issue #2006 — records, in walk order, that a definer other than an alias wrote the instance method `name`
|
|
3532
3751
|
# of `class_name` at `node`: `def_node` for a `def`, nil for a `define_method(:x)` or an `attr_*` reader or
|
|
3533
3752
|
# writer, and whether the write is {#certain}. {ScopeIndexer#apply_alias_def_nodes} reads the history to copy
|
|
@@ -4189,6 +4408,11 @@ module Rigor
|
|
|
4189
4408
|
census_scan(node.superclass, qualified_prefix, methods_acc)
|
|
4190
4409
|
end
|
|
4191
4410
|
record_declaration_facts(node, child_prefix, methods_acc.at(node)) unless child_cref
|
|
4411
|
+
unless child_cref
|
|
4412
|
+
methods_acc.mark_hook_blocks(concern_hook_blocks(node, child_prefix), child_prefix)
|
|
4413
|
+
record_concern_class_methods(node, child_prefix, methods_acc, def_nodes_acc)
|
|
4414
|
+
record_hook_definer_calls(node, child_prefix, methods_acc)
|
|
4415
|
+
end
|
|
4192
4416
|
body_prefix = child_cref ? [] : child_prefix
|
|
4193
4417
|
if node.body
|
|
4194
4418
|
walk_methods_and_def_nodes(node.body, body_prefix, false, methods_acc, def_nodes_acc,
|
|
@@ -4208,11 +4432,12 @@ module Rigor
|
|
|
4208
4432
|
if !node.expression.is_a?(Prism::SelfNode) || tables.in_block?(node)
|
|
4209
4433
|
record_singleton_body_prepends(node, qualified_prefix, tables)
|
|
4210
4434
|
end
|
|
4435
|
+
record_constant_singleton_prepends(node, qualified_prefix, tables)
|
|
4211
4436
|
walk_methods_and_def_nodes(node.expression, qualified_prefix, in_singleton_class,
|
|
4212
4437
|
methods_acc, def_nodes_acc, source_path, def_owner_prefix,
|
|
4213
4438
|
singleton_cref: singleton_cref,
|
|
4214
4439
|
defs_singleton: defs_singleton)
|
|
4215
|
-
if node.body
|
|
4440
|
+
if node.body && !hook_block_singleton?(node, in_singleton_class, owner_prefix, methods_acc)
|
|
4216
4441
|
# The body's lexical cref is unnameable AND its qualified prefix stays the
|
|
4217
4442
|
# ENCLOSING lexical one — `class C3::CD` below `class <<` resolves `C3`
|
|
4218
4443
|
# lexically, never against the singleton's name. `singleton_prefix` rides
|
|
@@ -4226,6 +4451,7 @@ module Rigor
|
|
|
4226
4451
|
when Prism::ConstantWriteNode, Prism::ConstantPathWriteNode, Prism::ConstantOrWriteNode,
|
|
4227
4452
|
Prism::ConstantPathOrWriteNode
|
|
4228
4453
|
record_meta_superclass(node, qualified_prefix, def_owner_prefix, methods_acc.at(node))
|
|
4454
|
+
record_struct_same_class(node, qualified_prefix, def_owner_prefix, methods_acc.at(node))
|
|
4229
4455
|
if (split = meta_new_block_split(node, qualified_prefix, def_owner_prefix, singleton_cref))
|
|
4230
4456
|
enclosing, body, body_self = split
|
|
4231
4457
|
# A meta-new block rebinds only `self` — `Module.nesting` stays lexical — so
|
|
@@ -4267,11 +4493,13 @@ module Rigor
|
|
|
4267
4493
|
node.rigor_each_child { |child| census_scan(child, qualified_prefix, methods_acc) }
|
|
4268
4494
|
end
|
|
4269
4495
|
record_foreign_singleton_def(node, qualified_prefix, owner_prefix, methods_acc.at(node))
|
|
4496
|
+
return if methods_acc.hook_block_self_def?(node, owner_prefix)
|
|
4497
|
+
|
|
4270
4498
|
unless def_owner_prefix&.empty? || defs_singleton == :unnameable ||
|
|
4271
4499
|
(singleton_cref && owner_prefix.empty?)
|
|
4272
|
-
singleton_def = in_singleton_class
|
|
4500
|
+
singleton_def = def_context(in_singleton_class, defs_singleton)
|
|
4273
4501
|
record_def_method(node, owner_prefix, singleton_def, methods_acc.at(node))
|
|
4274
|
-
record_def_body_evidence(node, owner_prefix, methods_acc)
|
|
4502
|
+
record_def_body_evidence(node, owner_prefix, methods_acc, singleton_def ? :singleton : :instance)
|
|
4275
4503
|
record_def_node(node, owner_prefix, singleton_def, def_nodes_acc, methods_acc)
|
|
4276
4504
|
end
|
|
4277
4505
|
return
|
|
@@ -4294,6 +4522,8 @@ module Rigor
|
|
|
4294
4522
|
when Prism::CallNode
|
|
4295
4523
|
census_literal_arguments(node, qualified_prefix, methods_acc) if methods_acc.mentions_refine?
|
|
4296
4524
|
if receiver_eval_call?(node)
|
|
4525
|
+
record_singleton_eval_definers(node, owner_prefix, in_singleton_class, methods_acc.at(node))
|
|
4526
|
+
record_constant_singleton_eval_defs(node, qualified_prefix, methods_acc.at(node))
|
|
4297
4527
|
return walk_eval_methods_and_defs(node, qualified_prefix, in_singleton_class, methods_acc,
|
|
4298
4528
|
def_nodes_acc, source_path, def_owner_prefix,
|
|
4299
4529
|
singleton_cref: singleton_cref,
|
|
@@ -4302,13 +4532,15 @@ module Rigor
|
|
|
4302
4532
|
anonymous = record_call_node_methods(node, owner_prefix, in_singleton_class, methods_acc.at(node),
|
|
4303
4533
|
source_path)
|
|
4304
4534
|
unless in_singleton_class
|
|
4305
|
-
superseded = call_superseded_names(node)
|
|
4535
|
+
superseded = call_superseded_names(node, owner_prefix, methods_acc)
|
|
4306
4536
|
supersede_def_nodes(superseded, owner_prefix, def_nodes_acc, methods_acc.at(node))
|
|
4307
4537
|
note_call_definers(node, superseded, owner_prefix, methods_acc.at(node))
|
|
4308
4538
|
end
|
|
4309
4539
|
tables = methods_acc.at(node)
|
|
4310
4540
|
record_singleton_prepend(node, qualified_prefix, tables,
|
|
4311
4541
|
self_ok: node.name == :prepend && tables.in_block?(node))
|
|
4542
|
+
own_prefix = def_owner_prefix&.empty? || defs_singleton == :unnameable ? EMPTY_PREFIX : owner_prefix
|
|
4543
|
+
record_own_singleton_prepend(node, [own_prefix, qualified_prefix], in_singleton_class, tables)
|
|
4312
4544
|
# Issue #1120 — a refine body's defs go to the refinement table and nowhere else.
|
|
4313
4545
|
if (target = methods_acc.refine_target_of(node))
|
|
4314
4546
|
return record_literal_target_refine(node, target, qualified_prefix, owner_prefix, methods_acc)
|
|
@@ -4422,6 +4654,27 @@ module Rigor
|
|
|
4422
4654
|
record_surface_mark(tables, child_prefix.join("::"), Scope::DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK)
|
|
4423
4655
|
end
|
|
4424
4656
|
|
|
4657
|
+
# Issue #2979 — `K = Struct.new("Named", :a)`, with or without a block, binds `K` and `Struct::Named` to one class
|
|
4658
|
+
# object, so each name's bucket takes a `[ENVELOPE_SAME_CLASS_KIND, other]` entry and the method chain of either
|
|
4659
|
+
# reads the other's tables (`Scope::ResolutionChain`). Only the bare factory call qualifies: under a wrapping
|
|
4660
|
+
# `Class.new(Struct.new("Named", :a))` the constant names a SUBCLASS of `Struct::Named`, which already reaches it
|
|
4661
|
+
# as an ancestor only one way.
|
|
4662
|
+
def record_struct_same_class(node, qualified_prefix, self_base, tables)
|
|
4663
|
+
rvalue = meta_new_rvalue(node)
|
|
4664
|
+
return unless struct_new_call?(rvalue)
|
|
4665
|
+
|
|
4666
|
+
named = AnonymousMetaClass.struct_class_name(rvalue)
|
|
4667
|
+
return if named.nil?
|
|
4668
|
+
|
|
4669
|
+
child_prefix = meta_new_child_prefix(node, qualified_prefix, self_base)
|
|
4670
|
+
return if child_prefix.nil? || child_prefix.empty?
|
|
4671
|
+
|
|
4672
|
+
class_name = child_prefix.join("::")
|
|
4673
|
+
kind = Scope::DiscoveryIndex::ENVELOPE_SAME_CLASS_KIND
|
|
4674
|
+
record_envelope(tables.envelopes, class_name, [kind, named], Source::ParameterEnvelope::OPAQUE)
|
|
4675
|
+
record_envelope(tables.envelopes, named, [kind, class_name], Source::ParameterEnvelope::OPAQUE)
|
|
4676
|
+
end
|
|
4677
|
+
|
|
4425
4678
|
def record_meta_new_facts(rvalue, child_prefix, tables)
|
|
4426
4679
|
record_meta_members(rvalue, child_prefix, tables)
|
|
4427
4680
|
# `Module.new(&BODY)`: the body runs on the new module but cannot be read, so its surface is unknown (#1816).
|
|
@@ -4449,11 +4702,111 @@ module Rigor
|
|
|
4449
4702
|
record_define_method(node, qualified_prefix, in_singleton_class, methods_acc) if node.name == :define_method
|
|
4450
4703
|
record_attr_methods(node, qualified_prefix, in_singleton_class, methods_acc) if attr_macro(node)
|
|
4451
4704
|
record_module_attr_methods(node, qualified_prefix, methods_acc) if MODULE_ATTR_MACROS.key?(node.name)
|
|
4705
|
+
record_forwardable_methods(node, qualified_prefix, in_singleton_class, methods_acc)
|
|
4452
4706
|
record_alias_method_call(node, qualified_prefix, in_singleton_class, methods_acc)
|
|
4453
4707
|
record_surface_evidence(node, qualified_prefix, methods_acc)
|
|
4708
|
+
record_self_definer_call(node, qualified_prefix, in_singleton_class, methods_acc)
|
|
4454
4709
|
AnonymousMetaClass.name_for(node, source_path)
|
|
4455
4710
|
end
|
|
4456
4711
|
|
|
4712
|
+
# Issue #2901 — the class-body calls on `self` that open the class to `call.undefined-method` without a name
|
|
4713
|
+
# this walk can read: `send(:define_method, m)` (the instance side, or the singleton side in a `class << self`
|
|
4714
|
+
# body), `define_singleton_method(m)` (the singleton side, outside `class << self`, where it would define on the
|
|
4715
|
+
# singleton's own singleton), and a call of any other name, filed as a
|
|
4716
|
+
# {Scope::DiscoveryIndex::ENVELOPE_BODY_CALL_KIND} entry for the reader to resolve against the class's macros.
|
|
4717
|
+
def record_self_definer_call(node, qualified_prefix, in_singleton_class, accumulator)
|
|
4718
|
+
return if qualified_prefix.empty? || !(node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
|
|
4719
|
+
|
|
4720
|
+
class_name = qualified_prefix.join("::")
|
|
4721
|
+
if node.name == :define_singleton_method
|
|
4722
|
+
record_open_singleton_definer(node, class_name, in_singleton_class, accumulator)
|
|
4723
|
+
record_literal_self_definer(node, class_name, in_singleton_class, accumulator)
|
|
4724
|
+
elsif SURFACE_SEND_CALLS.include?(node.name)
|
|
4725
|
+
record_sent_define_method(node, class_name, in_singleton_class, accumulator)
|
|
4726
|
+
record_literal_self_definer(node, class_name, in_singleton_class, accumulator)
|
|
4727
|
+
elsif MODULE_NEW_MIXIN_CALLS.include?(node.name)
|
|
4728
|
+
record_module_new_mixin(node, class_name, in_singleton_class, accumulator)
|
|
4729
|
+
elsif !in_singleton_class && !BODY_CALL_EXCLUDED.include?(node.name)
|
|
4730
|
+
key = [Scope::DiscoveryIndex::ENVELOPE_BODY_CALL_KIND, node.name]
|
|
4731
|
+
record_envelope(accumulator.envelopes, class_name, key, Source::ParameterEnvelope::OPAQUE)
|
|
4732
|
+
end
|
|
4733
|
+
end
|
|
4734
|
+
|
|
4735
|
+
# The class-body calls this walk reads as definers of their own, or that core's `Module` answers: a project
|
|
4736
|
+
# macro of one of these names is not looked for.
|
|
4737
|
+
BODY_CALL_EXCLUDED = %i[
|
|
4738
|
+
define_method define_singleton_method alias_method attr_reader attr_writer attr_accessor include extend prepend
|
|
4739
|
+
private public protected module_function private_class_method public_class_method private_constant
|
|
4740
|
+
public_constant require require_relative autoload autoload_relative
|
|
4741
|
+
].to_set.freeze
|
|
4742
|
+
private_constant :BODY_CALL_EXCLUDED
|
|
4743
|
+
|
|
4744
|
+
def record_open_singleton_definer(node, class_name, in_singleton_class, accumulator)
|
|
4745
|
+
return if in_singleton_class || !computed_first_name?(node.arguments&.arguments)
|
|
4746
|
+
|
|
4747
|
+
record_surface_mark(accumulator, class_name, Scope::DiscoveryIndex::ENVELOPE_OPEN_SINGLETON_NAMES_MARK)
|
|
4748
|
+
end
|
|
4749
|
+
|
|
4750
|
+
def record_sent_define_method(node, class_name, in_singleton_class, accumulator)
|
|
4751
|
+
arguments = node.arguments&.arguments
|
|
4752
|
+
return unless arguments && literal_method_name(arguments.first) == :define_method
|
|
4753
|
+
return unless computed_first_name?(arguments.drop(1))
|
|
4754
|
+
|
|
4755
|
+
record_surface_mark(accumulator, class_name, open_names_mark(in_singleton_class))
|
|
4756
|
+
end
|
|
4757
|
+
|
|
4758
|
+
# Issue #2975 — `include(Module.new { %i[a b].each { |x| define_method(x) {} } })` mixes in a module whose body
|
|
4759
|
+
# runs a computed `define_method` on its own `self`, so the side the mixin lands on (the instance side, or the
|
|
4760
|
+
# singleton side in a `class << self` body) is open as a direct `define_method(m)` there would leave it.
|
|
4761
|
+
def record_module_new_mixin(node, class_name, in_singleton_class, accumulator)
|
|
4762
|
+
return unless module_new_definer_argument?(node)
|
|
4763
|
+
|
|
4764
|
+
record_surface_mark(accumulator, class_name, open_names_mark(in_singleton_class))
|
|
4765
|
+
end
|
|
4766
|
+
|
|
4767
|
+
MODULE_NEW_MIXIN_CALLS = %i[include prepend].to_set.freeze
|
|
4768
|
+
private_constant :MODULE_NEW_MIXIN_CALLS
|
|
4769
|
+
|
|
4770
|
+
# Whether a mixin call's argument is a `Module.new { … }` whose body runs a computed `define_method` on the
|
|
4771
|
+
# module ({#runs_computed_self_definer?}).
|
|
4772
|
+
def module_new_definer_argument?(node)
|
|
4773
|
+
(node.arguments&.arguments || []).any? do |argument|
|
|
4774
|
+
module_new_call?(argument) && argument.block.is_a?(Prism::BlockNode) &&
|
|
4775
|
+
runs_computed_self_definer?(argument.block.body)
|
|
4776
|
+
end
|
|
4777
|
+
end
|
|
4778
|
+
|
|
4779
|
+
# Issue #2975 — `singleton_class.class_eval { def my_attr(*n) = n.each { |x| define_method(x) {} } }` in a class
|
|
4780
|
+
# body, or the same on `Const.singleton_class`, defines a class macro the eval-body walk files under no owner: the
|
|
4781
|
+
# method takes the singleton-side definer entry {#record_computed_definer} gives a `def self.my_attr`.
|
|
4782
|
+
def record_singleton_eval_definers(node, qualified_prefix, in_singleton_class, tables)
|
|
4783
|
+
return if in_singleton_class || !CLASS_EVAL_BLOCK_CALLS.include?(node.name)
|
|
4784
|
+
|
|
4785
|
+
receiver = node.receiver
|
|
4786
|
+
return unless singleton_class_of_receiver?(receiver)
|
|
4787
|
+
|
|
4788
|
+
current = qualified_prefix.empty? ? nil : qualified_prefix.join("::")
|
|
4789
|
+
owner = singleton_class_owner(node, receiver.receiver, qualified_prefix, current)
|
|
4790
|
+
return if owner.nil?
|
|
4791
|
+
|
|
4792
|
+
body = node.block.body
|
|
4793
|
+
return unless body.is_a?(Prism::StatementsNode)
|
|
4794
|
+
|
|
4795
|
+
body.body.each do |statement|
|
|
4796
|
+
next unless statement.is_a?(Prism::DefNode) && statement.receiver.nil?
|
|
4797
|
+
|
|
4798
|
+
record_computed_definer(statement, owner.split("::"), tables, :singleton)
|
|
4799
|
+
end
|
|
4800
|
+
end
|
|
4801
|
+
|
|
4802
|
+
CLASS_EVAL_BLOCK_CALLS = %i[class_eval module_eval class_exec module_exec].to_set.freeze
|
|
4803
|
+
private_constant :CLASS_EVAL_BLOCK_CALLS
|
|
4804
|
+
|
|
4805
|
+
# Whether a definer's argument list starts with a name that is not a Symbol or String literal.
|
|
4806
|
+
def computed_first_name?(arguments)
|
|
4807
|
+
!arguments.nil? && !arguments.empty? && literal_method_name(arguments.first).nil?
|
|
4808
|
+
end
|
|
4809
|
+
|
|
4457
4810
|
# The `alias_method :new, :old` CallNode twin of {#record_alias_method} (#533): the NEW name joins
|
|
4458
4811
|
# the discovered-methods table so calls to it stop reading undefined, and
|
|
4459
4812
|
# {#apply_alias_def_nodes}'s map walk gives it the ORIGINAL def node for return inference.
|
|
@@ -4754,17 +5107,27 @@ module Rigor
|
|
|
4754
5107
|
# (#590). Their bodies are entered as class bodies, so a member read inside one dispatches on the struct's
|
|
4755
5108
|
# own class and must find the reader there: a member that shadows a `Kernel` private (`lambda`) otherwise
|
|
4756
5109
|
# falls through to `Kernel#lambda`, and `lambda.upcase` reports an undefined method on `Proc` — a wrong-type
|
|
4757
|
-
# diagnostic on correct code. No-op for `Class.new` / `Module.new
|
|
4758
|
-
#
|
|
5110
|
+
# diagnostic on correct code. No-op for `Class.new` / `Module.new`. A splatted member list registers its literal
|
|
5111
|
+
# members and opens the instance side ({#record_meta_member_names}).
|
|
4759
5112
|
def record_meta_members(factory_call, qualified_prefix, accumulator, allow_outer_block: true)
|
|
4760
5113
|
factory_call = resolve_meta_factory_call(factory_call, allow_outer_block: allow_outer_block)
|
|
4761
5114
|
return unless factory_call
|
|
4762
5115
|
|
|
4763
|
-
|
|
4764
|
-
|
|
5116
|
+
# Issue #2968 — `K = Struct.new("Named", :a)` makes one class under two names, `K` and `Struct::Named`; the
|
|
5117
|
+
# members are credited on both.
|
|
5118
|
+
named = AnonymousMetaClass.struct_class_name(factory_call) if struct_new_call?(factory_call)
|
|
5119
|
+
[qualified_prefix.join("::"), named].compact.uniq.each do |class_name|
|
|
5120
|
+
record_meta_member_names(factory_call, class_name, accumulator)
|
|
5121
|
+
end
|
|
5122
|
+
end
|
|
4765
5123
|
|
|
4766
|
-
|
|
4767
|
-
|
|
5124
|
+
# Issue #2968 — a splatted member list (`Struct.new(*FIELDS)`) names members this walk cannot read, so the
|
|
5125
|
+
# instance side opens: no name is provably missing there.
|
|
5126
|
+
def record_meta_member_names(factory_call, class_name, accumulator)
|
|
5127
|
+
if meta_members_open?(factory_call)
|
|
5128
|
+
record_surface_mark(accumulator, class_name, Scope::DiscoveryIndex::ENVELOPE_OPEN_INSTANCE_NAMES_MARK)
|
|
5129
|
+
end
|
|
5130
|
+
meta_member_names(factory_call).each { |member| record_method(accumulator, class_name, member, :instance) }
|
|
4768
5131
|
end
|
|
4769
5132
|
|
|
4770
5133
|
# Unwinds nested single-parent `Class.new(...)` calls to a root `Struct.new(...)` / `Data.define(...)`.
|
|
@@ -4795,22 +5158,33 @@ module Rigor
|
|
|
4795
5158
|
data_define_call?(call_node) || struct_new_call?(call_node)
|
|
4796
5159
|
end
|
|
4797
5160
|
|
|
4798
|
-
# The Symbol member names of a `Data.define
|
|
4799
|
-
#
|
|
4800
|
-
# String class name is NOT stripped, so `Struct.new("Name", :a) do` registers no members and the body
|
|
4801
|
-
# is entered under the anonymous name (consistent between passes). `Data.define` args are all Symbols.
|
|
5161
|
+
# The literal Symbol member names of a `Data.define` / `Struct.new` call ({#meta_member_positionals}). A splatted
|
|
5162
|
+
# argument contributes none; {#meta_members_open?} says the list is then incomplete.
|
|
4802
5163
|
def meta_member_names(call_node)
|
|
5164
|
+
meta_member_positionals(call_node).filter_map { |arg| arg.unescaped.to_sym if arg.is_a?(Prism::SymbolNode) }
|
|
5165
|
+
end
|
|
5166
|
+
|
|
5167
|
+
# Issue #2968 — whether a `Data.define` / `Struct.new` call's member list holds a splat, so its members are not
|
|
5168
|
+
# all literal and the reader set is unknown. A layout table or a closed reader set must not be built from it.
|
|
5169
|
+
def meta_members_open?(call_node)
|
|
5170
|
+
meta_member_positionals(call_node).any?(Prism::SplatNode)
|
|
5171
|
+
end
|
|
5172
|
+
|
|
5173
|
+
# The member arguments of a `Data.define` / `Struct.new` call. For `Struct.new` the trailing `keyword_init:` hash
|
|
5174
|
+
# is stripped ({#struct_new_positionals}). A leading class-name String (`Struct.new("Named", :a)`) is neither a
|
|
5175
|
+
# Symbol nor a splat, so it is no member and does not open the list.
|
|
5176
|
+
def meta_member_positionals(call_node)
|
|
4803
5177
|
raw = call_node.arguments&.arguments || []
|
|
4804
|
-
|
|
4805
|
-
symbols.filter_map { |arg| arg.unescaped.to_sym if arg.is_a?(Prism::SymbolNode) }
|
|
5178
|
+
struct_new_call?(call_node) ? struct_new_positionals(raw) : raw
|
|
4806
5179
|
end
|
|
4807
5180
|
|
|
4808
5181
|
def record_def_method(def_node, qualified_prefix, in_singleton_class, accumulator)
|
|
4809
5182
|
return if qualified_prefix.empty?
|
|
4810
5183
|
|
|
4811
5184
|
class_name = qualified_prefix.join("::")
|
|
4812
|
-
|
|
4813
|
-
|
|
5185
|
+
kind = def_side(def_node, qualified_prefix, in_singleton_class)
|
|
5186
|
+
return if kind.nil?
|
|
5187
|
+
|
|
4814
5188
|
record_method(accumulator, class_name, def_node.name, kind, Source::ParameterEnvelope.of(def_node))
|
|
4815
5189
|
end
|
|
4816
5190
|
|
|
@@ -4819,12 +5193,20 @@ module Rigor
|
|
|
4819
5193
|
# `def decorate(obj) = obj.extend(Decorator)`. A class-body macro records the same evidence in
|
|
4820
5194
|
# {#record_surface_evidence}; `send` is left out here because inside a body it is an ordinary call far
|
|
4821
5195
|
# more often than a definition.
|
|
4822
|
-
def
|
|
5196
|
+
# The side a `def`'s body evidence is filed on: a `def self.x`'s singleton side, unless the caller files it as an
|
|
5197
|
+
# instance method elsewhere (`receiver_side: false`, #3005's `Owner::<included>`).
|
|
5198
|
+
def evidence_side(def_node, side, receiver_side) = receiver_side && def_node.receiver ? :singleton : side
|
|
5199
|
+
|
|
5200
|
+
def record_def_body_evidence(def_node, qualified_prefix, tables, side, receiver_side: true)
|
|
4823
5201
|
return if def_node.body.nil?
|
|
4824
5202
|
|
|
5203
|
+
side = evidence_side(def_node, side, receiver_side)
|
|
5204
|
+
|
|
5205
|
+
definer = false
|
|
4825
5206
|
Source::NodeWalker.each(def_node.body) do |node|
|
|
4826
5207
|
if node.is_a?(Prism::SingletonClassNode)
|
|
4827
5208
|
record_singleton_body_prepends(node, qualified_prefix, tables)
|
|
5209
|
+
record_constant_singleton_prepends(node, qualified_prefix, tables, owner: true)
|
|
4828
5210
|
next
|
|
4829
5211
|
end
|
|
4830
5212
|
next unless node.is_a?(Prism::CallNode)
|
|
@@ -4832,13 +5214,629 @@ module Rigor
|
|
|
4832
5214
|
if node.name == :extend
|
|
4833
5215
|
record_object_extension(node, qualified_prefix, tables)
|
|
4834
5216
|
elsif SURFACE_EVAL_CALLS.include?(node.name) || SURFACE_NAMING_CALLS.include?(node.name)
|
|
5217
|
+
definer ||= computed_self_define_method?(node)
|
|
4835
5218
|
record_body_rewrite(node, qualified_prefix, tables)
|
|
4836
5219
|
else
|
|
4837
5220
|
record_singleton_prepend(node, qualified_prefix, tables, self_ok: true)
|
|
4838
5221
|
end
|
|
4839
5222
|
end
|
|
5223
|
+
record_computed_definer(def_node, qualified_prefix, tables, side) if definer
|
|
5224
|
+
record_hook_extends(def_node, qualified_prefix, tables, side)
|
|
5225
|
+
end
|
|
5226
|
+
|
|
5227
|
+
# Issue #2975 — an ActiveSupport::Concern-style hook, `def self.included(base) = base.extend(ClassMethods)`, puts
|
|
5228
|
+
# `ClassMethods` on every includer's singleton chain, which no `extend` edge of the includer records. The module
|
|
5229
|
+
# takes a `[ENVELOPE_HOOK_EXTEND_KIND, "ClassMethods"]` entry per constant extended on the hook's parameter, as
|
|
5230
|
+
# written, so `call.undefined-method` can resolve an includer's class-body macro calls against it.
|
|
5231
|
+
#
|
|
5232
|
+
# Issue #1592 — the entry also names the hook (`[ENVELOPE_HOOK_EXTEND_KIND, "ClassMethods", :included]`), so the
|
|
5233
|
+
# includer's singleton resolution chain draws the module only for the edge whose hook runs. Only a direct
|
|
5234
|
+
# statement of the hook body with constant arguments and no `return` / `next` / `break` in an earlier
|
|
5235
|
+
# statement is a fact the chain can place. Any other extend of the parameter anywhere in the body (nested,
|
|
5236
|
+
# conditional, after an early exit, or with an argument that is not a constant), a `base.send(:extend, …)`, a
|
|
5237
|
+
# `base.singleton_class.include` / `prepend` and a `class_eval`-family block on the parameter take a
|
|
5238
|
+
# `[ENVELOPE_HOOK_EXTEND_OPEN_KIND, hook]` entry instead of, or beside, the named one. Each definition also takes
|
|
5239
|
+
# a `[ENVELOPE_HOOK_DEF_KIND, hook, source]` entry, so a module that defines the hook twice (in two files, or
|
|
5240
|
+
# twice in one) is read as open: which definition Ruby keeps is load order.
|
|
5241
|
+
def record_hook_extends(def_node, qualified_prefix, tables, side)
|
|
5242
|
+
return unless side == :singleton && HOOK_EXTEND_DEFS.include?(def_node.name) && !qualified_prefix.empty?
|
|
5243
|
+
|
|
5244
|
+
owner = qualified_prefix.join("::")
|
|
5245
|
+
hook = def_node.name
|
|
5246
|
+
def_key = [Scope::DiscoveryIndex::ENVELOPE_HOOK_DEF_KIND, hook, def_node.slice.split.join(" ")]
|
|
5247
|
+
record_envelope(tables.envelopes, owner, def_key, Source::ParameterEnvelope::OPAQUE)
|
|
5248
|
+
param = def_node.parameters&.requireds&.first
|
|
5249
|
+
return unless param.is_a?(Prism::RequiredParameterNode)
|
|
5250
|
+
|
|
5251
|
+
target = ->(call) { hook_param_receiver?(call.receiver, param.name) }
|
|
5252
|
+
scan_hook_body(def_node.body, target, owner, tables, hook) do |node|
|
|
5253
|
+
hook_singleton_reshape?(node, param.name)
|
|
5254
|
+
end
|
|
5255
|
+
end
|
|
5256
|
+
|
|
5257
|
+
# The shared walk of a hook body or a concern's hook block: `target` says whether a call's receiver is the
|
|
5258
|
+
# includer, the block says whether a call reshapes the includer's singleton some other way.
|
|
5259
|
+
def scan_hook_body(body, target, owner, tables, hook)
|
|
5260
|
+
placeable = placeable_hook_statements(body)
|
|
5261
|
+
Source::NodeWalker.each(body) do |node|
|
|
5262
|
+
if node.is_a?(Prism::CallNode) && node.name == :extend && target.call(node)
|
|
5263
|
+
complete = record_hook_extend_arguments(node, owner, tables, hook)
|
|
5264
|
+
record_hook_extend_open(owner, tables, hook) unless complete && placeable.include?(node)
|
|
5265
|
+
elsif (node.is_a?(Prism::CallNode) && yield(node)) || singleton_mixin_body?(node)
|
|
5266
|
+
record_hook_extend_open(owner, tables, hook)
|
|
5267
|
+
end
|
|
5268
|
+
end
|
|
5269
|
+
end
|
|
5270
|
+
|
|
5271
|
+
# The direct statements of a body that run unless an earlier statement raised: those before the first
|
|
5272
|
+
# statement holding a `return`, `next` or `break` anywhere in it. An endless `def` has its one expression.
|
|
5273
|
+
def placeable_hook_statements(body)
|
|
5274
|
+
statements = body.is_a?(Prism::StatementsNode) ? body.body : [body].compact
|
|
5275
|
+
placeable = Set.new.compare_by_identity
|
|
5276
|
+
statements.each do |statement|
|
|
5277
|
+
break if exits_early?(statement)
|
|
5278
|
+
|
|
5279
|
+
placeable << statement
|
|
5280
|
+
end
|
|
5281
|
+
placeable
|
|
5282
|
+
end
|
|
5283
|
+
|
|
5284
|
+
# A `next` or `break` inside a block or lambda leaves only that block, and a lambda's `return` only the lambda;
|
|
5285
|
+
# a `return` inside a block leaves the hook.
|
|
5286
|
+
def exits_early?(node, in_block: false)
|
|
5287
|
+
case node
|
|
5288
|
+
when Prism::ReturnNode then true
|
|
5289
|
+
when Prism::NextNode, Prism::BreakNode then !in_block
|
|
5290
|
+
when Prism::LambdaNode then false
|
|
5291
|
+
else
|
|
5292
|
+
inner = in_block || node.is_a?(Prism::BlockNode)
|
|
5293
|
+
node.rigor_each_child { |child| return true if exits_early?(child, in_block: inner) }
|
|
5294
|
+
false
|
|
5295
|
+
end
|
|
5296
|
+
end
|
|
5297
|
+
|
|
5298
|
+
# A `class << self` body that mixes in, anywhere under a hook body or block.
|
|
5299
|
+
def singleton_mixin_body?(node)
|
|
5300
|
+
return false unless node.is_a?(Prism::SingletonClassNode) && node.expression.is_a?(Prism::SelfNode)
|
|
5301
|
+
|
|
5302
|
+
found = false
|
|
5303
|
+
Source::NodeWalker.each(node.body) do |child|
|
|
5304
|
+
found ||= child.is_a?(Prism::CallNode) && MIXIN_SELF_CALLS.include?(child.name) && child.receiver.nil?
|
|
5305
|
+
end
|
|
5306
|
+
found
|
|
5307
|
+
end
|
|
5308
|
+
|
|
5309
|
+
MIXIN_SELF_CALLS = %i[include prepend extend].freeze
|
|
5310
|
+
private_constant :MIXIN_SELF_CALLS
|
|
5311
|
+
|
|
5312
|
+
def hook_param_receiver?(receiver, name)
|
|
5313
|
+
receiver.is_a?(Prism::LocalVariableReadNode) && receiver.name == name
|
|
5314
|
+
end
|
|
5315
|
+
|
|
5316
|
+
# A hook-body call that may mix into the parameter's singleton class without a plain `base.extend(…)`:
|
|
5317
|
+
# `base.send(:extend, M)` (or `__send__` / `public_send`), `base.singleton_class.include(M)` / `prepend(M)`, and a
|
|
5318
|
+
# `class_eval` / `instance_eval` / `module_eval`-family call on the parameter, whose block runs with it as `self`.
|
|
5319
|
+
def hook_singleton_reshape?(node, name)
|
|
5320
|
+
receiver = node.receiver
|
|
5321
|
+
if HOOK_SEND_CALLS.include?(node.name)
|
|
5322
|
+
send_extend?(node) && hook_param_receiver?(receiver, name)
|
|
5323
|
+
elsif HOOK_EVAL_CALLS.include?(node.name)
|
|
5324
|
+
hook_param_receiver?(receiver, name)
|
|
5325
|
+
else
|
|
5326
|
+
%i[include prepend].include?(node.name) && receiver.is_a?(Prism::CallNode) &&
|
|
5327
|
+
receiver.name == :singleton_class && hook_param_receiver?(receiver.receiver, name)
|
|
5328
|
+
end
|
|
5329
|
+
end
|
|
5330
|
+
|
|
5331
|
+
HOOK_EVAL_CALLS = %i[class_eval class_exec module_eval module_exec instance_eval instance_exec].freeze
|
|
5332
|
+
private_constant :HOOK_EVAL_CALLS
|
|
5333
|
+
|
|
5334
|
+
# `send(:extend, …)`, `__send__` or `public_send`, on whatever receiver.
|
|
5335
|
+
def send_extend?(node)
|
|
5336
|
+
first = node.arguments&.arguments&.first
|
|
5337
|
+
HOOK_SEND_CALLS.include?(node.name) && first.is_a?(Prism::SymbolNode) && first.unescaped == "extend"
|
|
5338
|
+
end
|
|
5339
|
+
|
|
5340
|
+
HOOK_SEND_CALLS = %i[send __send__ public_send].freeze
|
|
5341
|
+
private_constant :HOOK_SEND_CALLS
|
|
5342
|
+
|
|
5343
|
+
# Records each constant argument as written and answers whether every argument was one.
|
|
5344
|
+
def record_hook_extend_arguments(node, owner, tables, hook)
|
|
5345
|
+
arguments = node.arguments&.arguments || []
|
|
5346
|
+
complete = !arguments.empty?
|
|
5347
|
+
arguments.each do |argument|
|
|
5348
|
+
written = Source::ConstantPath.qualified_name_or_nil(argument)
|
|
5349
|
+
next complete = false if written.nil?
|
|
5350
|
+
|
|
5351
|
+
key = [Scope::DiscoveryIndex::ENVELOPE_HOOK_EXTEND_KIND, written, hook]
|
|
5352
|
+
record_envelope(tables.envelopes, owner, key, Source::ParameterEnvelope::OPAQUE)
|
|
5353
|
+
end
|
|
5354
|
+
complete
|
|
5355
|
+
end
|
|
5356
|
+
|
|
5357
|
+
def record_hook_extend_open(owner, tables, hook)
|
|
5358
|
+
key = [Scope::DiscoveryIndex::ENVELOPE_HOOK_EXTEND_OPEN_KIND, hook]
|
|
5359
|
+
record_envelope(tables.envelopes, owner, key, Source::ParameterEnvelope::OPAQUE)
|
|
5360
|
+
end
|
|
5361
|
+
|
|
5362
|
+
HOOK_EXTEND_DEFS = %i[included prepended].to_set.freeze
|
|
5363
|
+
private_constant :HOOK_EXTEND_DEFS
|
|
5364
|
+
|
|
5365
|
+
# Issue #2997 — a module's `included` / `prepended` hook defined by a call rather than `def self.included`:
|
|
5366
|
+
# `define_singleton_method(:included) { |base| base.extend(X) }` written as a statement of the module body is
|
|
5367
|
+
# read as {#record_hook_extends} reads the `def` (a `[ENVELOPE_HOOK_DEF_KIND, hook, source]` entry, and the
|
|
5368
|
+
# block body scanned with its first parameter as the includer). (`class << self; def included(base)` already
|
|
5369
|
+
# reaches {#record_hook_extends} as a singleton `def`.) Any other call that may define a hook takes a
|
|
5370
|
+
# `[ENVELOPE_HOOK_EXTEND_OPEN_KIND, hook]` entry, for each hook a non-literal name may spell: one nested in
|
|
5371
|
+
# a block, method or condition, one without a block of one plain parameter, `send(:define_singleton_method,
|
|
5372
|
+
# …)`, `singleton_class.define_method(…)`, and `define_method` in the body of `class << self`.
|
|
5373
|
+
def record_hook_definer_calls(node, child_prefix, methods_acc)
|
|
5374
|
+
return unless node.is_a?(Prism::ModuleNode) && node.body.is_a?(Prism::StatementsNode)
|
|
5375
|
+
|
|
5376
|
+
owner = child_prefix.join("::")
|
|
5377
|
+
direct = node.body.body
|
|
5378
|
+
each_hook_definer(node.body, false) do |call, hooks|
|
|
5379
|
+
tables = methods_acc.at(call)
|
|
5380
|
+
param = readable_hook_parameter(call) if direct.any? { |statement| statement.equal?(call) }
|
|
5381
|
+
if param
|
|
5382
|
+
record_hook_block(call, param, owner, tables, hooks.first)
|
|
5383
|
+
else
|
|
5384
|
+
hooks.each { |hook| record_hook_extend_open(owner, tables, hook) }
|
|
5385
|
+
end
|
|
5386
|
+
end
|
|
4840
5387
|
end
|
|
4841
5388
|
|
|
5389
|
+
# The entries of one readable `define_singleton_method(:hook) { |base| … }`.
|
|
5390
|
+
def record_hook_block(call, param, owner, tables, hook)
|
|
5391
|
+
def_key = [Scope::DiscoveryIndex::ENVELOPE_HOOK_DEF_KIND, hook, call.slice.split.join(" ")]
|
|
5392
|
+
record_envelope(tables.envelopes, owner, def_key, Source::ParameterEnvelope::OPAQUE)
|
|
5393
|
+
body = call.block&.body
|
|
5394
|
+
return if body.nil?
|
|
5395
|
+
|
|
5396
|
+
target = ->(extend_call) { hook_param_receiver?(extend_call.receiver, param.name) }
|
|
5397
|
+
scan_hook_body(body, target, owner, tables, hook) { |inner| hook_singleton_reshape?(inner, param.name) }
|
|
5398
|
+
end
|
|
5399
|
+
|
|
5400
|
+
# Yields each call under a module body that may define the module's `included` / `prepended`, with the hooks
|
|
5401
|
+
# it may define. A nested class or module body has its own self, and so has an instance `def`'s body; in a
|
|
5402
|
+
# `class << self` body `define_method` defines on the module and `define_singleton_method` on its singleton
|
|
5403
|
+
# class, and in a singleton `def`'s body `self` is the module again.
|
|
5404
|
+
def each_hook_definer(node, in_singleton, &)
|
|
5405
|
+
case node
|
|
5406
|
+
when Prism::ClassNode, Prism::ModuleNode then nil
|
|
5407
|
+
when Prism::SingletonClassNode
|
|
5408
|
+
each_hook_definer(node.expression, in_singleton, &)
|
|
5409
|
+
each_hook_definer(node.body, !in_singleton && node.expression.is_a?(Prism::SelfNode), &)
|
|
5410
|
+
when Prism::DefNode
|
|
5411
|
+
each_hook_definer(node.body, false, &) if in_singleton || node.receiver.is_a?(Prism::SelfNode)
|
|
5412
|
+
else
|
|
5413
|
+
if node.is_a?(Prism::CallNode)
|
|
5414
|
+
hooks = hook_definer_names(node, in_singleton)
|
|
5415
|
+
yield node, hooks unless hooks.nil?
|
|
5416
|
+
end
|
|
5417
|
+
node&.rigor_each_child { |child| each_hook_definer(child, in_singleton, &) }
|
|
5418
|
+
end
|
|
5419
|
+
end
|
|
5420
|
+
|
|
5421
|
+
# The hooks a call may define on the module whose body runs it, or nil for a call that defines none.
|
|
5422
|
+
def hook_definer_names(node, in_singleton)
|
|
5423
|
+
name_node = hook_definer_name_node(node, in_singleton)
|
|
5424
|
+
return nil if name_node.nil?
|
|
5425
|
+
|
|
5426
|
+
name = literal_method_name(name_node)
|
|
5427
|
+
return HOOK_EXTEND_DEFS.include?(name) ? [name] : nil if name
|
|
5428
|
+
|
|
5429
|
+
hooks = HOOK_EXTEND_DEFS.select { |hook| hook.start_with?(literal_name_prefix(name_node)) }
|
|
5430
|
+
hooks.empty? ? nil : hooks
|
|
5431
|
+
end
|
|
5432
|
+
|
|
5433
|
+
# The literal text an interpolated name starts with (`"is_"` for `"is_#{n}"`), or `""` for a name computed any
|
|
5434
|
+
# other way: only a hook whose name starts with it may be the one defined.
|
|
5435
|
+
def literal_name_prefix(node)
|
|
5436
|
+
return "" unless node.is_a?(Prism::InterpolatedSymbolNode) || node.is_a?(Prism::InterpolatedStringNode)
|
|
5437
|
+
|
|
5438
|
+
first = node.parts.first
|
|
5439
|
+
first.is_a?(Prism::StringNode) ? first.unescaped : ""
|
|
5440
|
+
end
|
|
5441
|
+
|
|
5442
|
+
# The argument naming the method a call that may define a singleton method of the module defines, or nil.
|
|
5443
|
+
def hook_definer_name_node(node, in_singleton)
|
|
5444
|
+
arguments = node.arguments&.arguments || []
|
|
5445
|
+
if HOOK_SEND_CALLS.include?(node.name) && self_receiver?(node.receiver)
|
|
5446
|
+
definer = literal_method_name(arguments.first)
|
|
5447
|
+
arguments[1] if definer == :define_singleton_method || (in_singleton && definer == :define_method)
|
|
5448
|
+
elsif singleton_definer_call?(node, in_singleton)
|
|
5449
|
+
arguments.first
|
|
5450
|
+
end
|
|
5451
|
+
end
|
|
5452
|
+
|
|
5453
|
+
def singleton_definer_call?(node, in_singleton)
|
|
5454
|
+
receiver = node.receiver
|
|
5455
|
+
case node.name
|
|
5456
|
+
when :define_singleton_method then !in_singleton && self_receiver?(receiver)
|
|
5457
|
+
when :define_method
|
|
5458
|
+
(in_singleton && self_receiver?(receiver)) ||
|
|
5459
|
+
(receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class && self_receiver?(receiver.receiver))
|
|
5460
|
+
else false
|
|
5461
|
+
end
|
|
5462
|
+
end
|
|
5463
|
+
|
|
5464
|
+
def self_receiver?(receiver) = receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
5465
|
+
|
|
5466
|
+
# The includer parameter of a `define_singleton_method(:hook) { |base| … }` the hook read can place: one literal
|
|
5467
|
+
# hook name, a literal block, and a plain first parameter. Nil for any other shape.
|
|
5468
|
+
def readable_hook_parameter(call)
|
|
5469
|
+
arguments = call.arguments&.arguments || []
|
|
5470
|
+
return nil unless call.name == :define_singleton_method && arguments.size == 1
|
|
5471
|
+
return nil unless literal_method_name(arguments.first) && call.block.is_a?(Prism::BlockNode)
|
|
5472
|
+
|
|
5473
|
+
params = call.block.parameters
|
|
5474
|
+
param = params.parameters&.requireds&.first if params.is_a?(Prism::BlockParametersNode)
|
|
5475
|
+
param.is_a?(Prism::RequiredParameterNode) ? param : nil
|
|
5476
|
+
end
|
|
5477
|
+
|
|
5478
|
+
# Issue #2986 — a module whose body runs `extend ActiveSupport::Concern` has `Concern#append_features` (or
|
|
5479
|
+
# `prepend_features`) extend each direct includer with `const_get(:ClassMethods)`, as #2975's hook does with its
|
|
5480
|
+
# constant, so the module takes a `[ENVELOPE_HOOK_EXTEND_KIND, "ClassMethods", :const_get]` entry. A
|
|
5481
|
+
# `class_methods do … end` block of that body is a `module_eval` of `Owner::ClassMethods`: each `def` in it is
|
|
5482
|
+
# also recorded on that module, with its body's evidence (a computed definer's entry among it), so the hook
|
|
5483
|
+
# reader resolves and reads it. The walk keeps recording the same `def` on the module itself as before. A call on
|
|
5484
|
+
# `self` in an `included do … end` or `prepended do … end` block takes a
|
|
5485
|
+
# `[ENVELOPE_HOOK_BODY_CALL_KIND, hook, name]` entry, since Ruby runs that block in the includer's class body.
|
|
5486
|
+
#
|
|
5487
|
+
# Issue #1592 — the `def` node is filed on `Owner::ClassMethods` too, so the includer's singleton resolution
|
|
5488
|
+
# chain, which holds that module, answers a class-side call with the `def`'s own return type.
|
|
5489
|
+
#
|
|
5490
|
+
# Issue #2997 — a `class_methods` block is read in every body of the module, not only one that runs `extend
|
|
5491
|
+
# ActiveSupport::Concern` itself: a reopening in another file runs it on a module that is already a concern, and
|
|
5492
|
+
# where it loads first Ruby raises `NoMethodError` at load, so no running program has the includer without the
|
|
5493
|
+
# method. The module is a concern, and its includers draw on `Owner::ClassMethods`, only when some body records
|
|
5494
|
+
# the concern entry.
|
|
5495
|
+
def record_concern_class_methods(node, child_prefix, methods_acc, def_nodes_acc)
|
|
5496
|
+
return unless node.is_a?(Prism::ModuleNode) && node.body.is_a?(Prism::StatementsNode)
|
|
5497
|
+
|
|
5498
|
+
concern = concern_body?(node.body, child_prefix)
|
|
5499
|
+
owner = child_prefix.join("::")
|
|
5500
|
+
if concern
|
|
5501
|
+
key = [Scope::DiscoveryIndex::ENVELOPE_HOOK_EXTEND_KIND, "ClassMethods", :const_get]
|
|
5502
|
+
record_envelope(methods_acc.at(node).envelopes, owner, key, Source::ParameterEnvelope::OPAQUE)
|
|
5503
|
+
record_concern_dependencies(node.body.body, child_prefix, owner, methods_acc)
|
|
5504
|
+
end
|
|
5505
|
+
node.body.body.each do |statement|
|
|
5506
|
+
next unless statement.is_a?(Prism::CallNode) && block_statement?(statement)
|
|
5507
|
+
|
|
5508
|
+
if statement.name == :class_methods
|
|
5509
|
+
record_class_methods_block(statement, [*child_prefix, "ClassMethods"], methods_acc, def_nodes_acc)
|
|
5510
|
+
elsif CONCERN_HOOK_BLOCKS.include?(statement.name)
|
|
5511
|
+
hook = concern ? statement.name : Scope::DiscoveryIndex::CONCERN_BLOCK_HOOKS.fetch(statement.name)
|
|
5512
|
+
record_concern_hook_block(statement, hook, owner, methods_acc)
|
|
5513
|
+
record_hook_block_singleton_defs(statement, hook, child_prefix, methods_acc, def_nodes_acc)
|
|
5514
|
+
end
|
|
5515
|
+
end
|
|
5516
|
+
end
|
|
5517
|
+
|
|
5518
|
+
# Issue #3005 — a `def self.x` in an `included do … end` (or `prepended do … end`) block defines `x` on the
|
|
5519
|
+
# includer's singleton class, since Ruby runs the block in the includer's class body. Each such `def` is filed
|
|
5520
|
+
# as an instance method of the module's `Owner::<included>` (`<prepended>`), a name no program can spell, and
|
|
5521
|
+
# the module takes a `[ENVELOPE_HOOK_EXTEND_KIND, "<included>", hook]` entry, so the includer's singleton
|
|
5522
|
+
# resolution chain holds that module where the hook's own extends go: after the includer's singleton and any
|
|
5523
|
+
# module prepended to it, ahead of the concern's `ClassMethods`.
|
|
5524
|
+
#
|
|
5525
|
+
# Issue #3015 — a receiverless `def` written as a statement of a `class << self` body in the block (found with
|
|
5526
|
+
# {#each_class_methods_def}'s stops) is filed the same way. A `class << self` body with a statement-level
|
|
5527
|
+
# visibility call (`private`, `private :x`, `private def x`) may make a `def` non-public, which the module does
|
|
5528
|
+
# not record, so it files none of its `def`s and opens the hook instead ({#record_hook_extend_open}).
|
|
5529
|
+
def record_hook_block_singleton_defs(statement, hook, child_prefix, methods_acc, def_nodes_acc)
|
|
5530
|
+
segment = "<#{statement.name}>"
|
|
5531
|
+
prefix = [*child_prefix, segment]
|
|
5532
|
+
name = prefix.join("::")
|
|
5533
|
+
owner = child_prefix.join("::")
|
|
5534
|
+
defs = hook_block_singleton_defs(statement.block.body, owner, hook, methods_acc)
|
|
5535
|
+
return if defs.empty?
|
|
5536
|
+
|
|
5537
|
+
defs.each do |def_node|
|
|
5538
|
+
record_method(methods_acc.at(def_node), name, def_node.name, :instance, Source::ParameterEnvelope.of(def_node))
|
|
5539
|
+
(def_nodes_acc[name] ||= {})[def_node.name] = def_node
|
|
5540
|
+
record_def_body_evidence(def_node, prefix, methods_acc, :instance, receiver_side: false)
|
|
5541
|
+
end
|
|
5542
|
+
key = [Scope::DiscoveryIndex::ENVELOPE_HOOK_EXTEND_KIND, segment, hook]
|
|
5543
|
+
record_envelope(methods_acc.at(statement).envelopes, owner, key, Source::ParameterEnvelope::OPAQUE)
|
|
5544
|
+
end
|
|
5545
|
+
|
|
5546
|
+
# The `def`s {#record_hook_block_singleton_defs} files, opening the hook of `owner` for each `class << self` body
|
|
5547
|
+
# with a visibility call.
|
|
5548
|
+
def hook_block_singleton_defs(body, owner, hook, methods_acc)
|
|
5549
|
+
defs = []
|
|
5550
|
+
each_class_methods_def(body) { |def_node| defs << def_node if def_node.receiver.is_a?(Prism::SelfNode) }
|
|
5551
|
+
each_hook_block_singleton_class(body) do |singleton|
|
|
5552
|
+
statements = statements_of(singleton.body)
|
|
5553
|
+
if statements.any? { |child| singleton_body_visibility_call?(child) }
|
|
5554
|
+
record_hook_extend_open(owner, methods_acc.at(singleton), hook)
|
|
5555
|
+
else
|
|
5556
|
+
statements.each { |child| defs << child if child.is_a?(Prism::DefNode) && child.receiver.nil? }
|
|
5557
|
+
end
|
|
5558
|
+
end
|
|
5559
|
+
defs
|
|
5560
|
+
end
|
|
5561
|
+
|
|
5562
|
+
# Issue #3015 — the `class << self` bodies of an `included` / `prepended` block, with {#each_class_methods_def}'s
|
|
5563
|
+
# stops.
|
|
5564
|
+
def each_hook_block_singleton_class(node, &)
|
|
5565
|
+
case node
|
|
5566
|
+
when Prism::SingletonClassNode then yield node if node.expression.is_a?(Prism::SelfNode)
|
|
5567
|
+
when *CONCERN_BLOCK_STOPS then nil
|
|
5568
|
+
else node&.rigor_each_child { |child| each_hook_block_singleton_class(child, &) }
|
|
5569
|
+
end
|
|
5570
|
+
end
|
|
5571
|
+
|
|
5572
|
+
SINGLETON_BODY_VISIBILITY_CALLS = %i[private protected public private_class_method public_class_method].freeze
|
|
5573
|
+
private_constant :SINGLETON_BODY_VISIBILITY_CALLS
|
|
5574
|
+
|
|
5575
|
+
def singleton_body_visibility_call?(statement)
|
|
5576
|
+
statement.is_a?(Prism::CallNode) && statement.receiver.nil? &&
|
|
5577
|
+
SINGLETON_BODY_VISIBILITY_CALLS.include?(statement.name)
|
|
5578
|
+
end
|
|
5579
|
+
|
|
5580
|
+
def record_class_methods_block(statement, class_methods_prefix, methods_acc, def_nodes_acc)
|
|
5581
|
+
each_class_methods_def(statement.block&.body) do |def_node|
|
|
5582
|
+
record_def_method(def_node, class_methods_prefix, false, methods_acc.at(def_node))
|
|
5583
|
+
record_def_node(def_node, class_methods_prefix, false, def_nodes_acc)
|
|
5584
|
+
record_def_body_evidence(def_node, class_methods_prefix, methods_acc, :instance)
|
|
5585
|
+
end
|
|
5586
|
+
end
|
|
5587
|
+
|
|
5588
|
+
# Issue #2992 — `Concern#append_features` (or `prepend_features`) of a concern `include`d (or `prepend`ed) into a
|
|
5589
|
+
# module that has already run `extend ActiveSupport::Concern` records it as a dependency of that module and
|
|
5590
|
+
# includes nothing; the module's final includer includes it. Only a constant argument of an `include` /
|
|
5591
|
+
# `prepend` on the module's own `self` ({#concern_dependency_arguments}) written as a statement of the body
|
|
5592
|
+
# AFTER the body's own `extend` is provably such a dependency: one written ahead of it, or in a reopening
|
|
5593
|
+
# without it, may run first and is an ordinary include.
|
|
5594
|
+
# Each such constant takes a `[ENVELOPE_CONCERN_DEPENDENCY_KIND, written]` entry; whether it names a concern is
|
|
5595
|
+
# the reader's question. One statement's constants are recorded last first: `include A, B` runs
|
|
5596
|
+
# `B.append_features` first, so the dependencies list it ahead, and a reader that reverses the entries to put a
|
|
5597
|
+
# later statement's nearer keeps one statement's in argument order, as Ruby searches them.
|
|
5598
|
+
def record_concern_dependencies(statements, child_prefix, owner, methods_acc)
|
|
5599
|
+
extend_at = statements.index { |statement| concern_extend?(statement, child_prefix) }
|
|
5600
|
+
statements.drop(extend_at + 1).each do |statement|
|
|
5601
|
+
arguments = concern_dependency_arguments(statement)
|
|
5602
|
+
next if arguments.nil?
|
|
5603
|
+
|
|
5604
|
+
arguments.reverse_each do |argument|
|
|
5605
|
+
written = Source::ConstantPath.qualified_name_or_nil(argument)
|
|
5606
|
+
next if written.nil?
|
|
5607
|
+
|
|
5608
|
+
key = [Scope::DiscoveryIndex::ENVELOPE_CONCERN_DEPENDENCY_KIND, written]
|
|
5609
|
+
record_envelope(methods_acc.at(statement).envelopes, owner, key, Source::ParameterEnvelope::OPAQUE)
|
|
5610
|
+
end
|
|
5611
|
+
end
|
|
5612
|
+
end
|
|
5613
|
+
|
|
5614
|
+
# The arguments of a statement that `include`s or `prepend`s on the module's own `self`, or nil. Issue #2995 —
|
|
5615
|
+
# `self.include Dep` and a receiverless or `self.` `send(:include, Dep)` run the same `append_features` as a
|
|
5616
|
+
# bare `include Dep`, and the include walk records them as it does.
|
|
5617
|
+
def concern_dependency_arguments(statement)
|
|
5618
|
+
return nil unless statement.is_a?(Prism::CallNode) && statement.block.nil? &&
|
|
5619
|
+
lexical_self_receiver?(statement.receiver)
|
|
5620
|
+
|
|
5621
|
+
kind, arguments, = mixin_call_view(statement)
|
|
5622
|
+
arguments if MIXIN_CALL_NAMES.include?(kind)
|
|
5623
|
+
end
|
|
5624
|
+
|
|
5625
|
+
# Issue #1592 — an `extend` on `self` in the block extends the includer as `base.extend` in a hook does, so it
|
|
5626
|
+
# records the same entries, named by the block's hook ({#record_hook_extends}), and any `extend` nested in the
|
|
5627
|
+
# block, a `class << self` mixin or a `send(:extend, …)` there is open.
|
|
5628
|
+
#
|
|
5629
|
+
# Issue #3005 — read in every body of the module, as a `class_methods` block is: in a body that does not run
|
|
5630
|
+
# `extend ActiveSupport::Concern` itself the entries name the block's gated hook
|
|
5631
|
+
# ({Scope::DiscoveryIndex::CONCERN_BLOCK_HOOKS}), which a reader takes as the plain hook only for a concern.
|
|
5632
|
+
def record_concern_hook_block(statement, hook, owner, methods_acc)
|
|
5633
|
+
body = statement.block.body
|
|
5634
|
+
each_self_call(body) do |call|
|
|
5635
|
+
entry = [Scope::DiscoveryIndex::ENVELOPE_HOOK_BODY_CALL_KIND, hook, call.name]
|
|
5636
|
+
record_envelope(methods_acc.at(call).envelopes, owner, entry, Source::ParameterEnvelope::OPAQUE)
|
|
5637
|
+
end
|
|
5638
|
+
return if body.nil?
|
|
5639
|
+
|
|
5640
|
+
target = ->(call) { call.receiver.nil? || call.receiver.is_a?(Prism::SelfNode) }
|
|
5641
|
+
scan_hook_body(body, target, owner, methods_acc.at(statement), hook) { |call| send_extend?(call) }
|
|
5642
|
+
end
|
|
5643
|
+
|
|
5644
|
+
CONCERN_HOOK_BLOCKS = %i[included prepended].to_set.freeze
|
|
5645
|
+
HOOK_BLOCK_CALLS = (CONCERN_HOOK_BLOCKS | [:class_methods]).freeze
|
|
5646
|
+
|
|
5647
|
+
# Issue #3015 — the blocks of the `included` / `prepended` / `class_methods` statements of a module body that
|
|
5648
|
+
# runs `extend ActiveSupport::Concern` itself; none for any other body, whose `included` may be its own DSL. A
|
|
5649
|
+
# reopening without the `extend` is not read.
|
|
5650
|
+
def concern_hook_blocks(node, child_prefix)
|
|
5651
|
+
return [] unless node.is_a?(Prism::ModuleNode) && concern_body?(node.body, child_prefix)
|
|
5652
|
+
|
|
5653
|
+
node.body.body.filter_map do |statement|
|
|
5654
|
+
statement.block if block_statement?(statement) && HOOK_BLOCK_CALLS.include?(statement.name)
|
|
5655
|
+
end
|
|
5656
|
+
end
|
|
5657
|
+
|
|
5658
|
+
# Issue #3015 — whether `node` is a `class << self` in a block {HookBlockSpans#mark_hook_blocks} marked for the
|
|
5659
|
+
# module the walk is in: the defs of its body are not the module's own singleton methods, and the walks do not
|
|
5660
|
+
# file them there.
|
|
5661
|
+
def hook_block_singleton?(node, in_singleton_class, owner_prefix, accumulator)
|
|
5662
|
+
!in_singleton_class && node.expression.is_a?(Prism::SelfNode) && accumulator.hook_block?(node, owner_prefix)
|
|
5663
|
+
end
|
|
5664
|
+
private_constant :CONCERN_HOOK_BLOCKS, :HOOK_BLOCK_CALLS
|
|
5665
|
+
|
|
5666
|
+
# Whether a module body runs `extend ActiveSupport::Concern` as one of its own statements (`extend Concern`
|
|
5667
|
+
# in a module nested in `module ActiveSupport`).
|
|
5668
|
+
def concern_body?(body, child_prefix)
|
|
5669
|
+
return false unless body.is_a?(Prism::StatementsNode)
|
|
5670
|
+
|
|
5671
|
+
body.body.any? { |statement| concern_extend?(statement, child_prefix) }
|
|
5672
|
+
end
|
|
5673
|
+
|
|
5674
|
+
#
|
|
5675
|
+
# Issue #3005 — `self.extend ActiveSupport::Concern` and a receiverless or `self.` `send` / `public_send` /
|
|
5676
|
+
# `__send__(:extend, ActiveSupport::Concern)` run the same `extend` on the module ({#mixin_call_view}).
|
|
5677
|
+
def concern_extend?(statement, child_prefix)
|
|
5678
|
+
return false unless statement.is_a?(Prism::CallNode) && lexical_self_receiver?(statement.receiver)
|
|
5679
|
+
|
|
5680
|
+
kind, arguments, = mixin_call_view(statement)
|
|
5681
|
+
return false unless kind == :extend
|
|
5682
|
+
|
|
5683
|
+
arguments.any? do |argument|
|
|
5684
|
+
written = Source::ConstantPath.qualified_name_or_nil(argument)&.delete_prefix("::")
|
|
5685
|
+
written == "ActiveSupport::Concern" || (written == "Concern" && child_prefix.first == "ActiveSupport")
|
|
5686
|
+
end
|
|
5687
|
+
end
|
|
5688
|
+
|
|
5689
|
+
# A receiverless block call (`class_methods do … end`, `included do … end`). Concern's methods take no
|
|
5690
|
+
# positional argument here (`class_methods` raises on one), so arguments are not looked at.
|
|
5691
|
+
def block_statement?(node)
|
|
5692
|
+
node.is_a?(Prism::CallNode) && node.receiver.nil? && node.block.is_a?(Prism::BlockNode)
|
|
5693
|
+
end
|
|
5694
|
+
|
|
5695
|
+
# The `def`s a `class_methods` block body writes, directly or wrapped in a call (`private def x`), not those in
|
|
5696
|
+
# a nested `def`, class, module, singleton-class, block or lambda body, which define elsewhere or later.
|
|
5697
|
+
def each_class_methods_def(node, &)
|
|
5698
|
+
case node
|
|
5699
|
+
when Prism::DefNode then yield node
|
|
5700
|
+
when *CONCERN_BLOCK_STOPS then nil
|
|
5701
|
+
else node&.rigor_each_child { |child| each_class_methods_def(child, &) }
|
|
5702
|
+
end
|
|
5703
|
+
end
|
|
5704
|
+
|
|
5705
|
+
# The calls on `self` (no receiver, or `self.`) an `included` / `prepended` block body makes, not those in a
|
|
5706
|
+
# nested `def`, class, module, singleton-class or lambda body.
|
|
5707
|
+
#
|
|
5708
|
+
# Issue #2992 — a block that keeps `self` and runs as the block body runs (`[1].each { my_c :x }`) makes its
|
|
5709
|
+
# calls on the includer too, so the walk enters it ({#self_call_block?}). Any other block stays out; its call's
|
|
5710
|
+
# receiver and arguments are still read.
|
|
5711
|
+
def each_self_call(node, &)
|
|
5712
|
+
case node
|
|
5713
|
+
when *SELF_CALL_STOPS then nil
|
|
5714
|
+
when Prism::CallNode
|
|
5715
|
+
yield node if node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
|
5716
|
+
if node.block.is_a?(Prism::BlockNode) && !self_call_block?(node)
|
|
5717
|
+
[node.receiver, node.arguments].each { |part| each_self_call(part, &) }
|
|
5718
|
+
else
|
|
5719
|
+
node.rigor_each_child { |child| each_self_call(child, &) }
|
|
5720
|
+
end
|
|
5721
|
+
else node&.rigor_each_child { |child| each_self_call(child, &) }
|
|
5722
|
+
end
|
|
5723
|
+
end
|
|
5724
|
+
|
|
5725
|
+
# Whether a call's block may run on the includer while the hook block runs. Every block may, since leaving one
|
|
5726
|
+
# out would report the macro's names, except one that rebinds `self` ({#rebinds_self_block?}) and the calls
|
|
5727
|
+
# known to keep a block for later: a receiverless `lambda` / `proc` and `Proc.new`, which run it only when
|
|
5728
|
+
# called, and `Kernel.lambda` / `Kernel.proc`; `on_load` on any receiver, which runs it when a framework
|
|
5729
|
+
# component loads; and a receiverless framework callback macro (`after_save { }`), which runs it on an instance.
|
|
5730
|
+
#
|
|
5731
|
+
# Issue #3000 — the callback stop is a list of the Rails callback macros, not a `before_` / `after_` /
|
|
5732
|
+
# `around_` prefix: a project macro so named may yield at once (`def self.after_foo = yield`), and entering a
|
|
5733
|
+
# block that runs later only loses a report.
|
|
5734
|
+
def self_call_block?(node)
|
|
5735
|
+
return false if rebinds_self_block?(node)
|
|
5736
|
+
return false if node.name == :on_load
|
|
5737
|
+
|
|
5738
|
+
receiver = node.receiver
|
|
5739
|
+
if receiver.nil?
|
|
5740
|
+
!DEFERRED_BLOCK_CALLS.include?(node.name) && !CALLBACK_MACROS.include?(node.name)
|
|
5741
|
+
elsif DEFERRED_BLOCK_CALLS.include?(node.name)
|
|
5742
|
+
!constant_named?(receiver, "Kernel")
|
|
5743
|
+
else
|
|
5744
|
+
!(node.name == :new && constant_named?(receiver, "Proc"))
|
|
5745
|
+
end
|
|
5746
|
+
end
|
|
5747
|
+
|
|
5748
|
+
def constant_named?(node, name)
|
|
5749
|
+
Source::ConstantPath.qualified_name_or_nil(node)&.delete_prefix("::") == name
|
|
5750
|
+
end
|
|
5751
|
+
|
|
5752
|
+
DEFERRED_BLOCK_CALLS = %i[lambda proc].freeze
|
|
5753
|
+
# The callback macros of ActiveModel (validations), ActiveRecord (`Callbacks::CALLBACKS`, transactions),
|
|
5754
|
+
# ActiveJob (callbacks, `after_discard`), AbstractController (with its `prepend_` / `append_` forms),
|
|
5755
|
+
# ActionMailer, ActionMailbox, ActionCable (channel, connection), ActiveSupport::CurrentAttributes and
|
|
5756
|
+
# ActiveSupport::Reloader, read from Rails 8.1.3.
|
|
5757
|
+
CALLBACK_MACROS = %i[
|
|
5758
|
+
before_validation after_validation
|
|
5759
|
+
after_initialize after_find after_touch
|
|
5760
|
+
before_save around_save after_save before_create around_create after_create
|
|
5761
|
+
before_update around_update after_update before_destroy around_destroy after_destroy
|
|
5762
|
+
before_commit after_commit after_rollback
|
|
5763
|
+
after_save_commit after_create_commit after_update_commit after_destroy_commit
|
|
5764
|
+
before_perform around_perform after_perform before_enqueue around_enqueue after_enqueue after_discard
|
|
5765
|
+
before_action around_action after_action
|
|
5766
|
+
prepend_before_action prepend_around_action prepend_after_action
|
|
5767
|
+
append_before_action append_around_action append_after_action
|
|
5768
|
+
before_processing around_processing after_processing
|
|
5769
|
+
before_reset after_reset before_class_unload after_class_unload
|
|
5770
|
+
before_deliver around_deliver after_deliver
|
|
5771
|
+
before_subscribe after_subscribe before_unsubscribe after_unsubscribe
|
|
5772
|
+
before_command around_command after_command
|
|
5773
|
+
].to_set.freeze
|
|
5774
|
+
private_constant :DEFERRED_BLOCK_CALLS, :CALLBACK_MACROS
|
|
5775
|
+
|
|
5776
|
+
CONCERN_BLOCK_STOPS = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode,
|
|
5777
|
+
Prism::BlockNode, Prism::LambdaNode].freeze
|
|
5778
|
+
SELF_CALL_STOPS = (CONCERN_BLOCK_STOPS - [Prism::BlockNode]).freeze
|
|
5779
|
+
private_constant :CONCERN_BLOCK_STOPS, :SELF_CALL_STOPS
|
|
5780
|
+
|
|
5781
|
+
# Issue #2901 — a method whose body runs a `define_method` on its own `self` with a computed name, directly or in
|
|
5782
|
+
# a block that keeps `self` (`def self.my_attr(*n) = n.each { |x| define_method(x) {} }`), is a class macro: a
|
|
5783
|
+
# class body that calls it gains members no table names. The method takes a
|
|
5784
|
+
# `[ENVELOPE_COMPUTED_DEFINER_KIND, side, name]` entry, which `call.undefined-method` resolves a class's body
|
|
5785
|
+
# calls against. A `def Other.x` (another object's method) records none, and neither does a `define_method` inside
|
|
5786
|
+
# a nested `def`, a class body or a block that rebinds `self` ({#rebinds_self_block?}).
|
|
5787
|
+
#
|
|
5788
|
+
# Issue #2975 — the entry carries the side the method is defined on (`:singleton` for `def self.x` or a
|
|
5789
|
+
# `class << self` body, `:instance` otherwise), so a body call resolves only against a macro on the side its
|
|
5790
|
+
# singleton chain reaches.
|
|
5791
|
+
def record_computed_definer(def_node, qualified_prefix, tables, side)
|
|
5792
|
+
return if qualified_prefix.empty?
|
|
5793
|
+
return unless def_node.receiver.nil? || def_node.receiver.is_a?(Prism::SelfNode)
|
|
5794
|
+
return unless runs_computed_self_definer?(def_node.body)
|
|
5795
|
+
|
|
5796
|
+
key = [Scope::DiscoveryIndex::ENVELOPE_COMPUTED_DEFINER_KIND, side, def_node.name]
|
|
5797
|
+
record_envelope(tables.envelopes, qualified_prefix.join("::"), key, Source::ParameterEnvelope::OPAQUE)
|
|
5798
|
+
end
|
|
5799
|
+
|
|
5800
|
+
def computed_self_define_method?(node)
|
|
5801
|
+
node.name == :define_method && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)) &&
|
|
5802
|
+
computed_first_name?(node.arguments&.arguments)
|
|
5803
|
+
end
|
|
5804
|
+
|
|
5805
|
+
def runs_computed_self_definer?(node)
|
|
5806
|
+
case node
|
|
5807
|
+
when Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode
|
|
5808
|
+
return false
|
|
5809
|
+
when Prism::CallNode
|
|
5810
|
+
return true if computed_self_define_method?(node)
|
|
5811
|
+
# Issue #2975 — `include(Module.new { define_method(x) {} })` on `self` puts the computed method on `self`'s
|
|
5812
|
+
# instance side, as a direct `define_method(x)` would.
|
|
5813
|
+
return true if MODULE_NEW_MIXIN_CALLS.include?(node.name) &&
|
|
5814
|
+
(node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)) &&
|
|
5815
|
+
module_new_definer_argument?(node)
|
|
5816
|
+
if rebinds_self_block?(node)
|
|
5817
|
+
return [node.receiver, node.arguments].any? do |part|
|
|
5818
|
+
runs_computed_self_definer?(part)
|
|
5819
|
+
end
|
|
5820
|
+
end
|
|
5821
|
+
end
|
|
5822
|
+
return false unless node.is_a?(Prism::Node)
|
|
5823
|
+
|
|
5824
|
+
node.rigor_each_child { |child| return true if runs_computed_self_definer?(child) }
|
|
5825
|
+
false
|
|
5826
|
+
end
|
|
5827
|
+
|
|
5828
|
+
# A call whose block runs with another `self`: a class factory (`Class.new { }`), a definer's own body
|
|
5829
|
+
# (`define_method(:x) { }` runs on an instance), or an eval on a receiver other than `self`.
|
|
5830
|
+
def rebinds_self_block?(node)
|
|
5831
|
+
return false unless node.block.is_a?(Prism::BlockNode)
|
|
5832
|
+
return true if meta_new_constant_rvalue?(node) || SELF_DEFINER_BLOCK_CALLS.include?(node.name)
|
|
5833
|
+
|
|
5834
|
+
RECEIVER_EVAL_CALLS.include?(node.name) && !(node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
|
|
5835
|
+
end
|
|
5836
|
+
|
|
5837
|
+
SELF_DEFINER_BLOCK_CALLS = %i[define_method define_singleton_method].freeze
|
|
5838
|
+
private_constant :SELF_DEFINER_BLOCK_CALLS
|
|
5839
|
+
|
|
4842
5840
|
# Issue #1820 — `obj.singleton_class.prepend(M)`, or `prepend M` in a `class << obj` body, puts `M` ahead of
|
|
4843
5841
|
# `obj`'s class even when the class already includes it (`prepend` re-inserts it; `extend` would not), so
|
|
4844
5842
|
# `M`'s refinement answers first for that object. `M` takes {Scope::DiscoveryIndex::ENVELOPE_OBJECT_EXTENDED_MARK},
|
|
@@ -4856,6 +5854,56 @@ module Rigor
|
|
|
4856
5854
|
record_object_extension(node, qualified_prefix, tables) if singleton
|
|
4857
5855
|
end
|
|
4858
5856
|
|
|
5857
|
+
# Issue #2998 — a prepend to the class object's own singleton class, `prepend P` in a `class << self` body or
|
|
5858
|
+
# `singleton_class.prepend P` / `singleton_class.send(:prepend, P)` in the class body, files a
|
|
5859
|
+
# `[ENVELOPE_SINGLETON_PREPEND_KIND, "P"]` entry per constant argument, as the extends walk spells it, under the
|
|
5860
|
+
# owner the walk passes in. The extends walk records the same edge in `discovered_extends`, and the singleton
|
|
5861
|
+
# resolution chain reads the entries that match one of those edges as prepends.
|
|
5862
|
+
#
|
|
5863
|
+
# A constant receiver's `Const.singleton_class.prepend P` (or its `send` spelling) files the entry on every name
|
|
5864
|
+
# the constant can denote from the call site, as {#record_body_rewrite} marks it; the extends walk records that
|
|
5865
|
+
# edge unpositioned, so the chain's reading of it is master's and the typing read declines where they differ.
|
|
5866
|
+
# `prefixes` is `[owner_prefix, qualified_prefix]`: the class `self` names (empty where it names none) and the
|
|
5867
|
+
# lexical prefix a constant resolves against.
|
|
5868
|
+
#
|
|
5869
|
+
# Issue #3020 — an argument that names no module (`prepend(*MODS)`, `prepend helper`; {#opaque_mixin_argument?})
|
|
5870
|
+
# files `[ENVELOPE_SINGLETON_PREPEND_KIND, "?"]`: the extends walk lists that edge as {MixinAccumulator::UNNAMED},
|
|
5871
|
+
# which cannot say a prepend from an `extend`, and the chain reads the entry as a prepend region it cannot name.
|
|
5872
|
+
def record_own_singleton_prepend(node, prefixes, in_singleton_class, tables)
|
|
5873
|
+
kind, arguments, = mixin_call_view(node)
|
|
5874
|
+
return unless kind == :prepend
|
|
5875
|
+
|
|
5876
|
+
owners = singleton_prepend_owners(node.receiver, *prefixes, in_singleton_class)
|
|
5877
|
+
arguments.each do |argument|
|
|
5878
|
+
name = Source::ConstantPath.qualified_name(argument)
|
|
5879
|
+
name ||= MixinAccumulator::UNNAMED if opaque_mixin_argument?(argument)
|
|
5880
|
+
next unless name
|
|
5881
|
+
|
|
5882
|
+
key = [Scope::DiscoveryIndex::ENVELOPE_SINGLETON_PREPEND_KIND, name]
|
|
5883
|
+
owners.each { |owner| record_envelope(tables.envelopes, owner, key, Source::ParameterEnvelope::OPAQUE) }
|
|
5884
|
+
end
|
|
5885
|
+
end
|
|
5886
|
+
|
|
5887
|
+
# The classes whose singleton a prepend call on `receiver` reaches: the enclosing class for `prepend` in its
|
|
5888
|
+
# `class << self` body or `singleton_class.prepend` on a bare or `self.` receiver, and every name a constant
|
|
5889
|
+
# receiver's `Const.singleton_class` can denote.
|
|
5890
|
+
def singleton_prepend_owners(receiver, owner_prefix, qualified_prefix, in_singleton_class)
|
|
5891
|
+
own = owner_prefix.empty? ? EMPTY_PREFIX : [owner_prefix.join("::")]
|
|
5892
|
+
return own if in_singleton_class && lexical_self_receiver?(receiver)
|
|
5893
|
+
return EMPTY_PREFIX unless singleton_class_of_receiver?(receiver)
|
|
5894
|
+
|
|
5895
|
+
object = receiver.receiver
|
|
5896
|
+
return in_singleton_class ? EMPTY_PREFIX : own if lexical_self_receiver?(object)
|
|
5897
|
+
return EMPTY_PREFIX unless object.is_a?(Prism::ConstantReadNode) || object.is_a?(Prism::ConstantPathNode)
|
|
5898
|
+
|
|
5899
|
+
constant_receiver_candidates(object, qualified_prefix)
|
|
5900
|
+
end
|
|
5901
|
+
|
|
5902
|
+
# A bare or `self.` receiver's `singleton_class` call: `singleton_class` or `self.singleton_class`.
|
|
5903
|
+
def self_singleton_class?(receiver)
|
|
5904
|
+
singleton_class_of_receiver?(receiver) && lexical_self_receiver?(receiver.receiver)
|
|
5905
|
+
end
|
|
5906
|
+
|
|
4859
5907
|
def singleton_class_call?(receiver, self_ok)
|
|
4860
5908
|
return false unless receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class
|
|
4861
5909
|
return false unless receiver.arguments.nil?
|
|
@@ -4864,6 +5912,28 @@ module Rigor
|
|
|
4864
5912
|
self_ok || !(object.nil? || object.is_a?(Prism::SelfNode))
|
|
4865
5913
|
end
|
|
4866
5914
|
|
|
5915
|
+
# Issue #3010 — the singleton-prepend entries of a `class << Const` body's prepends on the other names the
|
|
5916
|
+
# constant can denote ({#constant_singleton_owners}); the body's own walk files them under the owner. The
|
|
5917
|
+
# extends walk records the matching edges, unpositioned ({#record_constant_singleton_candidates}). In a method
|
|
5918
|
+
# body, which the methods walk does not enter, `owner:` files them under the owner too.
|
|
5919
|
+
def record_constant_singleton_prepends(node, qualified_prefix, tables, owner: false)
|
|
5920
|
+
return unless constant_singleton_class?(node)
|
|
5921
|
+
|
|
5922
|
+
resolved, others = constant_singleton_owners(node, qualified_prefix)
|
|
5923
|
+
others = [resolved, *others] if owner
|
|
5924
|
+
return if others.empty? || node.body.nil?
|
|
5925
|
+
|
|
5926
|
+
each_singleton_body_prepend(node.body) do |call|
|
|
5927
|
+
mixin_call_view(call)[1].each do |argument|
|
|
5928
|
+
name = Source::ConstantPath.qualified_name(argument)
|
|
5929
|
+
next unless name
|
|
5930
|
+
|
|
5931
|
+
key = [Scope::DiscoveryIndex::ENVELOPE_SINGLETON_PREPEND_KIND, name]
|
|
5932
|
+
others.each { |owner| record_envelope(tables.envelopes, owner, key, Source::ParameterEnvelope::OPAQUE) }
|
|
5933
|
+
end
|
|
5934
|
+
end
|
|
5935
|
+
end
|
|
5936
|
+
|
|
4867
5937
|
def record_singleton_body_prepends(node, qualified_prefix, tables)
|
|
4868
5938
|
return if node.body.nil?
|
|
4869
5939
|
|
|
@@ -4895,17 +5965,44 @@ module Rigor
|
|
|
4895
5965
|
end
|
|
4896
5966
|
end
|
|
4897
5967
|
|
|
4898
|
-
#
|
|
4899
|
-
#
|
|
4900
|
-
#
|
|
5968
|
+
# The side of the enclosing class `qualified_prefix` a `def` defines its method on: `:instance`, `:singleton`, or
|
|
5969
|
+
# nil when the def defines no method of that class.
|
|
5970
|
+
#
|
|
5971
|
+
# `in_singleton_class` is true in a `class << self` body and `:eval` in an `instance_eval` block on the class
|
|
5972
|
+
# ({#def_context}), whose `self` is the class itself.
|
|
4901
5973
|
#
|
|
4902
|
-
#
|
|
4903
|
-
#
|
|
4904
|
-
#
|
|
5974
|
+
# - A receiverless def lands on the instance side, or on the singleton side in either body.
|
|
5975
|
+
# - `def self.x` is a singleton method. `def Foo.x` inside `module Foo` (or `def Meta.init` inside `module Meta`)
|
|
5976
|
+
# is the same: at the def-site the constant `Foo` is the module itself (== `self`).
|
|
5977
|
+
# - Issue #3155 — any other receiver (`def String.q`, `def A.z` in `A::B`, `def obj.q` on a local or a constant
|
|
5978
|
+
# holding an object) is a singleton method of that object, not the enclosing class, and `def self.x` in a
|
|
5979
|
+
# `class << self` body is a method of the singleton class's own singleton: nil. The def's body is still
|
|
5980
|
+
# walked; only the method is not filed on the enclosing class. A constant receiver's own singleton is
|
|
5981
|
+
# {#record_foreign_singleton_def}'s.
|
|
5982
|
+
def def_side(def_node, qualified_prefix, in_singleton_class)
|
|
5983
|
+
receiver = def_node.receiver
|
|
5984
|
+
if receiver.nil?
|
|
5985
|
+
in_singleton_class ? :singleton : :instance
|
|
5986
|
+
elsif receiver.is_a?(Prism::SelfNode)
|
|
5987
|
+
in_singleton_class == true ? nil : :singleton
|
|
5988
|
+
elsif def_receiver_targets_lexical_self?(receiver, qualified_prefix)
|
|
5989
|
+
:singleton
|
|
5990
|
+
end
|
|
5991
|
+
end
|
|
5992
|
+
|
|
5993
|
+
# The `in_singleton_class` {#def_side} reads for a def under a walk's two flags: an `instance_eval` block's
|
|
5994
|
+
# `defs_singleton` (`self` is the receiver, so `def self.x` is its singleton method) unless the def is also in
|
|
5995
|
+
# a `class << self` body.
|
|
5996
|
+
def def_context(in_singleton_class, defs_singleton)
|
|
5997
|
+
in_singleton_class || (defs_singleton ? :eval : false)
|
|
5998
|
+
end
|
|
5999
|
+
|
|
4905
6000
|
def def_singleton?(def_node, qualified_prefix, in_singleton_class)
|
|
4906
|
-
|
|
6001
|
+
def_side(def_node, qualified_prefix, in_singleton_class) == :singleton
|
|
6002
|
+
end
|
|
4907
6003
|
|
|
4908
|
-
|
|
6004
|
+
def foreign_def?(def_node, qualified_prefix, in_singleton_class)
|
|
6005
|
+
def_side(def_node, qualified_prefix, in_singleton_class).nil?
|
|
4909
6006
|
end
|
|
4910
6007
|
|
|
4911
6008
|
# Only `Prism::ConstantReadNode` is observed in real Ruby — Prism mis-parses `def C::P.method` as `def C.P` (Ruby
|
|
@@ -4938,7 +6035,7 @@ module Rigor
|
|
|
4938
6035
|
# ADR-119 WD3 — the slot follows its writer (later-wins): a certain write clears the key from
|
|
4939
6036
|
# `contested_discovered_def_nodes` and a possible one adds it ({MethodTables#certain}, set for `def_node`).
|
|
4940
6037
|
def record_def_node(def_node, qualified_prefix, in_singleton_class, accumulator, tables = nil)
|
|
4941
|
-
return
|
|
6038
|
+
return unless def_side(def_node, qualified_prefix, in_singleton_class) == :instance
|
|
4942
6039
|
|
|
4943
6040
|
class_name = qualified_prefix.empty? ? TOP_LEVEL_DEF_KEY : qualified_prefix.join("::")
|
|
4944
6041
|
accumulator[class_name] ||= {}
|
|
@@ -4963,8 +6060,9 @@ module Rigor
|
|
|
4963
6060
|
|
|
4964
6061
|
# Issue #1989 — the instance method names a non-`def` definer written on the class body's self replaces:
|
|
4965
6062
|
# each reader (and `x=` writer) an `attr_*` call declares, a literal `define_method(:x)`, and the new name of
|
|
4966
|
-
# an `alias_method :x, :y` call
|
|
4967
|
-
|
|
6063
|
+
# an `alias_method :x, :y` call, and each instance-side name of a Forwardable delegator (#2793). A non-literal
|
|
6064
|
+
# name replaces nothing Rigor can name.
|
|
6065
|
+
def call_superseded_names(call_node, qualified_prefix, accumulator)
|
|
4968
6066
|
if (macro = attr_macro(call_node))
|
|
4969
6067
|
names = []
|
|
4970
6068
|
each_attr_macro_name(call_node) do |base|
|
|
@@ -4974,6 +6072,8 @@ module Rigor
|
|
|
4974
6072
|
names
|
|
4975
6073
|
elsif call_node.name == :define_method
|
|
4976
6074
|
define_method_superseded_name(call_node)
|
|
6075
|
+
elsif (forwarded = forwardable_superseded_names(call_node, qualified_prefix, accumulator)).any?
|
|
6076
|
+
forwarded
|
|
4977
6077
|
else
|
|
4978
6078
|
Source::AliasNames.alias_method_call_names(call_node)&.first(1) || []
|
|
4979
6079
|
end
|
|
@@ -5059,6 +6159,10 @@ module Rigor
|
|
|
5059
6159
|
# The singleton walk's accumulator: the `{class => {name => DefNode}}` table, plus the contested slots, which
|
|
5060
6160
|
# follow each slot's writer ({#follow_slot_writer}'s rule).
|
|
5061
6161
|
class SingletonDefTable < Hash
|
|
6162
|
+
include HookBlockSpans
|
|
6163
|
+
|
|
6164
|
+
attr_accessor :hook_block_spans
|
|
6165
|
+
|
|
5062
6166
|
# Issue #1936 — contests the slots of `names` (every slot of `class_name` for `:all`) that an earlier write
|
|
5063
6167
|
# filled: a later non-`def` definer replaced the `def` they hold. The slot keeps the `def`, as
|
|
5064
6168
|
# {ScopeIndexer#supersede_def_nodes} keeps the instance one, and a later write clears the contest.
|
|
@@ -5250,7 +6354,7 @@ module Rigor
|
|
|
5250
6354
|
mf_offsets, ranges, def_owner_prefix, singleton_cref,
|
|
5251
6355
|
defs_singleton)
|
|
5252
6356
|
owner = defs_singleton == :unnameable ? EMPTY_PREFIX : (def_owner_prefix || qualified_prefix)
|
|
5253
|
-
record_deferred_def(node, owner, in_singleton_class
|
|
6357
|
+
record_deferred_def(node, owner, def_context(in_singleton_class, defs_singleton), inside_deferred,
|
|
5254
6358
|
mf_offsets, ranges)
|
|
5255
6359
|
walk_deferred_children(node, qualified_prefix, in_singleton_class, true, mf_offsets,
|
|
5256
6360
|
ranges, def_owner_prefix, singleton_cref: singleton_cref,
|
|
@@ -5518,18 +6622,35 @@ module Rigor
|
|
|
5518
6622
|
# proves `S::A` exists). Memoized on the `Prism::Source` every node's
|
|
5519
6623
|
# location shares — threading the set through the dozen eval-consuming
|
|
5520
6624
|
# walks would dwarf the resolution it feeds, so the one re-parse this
|
|
5521
|
-
# needs happens once per file. The memo
|
|
5522
|
-
#
|
|
5523
|
-
#
|
|
5524
|
-
# the same text gets its own source
|
|
6625
|
+
# needs happens once per file. The memo is a {TypingState} Memo, a
|
|
6626
|
+
# WeakKeyMap keyed on the source: the entry's lifetime is the parse's own
|
|
6627
|
+
# (the map holds the key weakly), the set is a pure function of the
|
|
6628
|
+
# source's text, and a worker parsing the same text gets its own source
|
|
6629
|
+
# and its own set. Not run-scoped: an entry holds no run's tables.
|
|
6630
|
+
DECLARED_NAMES = TypingState.memo(
|
|
6631
|
+
:scope_index_declared_names,
|
|
6632
|
+
audit: "scope index declared names",
|
|
6633
|
+
reason: "a WeakKeyMap keyed on the Prism::Source; the frozen set is a pure function of the source's text"
|
|
6634
|
+
)
|
|
6635
|
+
private_constant :DECLARED_NAMES
|
|
6636
|
+
|
|
5525
6637
|
def eval_file_declared_names(node)
|
|
5526
6638
|
source = node.location.send(:source)
|
|
5527
|
-
source
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
6639
|
+
return scan_declared_names(source) if MemoAudit.enabled? && MemoAudit.suspended?
|
|
6640
|
+
|
|
6641
|
+
table = TypingState.current.weak_key_map(DECLARED_NAMES)
|
|
6642
|
+
stored = table[source]
|
|
6643
|
+
if stored.nil?
|
|
6644
|
+
return table[source] = MemoAudit.key_audit("scope index declared names") { scan_declared_names(source) }
|
|
6645
|
+
end
|
|
6646
|
+
|
|
6647
|
+
MemoAudit.check("scope index declared names", stored, source.source.bytesize) { scan_declared_names(source) }
|
|
6648
|
+
end
|
|
6649
|
+
|
|
6650
|
+
def scan_declared_names(source)
|
|
6651
|
+
names = Set.new
|
|
6652
|
+
collect_declared_constant_names(Prism.parse(source.source).value, [], names)
|
|
6653
|
+
names.freeze
|
|
5533
6654
|
end
|
|
5534
6655
|
|
|
5535
6656
|
# The walk behind {eval_file_declared_names}: every qualified constant
|
|
@@ -5759,7 +6880,11 @@ module Rigor
|
|
|
5759
6880
|
return
|
|
5760
6881
|
end
|
|
5761
6882
|
|
|
5762
|
-
|
|
6883
|
+
side = def_side(def_node, qualified_prefix, in_singleton_class)
|
|
6884
|
+
# Issue #3155 — a def on another object's singleton claims no owner or side, as a deferred one.
|
|
6885
|
+
return ranges << [start, def_node.location.end_offset, def_node.name, nil, nil] if side.nil?
|
|
6886
|
+
|
|
6887
|
+
kind = if side == :singleton
|
|
5763
6888
|
:singleton
|
|
5764
6889
|
elsif ModuleFunctionState.deferred_module_function?(mf_offsets, start)
|
|
5765
6890
|
:both
|
|
@@ -5774,7 +6899,7 @@ module Rigor
|
|
|
5774
6899
|
# `module_function` toggle threads correctly across the body's *sibling* statements (a child-by-child recursion
|
|
5775
6900
|
# would reset it). At the top level / inside an arbitrary node there is no `module_function` state to carry, so
|
|
5776
6901
|
# descent is a plain per-child walk.
|
|
5777
|
-
def walk_singleton_def_nodes(node, qualified_prefix, in_singleton_class, accumulator, # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
6902
|
+
def walk_singleton_def_nodes(node, qualified_prefix, in_singleton_class, accumulator, # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
5778
6903
|
def_owner_prefix = nil, singleton_cref: false,
|
|
5779
6904
|
defs_singleton: false)
|
|
5780
6905
|
return unless node.is_a?(Prism::Node)
|
|
@@ -5798,6 +6923,7 @@ module Rigor
|
|
|
5798
6923
|
# ownerless like `class << <non-constant>`; nameable headers re-anchor.
|
|
5799
6924
|
child_cref = unnameable_decl?(node, self_decl, singleton_cref)
|
|
5800
6925
|
body_prefix = child_cref ? [] : child_prefix
|
|
6926
|
+
accumulator.mark_hook_blocks(concern_hook_blocks(node, child_prefix), child_prefix) unless child_cref
|
|
5801
6927
|
if node.body
|
|
5802
6928
|
walk_singleton_body(node.body, body_prefix, false, accumulator, nil,
|
|
5803
6929
|
singleton_cref: child_cref)
|
|
@@ -5813,7 +6939,8 @@ module Rigor
|
|
|
5813
6939
|
walk_singleton_def_nodes(node.expression, qualified_prefix, in_singleton_class,
|
|
5814
6940
|
accumulator, def_owner_prefix, singleton_cref: singleton_cref,
|
|
5815
6941
|
defs_singleton: defs_singleton)
|
|
5816
|
-
if node.body
|
|
6942
|
+
if node.body && !hook_block_singleton?(node, in_singleton_class, def_owner_prefix || qualified_prefix,
|
|
6943
|
+
accumulator)
|
|
5817
6944
|
walk_singleton_body(node.body, qualified_prefix, true, accumulator, singleton_prefix,
|
|
5818
6945
|
singleton_cref: true)
|
|
5819
6946
|
end
|
|
@@ -5825,9 +6952,11 @@ module Rigor
|
|
|
5825
6952
|
when Prism::DefNode
|
|
5826
6953
|
# `:unnameable` is the `class <<` + `instance_eval` definee — the singleton's own
|
|
5827
6954
|
# singleton — which this table cannot name either.
|
|
6955
|
+
owner = def_owner_prefix || qualified_prefix
|
|
6956
|
+
return if accumulator.hook_block_self_def?(node, owner)
|
|
6957
|
+
|
|
5828
6958
|
unless defs_singleton == :unnameable
|
|
5829
|
-
record_singleton_def_node(node,
|
|
5830
|
-
in_singleton_class || defs_singleton, false, accumulator)
|
|
6959
|
+
record_singleton_def_node(node, owner, def_context(in_singleton_class, defs_singleton), false, accumulator)
|
|
5831
6960
|
end
|
|
5832
6961
|
return
|
|
5833
6962
|
end
|
|
@@ -6085,7 +7214,7 @@ module Rigor
|
|
|
6085
7214
|
# `:unnameable` is the `class <<` + `instance_eval` definee — the singleton's own
|
|
6086
7215
|
# singleton — which this table cannot name either.
|
|
6087
7216
|
unless defs_singleton == :unnameable
|
|
6088
|
-
record_singleton_def_node(stmt, owner_prefix, in_singleton_class
|
|
7217
|
+
record_singleton_def_node(stmt, owner_prefix, def_context(in_singleton_class, defs_singleton),
|
|
6089
7218
|
module_function_on, accumulator)
|
|
6090
7219
|
end
|
|
6091
7220
|
next
|
|
@@ -6127,8 +7256,9 @@ module Rigor
|
|
|
6127
7256
|
# function's copy as possible, so the slot holds the later def, contested, rather than the def the
|
|
6128
7257
|
# slot held before (#1968).
|
|
6129
7258
|
def record_singleton_def_node(def_node, qualified_prefix, in_singleton_class, module_function_on, accumulator)
|
|
6130
|
-
|
|
6131
|
-
|
|
7259
|
+
side = def_side(def_node, qualified_prefix, in_singleton_class)
|
|
7260
|
+
declared = side == :singleton
|
|
7261
|
+
return unless declared || (module_function_on && side == :instance)
|
|
6132
7262
|
return if qualified_prefix.empty?
|
|
6133
7263
|
|
|
6134
7264
|
class_name = qualified_prefix.join("::")
|
|
@@ -6724,6 +7854,7 @@ module Rigor
|
|
|
6724
7854
|
def record_data_member_layout(accumulator, qualified_parts, expr, allow_outer_block: true)
|
|
6725
7855
|
expr = resolve_meta_factory_call(expr, allow_outer_block: allow_outer_block)
|
|
6726
7856
|
return unless expr && data_define_call?(expr)
|
|
7857
|
+
return if meta_members_open?(expr)
|
|
6727
7858
|
|
|
6728
7859
|
members = meta_member_names(expr)
|
|
6729
7860
|
return if members.empty?
|
|
@@ -6846,6 +7977,7 @@ module Rigor
|
|
|
6846
7977
|
def record_struct_member_layout(accumulator, qualified_parts, expr, allow_outer_block: true)
|
|
6847
7978
|
expr = resolve_meta_factory_call(expr, allow_outer_block: allow_outer_block)
|
|
6848
7979
|
return unless expr && struct_new_call?(expr)
|
|
7980
|
+
return if meta_members_open?(expr)
|
|
6849
7981
|
|
|
6850
7982
|
members = meta_member_names(expr)
|
|
6851
7983
|
return if members.empty?
|
|
@@ -7014,6 +8146,7 @@ module Rigor
|
|
|
7014
8146
|
|
|
7015
8147
|
_self_decl, child_prefix, child_cref = ctx
|
|
7016
8148
|
accumulator.direct_body(node.body) if accumulator.direct?(node)
|
|
8149
|
+
accumulator.class_body(node.body)
|
|
7017
8150
|
walk_class_includes(node.body, child_cref ? [] : child_prefix,
|
|
7018
8151
|
child_cref ? nil : child_prefix.join("::"), accumulator,
|
|
7019
8152
|
singleton_cref: child_cref)
|
|
@@ -7110,7 +8243,8 @@ module Rigor
|
|
|
7110
8243
|
def record_mixin_call(node, qualified_prefix, current_class, accumulator)
|
|
7111
8244
|
taint_opaque_eval(node, current_class, accumulator)
|
|
7112
8245
|
view = mixin_call_view(node)
|
|
7113
|
-
return
|
|
8246
|
+
return taint_computed_mixin_send(node, current_class, accumulator, INSTANCE_MIXIN_NAMES, :include) if view.nil?
|
|
8247
|
+
return if view.first.nil?
|
|
7114
8248
|
|
|
7115
8249
|
kind = view.first
|
|
7116
8250
|
current_class ||= toplevel_include_owner(node, qualified_prefix, current_class, accumulator)
|
|
@@ -7120,20 +8254,68 @@ module Rigor
|
|
|
7120
8254
|
return if instance_mixin_recorded?(node, view, qualified_prefix, current_class, accumulator)
|
|
7121
8255
|
|
|
7122
8256
|
# A mixin call the tables cannot hold still reshapes the ancestry: taint the whole owner side.
|
|
7123
|
-
|
|
8257
|
+
taint_unrecorded_mixin(node, kind, view[1], effects, accumulator)
|
|
7124
8258
|
end
|
|
7125
8259
|
|
|
7126
|
-
#
|
|
7127
|
-
#
|
|
7128
|
-
# `
|
|
8260
|
+
# Issue #2991 — an unrecorded `include` / `prepend` that reaches the instance side (its receiver is not a
|
|
8261
|
+
# `singleton_class` call) leaves that side's module SET unknown when an argument is not a constant
|
|
8262
|
+
# (`include(*MODS)`, `include helper`) or the receiver is a hook parameter (`base.include M`), which lists
|
|
8263
|
+
# {MixinAccumulator::UNNAMED}. A `Module.new { … }` argument keeps its #2975 reading. A hook's `base.extend M` or
|
|
8264
|
+
# `base.singleton_class.include M` taints the instance side's order alone: it mixes nothing into it.
|
|
8265
|
+
#
|
|
8266
|
+
# Issue #2995 — an instance-side call whose arguments are all constants is recorded wherever the walk names its
|
|
8267
|
+
# owner (`self.include M` and a receiverless `send(:include, M)` included), so none reaches here with a module
|
|
8268
|
+
# the tables do not hold. The extends walk's taint of `Const.include M` inside `class << self` is the one
|
|
8269
|
+
# constant-argument call that does, and the include walk records that edge under `Const`.
|
|
8270
|
+
#
|
|
8271
|
+
# Issue #2994 — the singleton side lists {MixinAccumulator::UNNAMED} too when an unrecorded call reaching it has
|
|
8272
|
+
# an argument that is not a constant: `extend(*EMODS)`, `extend helper`, `include(*MODS)` inside `class << self`,
|
|
8273
|
+
# or a `self` argument. A hook parameter's `base.extend(*X)` extends the includer, not the module the hook is
|
|
8274
|
+
# written in, so it leaves that module's singleton side unnamed-free.
|
|
8275
|
+
def taint_unrecorded_mixin(node, kind, arguments, effects, accumulator)
|
|
8276
|
+
opaque = arguments.any? { |arg| opaque_mixin_argument?(arg) }
|
|
8277
|
+
hook = hook_parameter_receiver?(node.receiver, accumulator)
|
|
8278
|
+
unnamed = INSTANCE_MIXIN_NAMES.include?(kind) && !singleton_class_eval_receiver?(node.receiver) &&
|
|
8279
|
+
(hook || opaque)
|
|
8280
|
+
effects.each do |owner, side|
|
|
8281
|
+
accumulator.taint(owner, side, unnamed: side == :include ? unnamed : opaque && !hook)
|
|
8282
|
+
end
|
|
8283
|
+
end
|
|
8284
|
+
|
|
8285
|
+
# A `self` argument counts as opaque, as the instance side reads it: a `Target.send(:extend, self)` written in a
|
|
8286
|
+
# module body reaches here with the module no table records under `Target`.
|
|
8287
|
+
def opaque_mixin_argument?(arg)
|
|
8288
|
+
Source::ConstantPath.qualified_name(arg).nil? && !module_new_call?(arg)
|
|
8289
|
+
end
|
|
8290
|
+
|
|
8291
|
+
# A hook parameter itself (`base` in `def self.included(base)`): its class is the includer, which the walk cannot
|
|
8292
|
+
# name, so a mixin on it is read as unnamed rather than by its constant (#2991).
|
|
8293
|
+
def hook_parameter_receiver?(receiver, accumulator)
|
|
8294
|
+
receiver.is_a?(Prism::LocalVariableReadNode) && accumulator.hook_local?(receiver.name)
|
|
8295
|
+
end
|
|
8296
|
+
|
|
8297
|
+
# Records the instance-side edges of an `include` / `prepend` call. False when it recorded nothing — the caller
|
|
8298
|
+
# then taints the owner instead. `view` is {#mixin_call_view}'s, so `Integer.send(:include, Ext)` records `Ext` as
|
|
8299
|
+
# `Integer.include(Ext)` does (issue #1722).
|
|
8300
|
+
#
|
|
8301
|
+
# A receiverless `include` / `prepend` needs an enclosing declaration to file the edge under, and the RECEIVER
|
|
8302
|
+
# form (`Base.prepend(Loud)`, issue #1123; `Integer.include(Ext)` and `Integer.send(:include, Ext)`, issue
|
|
8303
|
+
# #1722) files it under the class the receiver names. A receiver-form edge is unpositioned wherever it is written
|
|
8304
|
+
# ({MixinAccumulator#note}), so a reader that depends on ORDER declines on it, while the set-shaped readers
|
|
8305
|
+
# (undefined-method suppression, arity, visibility) see the module as they see an in-body `include`.
|
|
8306
|
+
#
|
|
8307
|
+
# Issue #2995 — `self.include M`, `self.prepend M` and a receiverless or `self.` `send` / `public_send` /
|
|
8308
|
+
# `__send__(:include | :prepend, M)` are the same call on the same `self` as a bare `include M`, so they file
|
|
8309
|
+
# the edge under the enclosing class as it does, positioned where it would be ({MixinAccumulator#note}). The
|
|
8310
|
+
# walk names that `self` exactly where it names a bare `include`'s: `current_class` is nil wherever it cannot.
|
|
7129
8311
|
def instance_mixin_recorded?(node, view, qualified_prefix, current_class, accumulator)
|
|
7130
|
-
kind, arguments,
|
|
7131
|
-
return false unless mixin_call_recorded?(
|
|
8312
|
+
kind, arguments, = view
|
|
8313
|
+
return false unless mixin_call_recorded?(kind)
|
|
7132
8314
|
|
|
7133
8315
|
targets = arguments.filter_map { |arg| Source::ConstantPath.qualified_name(arg) }
|
|
7134
8316
|
return false if targets.empty?
|
|
7135
8317
|
|
|
7136
|
-
owner = node.receiver
|
|
8318
|
+
owner = lexical_self_receiver?(node.receiver) ? current_class : prepend_call_receiver(node, qualified_prefix)
|
|
7137
8319
|
return false if owner.nil?
|
|
7138
8320
|
|
|
7139
8321
|
write_mixin_targets(accumulator, owner, targets, prepend: kind == :prepend)
|
|
@@ -7143,7 +8325,234 @@ module Rigor
|
|
|
7143
8325
|
|
|
7144
8326
|
MIXIN_KINDS = %i[include prepend extend].freeze
|
|
7145
8327
|
SEND_CALL_NAMES = %i[send public_send __send__].freeze
|
|
7146
|
-
|
|
8328
|
+
EXTEND_MIXIN_NAMES = %i[extend].freeze
|
|
8329
|
+
private_constant :MIXIN_KINDS, :SEND_CALL_NAMES, :EXTEND_MIXIN_NAMES
|
|
8330
|
+
|
|
8331
|
+
# Issue #3006 — a receiverless or `self.` `send` / `public_send` / `__send__` written in a class body
|
|
8332
|
+
# ({MixinAccumulator#class_body_scope}) whose name is computed and which passes at least one further argument
|
|
8333
|
+
# may be an `include`, `prepend` or `extend` the walk cannot record: it lists {MixinAccumulator::UNNAMED} on
|
|
8334
|
+
# `side` of the enclosing class when the name may be one of `kinds`. A name that is a local of the call's scope
|
|
8335
|
+
# bound only to name literals is read as those names ({#local_send_names}), so `m = :attr_reader; send(m, :x)`
|
|
8336
|
+
# marks nothing. A hook parameter's `base.send(meth, M)` is read wherever the hook is, as
|
|
8337
|
+
# {#taint_unrecorded_mixin} reads `base.send(:include, M)`.
|
|
8338
|
+
def taint_computed_mixin_send(node, current_class, accumulator, kinds, side, in_singleton: false)
|
|
8339
|
+
return unless current_class
|
|
8340
|
+
|
|
8341
|
+
name = computed_send_name(node, accumulator)
|
|
8342
|
+
return if name.nil?
|
|
8343
|
+
|
|
8344
|
+
scope = accumulator.class_body_scope(node)
|
|
8345
|
+
hook = hook_parameter_receiver?(node.receiver, accumulator)
|
|
8346
|
+
return if scope.nil? && !hook
|
|
8347
|
+
|
|
8348
|
+
names = scope && local_send_names(name, scope)
|
|
8349
|
+
# Inside `class << self` the instance walk is ownerless, so this walk keeps the hook's instance-side taint too,
|
|
8350
|
+
# as {#record_extend_call} does for `base.send(:include, M)`.
|
|
8351
|
+
sides = in_singleton ? [side, :include] : [side]
|
|
8352
|
+
return taint_computed_hook_send(current_class, accumulator, names, sides) if hook
|
|
8353
|
+
return if names&.none? { |literal| kinds.any? { |kind| kind.name == literal } }
|
|
8354
|
+
|
|
8355
|
+
accumulator.taint(current_class, side, unnamed: true)
|
|
8356
|
+
end
|
|
8357
|
+
|
|
8358
|
+
# A hook parameter's computed `send`, read as {#taint_unrecorded_mixin} reads the literal one: it taints the
|
|
8359
|
+
# enclosing module's `sides` when the name may be any mixin's, and lists the unnamed marker on the instance side
|
|
8360
|
+
# alone, when the name may be `include` or `prepend`.
|
|
8361
|
+
def taint_computed_hook_send(current_class, accumulator, names, sides)
|
|
8362
|
+
return if names&.none? { |literal| MIXIN_KINDS.any? { |kind| kind.name == literal } }
|
|
8363
|
+
|
|
8364
|
+
instance = names.nil? || names.any? { |literal| INSTANCE_MIXIN_NAMES.any? { |kind| kind.name == literal } }
|
|
8365
|
+
sides.each { |side| accumulator.taint(current_class, side, unnamed: side == :include && instance) }
|
|
8366
|
+
end
|
|
8367
|
+
|
|
8368
|
+
# The name argument of a receiverless, `self.` or hook-parameter `send`-family call that passes at least one
|
|
8369
|
+
# further argument, when no literal spells it; else nil.
|
|
8370
|
+
def computed_send_name(node, accumulator)
|
|
8371
|
+
return unless SEND_CALL_NAMES.include?(node.name)
|
|
8372
|
+
return unless lexical_self_receiver?(node.receiver) || hook_parameter_receiver?(node.receiver, accumulator)
|
|
8373
|
+
|
|
8374
|
+
arguments = node.arguments&.arguments
|
|
8375
|
+
return if arguments.nil? || arguments.size < 2
|
|
8376
|
+
|
|
8377
|
+
first = arguments.first
|
|
8378
|
+
first unless literal_send_name(first)
|
|
8379
|
+
end
|
|
8380
|
+
|
|
8381
|
+
LOCAL_SCOPE_BOUNDARIES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
8382
|
+
private_constant :LOCAL_SCOPE_BOUNDARIES
|
|
8383
|
+
|
|
8384
|
+
# The names a local `send` argument can hold: every literal a `local = :name` / `local = "name"` in `scope`
|
|
8385
|
+
# writes, or nil when the argument is not a local, or when the local has no such write or any other binding in
|
|
8386
|
+
# `scope` — a parameter (the `def`'s or a block's, which may shadow it), a multiple or operator write, a pattern
|
|
8387
|
+
# or a regexp capture. Blocks share their scope's locals, so they are searched; a nested `def` or declaration
|
|
8388
|
+
# opens a scope of its own, so it is not.
|
|
8389
|
+
#
|
|
8390
|
+
# Issue #3024 — one block parameter is not opaque: as the FIRST parameter of `each` / `each_with_index` /
|
|
8391
|
+
# `map` over a literal array (or a constant the same scope assigns a literal array), it holds each element in
|
|
8392
|
+
# turn, so the elements are read as the candidate names ({#iterated_element_source}). An element that is not
|
|
8393
|
+
# a name literal still means "may be anything".
|
|
8394
|
+
def local_send_names(argument, scope)
|
|
8395
|
+
return nil unless argument.is_a?(Prism::LocalVariableReadNode)
|
|
8396
|
+
|
|
8397
|
+
names = []
|
|
8398
|
+
root = scope.is_a?(Prism::DefNode) ? scope.compact_child_nodes : [scope]
|
|
8399
|
+
return nil unless collect_local_send_names(root, argument.name, names, root)
|
|
8400
|
+
|
|
8401
|
+
names.empty? ? nil : names
|
|
8402
|
+
end
|
|
8403
|
+
|
|
8404
|
+
def collect_local_send_names(nodes, local, names, scope_nodes)
|
|
8405
|
+
nodes.all? do |node|
|
|
8406
|
+
next true if LOCAL_SCOPE_BOUNDARIES.any? { |kind| node.is_a?(kind) }
|
|
8407
|
+
|
|
8408
|
+
iterated = iterated_element_names(node, local, scope_nodes)
|
|
8409
|
+
next false if iterated == false
|
|
8410
|
+
|
|
8411
|
+
if iterated
|
|
8412
|
+
names.concat(iterated)
|
|
8413
|
+
next collect_local_send_names(iterated_element_children(node), local, names, scope_nodes)
|
|
8414
|
+
end
|
|
8415
|
+
|
|
8416
|
+
literal = local_send_name_literal(node, local)
|
|
8417
|
+
next false if literal == :clash
|
|
8418
|
+
|
|
8419
|
+
names << literal if literal
|
|
8420
|
+
|
|
8421
|
+
collect_local_send_names(node.compact_child_nodes, local, names, scope_nodes)
|
|
8422
|
+
end
|
|
8423
|
+
end
|
|
8424
|
+
|
|
8425
|
+
# Issue #3024 — the `each` / `each_with_index` / `map` arm of {#collect_local_send_names}: when `node` is
|
|
8426
|
+
# such an iterator whose first block parameter is `local`, the iterated array's elements are `local`'s
|
|
8427
|
+
# candidate names ({#iterated_element_source}), each read as {#literal_send_name}. False when an element
|
|
8428
|
+
# may spell any name; nil when `node` is not that iterator, so the caller's plain-write and binding checks
|
|
8429
|
+
# apply.
|
|
8430
|
+
def iterated_element_names(node, local, scope_nodes)
|
|
8431
|
+
elements = iterated_element_source(node, local, scope_nodes)
|
|
8432
|
+
return if elements.nil?
|
|
8433
|
+
return false if elements == :unknown
|
|
8434
|
+
|
|
8435
|
+
names = elements.map { |element| literal_send_name(element) }
|
|
8436
|
+
return false if names.any?(&:nil?)
|
|
8437
|
+
|
|
8438
|
+
names
|
|
8439
|
+
end
|
|
8440
|
+
|
|
8441
|
+
# The name literal a `local = :name` / `local = "name"` write binds `local` to for
|
|
8442
|
+
# {#collect_local_send_names}, `:clash` when `node` binds `local` to anything else — a non-literal write,
|
|
8443
|
+
# a target, an operator write, a parameter — or nil when `node` does not bind `local` at all.
|
|
8444
|
+
def local_send_name_literal(node, local)
|
|
8445
|
+
if node.is_a?(Prism::LocalVariableWriteNode) && node.name == local
|
|
8446
|
+
name = literal_send_name(node.value)
|
|
8447
|
+
return :clash if name.nil?
|
|
8448
|
+
|
|
8449
|
+
return name
|
|
8450
|
+
end
|
|
8451
|
+
:clash if local_binding?(node, local)
|
|
8452
|
+
end
|
|
8453
|
+
|
|
8454
|
+
# Issue #3024 — the array elements `node` iterates when it is an `each` / `each_with_index` / `map` call
|
|
8455
|
+
# whose FIRST block parameter is `local`: a literal `ArrayNode` receiver's elements, or a bare constant the
|
|
8456
|
+
# same scope assigns a literal array exactly once ({#constant_array_elements}). `:unknown` when the call is
|
|
8457
|
+
# such an iterator on `local` but its elements cannot be read — a non-literal receiver, or a constant that
|
|
8458
|
+
# is unwritten, multiply written or not array-valued — so the parameter may hold anything; nil when `node`
|
|
8459
|
+
# is not the iterator or `local` is not its first parameter.
|
|
8460
|
+
def iterated_element_source(node, local, scope_nodes)
|
|
8461
|
+
return unless node.is_a?(Prism::CallNode) && ARRAY_ITERATORS.include?(node.name)
|
|
8462
|
+
|
|
8463
|
+
block = node.block
|
|
8464
|
+
return unless block.is_a?(Prism::BlockNode)
|
|
8465
|
+
return unless iterated_first_parameter?(block, local)
|
|
8466
|
+
|
|
8467
|
+
iterated_receiver_elements(node.receiver, scope_nodes) || :unknown
|
|
8468
|
+
end
|
|
8469
|
+
|
|
8470
|
+
# Whether `local` is the first parameter of `block` and no other parameter binds it too (`|m, m = x|` — the
|
|
8471
|
+
# later binding wins, so the first is not the element).
|
|
8472
|
+
def iterated_first_parameter?(block, local)
|
|
8473
|
+
declared = block.parameters
|
|
8474
|
+
return false unless declared.is_a?(Prism::BlockParametersNode)
|
|
8475
|
+
|
|
8476
|
+
parameters = declared.parameters
|
|
8477
|
+
first = parameters&.requireds&.first
|
|
8478
|
+
return false unless first.is_a?(Prism::RequiredParameterNode) && first.name == local
|
|
8479
|
+
|
|
8480
|
+
Source::NodeWalker.each(parameters).none? do |param|
|
|
8481
|
+
!param.equal?(first) && local_binding?(param, local)
|
|
8482
|
+
end
|
|
8483
|
+
end
|
|
8484
|
+
|
|
8485
|
+
# The elements of the array `receiver` denotes for {#iterated_element_source}: a literal array's own, or a
|
|
8486
|
+
# bare constant's when the same scope assigns it a literal array exactly once. Nil when it denotes anything
|
|
8487
|
+
# else — a qualified or rooted constant, a local, a call — so the iterated value may be anything.
|
|
8488
|
+
def iterated_receiver_elements(receiver, scope_nodes)
|
|
8489
|
+
return receiver.elements if receiver.is_a?(Prism::ArrayNode)
|
|
8490
|
+
return unless receiver.is_a?(Prism::ConstantReadNode)
|
|
8491
|
+
|
|
8492
|
+
constant_array_elements(receiver.name, scope_nodes)
|
|
8493
|
+
end
|
|
8494
|
+
|
|
8495
|
+
# The elements of the literal array `name` is assigned in `nodes`' scope, or nil when it is written any
|
|
8496
|
+
# other number of times or its value is not an array literal — the constant may then hold anything. Only
|
|
8497
|
+
# a single plain `NAME = […]` write closes; in-place mutation (`KINDS << :include`) is unmodeled.
|
|
8498
|
+
def constant_array_elements(name, nodes)
|
|
8499
|
+
writes = constant_write_nodes(nodes, name)
|
|
8500
|
+
return unless writes.one? && writes.first.is_a?(Prism::ConstantWriteNode)
|
|
8501
|
+
|
|
8502
|
+
value = writes.first.value
|
|
8503
|
+
value.elements if value.is_a?(Prism::ArrayNode)
|
|
8504
|
+
end
|
|
8505
|
+
|
|
8506
|
+
# Every write binding the bare constant `name` in `nodes`' scope — a plain `NAME = …`, a multi-assign
|
|
8507
|
+
# target or an operator / `&&=` / `||=` write, mirroring {#local_binding?} — stopping where
|
|
8508
|
+
# {#collect_local_send_names} stops, since a nested `def` or declaration binds its own constants — so
|
|
8509
|
+
# {#constant_array_elements} can tell one literal-array assignment from none or several.
|
|
8510
|
+
def constant_write_nodes(nodes, name)
|
|
8511
|
+
nodes.flat_map do |node|
|
|
8512
|
+
next [] if LOCAL_SCOPE_BOUNDARIES.any? { |kind| node.is_a?(kind) }
|
|
8513
|
+
|
|
8514
|
+
own = constant_binding?(node, name) ? [node] : []
|
|
8515
|
+
own + constant_write_nodes(node.compact_child_nodes, name)
|
|
8516
|
+
end
|
|
8517
|
+
end
|
|
8518
|
+
|
|
8519
|
+
# A node that binds the bare constant `name`: every `Prism::Constant*` binding class — write, target,
|
|
8520
|
+
# operator, `&&=` and `||=` — except the `ConstantReadNode` and `ConstantPath*` spellings, which read
|
|
8521
|
+
# `name` or bind a qualified `A::NAME` rather than `name` itself.
|
|
8522
|
+
def constant_binding?(node, name)
|
|
8523
|
+
return false unless node.respond_to?(:name) && node.name == name
|
|
8524
|
+
|
|
8525
|
+
klass = node.class.name
|
|
8526
|
+
klass.start_with?("Prism::Constant") && !klass.start_with?("Prism::ConstantPath") &&
|
|
8527
|
+
!node.is_a?(Prism::ConstantReadNode)
|
|
8528
|
+
end
|
|
8529
|
+
|
|
8530
|
+
# The children of an iterator call {#iterated_element_source} matched, minus its block's parameters — the
|
|
8531
|
+
# first is `local` bound to the elements and the rest bind other names — so the usual scan still sees every
|
|
8532
|
+
# other binding of `local` in the receiver, arguments and body.
|
|
8533
|
+
def iterated_element_children(node)
|
|
8534
|
+
[node.receiver, node.arguments, node.block.body].compact
|
|
8535
|
+
end
|
|
8536
|
+
|
|
8537
|
+
# The method name a `send` argument spells: a name literal's, or `""` — no mixin's — for an interpolation with a
|
|
8538
|
+
# literal part no mixin name contains (`"#{name}_from_params"`). Nil when it may spell any name.
|
|
8539
|
+
def literal_send_name(value)
|
|
8540
|
+
return value.unescaped if value.is_a?(Prism::SymbolNode) || value.is_a?(Prism::StringNode)
|
|
8541
|
+
return nil unless value.is_a?(Prism::InterpolatedStringNode) || value.is_a?(Prism::InterpolatedSymbolNode)
|
|
8542
|
+
|
|
8543
|
+
excluded = value.parts.any? do |part|
|
|
8544
|
+
part.is_a?(Prism::StringNode) && MIXIN_KINDS.none? { |kind| kind.name.include?(part.unescaped) }
|
|
8545
|
+
end
|
|
8546
|
+
excluded ? "" : nil
|
|
8547
|
+
end
|
|
8548
|
+
|
|
8549
|
+
# A node other than a plain write that binds `local`: a target, an operator write or a parameter.
|
|
8550
|
+
def local_binding?(node, local)
|
|
8551
|
+
return false if node.is_a?(Prism::LocalVariableReadNode) || !node.respond_to?(:name) || node.name != local
|
|
8552
|
+
|
|
8553
|
+
node.class.name.start_with?("Prism::LocalVariable") || node.class.name.end_with?("ParameterNode") ||
|
|
8554
|
+
node.is_a?(Prism::BlockLocalVariableNode)
|
|
8555
|
+
end
|
|
7147
8556
|
|
|
7148
8557
|
# `[kind, arguments, via_send]` for a call that mixes modules in — `include` / `prepend` / `extend`, or
|
|
7149
8558
|
# `send(:include, M)` and its `public_send` / `__send__` twins with a literal first argument — else nil.
|
|
@@ -7202,8 +8611,9 @@ module Rigor
|
|
|
7202
8611
|
end
|
|
7203
8612
|
|
|
7204
8613
|
HOOK_DEFS = %i[included extended prepended inherited].freeze
|
|
8614
|
+
INSTANCE_MIXIN_NAMES = %i[include prepend].freeze
|
|
7205
8615
|
MIXIN_EVAL_CALLS = %i[class_eval module_eval class_exec module_exec instance_eval instance_exec].freeze
|
|
7206
|
-
private_constant :HOOK_DEFS, :MIXIN_EVAL_CALLS
|
|
8616
|
+
private_constant :HOOK_DEFS, :MIXIN_EVAL_CALLS, :INSTANCE_MIXIN_NAMES
|
|
7207
8617
|
|
|
7208
8618
|
# The parameter names of a `def included(base)`-style hook (a hook is written `def self.included`, or
|
|
7209
8619
|
# `def included` inside `class << self`), else nil.
|
|
@@ -7238,9 +8648,14 @@ module Rigor
|
|
|
7238
8648
|
receiver = node.receiver
|
|
7239
8649
|
return if receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
7240
8650
|
return if Source::ConstantPath.qualified_name_or_nil(receiver)
|
|
7241
|
-
return unless hook_receiver?(receiver, accumulator) || block_mixes_in?(node.block)
|
|
7242
8651
|
|
|
7243
|
-
|
|
8652
|
+
hook = hook_receiver?(receiver, accumulator)
|
|
8653
|
+
return unless hook || block_mixes_in?(node.block)
|
|
8654
|
+
|
|
8655
|
+
# Issue #2991 — the set is unknown only where the block may mix into the class itself: a hook's `base`, or a
|
|
8656
|
+
# receiverless `include` / `prepend` in the block of a receiver other than a `singleton_class`.
|
|
8657
|
+
unnamed = hook || (!singleton_class_eval_receiver?(receiver) && block_mixes_into_self?(node.block))
|
|
8658
|
+
accumulator.taint(current_class, :include, unnamed: unnamed)
|
|
7244
8659
|
accumulator.taint(current_class, :extend)
|
|
7245
8660
|
end
|
|
7246
8661
|
|
|
@@ -7252,6 +8667,22 @@ module Rigor
|
|
|
7252
8667
|
found
|
|
7253
8668
|
end
|
|
7254
8669
|
|
|
8670
|
+
# A receiverless or `self.` `include` / `prepend` (or its `send` spelling) anywhere in `node`.
|
|
8671
|
+
def block_mixes_into_self?(node)
|
|
8672
|
+
if node.is_a?(Prism::CallNode) && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
|
|
8673
|
+
view = mixin_call_view(node)
|
|
8674
|
+
return true if view && INSTANCE_MIXIN_NAMES.include?(view.first)
|
|
8675
|
+
end
|
|
8676
|
+
|
|
8677
|
+
found = false
|
|
8678
|
+
node.rigor_each_child { |child| found ||= block_mixes_into_self?(child) }
|
|
8679
|
+
found
|
|
8680
|
+
end
|
|
8681
|
+
|
|
8682
|
+
def singleton_class_eval_receiver?(receiver)
|
|
8683
|
+
receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class
|
|
8684
|
+
end
|
|
8685
|
+
|
|
7255
8686
|
def singleton_class_receiver?(receiver)
|
|
7256
8687
|
receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class && receiver.arguments.nil? &&
|
|
7257
8688
|
(receiver.receiver.nil? || receiver.receiver.is_a?(Prism::SelfNode))
|
|
@@ -7283,19 +8714,8 @@ module Rigor
|
|
|
7283
8714
|
list.unshift(*(prepend ? targets : targets.reject { |target| list.include?(target) }))
|
|
7284
8715
|
end
|
|
7285
8716
|
|
|
7286
|
-
# Whether a mixin call
|
|
7287
|
-
|
|
7288
|
-
# `Integer.include(Ext)` and `Integer.send(:include, Ext)`, issue #1722) files it under the class the
|
|
7289
|
-
# receiver names. A receiver-form edge is unpositioned wherever it is written ({MixinAccumulator#note}), so
|
|
7290
|
-
# a reader that depends on ORDER declines on it, while the set-shaped readers (undefined-method
|
|
7291
|
-
# suppression, arity, visibility) see the module as they see an in-body `include`. A receiverless `send`
|
|
7292
|
-
# stays a taint: the walk does not resolve its `self`.
|
|
7293
|
-
def mixin_call_recorded?(node, kind, via_send, current_class)
|
|
7294
|
-
return false unless MIXIN_CALL_NAMES.include?(kind)
|
|
7295
|
-
return !via_send && !current_class.nil? if node.receiver.nil?
|
|
7296
|
-
|
|
7297
|
-
true
|
|
7298
|
-
end
|
|
8717
|
+
# Whether a mixin call's kind reaches the instance-side tables: `include` and `prepend`.
|
|
8718
|
+
def mixin_call_recorded?(kind) = MIXIN_CALL_NAMES.include?(kind)
|
|
7299
8719
|
|
|
7300
8720
|
# Issue #1697 — a receiverless `include M` written as a statement of the file's own top level is
|
|
7301
8721
|
# `main.include`, which is `Object.include`: it mixes M into `Object`. It is recorded under its own
|
|
@@ -7304,14 +8724,22 @@ module Rigor
|
|
|
7304
8724
|
# walk that types a call reaches it: what Ruby reaches first for a name such an include supplies
|
|
7305
8725
|
# depends on more than the chain shows (a block that rebinds `self`, an `extend` or a singleton `def`
|
|
7306
8726
|
# on `main`), so only the diagnostic silences read it (`Inference::ObjectMixins`), and typing is
|
|
7307
|
-
# issue #1715's.
|
|
7308
|
-
#
|
|
7309
|
-
#
|
|
7310
|
-
#
|
|
7311
|
-
#
|
|
8727
|
+
# issue #1715's. `main` has no `prepend`, and a top-level `extend M` reaches `main`'s singleton alone, so
|
|
8728
|
+
# neither is recorded.
|
|
8729
|
+
#
|
|
8730
|
+
# Issue #3007 — the call counts wherever {MixinAccumulator::MainSelfCalls} takes its `self` to be `main`: a
|
|
8731
|
+
# program statement, or a call under top-level control flow (`if`, `begin`, `case`, …) or in a top-level
|
|
8732
|
+
# `def` body. Such a call may not run, so its edge is in the set with no position ({MixinAccumulator#note}),
|
|
8733
|
+
# as ADR-119 WD7 records an `include` in a method body. The same call inside a top-level block stays
|
|
8734
|
+
# unrecorded: its `self` may not be `main` (a `describe do include M end` or `class_exec` block rebinds it,
|
|
8735
|
+
# while `[1].each do include M end` does not, and the walk cannot tell them apart).
|
|
8736
|
+
#
|
|
8737
|
+
# Issue #2995 — `self.include M`, a receiverless or `self.` `send(:include, M)` and its `__send__` twin are the
|
|
8738
|
+
# same `main.include`. `public_send(:include, M)` is not: `main`'s `include` is private, so it raises.
|
|
7312
8739
|
def toplevel_include_owner(node, qualified_prefix, current_class, accumulator)
|
|
7313
8740
|
return nil unless current_class.nil? && qualified_prefix.empty?
|
|
7314
|
-
return nil unless node.receiver
|
|
8741
|
+
return nil unless lexical_self_receiver?(node.receiver) && node.name != :public_send
|
|
8742
|
+
return nil unless mixin_call_view(node)&.first == :include
|
|
7315
8743
|
|
|
7316
8744
|
accumulator.program_statement?(node) ? ObjectMixins::TOPLEVEL_INCLUDE_KEY : nil
|
|
7317
8745
|
end
|
|
@@ -7373,8 +8801,8 @@ module Rigor
|
|
|
7373
8801
|
singleton_cref)
|
|
7374
8802
|
when Prism::SingletonClassNode
|
|
7375
8803
|
# Issue #915 — `class << self` opens the enclosing declaration's OWN singleton, so an `include`
|
|
7376
|
-
# written in it is the same singleton ancestor an `extend` in the class body would add.
|
|
7377
|
-
# `
|
|
8804
|
+
# written in it is the same singleton ancestor an `extend` in the class body would add. Issue #3010 —
|
|
8805
|
+
# `class << Const` is followed too; `class << obj` names something this walk cannot resolve to a class.
|
|
7378
8806
|
# Nested declarations inside the body take the `ClassNode` branch above and reset the flag with
|
|
7379
8807
|
# their own owner, so the marker cannot leak past the singleton body it belongs to.
|
|
7380
8808
|
return walk_extends_singleton_class(node, qualified_prefix, current_class, accumulator,
|
|
@@ -7427,7 +8855,8 @@ module Rigor
|
|
|
7427
8855
|
|
|
7428
8856
|
# The `class <<` arm of {#walk_class_extends}: the expression evaluates in the enclosing
|
|
7429
8857
|
# context while the body is the singleton's — `class << self` keeps the enclosing owner
|
|
7430
|
-
# (its `include` is the singleton ancestor #915 folds),
|
|
8858
|
+
# (its `include` is the singleton ancestor #915 folds), `class << Const` takes the class the
|
|
8859
|
+
# constant names (#3010), other operands walk ownerless.
|
|
7431
8860
|
# `self` inside is the singleton — `self::` receivers decline — and the lexical cref
|
|
7432
8861
|
# below is unnameable in every case.
|
|
7433
8862
|
def walk_extends_singleton_class(node, qualified_prefix, current_class, accumulator,
|
|
@@ -7436,17 +8865,114 @@ module Rigor
|
|
|
7436
8865
|
in_singleton: in_singleton, singleton_self: singleton_self,
|
|
7437
8866
|
singleton_cref: singleton_cref)
|
|
7438
8867
|
return unless node.body
|
|
8868
|
+
return walk_extends_constant_singleton(node, qualified_prefix, accumulator) if constant_singleton_class?(node)
|
|
7439
8869
|
|
|
7440
8870
|
# `class << self` keeps the owner only at the FIRST level — nested inside a
|
|
7441
8871
|
# singleton body, `self` IS the singleton and `class << self` opens the
|
|
7442
8872
|
# singleton's own singleton (`#<Class:#<Class:C>>`), which nothing names.
|
|
7443
8873
|
opens_self = node.expression.is_a?(Prism::SelfNode) && !in_singleton
|
|
7444
8874
|
accumulator.direct_body(node.body) if opens_self && accumulator.direct?(node)
|
|
8875
|
+
accumulator.class_body(node.body) if opens_self
|
|
7445
8876
|
walk_class_extends(node.body, qualified_prefix, opens_self ? current_class : nil,
|
|
7446
8877
|
accumulator, in_singleton: opens_self,
|
|
7447
8878
|
singleton_self: true, singleton_cref: true)
|
|
7448
8879
|
end
|
|
7449
8880
|
|
|
8881
|
+
# Issue #3010 — `class << Const` opens the class object's own singleton class, so an `include` / `prepend` in its
|
|
8882
|
+
# body is a singleton mixin on `Const`, as one in `class << self` inside `Const`'s body is. The body walks with
|
|
8883
|
+
# the class `Const` resolves to from the lexical scope ({#constant_singleton_owners}, the class its `def`s record
|
|
8884
|
+
# under) as owner. Its edges are positioned only where the order is a fact: the `class <<` is a direct statement
|
|
8885
|
+
# of a file that declares that class, and no more-nested name the constant could denote is left. Anywhere else (a
|
|
8886
|
+
# method body, a conditional, another file) they are unpositioned, and a prepend also records, unpositioned, on
|
|
8887
|
+
# every other name the constant can denote ({#record_constant_singleton_candidates}), as #2998's
|
|
8888
|
+
# `Const.singleton_class.prepend P` does.
|
|
8889
|
+
def walk_extends_constant_singleton(node, qualified_prefix, accumulator)
|
|
8890
|
+
owner, others = constant_singleton_owners(node, qualified_prefix)
|
|
8891
|
+
if others.empty? && accumulator.direct?(node) && eval_file_declared_names(node).include?(owner)
|
|
8892
|
+
accumulator.direct_body(node.body)
|
|
8893
|
+
end
|
|
8894
|
+
walk_class_extends(node.body, qualified_prefix, owner, accumulator,
|
|
8895
|
+
in_singleton: true, singleton_self: true, singleton_cref: true)
|
|
8896
|
+
record_constant_singleton_candidates(node.body, others, accumulator)
|
|
8897
|
+
end
|
|
8898
|
+
|
|
8899
|
+
# Each candidate class past the owner takes the body's prepends unpositioned; an argument that names no module
|
|
8900
|
+
# (`prepend(*MODS)`) leaves its singleton set unknown, as {#taint_unrecorded_mixin} reads it on the owner.
|
|
8901
|
+
def record_constant_singleton_candidates(body, candidates, accumulator)
|
|
8902
|
+
return if candidates.empty?
|
|
8903
|
+
|
|
8904
|
+
each_singleton_body_prepend(body) do |call|
|
|
8905
|
+
candidates.each do |candidate|
|
|
8906
|
+
next unless record_extend_targets(call, candidate, accumulator).empty?
|
|
8907
|
+
|
|
8908
|
+
accumulator.taint(candidate, :extend, unnamed: true)
|
|
8909
|
+
end
|
|
8910
|
+
end
|
|
8911
|
+
end
|
|
8912
|
+
|
|
8913
|
+
# A `class << Const` whose operand is a constant read (`class << Foo = expr` opens an object's singleton).
|
|
8914
|
+
def constant_singleton_class?(node)
|
|
8915
|
+
node.expression.is_a?(Prism::ConstantReadNode) || node.expression.is_a?(Prism::ConstantPathNode)
|
|
8916
|
+
end
|
|
8917
|
+
|
|
8918
|
+
# `[owner, others]` for a `class << Const`: the class the constant resolves to from the lexical scope
|
|
8919
|
+
# ({#singleton_class_prefix}, where the body's `def`s record), and the other names it can denote from there
|
|
8920
|
+
# ({#constant_receiver_candidates}). `others` is empty when the constant can denote one name only, when it names
|
|
8921
|
+
# the enclosing class itself (`class << Foo` inside `class Foo`, which {#singleton_class_prefix} reads as
|
|
8922
|
+
# `class << self`), or when the owner is the innermost candidate and the file declares it ahead of the `class <<`
|
|
8923
|
+
# ({#declared_before?}): Ruby resolves the constant when the statement runs, so a nested class the file declares
|
|
8924
|
+
# later does not exist yet there.
|
|
8925
|
+
def constant_singleton_owners(node, qualified_prefix)
|
|
8926
|
+
owner = singleton_class_prefix(node, nil, qualified_prefix).join("::")
|
|
8927
|
+
candidates = constant_receiver_candidates(node.expression, qualified_prefix)
|
|
8928
|
+
# One candidate needs no re-parse: whichever statement declares it, the constant can name nothing else.
|
|
8929
|
+
return [owner, EMPTY_PREFIX] if candidates.size == 1 || qualified_prefix.join("::") == owner
|
|
8930
|
+
return [owner, EMPTY_PREFIX] if candidates.first == owner && declared_before?(node, owner)
|
|
8931
|
+
|
|
8932
|
+
[owner, candidates - [owner]]
|
|
8933
|
+
end
|
|
8934
|
+
|
|
8935
|
+
# Issue #3010 — whether `node`'s file declares `name` (a `class` / `module` declaration or a bare constant
|
|
8936
|
+
# write) at a statement that starts ahead of `node`. Asked only where a `class << Const` has a nested candidate
|
|
8937
|
+
# the file declares, so it re-parses the file rather than keep a table every file would pay for.
|
|
8938
|
+
def declared_before?(node, name)
|
|
8939
|
+
source = node.location.send(:source).source
|
|
8940
|
+
declaration_before?(Prism.parse(source).value, EMPTY_PREFIX, name, node.location.start_offset)
|
|
8941
|
+
end
|
|
8942
|
+
|
|
8943
|
+
def declaration_before?(node, prefix, name, limit)
|
|
8944
|
+
return false unless node.is_a?(Prism::Node) && node.location.start_offset < limit
|
|
8945
|
+
|
|
8946
|
+
case node
|
|
8947
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
8948
|
+
child = Source::ConstantPath.declaration_prefix(prefix, node.constant_path)
|
|
8949
|
+
return false if child.nil?
|
|
8950
|
+
return true if child.join("::") == name
|
|
8951
|
+
|
|
8952
|
+
return declaration_before?(node.body, child, name, limit)
|
|
8953
|
+
when Prism::ConstantWriteNode
|
|
8954
|
+
return true if (prefix + [node.name.to_s]).join("::") == name
|
|
8955
|
+
when Prism::DefNode, Prism::SingletonClassNode
|
|
8956
|
+
# A `def` body declares no constant, and a `class <<` body's are the singleton class's.
|
|
8957
|
+
return false
|
|
8958
|
+
end
|
|
8959
|
+
node.rigor_each_child { |child| return true if declaration_before?(child, prefix, name, limit) }
|
|
8960
|
+
false
|
|
8961
|
+
end
|
|
8962
|
+
|
|
8963
|
+
# The receiverless or `self.` `prepend` calls (direct or `send` spelling) of a `class <<` body, outside any
|
|
8964
|
+
# nested `def` or declaration body.
|
|
8965
|
+
def each_singleton_body_prepend(node, &)
|
|
8966
|
+
return unless node.is_a?(Prism::Node)
|
|
8967
|
+
return if CODE_BODY_NODES.any? { |klass| node.is_a?(klass) }
|
|
8968
|
+
|
|
8969
|
+
if node.is_a?(Prism::CallNode) && lexical_self_receiver?(node.receiver) &&
|
|
8970
|
+
mixin_call_view(node)&.first == :prepend
|
|
8971
|
+
yield node
|
|
8972
|
+
end
|
|
8973
|
+
node.rigor_each_child { |child| each_singleton_body_prepend(child, &) }
|
|
8974
|
+
end
|
|
8975
|
+
|
|
7450
8976
|
# The declaration arm of {#walk_class_extends}: under an unnameable cref a bare/`self::`
|
|
7451
8977
|
# header opens `#<singleton>::Name` — ownerless; nameable headers re-anchor at a
|
|
7452
8978
|
# real cref.
|
|
@@ -7460,6 +8986,7 @@ module Rigor
|
|
|
7460
8986
|
child_cref = unnameable_decl?(node, self_decl, singleton_cref)
|
|
7461
8987
|
accumulator.direct_body(node.body) if accumulator.direct?(node)
|
|
7462
8988
|
accumulator.body_statements(node.body)
|
|
8989
|
+
accumulator.class_body(node.body)
|
|
7463
8990
|
walk_class_extends(node.body, child_cref ? [] : child_prefix,
|
|
7464
8991
|
child_cref ? nil : child_prefix.join("::"), accumulator,
|
|
7465
8992
|
singleton_cref: child_cref)
|
|
@@ -7736,16 +9263,24 @@ module Rigor
|
|
|
7736
9263
|
return if singleton_class_rewrite_tainted?(node, qualified_prefix, current_class, accumulator,
|
|
7737
9264
|
in_singleton: in_singleton)
|
|
7738
9265
|
|
|
7739
|
-
kind,
|
|
7740
|
-
|
|
9266
|
+
kind, arguments, via_send = mixin_call_view(node)
|
|
9267
|
+
if kind.nil?
|
|
9268
|
+
# Inside `class << self` a computed `include` / `prepend` reaches the singleton side; an `extend` reaches the
|
|
9269
|
+
# singleton's own singleton, which nothing names.
|
|
9270
|
+
kinds = in_singleton ? INSTANCE_MIXIN_NAMES : EXTEND_MIXIN_NAMES
|
|
9271
|
+
return taint_computed_mixin_send(node, current_class, accumulator, kinds, :extend, in_singleton: in_singleton)
|
|
9272
|
+
end
|
|
7741
9273
|
|
|
7742
9274
|
effects = mixin_effects(node, kind, qualified_prefix, current_class, accumulator, in_singleton: in_singleton)
|
|
7743
9275
|
.select { |_owner, side| side == :extend || in_singleton }
|
|
7744
9276
|
return if effects.empty?
|
|
7745
|
-
|
|
7746
|
-
|
|
9277
|
+
# A `send` spelling is recorded only on the lexical `self` (#2995); `Const.send(:extend, M)` stays a taint.
|
|
9278
|
+
return if (!via_send || lexical_self_receiver?(node.receiver)) &&
|
|
9279
|
+
singleton_mixin_recorded?(node, kind, qualified_prefix, current_class, accumulator, in_singleton)
|
|
7747
9280
|
|
|
7748
|
-
|
|
9281
|
+
# Issue #2991 — as in {#record_mixin_call}: an unrecorded instance-side call (a hook's `base.include M` written
|
|
9282
|
+
# in `class << self`) lists its names, or the unnamed marker.
|
|
9283
|
+
taint_unrecorded_mixin(node, kind, arguments, effects, accumulator)
|
|
7749
9284
|
end
|
|
7750
9285
|
|
|
7751
9286
|
# Issue #1752 — a call that rewrites a singleton class reached through `singleton_class` (`singleton_class.
|
|
@@ -7769,23 +9304,53 @@ module Rigor
|
|
|
7769
9304
|
# Inside `class << self` a bare `singleton_class` is the singleton's own singleton, one level up from the
|
|
7770
9305
|
# owner's extend side, so that call stays a taint.
|
|
7771
9306
|
up_level = in_singleton && lexical_self_receiver?(receiver.receiver)
|
|
7772
|
-
|
|
9307
|
+
if !up_level && singleton_class_include_recorded?(node, owner, accumulator)
|
|
9308
|
+
record_prepend_candidates(node, receiver.receiver, qualified_prefix, owner, accumulator)
|
|
9309
|
+
return true
|
|
9310
|
+
end
|
|
7773
9311
|
|
|
7774
|
-
|
|
9312
|
+
# Issue #2994 — an `include` / `prepend` whose argument names no module (`singleton_class.include(*MODS)`)
|
|
9313
|
+
# leaves the owner's singleton set unknown, as {#taint_unrecorded_mixin} reads an `extend(*EMODS)`.
|
|
9314
|
+
accumulator.taint(owner, :extend, unnamed: unnamed_singleton_include?(node))
|
|
7775
9315
|
true
|
|
7776
9316
|
end
|
|
7777
9317
|
|
|
9318
|
+
def unnamed_singleton_include?(node)
|
|
9319
|
+
INSTANCE_MIXIN_NAMES.include?(node.name) &&
|
|
9320
|
+
(node.arguments&.arguments || []).any? { |arg| opaque_mixin_argument?(arg) }
|
|
9321
|
+
end
|
|
9322
|
+
|
|
7778
9323
|
# Issue #2094 — `Float.singleton_class.include(Ext)` (or `prepend`, which the extend table cannot order, so the
|
|
7779
9324
|
# edge is unpositioned as every receiver-form edge is) puts `Ext` on the singleton side as `Float.extend(Ext)`
|
|
7780
9325
|
# does, the fold #915 gives `class << self; include M`. A `send` spelling or an argument that is not a constant
|
|
7781
9326
|
# stays a taint.
|
|
9327
|
+
#
|
|
9328
|
+
# Issue #2998 — a prepend, spelt directly or as `send(:prepend, P)`, is recorded, and the methods walk files it as
|
|
9329
|
+
# a singleton prepend ({#record_own_singleton_prepend}). One on the enclosing class's own `singleton_class` (a
|
|
9330
|
+
# bare or `self.` receiver) runs where it is written, as `prepend P` in `class << self` does, so a direct
|
|
9331
|
+
# statement of the body is positioned; a constant receiver's stays unpositioned.
|
|
7782
9332
|
def singleton_class_include_recorded?(node, owner, accumulator)
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
arguments = node.arguments&.arguments || []
|
|
9333
|
+
kind, sent, = mixin_call_view(node)
|
|
9334
|
+
prepend = kind == :prepend
|
|
9335
|
+
arguments = prepend ? sent : node.arguments&.arguments || []
|
|
9336
|
+
return false unless prepend || node.name == :include
|
|
7786
9337
|
return false if arguments.empty? || arguments.any? { |arg| Source::ConstantPath.qualified_name(arg).nil? }
|
|
7787
9338
|
|
|
7788
|
-
|
|
9339
|
+
positioned = prepend && lexical_self_receiver?(node.receiver.receiver)
|
|
9340
|
+
!record_extend_targets(node, owner, accumulator, positioned: positioned).empty?
|
|
9341
|
+
end
|
|
9342
|
+
|
|
9343
|
+
# Issue #2998 — a constant receiver's `Const.singleton_class.prepend P` also records the edge, unpositioned, under
|
|
9344
|
+
# every other name the constant can denote from the call site, the names the methods walk files its
|
|
9345
|
+
# singleton-prepend entry under ({#singleton_prepend_owners}): which class Ruby reaches is not a fact here, so
|
|
9346
|
+
# each candidate's typing read declines where the prepend could answer.
|
|
9347
|
+
def record_prepend_candidates(node, object, qualified_prefix, owner, accumulator)
|
|
9348
|
+
return unless mixin_call_view(node)&.first == :prepend
|
|
9349
|
+
return unless object.is_a?(Prism::ConstantReadNode) || object.is_a?(Prism::ConstantPathNode)
|
|
9350
|
+
|
|
9351
|
+
(constant_receiver_candidates(object, qualified_prefix) - [owner]).each do |candidate|
|
|
9352
|
+
record_extend_targets(node, candidate, accumulator)
|
|
9353
|
+
end
|
|
7789
9354
|
end
|
|
7790
9355
|
|
|
7791
9356
|
# A bare or `self.` receiver: the lexical `self`.
|
|
@@ -7849,10 +9414,12 @@ module Rigor
|
|
|
7849
9414
|
accumulator.note(node, current_class, :extend, [current_class], complete: true)
|
|
7850
9415
|
end
|
|
7851
9416
|
|
|
7852
|
-
# Records the singleton-side edges of a receiverless `extend` (or, inside `class << self`,
|
|
7853
|
-
# `prepend`). False when it recorded nothing.
|
|
9417
|
+
# Records the singleton-side edges of a receiverless or `self.` `extend` (or, inside `class << self`,
|
|
9418
|
+
# `include` / `prepend`), and of their `send` spellings (#2995). False when it recorded nothing.
|
|
7854
9419
|
def singleton_mixin_recorded?(node, kind, qualified_prefix, current_class, accumulator, in_singleton)
|
|
7855
|
-
|
|
9420
|
+
unless lexical_self_receiver?(node.receiver)
|
|
9421
|
+
return receiver_extend_recorded?(node, kind, qualified_prefix, accumulator)
|
|
9422
|
+
end
|
|
7856
9423
|
return false unless current_class
|
|
7857
9424
|
return false unless in_singleton ? MIXIN_CALL_NAMES.include?(kind) : kind == :extend
|
|
7858
9425
|
|
|
@@ -7878,9 +9445,12 @@ module Rigor
|
|
|
7878
9445
|
# (`extend A; extend B` → singleton ancestors `[B, A]`) while the arguments of a single
|
|
7879
9446
|
# `extend A, B` keep call order (`[A, B]`). Prepending each call's argument list preserves both,
|
|
7880
9447
|
# and consumers iterating the list forward read the same order the singleton ancestry searches.
|
|
7881
|
-
|
|
9448
|
+
#
|
|
9449
|
+
# The arguments are {#mixin_call_view}'s, so a `send(:extend, M)` (#2995) reads `M` and not its name literal.
|
|
9450
|
+
# `positioned` says a receiver-form call still runs where it is written ({#singleton_class_include_recorded?}).
|
|
9451
|
+
def record_extend_targets(node, current_class, accumulator, positioned: false)
|
|
7882
9452
|
targets = []
|
|
7883
|
-
arguments = node
|
|
9453
|
+
arguments = mixin_call_view(node)[1]
|
|
7884
9454
|
arguments.each do |arg|
|
|
7885
9455
|
target = arg.is_a?(Prism::SelfNode) ? current_class : Source::ConstantPath.qualified_name(arg)
|
|
7886
9456
|
targets << target if target
|
|
@@ -7894,7 +9464,8 @@ module Rigor
|
|
|
7894
9464
|
# unpositioned and the chain settles to the readers' own answer.
|
|
7895
9465
|
accumulator.taint(current_class, :extend) if targets.any? { |target| list.include?(target) }
|
|
7896
9466
|
list.unshift(*targets)
|
|
7897
|
-
accumulator.note(node, current_class, :extend, targets, complete: targets.size == arguments.size
|
|
9467
|
+
accumulator.note(node, current_class, :extend, targets, complete: targets.size == arguments.size,
|
|
9468
|
+
positioned: positioned)
|
|
7898
9469
|
targets
|
|
7899
9470
|
end
|
|
7900
9471
|
|
|
@@ -8080,6 +9651,10 @@ module Rigor
|
|
|
8080
9651
|
# written directly in the body settles the default again. A named target does not read the running default,
|
|
8081
9652
|
# so a toggle does not contest it. A certain write clears the slot's contest.
|
|
8082
9653
|
class VisibilityTable < Hash
|
|
9654
|
+
include HookBlockSpans
|
|
9655
|
+
|
|
9656
|
+
attr_accessor :hook_block_spans
|
|
9657
|
+
|
|
8083
9658
|
# The bare toggles the walk has met so far; a body's statement loop compares it around each child.
|
|
8084
9659
|
attr_accessor :toggles
|
|
8085
9660
|
# Whether the running default of the body being walked rests on an uncertain toggle.
|
|
@@ -8202,6 +9777,22 @@ module Rigor
|
|
|
8202
9777
|
VISIBILITY_TOGGLES.include?(name) ? [name, arguments] : nil
|
|
8203
9778
|
end
|
|
8204
9779
|
|
|
9780
|
+
# Issue #1592 — a concern's `class_methods do … end` block is a `module_eval` of `Owner::ClassMethods`, whose
|
|
9781
|
+
# visibility starts public: its `def`s' visibilities are recorded on that module too, as its `def`s are.
|
|
9782
|
+
# Issue #2997 — in every body of the module, as {#record_concern_class_methods} reads its `def`s.
|
|
9783
|
+
def walk_class_methods_visibilities(node, child_prefix, accumulator)
|
|
9784
|
+
return unless node.is_a?(Prism::ModuleNode) && node.body.is_a?(Prism::StatementsNode)
|
|
9785
|
+
|
|
9786
|
+
node.body.body.each do |statement|
|
|
9787
|
+
next unless statement.is_a?(Prism::CallNode) && block_statement?(statement) &&
|
|
9788
|
+
statement.name == :class_methods && statement.block.body
|
|
9789
|
+
|
|
9790
|
+
accumulator.body do
|
|
9791
|
+
walk_method_visibilities(statement.block.body, [*child_prefix, "ClassMethods"], false, :public, accumulator)
|
|
9792
|
+
end
|
|
9793
|
+
end
|
|
9794
|
+
end
|
|
9795
|
+
|
|
8205
9796
|
# rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/AbcSize
|
|
8206
9797
|
def walk_method_visibilities(node, qualified_prefix, in_singleton_class, current_visibility, accumulator, # rubocop:disable Metrics/ParameterLists
|
|
8207
9798
|
def_owner_prefix = nil, singleton_cref: false,
|
|
@@ -8220,11 +9811,13 @@ module Rigor
|
|
|
8220
9811
|
# ownerless; nameable headers re-anchor at a real cref.
|
|
8221
9812
|
child_cref = unnameable_decl?(node, self_decl, singleton_cref)
|
|
8222
9813
|
body_prefix = child_cref ? [] : child_prefix
|
|
9814
|
+
accumulator.mark_hook_blocks(concern_hook_blocks(node, child_prefix), child_prefix) unless child_cref
|
|
8223
9815
|
if node.body
|
|
8224
9816
|
accumulator.body do
|
|
8225
9817
|
walk_method_visibilities(node.body, body_prefix, false, :public,
|
|
8226
9818
|
accumulator, nil, singleton_cref: child_cref)
|
|
8227
9819
|
end
|
|
9820
|
+
walk_class_methods_visibilities(node, child_prefix, accumulator) unless child_cref
|
|
8228
9821
|
end
|
|
8229
9822
|
return current_visibility
|
|
8230
9823
|
end
|
|
@@ -8253,6 +9846,8 @@ module Rigor
|
|
|
8253
9846
|
return current_visibility
|
|
8254
9847
|
end
|
|
8255
9848
|
when Prism::DefNode
|
|
9849
|
+
return current_visibility if accumulator.hook_block_self_def?(node, owner_prefix)
|
|
9850
|
+
|
|
8256
9851
|
contest_foreign_singleton_visibility(node, qualified_prefix, owner_prefix, in_singleton_class, accumulator)
|
|
8257
9852
|
record_def_visibility(node, owner_prefix, visibility_side(in_singleton_class, defs_singleton),
|
|
8258
9853
|
current_visibility, accumulator)
|
|
@@ -8363,6 +9958,7 @@ module Rigor
|
|
|
8363
9958
|
singleton_cref: singleton_cref,
|
|
8364
9959
|
defs_singleton: defs_singleton)
|
|
8365
9960
|
return unless node.body
|
|
9961
|
+
return if hook_block_singleton?(node, in_singleton_class, def_owner_prefix || qualified_prefix, accumulator)
|
|
8366
9962
|
|
|
8367
9963
|
accumulator.body do
|
|
8368
9964
|
walk_method_visibilities(node.body, qualified_prefix, true, :public, accumulator,
|
|
@@ -8865,6 +10461,7 @@ module Rigor
|
|
|
8865
10461
|
constant_receiver_candidates(receiver, qualified_prefix).each do |name|
|
|
8866
10462
|
record_surface_mark(tables, name, Scope::DiscoveryIndex::ENVELOPE_DYNAMIC_MARK)
|
|
8867
10463
|
end
|
|
10464
|
+
record_constant_singleton_definer(node, receiver, qualified_prefix, tables)
|
|
8868
10465
|
elsif singleton_class_of_receiver?(receiver) && singleton_class_named_call?(node)
|
|
8869
10466
|
record_singleton_class_names(node, receiver.receiver, qualified_prefix, tables)
|
|
8870
10467
|
end
|
|
@@ -8899,8 +10496,64 @@ module Rigor
|
|
|
8899
10496
|
return if def_receiver_targets_lexical_self?(receiver, owner_prefix)
|
|
8900
10497
|
|
|
8901
10498
|
constant_receiver_candidates(receiver, qualified_prefix).each do |name|
|
|
8902
|
-
|
|
8903
|
-
|
|
10499
|
+
record_object_singleton(tables, name, node.name)
|
|
10500
|
+
end
|
|
10501
|
+
end
|
|
10502
|
+
|
|
10503
|
+
# Issue #3016 — `Const.define_singleton_method(:x) { … }` with a literal name defines `x` on `Const`'s object, and
|
|
10504
|
+
# is filed as {#record_foreign_singleton_def} files `def Const.x`: an opaque singleton envelope on every name the
|
|
10505
|
+
# constant can denote. A computed name may define any name, so it marks each candidate with the singleton side's
|
|
10506
|
+
# open-names mark instead, as a computed `define_method` on a constant marks the instance side.
|
|
10507
|
+
def record_constant_singleton_definer(node, receiver, qualified_prefix, tables)
|
|
10508
|
+
names = define_singleton_method_names(node)
|
|
10509
|
+
return if names.nil?
|
|
10510
|
+
|
|
10511
|
+
candidates = constant_receiver_candidates(receiver, qualified_prefix)
|
|
10512
|
+
if names == :all
|
|
10513
|
+
candidates.each do |owner|
|
|
10514
|
+
record_surface_mark(tables, owner, Scope::DiscoveryIndex::ENVELOPE_OPEN_SINGLETON_NAMES_MARK)
|
|
10515
|
+
end
|
|
10516
|
+
return
|
|
10517
|
+
end
|
|
10518
|
+
|
|
10519
|
+
candidates.product(names) do |owner, name|
|
|
10520
|
+
record_envelope(tables.envelopes, owner, [:singleton, name], Source::ParameterEnvelope::OPAQUE)
|
|
10521
|
+
record_envelope(tables.envelopes, owner, [Scope::DiscoveryIndex::ENVELOPE_OBJECT_SINGLETON_KIND, name],
|
|
10522
|
+
Source::ParameterEnvelope::OPAQUE)
|
|
10523
|
+
end
|
|
10524
|
+
end
|
|
10525
|
+
|
|
10526
|
+
# A `def Const.x` written outside `Const`'s body, or one in a `Const.singleton_class.class_eval` block: the opaque
|
|
10527
|
+
# `[:singleton, x]` envelope, contested unless certain, and the
|
|
10528
|
+
# {Scope::DiscoveryIndex::ENVELOPE_OBJECT_SINGLETON_KIND} entry the class-object path reads (#3018).
|
|
10529
|
+
def record_object_singleton(tables, owner, method_name)
|
|
10530
|
+
record_envelope(tables.envelopes, owner, [:singleton, method_name], Source::ParameterEnvelope::OPAQUE)
|
|
10531
|
+
contest_envelope(tables, owner, [:singleton, method_name]) unless tables.certain
|
|
10532
|
+
record_envelope(tables.envelopes, owner, [Scope::DiscoveryIndex::ENVELOPE_OBJECT_SINGLETON_KIND, method_name],
|
|
10533
|
+
Source::ParameterEnvelope::OPAQUE)
|
|
10534
|
+
end
|
|
10535
|
+
|
|
10536
|
+
# Issue #3018 — `Const.singleton_class.class_eval { def x … }` (or `module_eval` / `class_exec` / `module_exec`)
|
|
10537
|
+
# defines `x` on `Const`'s object, as `def Const.x` does, and is filed as {#record_foreign_singleton_def} files
|
|
10538
|
+
# that def ({#record_object_singleton}) on every name the constant can denote. The eval-body walk files these
|
|
10539
|
+
# defs under no owner, since the receiver is a call. Every receiverless `def` in the block counts, one under a
|
|
10540
|
+
# conditional included, as {#each_refinement_def} yields them: not one under a `class` / `module` / `class <<`
|
|
10541
|
+
# body, which opens another definee, nor one inside another `def`, which installs only when that runs.
|
|
10542
|
+
# `instance_eval` on the singleton class is not filed: its defs land on the singleton class's own singleton (Ruby
|
|
10543
|
+
# 4.0.5 raises NoMethodError on `Const.x` there).
|
|
10544
|
+
def record_constant_singleton_eval_defs(node, qualified_prefix, tables)
|
|
10545
|
+
return unless CLASS_EVAL_BLOCK_CALLS.include?(node.name)
|
|
10546
|
+
|
|
10547
|
+
receiver = node.receiver
|
|
10548
|
+
return unless singleton_class_of_receiver?(receiver)
|
|
10549
|
+
|
|
10550
|
+
body = node.block.body
|
|
10551
|
+
return if body.nil?
|
|
10552
|
+
|
|
10553
|
+
# A bare, `self` or local receiver of `singleton_class` renders no constant, so it yields no candidate.
|
|
10554
|
+
candidates = constant_receiver_candidates(receiver.receiver, qualified_prefix)
|
|
10555
|
+
each_refinement_def(body) do |def_node|
|
|
10556
|
+
candidates.each { |owner| record_object_singleton(tables, owner, def_node.name) }
|
|
8904
10557
|
end
|
|
8905
10558
|
end
|
|
8906
10559
|
|
|
@@ -8949,22 +10602,33 @@ module Rigor
|
|
|
8949
10602
|
SURFACE_MIXIN_CALLS.include?(node.name) || SURFACE_MIXIN_HELPER.match?(node.name.to_s)
|
|
8950
10603
|
end
|
|
8951
10604
|
|
|
10605
|
+
# Issue #2995 — a `send` / `public_send` / `__send__` of `:include`, `:prepend` or `:extend` is that mixin call,
|
|
10606
|
+
# which the mixin walk records as the bare call, so it is judged by its own arguments as the bare call is.
|
|
8952
10607
|
def dynamic_surface_call?(node)
|
|
8953
|
-
return true if SURFACE_EVAL_CALLS.include?(node.name)
|
|
10608
|
+
return true if SURFACE_EVAL_CALLS.include?(node.name)
|
|
10609
|
+
|
|
10610
|
+
if SURFACE_SEND_CALLS.include?(node.name)
|
|
10611
|
+
kind, arguments, = mixin_call_view(node)
|
|
10612
|
+
return kind.nil? || dynamic_mixin_arguments?(arguments)
|
|
10613
|
+
end
|
|
8954
10614
|
|
|
8955
10615
|
arguments = node.arguments&.arguments || []
|
|
8956
10616
|
if SURFACE_NAMING_CALLS.include?(node.name)
|
|
8957
10617
|
arguments.empty? || literal_method_name(arguments.first).nil?
|
|
8958
10618
|
elsif surface_mixin_call?(node)
|
|
8959
|
-
arguments
|
|
8960
|
-
!(argument.is_a?(Prism::ConstantReadNode) || argument.is_a?(Prism::ConstantPathNode) ||
|
|
8961
|
-
argument.is_a?(Prism::SelfNode))
|
|
8962
|
-
end
|
|
10619
|
+
dynamic_mixin_arguments?(arguments)
|
|
8963
10620
|
else
|
|
8964
10621
|
false
|
|
8965
10622
|
end
|
|
8966
10623
|
end
|
|
8967
10624
|
|
|
10625
|
+
def dynamic_mixin_arguments?(arguments)
|
|
10626
|
+
arguments.any? do |argument|
|
|
10627
|
+
!(argument.is_a?(Prism::ConstantReadNode) || argument.is_a?(Prism::ConstantPathNode) ||
|
|
10628
|
+
argument.is_a?(Prism::SelfNode))
|
|
10629
|
+
end
|
|
10630
|
+
end
|
|
10631
|
+
|
|
8968
10632
|
# Every qualified name a constant receiver written inside `qualified_prefix` can denote, innermost
|
|
8969
10633
|
# first: the walk runs before any scope exists, so it cannot resolve the constant, and over-marking a
|
|
8970
10634
|
# same-named class elsewhere only withholds a check.
|
|
@@ -9245,15 +10909,76 @@ module Rigor
|
|
|
9245
10909
|
end
|
|
9246
10910
|
|
|
9247
10911
|
def record_define_method(call_node, qualified_prefix, in_singleton_class, accumulator)
|
|
9248
|
-
return if qualified_prefix.empty?
|
|
9249
10912
|
return if call_node.arguments.nil? || call_node.arguments.arguments.empty?
|
|
9250
10913
|
|
|
9251
|
-
|
|
9252
|
-
|
|
9253
|
-
|
|
10914
|
+
method_name = literal_method_name(call_node.arguments.arguments.first)
|
|
10915
|
+
receiver = call_node.receiver
|
|
10916
|
+
if method_name && (receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode))
|
|
10917
|
+
return record_constant_define_method(call_node, receiver, method_name, qualified_prefix, accumulator)
|
|
10918
|
+
end
|
|
10919
|
+
return if qualified_prefix.empty?
|
|
10920
|
+
if method_name.nil?
|
|
10921
|
+
return record_open_define_method(call_node, qualified_prefix, in_singleton_class,
|
|
10922
|
+
accumulator)
|
|
10923
|
+
end
|
|
9254
10924
|
|
|
9255
|
-
|
|
9256
|
-
|
|
10925
|
+
# A receiver other than a constant may still be `self` (`[self, k].each { |c| c.define_method(:x) {} }`), so
|
|
10926
|
+
# the name stays on the class the walk places the call in.
|
|
10927
|
+
record_method(accumulator, qualified_prefix.join("::"), method_name,
|
|
10928
|
+
in_singleton_class ? :singleton : :instance)
|
|
10929
|
+
end
|
|
10930
|
+
|
|
10931
|
+
# Issue #2975 — `Other.define_method(:x)` defines `Other#x`, whichever class body or side it is written in: the
|
|
10932
|
+
# constant resolves as a `class << Other` operand does ({#singleton_class_owner}), and the enclosing class
|
|
10933
|
+
# records nothing.
|
|
10934
|
+
def record_constant_define_method(call_node, receiver, method_name, qualified_prefix, accumulator)
|
|
10935
|
+
owner = singleton_class_owner(call_node, receiver, qualified_prefix, nil)
|
|
10936
|
+
record_method(accumulator, owner, method_name, :instance) if owner
|
|
10937
|
+
end
|
|
10938
|
+
|
|
10939
|
+
# Issue #2975 — the literal twins of {#record_self_definer_call}'s computed definers on `self`:
|
|
10940
|
+
# `send(:define_method, :x)` records `x` where a direct `define_method(:x)` there would, and
|
|
10941
|
+
# `define_singleton_method(:x)` records the class object's `x`. In a `class << self` body the latter defines on
|
|
10942
|
+
# the singleton's own singleton, which nothing names, so it records nothing.
|
|
10943
|
+
def record_literal_self_definer(node, class_name, in_singleton_class, accumulator)
|
|
10944
|
+
arguments = node.arguments&.arguments
|
|
10945
|
+
return if arguments.nil? || arguments.empty?
|
|
10946
|
+
|
|
10947
|
+
if node.name == :define_singleton_method
|
|
10948
|
+
name = literal_method_name(arguments.first)
|
|
10949
|
+
record_method(accumulator, class_name, name, :singleton) if name && !in_singleton_class
|
|
10950
|
+
elsif literal_method_name(arguments.first) == :define_method && arguments.length > 1
|
|
10951
|
+
name = literal_method_name(arguments[1])
|
|
10952
|
+
record_method(accumulator, class_name, name, in_singleton_class ? :singleton : :instance) if name
|
|
10953
|
+
end
|
|
10954
|
+
end
|
|
10955
|
+
|
|
10956
|
+
# Issue #2793 — a `define_method` on `self` whose name is not a literal defines a method this walk cannot name,
|
|
10957
|
+
# on the side and owner a literal one would record under (the walk's placement: a `Class.new { }` or
|
|
10958
|
+
# `Struct.new { }` body is its own class's, an eval body its receiver's, a `class << self` body the singleton
|
|
10959
|
+
# side), so that side reads as open to `call.undefined-method`.
|
|
10960
|
+
#
|
|
10961
|
+
# Issue #2901 — on a constant receiver (`Other.define_method(m)`) it defines instance methods of that constant's
|
|
10962
|
+
# class, whichever side the call sits on: each class the constant can denote from here opens its instance side
|
|
10963
|
+
# ({#constant_receiver_candidates}), and the enclosing class does not. Any other receiver opens nothing.
|
|
10964
|
+
def record_open_define_method(call_node, qualified_prefix, in_singleton_class, accumulator)
|
|
10965
|
+
receiver = call_node.receiver
|
|
10966
|
+
if receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)
|
|
10967
|
+
constant_receiver_candidates(receiver, qualified_prefix).each do |name|
|
|
10968
|
+
record_surface_mark(accumulator, name, Scope::DiscoveryIndex::ENVELOPE_OPEN_INSTANCE_NAMES_MARK)
|
|
10969
|
+
end
|
|
10970
|
+
elsif receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
10971
|
+
record_surface_mark(accumulator, qualified_prefix.join("::"), open_names_mark(in_singleton_class))
|
|
10972
|
+
end
|
|
10973
|
+
end
|
|
10974
|
+
|
|
10975
|
+
# The side's open-names mark a computed definer on `self` records: the singleton side in a `class << self` body.
|
|
10976
|
+
def open_names_mark(in_singleton_class)
|
|
10977
|
+
if in_singleton_class
|
|
10978
|
+
Scope::DiscoveryIndex::ENVELOPE_OPEN_SINGLETON_NAMES_MARK
|
|
10979
|
+
else
|
|
10980
|
+
Scope::DiscoveryIndex::ENVELOPE_OPEN_INSTANCE_NAMES_MARK
|
|
10981
|
+
end
|
|
9257
10982
|
end
|
|
9258
10983
|
|
|
9259
10984
|
# The `attr_*` accessor macros that introduce methods Rigor must treat as source-declared. Without this, a class
|
|
@@ -9277,6 +11002,19 @@ module Rigor
|
|
|
9277
11002
|
record_method(accumulator, class_name, base, kind) if reader
|
|
9278
11003
|
record_method(accumulator, class_name, :"#{base}=", kind) if writer
|
|
9279
11004
|
end
|
|
11005
|
+
return unless attr_macro_computed?(call_node)
|
|
11006
|
+
|
|
11007
|
+
# Issue #2901 — `attr_accessor(*NAMES)` defines members this walk cannot name, so its side opens.
|
|
11008
|
+
record_surface_mark(accumulator, class_name, open_names_mark(in_singleton_class))
|
|
11009
|
+
end
|
|
11010
|
+
|
|
11011
|
+
# Whether an `attr_*` call ({#attr_macro}) passes a name argument that is not a Symbol or String literal.
|
|
11012
|
+
def attr_macro_computed?(call_node)
|
|
11013
|
+
arguments = call_node.arguments&.arguments
|
|
11014
|
+
return false if arguments.nil?
|
|
11015
|
+
|
|
11016
|
+
arguments = arguments.drop(1) if SURFACE_SEND_CALLS.include?(call_node.name)
|
|
11017
|
+
arguments.any? { |arg| literal_method_name(arg).nil? }
|
|
9280
11018
|
end
|
|
9281
11019
|
|
|
9282
11020
|
# Yields each name an `attr_*` call declares ({#attr_macro}): a Symbol or String literal argument. A
|
|
@@ -9321,13 +11059,14 @@ module Rigor
|
|
|
9321
11059
|
# because they are not monkey-patches), and no plugin surface reaches it — the ADR-16 synthetic-method
|
|
9322
11060
|
# tier feeds the DISPATCHER, which `Analysis::CheckRules` does not consult. A name table is the smallest
|
|
9323
11061
|
# thing that works; a plugin-supplied one can replace it whenever that surface exists.
|
|
9324
|
-
MODULE_ATTR_MACROS =
|
|
9325
|
-
|
|
9326
|
-
|
|
9327
|
-
|
|
9328
|
-
|
|
9329
|
-
|
|
9330
|
-
|
|
11062
|
+
MODULE_ATTR_MACROS = Ractor.make_shareable(
|
|
11063
|
+
{
|
|
11064
|
+
mattr_reader: [true, false, false], mattr_writer: [false, true, false],
|
|
11065
|
+
mattr_accessor: [true, true, false], cattr_reader: [true, false, false],
|
|
11066
|
+
cattr_writer: [false, true, false], cattr_accessor: [true, true, false],
|
|
11067
|
+
class_attribute: [true, true, true]
|
|
11068
|
+
}
|
|
11069
|
+
)
|
|
9331
11070
|
|
|
9332
11071
|
# Both kinds, unconditionally. `instance_accessor: false` (and its `instance_reader:` /
|
|
9333
11072
|
# `instance_writer:` / `instance_predicate:` siblings) narrows the real surface, and honouring them here
|
|
@@ -9350,6 +11089,70 @@ module Rigor
|
|
|
9350
11089
|
end
|
|
9351
11090
|
end
|
|
9352
11091
|
|
|
11092
|
+
# Issue #2793 — the `extend` that decides the side of an {Source::ForwardableDelegators::AMBIGUOUS} macro
|
|
11093
|
+
# (`def_delegators`, `def_delegator`, `delegate`): `Forwardable` makes them instance methods,
|
|
11094
|
+
# `SingleForwardable` singleton methods.
|
|
11095
|
+
FORWARDABLE_MODULES = { Forwardable: :instance, SingleForwardable: :singleton }.freeze
|
|
11096
|
+
|
|
11097
|
+
# Issue #2793 — a Forwardable delegator macro defines its literal names as methods, which join the existence
|
|
11098
|
+
# table so `call.undefined-method` stops firing on them (their return types stay unknown: the envelope is
|
|
11099
|
+
# opaque and no `def` node backs them). The side is the macro's own, or, for an ambiguous macro, the side
|
|
11100
|
+
# of the `extend` this walk met earlier in the body: singleton under a lone `extend SingleForwardable`,
|
|
11101
|
+
# instance otherwise (a class extended in another file or by a superclass is far likelier `Forwardable`).
|
|
11102
|
+
# Inside `class << self` the macro defines on the singleton class. A name the syntax hides
|
|
11103
|
+
# (`def_delegators :@x, *NAMES`) marks the class {Scope::DiscoveryIndex::ENVELOPE_OPEN_NAMES_MARK}.
|
|
11104
|
+
def record_forwardable_methods(call_node, qualified_prefix, in_singleton_class, accumulator)
|
|
11105
|
+
return if qualified_prefix.empty?
|
|
11106
|
+
|
|
11107
|
+
class_name = qualified_prefix.join("::")
|
|
11108
|
+
note_forwardable_extend(call_node, class_name, accumulator) unless in_singleton_class
|
|
11109
|
+
delegation = Source::ForwardableDelegators.of(call_node)
|
|
11110
|
+
return if delegation.nil?
|
|
11111
|
+
|
|
11112
|
+
kind = forwardable_side(delegation, class_name, in_singleton_class, accumulator)
|
|
11113
|
+
delegation.names.each { |name| record_method(accumulator, class_name, name, kind) }
|
|
11114
|
+
return unless delegation.open?
|
|
11115
|
+
|
|
11116
|
+
record_surface_mark(accumulator, class_name, Scope::DiscoveryIndex::ENVELOPE_OPEN_NAMES_MARK)
|
|
11117
|
+
end
|
|
11118
|
+
|
|
11119
|
+
def note_forwardable_extend(call_node, class_name, accumulator)
|
|
11120
|
+
return unless call_node.name == :extend && call_node.receiver.nil?
|
|
11121
|
+
|
|
11122
|
+
(call_node.arguments&.arguments || []).each do |argument|
|
|
11123
|
+
side = FORWARDABLE_MODULES[forwardable_module_name(argument)]
|
|
11124
|
+
next if side.nil?
|
|
11125
|
+
|
|
11126
|
+
((accumulator.forwardable_extends ||= {})[class_name] ||= Set.new) << side
|
|
11127
|
+
end
|
|
11128
|
+
end
|
|
11129
|
+
|
|
11130
|
+
def forwardable_module_name(node)
|
|
11131
|
+
case node
|
|
11132
|
+
when Prism::ConstantReadNode then node.name
|
|
11133
|
+
when Prism::ConstantPathNode then node.parent.nil? ? node.name : nil
|
|
11134
|
+
end
|
|
11135
|
+
end
|
|
11136
|
+
|
|
11137
|
+
def forwardable_side(delegation, class_name, in_singleton_class, accumulator)
|
|
11138
|
+
return :singleton if in_singleton_class
|
|
11139
|
+
return delegation.side unless delegation.side == Source::ForwardableDelegators::AMBIGUOUS
|
|
11140
|
+
|
|
11141
|
+
extended = accumulator.forwardable_extends&.[](class_name)
|
|
11142
|
+
extended == Set[:singleton] ? :singleton : :instance
|
|
11143
|
+
end
|
|
11144
|
+
|
|
11145
|
+
# The instance names a Forwardable macro defines, for {#call_superseded_names}: a delegator written after a
|
|
11146
|
+
# `def` of the name replaces it. Asked only outside `class << self`, and a macro whose side settles to the
|
|
11147
|
+
# singleton (`extend SingleForwardable; def_delegators :@x, :size`) leaves an instance `def size` standing.
|
|
11148
|
+
def forwardable_superseded_names(call_node, qualified_prefix, accumulator)
|
|
11149
|
+
delegation = Source::ForwardableDelegators.of(call_node)
|
|
11150
|
+
return [] if delegation.nil? || qualified_prefix.empty?
|
|
11151
|
+
|
|
11152
|
+
side = forwardable_side(delegation, qualified_prefix.join("::"), false, accumulator)
|
|
11153
|
+
side == :instance ? delegation.names : []
|
|
11154
|
+
end
|
|
11155
|
+
|
|
9353
11156
|
def record_both_kinds(accumulator, class_name, method_name)
|
|
9354
11157
|
record_method(accumulator, class_name, method_name, :instance)
|
|
9355
11158
|
record_method(accumulator, class_name, method_name, :singleton)
|
|
@@ -9380,8 +11183,12 @@ module Rigor
|
|
|
9380
11183
|
physical = buffer ? buffer.resolve(path) : path
|
|
9381
11184
|
source = File.read(physical)
|
|
9382
11185
|
root = Prism.parse(source, filepath: path).value
|
|
9383
|
-
|
|
9384
|
-
|
|
11186
|
+
file_classes = {}
|
|
11187
|
+
collect_class_decls(root, [], file_classes)
|
|
11188
|
+
accumulator.merge!(file_classes) # all or nothing: an overflow above contributes none of this file (#2455)
|
|
11189
|
+
rescue StandardError, SystemStackError => e
|
|
11190
|
+
raise if uncontainable_overflow?(e)
|
|
11191
|
+
|
|
9385
11192
|
# Skip files that fail to parse or read; the per-file analyzer surfaces the parse error separately.
|
|
9386
11193
|
next
|
|
9387
11194
|
end
|
|
@@ -9408,10 +11215,12 @@ module Rigor
|
|
|
9408
11215
|
paths.each do |path|
|
|
9409
11216
|
physical = buffer ? buffer.resolve(path) : path
|
|
9410
11217
|
root = Prism.parse(File.read(physical), filepath: path).value
|
|
9411
|
-
|
|
11218
|
+
fold_file_index(acc, build_file_index(path, root)) # all or nothing per file (#2455)
|
|
9412
11219
|
rescue DeclarationWalk::ContractError
|
|
9413
11220
|
raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
|
|
9414
|
-
rescue StandardError
|
|
11221
|
+
rescue StandardError, SystemStackError => e
|
|
11222
|
+
raise if uncontainable_overflow?(e)
|
|
11223
|
+
|
|
9415
11224
|
# Skip files that fail to parse or read; the per-file analyzer surfaces the parse error separately.
|
|
9416
11225
|
next
|
|
9417
11226
|
end
|
|
@@ -9448,7 +11257,9 @@ module Rigor
|
|
|
9448
11257
|
file_summary(file_index).each { |key, table| per_file[key][path] = table unless table.nil? }
|
|
9449
11258
|
rescue DeclarationWalk::ContractError
|
|
9450
11259
|
raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
|
|
9451
|
-
rescue StandardError
|
|
11260
|
+
rescue StandardError, SystemStackError => e
|
|
11261
|
+
raise if uncontainable_overflow?(e)
|
|
11262
|
+
|
|
9452
11263
|
next
|
|
9453
11264
|
end
|
|
9454
11265
|
{ def_index: finalize_def_index(acc), code_fingerprints: code_fingerprints,
|
|
@@ -9732,21 +11543,35 @@ module Rigor
|
|
|
9732
11543
|
paths.each do |path|
|
|
9733
11544
|
physical = buffer ? buffer.resolve(path) : path
|
|
9734
11545
|
root = Prism.parse(File.read(physical), filepath: path).value
|
|
9735
|
-
|
|
9736
|
-
|
|
9737
|
-
|
|
9738
|
-
|
|
9739
|
-
|
|
9740
|
-
|
|
11546
|
+
# All or nothing per file, as the incremental loop below: both tables are built in isolation and folded only
|
|
11547
|
+
# once the whole file walked, so a file that overflows the stack contributes none of either (#2455).
|
|
11548
|
+
file_classes = {}
|
|
11549
|
+
collect_class_decls(root, [], file_classes)
|
|
11550
|
+
file_index = if uncertain.include?(path)
|
|
11551
|
+
without_written_slots(build_file_index(path, root, uncertain: true), acc)
|
|
11552
|
+
else
|
|
11553
|
+
build_file_index(path, root)
|
|
11554
|
+
end
|
|
11555
|
+
fold_file_index(acc, file_index)
|
|
11556
|
+
classes.merge!(file_classes)
|
|
9741
11557
|
rescue DeclarationWalk::ContractError
|
|
9742
11558
|
raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
|
|
9743
|
-
rescue StandardError
|
|
11559
|
+
rescue StandardError, SystemStackError => e
|
|
11560
|
+
raise if uncontainable_overflow?(e)
|
|
11561
|
+
|
|
9744
11562
|
# Skip files that fail to parse or read; the per-file analyzer surfaces the parse error separately.
|
|
9745
11563
|
next
|
|
9746
11564
|
end
|
|
9747
11565
|
finalize_project_index(classes, acc)
|
|
9748
11566
|
end
|
|
9749
11567
|
|
|
11568
|
+
# #2455 — a `SystemStackError` raised from C (`Prism.parse` of a source nested near its own cap) has no
|
|
11569
|
+
# backtrace and leaves the process unable to survive a second one, so it is not contained: it keeps the
|
|
11570
|
+
# run-level abort.
|
|
11571
|
+
def uncontainable_overflow?(error)
|
|
11572
|
+
error.is_a?(SystemStackError) && error.backtrace.nil?
|
|
11573
|
+
end
|
|
11574
|
+
|
|
9750
11575
|
# The shared tail of the two whole-project discovery walks: settles the def-index and re-anchors the
|
|
9751
11576
|
# `classes` table it accumulated beside it.
|
|
9752
11577
|
def finalize_project_index(classes, acc)
|
|
@@ -9787,7 +11612,7 @@ module Rigor
|
|
|
9787
11612
|
# @return `{ classes:, def_index:, bundles: }`.
|
|
9788
11613
|
# @param uncertain — issue #2021: as {#discovered_project_index_for_paths}. Such a file is always re-walked and
|
|
9789
11614
|
# keeps no bundle, since a bundle does not record that its contributions were all possible.
|
|
9790
|
-
def discovered_project_index_incremental(paths, seed_bundles:, buffer: nil, uncertain: EMPTY_PATH_SET) # rubocop:disable Metrics/AbcSize
|
|
11615
|
+
def discovered_project_index_incremental(paths, seed_bundles:, buffer: nil, uncertain: EMPTY_PATH_SET) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
9791
11616
|
classes = {}
|
|
9792
11617
|
acc = new_def_index_accumulator
|
|
9793
11618
|
bundles = {}
|
|
@@ -9816,7 +11641,9 @@ module Rigor
|
|
|
9816
11641
|
end
|
|
9817
11642
|
rescue DeclarationWalk::ContractError
|
|
9818
11643
|
raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
|
|
9819
|
-
rescue StandardError
|
|
11644
|
+
rescue StandardError, SystemStackError => e
|
|
11645
|
+
raise if uncontainable_overflow?(e)
|
|
11646
|
+
|
|
9820
11647
|
# Skip files that fail to parse / read; the per-file analyzer surfaces the parse error separately.
|
|
9821
11648
|
next
|
|
9822
11649
|
end
|
|
@@ -10653,7 +12480,8 @@ module Rigor
|
|
|
10653
12480
|
singleton_cref)
|
|
10654
12481
|
else
|
|
10655
12482
|
census_constant_write(node, qualified_prefix, tables, self_owner, singleton_cref: singleton_cref)
|
|
10656
|
-
tables.write_census.visit(node, top_level: qualified_prefix.empty?, owner: ivar_owner
|
|
12483
|
+
tables.write_census.visit(node, top_level: qualified_prefix.empty?, owner: ivar_owner,
|
|
12484
|
+
receiver_owner: census_receiver_owner(node, qualified_prefix, self_owner))
|
|
10657
12485
|
end
|
|
10658
12486
|
|
|
10659
12487
|
rebound = rebound_block_self(node, qualified_prefix, nil, meta_owner, self_owner)
|
|
@@ -10695,6 +12523,16 @@ module Rigor
|
|
|
10695
12523
|
qualified_prefix
|
|
10696
12524
|
end
|
|
10697
12525
|
|
|
12526
|
+
# Issue #2488 — the class an explicit-receiver writer call (`E.attr_writer :w`, `E.define_method(:m) { … }`)
|
|
12527
|
+
# names, resolved as a `class_eval` receiver is in this census (keyed as written), or nil.
|
|
12528
|
+
def census_receiver_owner(node, qualified_prefix, self_owner)
|
|
12529
|
+
receiver = ReceiverWriterCall.receiver(node)
|
|
12530
|
+
return nil if receiver.nil?
|
|
12531
|
+
|
|
12532
|
+
name = eval_receiver_self(receiver, qualified_prefix, nil, rebound_self_base(self_owner))
|
|
12533
|
+
name if name.is_a?(String)
|
|
12534
|
+
end
|
|
12535
|
+
|
|
10698
12536
|
# Under an unnameable cref a bare/`self::` header pushes `#<singleton>::Name` — still
|
|
10699
12537
|
# unnameable — so the body keeps the OPAQUE self and the cref flag; nameable
|
|
10700
12538
|
# headers re-anchor at a real cref.
|
|
@@ -11698,20 +13536,25 @@ module Rigor
|
|
|
11698
13536
|
|
|
11699
13537
|
# Recognises `Data.define(*Symbol)` and `Data.define(*Symbol) do ... end` at constant-write rvalue position. The
|
|
11700
13538
|
# receiver MUST be the bare `Data` constant (or `::Data`); other receivers (a local variable, a method call
|
|
11701
|
-
# return) are rejected because their identity is not statically known.
|
|
13539
|
+
# return) are rejected because their identity is not statically known. Issue #2968 — a splatted member list
|
|
13540
|
+
# (`Data.define(*FIELDS)`) still creates the class; {#meta_members_open?} marks its members unknown.
|
|
11702
13541
|
def data_define_call?(node)
|
|
11703
13542
|
return false unless node.is_a?(Prism::CallNode)
|
|
11704
13543
|
return false unless node.name == :define
|
|
11705
13544
|
return false unless meta_constant_receiver?(node.receiver, :Data)
|
|
11706
13545
|
|
|
11707
13546
|
args = node.arguments&.arguments || []
|
|
11708
|
-
args.all?(
|
|
13547
|
+
args.all? { |arg| meta_member_argument?(arg) }
|
|
11709
13548
|
end
|
|
11710
13549
|
|
|
11711
13550
|
# Recognises `Struct.new(*Symbol)` and `Struct.new(*Symbol, keyword_init: <expr>)` at constant-write rvalue
|
|
11712
13551
|
# position. A trailing `KeywordHashNode` (the `keyword_init: ...` form) is accepted but does not contribute to
|
|
11713
13552
|
# member discovery; every other argument MUST be a `Prism::SymbolNode`. At least one Symbol member is required —
|
|
11714
13553
|
# `Struct.new()` is a degenerate form callers don't typically use.
|
|
13554
|
+
#
|
|
13555
|
+
# Issue #2968 — a leading String (`Struct.new("Named", :a)`, the class `Struct::Named`) and a splatted member list
|
|
13556
|
+
# (`Struct.new(*FIELDS)`) create the class just the same. {#meta_member_positionals} drops the String;
|
|
13557
|
+
# {#meta_members_open?} marks a splatted list's members unknown.
|
|
11715
13558
|
def struct_new_call?(node)
|
|
11716
13559
|
return false unless meta_call_with_name?(node, :Struct, :new)
|
|
11717
13560
|
|
|
@@ -11719,7 +13562,12 @@ module Rigor
|
|
|
11719
13562
|
positional = struct_new_positionals(args)
|
|
11720
13563
|
return false if positional.nil? || positional.empty?
|
|
11721
13564
|
|
|
11722
|
-
positional.
|
|
13565
|
+
positional = positional.drop(1) if positional.first.is_a?(Prism::StringNode)
|
|
13566
|
+
positional.all? { |arg| meta_member_argument?(arg) }
|
|
13567
|
+
end
|
|
13568
|
+
|
|
13569
|
+
def meta_member_argument?(arg)
|
|
13570
|
+
arg.is_a?(Prism::SymbolNode) || arg.is_a?(Prism::SplatNode)
|
|
11723
13571
|
end
|
|
11724
13572
|
|
|
11725
13573
|
# Recognises `Module.new` and `Module.new(&block)` / `Module.new do ... end` at constant-write rvalue position.
|
|
@@ -11825,8 +13673,18 @@ module Rigor
|
|
|
11825
13673
|
}.freeze
|
|
11826
13674
|
private_constant :UNENTERED_FLOW_ENTRY
|
|
11827
13675
|
|
|
11828
|
-
|
|
11829
|
-
|
|
13676
|
+
# ADR-129 WD1 — a Frame: {#with_unentered_evaluator} installs the walk's evaluation for the walk and restores the
|
|
13677
|
+
# outer one, and whether one is installed changes answers (an unentered construct is evaluated only inside a
|
|
13678
|
+
# walk).
|
|
13679
|
+
UNENTERED_EVALUATOR = TypingState.frame(
|
|
13680
|
+
:scope_index_unentered_evaluator,
|
|
13681
|
+
reason: "installed around the index walk that evaluates its unentered constructs; the lambda writes the " \
|
|
13682
|
+
"walk's own table (the walk's state, not typing state) and reads its converged-loop recording and " \
|
|
13683
|
+
"clause widening, and is frozen while installed",
|
|
13684
|
+
# G1′: the evaluation types under the Frame, but only the index walk, outside `type_of`, reads it.
|
|
13685
|
+
typing_never_reads: true
|
|
13686
|
+
)
|
|
13687
|
+
private_constant :UNENTERED_EVALUATOR
|
|
11830
13688
|
|
|
11831
13689
|
# Issues #2196 and #2056 — an `&&` / `||` / `and` / `or`, an `if` / `unless` / ternary or a `while` / `until`
|
|
11832
13690
|
# in a value position (an array element, a hash value, a call argument, an interpolation, a multi-value
|
|
@@ -11837,6 +13695,8 @@ module Rigor
|
|
|
11837
13695
|
# the scope this walk reached it with, under the index's own recorder, so each part is recorded with the scope
|
|
11838
13696
|
# the statement form records it with. What it does not visit (a dead arm) is walked as before.
|
|
11839
13697
|
def evaluate_unentered_flow(node, table, current_scope)
|
|
13698
|
+
return evaluate_unentered_block(node, table, current_scope) if node.is_a?(Prism::CallNode)
|
|
13699
|
+
|
|
11840
13700
|
entry = UNENTERED_FLOW_ENTRY[node.class]
|
|
11841
13701
|
return if entry.nil?
|
|
11842
13702
|
|
|
@@ -11845,10 +13705,27 @@ module Rigor
|
|
|
11845
13705
|
part = node.public_send(entry) || (node.conditions.first&.conditions&.first if node.is_a?(Prism::CaseNode))
|
|
11846
13706
|
return if part.nil? || table.key?(part)
|
|
11847
13707
|
|
|
11848
|
-
evaluate =
|
|
13708
|
+
evaluate = TypingState.current[UNENTERED_EVALUATOR]
|
|
11849
13709
|
evaluate&.call(node, current_scope, code_body_nodes(node, current_scope.source_path))
|
|
11850
13710
|
end
|
|
11851
13711
|
|
|
13712
|
+
# Issue #1246 — the block of a call in a value position (`show([1].map { x = "s"; x.upcase })`, a receiver
|
|
13713
|
+
# chain, an array element, a `->`'s body) is never entered: {StatementEvaluator} types the call through
|
|
13714
|
+
# `ExpressionTyper`, and this walk could only floor a local the block rebinds ({#closure_scope}). Such a block
|
|
13715
|
+
# is entered here instead, alone, from the scope this walk reached its call with, so its body is recorded with
|
|
13716
|
+
# the scopes the statement form records: a capture read before the block rebinds it reads its binding, and the
|
|
13717
|
+
# block's parameters are bound. The call's receiver and arguments stay with the walk. A class-creating or
|
|
13718
|
+
# `refine` block keeps the walk ({#unentered_block_entry}), and a code body inside the block is left out of the
|
|
13719
|
+
# recording ({#code_body_nodes}). A `super` block is a `SuperNode`'s, which the evaluator never enters either.
|
|
13720
|
+
def evaluate_unentered_block(node, table, current_scope)
|
|
13721
|
+
block = node.block
|
|
13722
|
+
return unless block.is_a?(Prism::BlockNode)
|
|
13723
|
+
return if table.key?(block) || class_body_block_call?(node, current_scope.source_path)
|
|
13724
|
+
|
|
13725
|
+
evaluate = TypingState.current[UNENTERED_EVALUATOR]
|
|
13726
|
+
evaluate&.call(node, current_scope, code_body_nodes(block, current_scope.source_path), block_only: true)
|
|
13727
|
+
end
|
|
13728
|
+
|
|
11852
13729
|
# The evaluation of an unentered construct starts with no class frame and no lexical nesting, which only a code
|
|
11853
13730
|
# body observes: a `def`, a `class` / `module` / `class << …` body, and the block of a class-creating call
|
|
11854
13731
|
# (`Class.new { … }`, `Struct.new`, `Data.define`, `Module.new`) or of a `refine`, whose `def`s and constants
|
|
@@ -11882,12 +13759,8 @@ module Rigor
|
|
|
11882
13759
|
# Index walks nest (a rule may re-index a file while another file's walk is suspended), so the outer one is
|
|
11883
13760
|
# restored on exit. A `propagate` called directly while an outer index walk is running would record into the
|
|
11884
13761
|
# OUTER index's table through it; nothing outside {#index} calls `propagate`.
|
|
11885
|
-
def with_unentered_evaluator(evaluate)
|
|
11886
|
-
|
|
11887
|
-
Thread.current[UNENTERED_EVALUATOR_KEY] = evaluate
|
|
11888
|
-
yield
|
|
11889
|
-
ensure
|
|
11890
|
-
Thread.current[UNENTERED_EVALUATOR_KEY] = outer
|
|
13762
|
+
def with_unentered_evaluator(evaluate, &)
|
|
13763
|
+
TypingState.current.with(UNENTERED_EVALUATOR, evaluate.freeze, &)
|
|
11891
13764
|
end
|
|
11892
13765
|
|
|
11893
13766
|
SELF_OPENING_BODIES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
@@ -11895,6 +13768,12 @@ module Rigor
|
|
|
11895
13768
|
|
|
11896
13769
|
# Issue #1717 — a `def` / `class` / `module` body opens its own `self`, so an enclosing unentered block's
|
|
11897
13770
|
# unknown-`self` mark ({Scope#block_self_unknown?}) stops at it, as the evaluator's fresh body scope does.
|
|
13771
|
+
# Issue #1246 — it opens its own local table too, so a name in it (a `def`'s parameter, a body local) is a new
|
|
13772
|
+
# variable, never the enclosing statement's local of that name: `show(Class.new { def call(o) = o + 1 })` after
|
|
13773
|
+
# `o = { x: 1 }` read the parameter as the Hash. Such a name reads `Dynamic[top]` ({#shadow_local}), as in an
|
|
13774
|
+
# unentered closure ({#closure_scope}); a body the evaluator entered is recorded and keeps its own scopes. Only
|
|
13775
|
+
# the parameters and the body take that table: a `def`'s receiver, a `class`'s name and superclass and a
|
|
13776
|
+
# `class << …`'s expression run in the enclosing scope (`class K < base.nope; base = 2; end`).
|
|
11898
13777
|
# A `case` narrows its clauses ({#propagate_subjectless_case}, {#propagate_subject_case}). Every other node
|
|
11899
13778
|
# hands its children the scope as is.
|
|
11900
13779
|
def propagate_children(node, table, current_scope)
|
|
@@ -11903,10 +13782,19 @@ module Rigor
|
|
|
11903
13782
|
|
|
11904
13783
|
return propagate_subject_case(node, table, current_scope)
|
|
11905
13784
|
end
|
|
13785
|
+
return propagate_code_body(node, table, current_scope) if SELF_OPENING_BODIES.include?(node.class)
|
|
11906
13786
|
|
|
11907
|
-
|
|
11908
|
-
|
|
11909
|
-
|
|
13787
|
+
node.rigor_each_child { |child| propagate(child, table, current_scope) }
|
|
13788
|
+
end
|
|
13789
|
+
|
|
13790
|
+
def propagate_code_body(node, table, current_scope)
|
|
13791
|
+
outer_scope = current_scope.with_block_self_unknown(false)
|
|
13792
|
+
inner = [node.body]
|
|
13793
|
+
inner << node.parameters if node.is_a?(Prism::DefNode)
|
|
13794
|
+
inner_scope = node.locals.reduce(outer_scope) { |acc, name| shadow_local(acc, name) }
|
|
13795
|
+
node.rigor_each_child do |child|
|
|
13796
|
+
propagate(child, table, inner.any? { |part| part.equal?(child) } ? inner_scope : outer_scope)
|
|
13797
|
+
end
|
|
11910
13798
|
end
|
|
11911
13799
|
|
|
11912
13800
|
# Issue #1361 — the block of `Thread.new`, `Fiber.new`, `define_method` and the other calls
|
|
@@ -11924,6 +13812,10 @@ module Rigor
|
|
|
11924
13812
|
# argument one scope and threads none, so arguments that write keep the call's: an earlier one may rebind the
|
|
11925
13813
|
# receiver a later one reads (`return y&.concat((y = nil).to_s, y.upcase)`, where the evaluator threads no
|
|
11926
13814
|
# operand).
|
|
13815
|
+
#
|
|
13816
|
+
# Issue #1246 — the block of a value-position call is evaluated before this walk reaches it
|
|
13817
|
+
# ({#evaluate_unentered_block}), so it arrives recorded and keeps the evaluator's entry. The block entries
|
|
13818
|
+
# above therefore reach only a block that evaluation declines: a class-creating or `refine` block.
|
|
11927
13819
|
def propagate_call(node, table, current_scope, recorded: false)
|
|
11928
13820
|
block = node.block
|
|
11929
13821
|
operands =
|
|
@@ -11959,6 +13851,7 @@ module Rigor
|
|
|
11959
13851
|
current_scope = Narrowing.safe_navigation_block_scope(node, current_scope)
|
|
11960
13852
|
# Issue #1429 — nor a guard's narrowing of a global or constant the body may run after code rebinds.
|
|
11961
13853
|
current_scope = GuardRebinding.block_entry(current_scope, block, node)
|
|
13854
|
+
current_scope = current_scope.entering_block_without_gradual_marks(block)
|
|
11962
13855
|
entry =
|
|
11963
13856
|
if FreshFrameBlocks.fresh_entry?(node, current_scope)
|
|
11964
13857
|
FreshFrameBlocks.entry(current_scope, node)
|
|
@@ -12088,14 +13981,17 @@ module Rigor
|
|
|
12088
13981
|
# `it` as the outer block's). An enclosing binding of such a name is replaced with `Dynamic[top]`. Not the
|
|
12089
13982
|
# signature's parameter type: this walk evaluates nothing, and a body write to the name is never threaded,
|
|
12090
13983
|
# so any narrower claim could be stale. A name the enclosing scope does not bind is left unbound, which
|
|
12091
|
-
# reads the same `Dynamic[top]`.
|
|
12092
|
-
#
|
|
12093
|
-
#
|
|
13984
|
+
# reads the same `Dynamic[top]`. Issue #1246 — for the same reason a captured name the body (or a closure
|
|
13985
|
+
# nested in it) writes reads `Dynamic[top]` too ({#widen_condition_writes}): `x = 1; show([1].map { x = "s";
|
|
13986
|
+
# x.upcase })` read `x` as `1` after the write. A captured name the body only reads keeps the enclosing
|
|
13987
|
+
# binding. Issue #1360 — a `->` body and its parameter defaults run whenever the lambda is called, so they read
|
|
13988
|
+
# `$!`, `$@` and `$?` unbound ({FreshFrameBlocks.closure_entry}).
|
|
12094
13989
|
def closure_scope(closure, scope)
|
|
12095
13990
|
scope = GuardRebinding.block_entry(scope, closure, nil)
|
|
12096
13991
|
scope = FreshFrameBlocks.closure_entry(scope, closure, nil)
|
|
12097
13992
|
scope = shadow_local(scope, :it) if closure.parameters.is_a?(Prism::ItParametersNode)
|
|
12098
|
-
closure.locals.reduce(scope) { |acc, name| shadow_local(acc, name) }
|
|
13993
|
+
scope = closure.locals.reduce(scope) { |acc, name| shadow_local(acc, name) }
|
|
13994
|
+
widen_condition_writes(closure, scope)
|
|
12099
13995
|
end
|
|
12100
13996
|
|
|
12101
13997
|
def shadow_local(scope, name)
|