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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "../
|
|
3
|
+
require_relative "../typing_state"
|
|
4
|
+
require_relative "../memo_audit"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
class Scope
|
|
@@ -92,6 +93,39 @@ module Rigor
|
|
|
92
93
|
# when the entry is interned.
|
|
93
94
|
Entry = Data.define(:name, :side, :raw, :candidates, :superclass_edge, :last_segment, :owner) do
|
|
94
95
|
def external? = name.nil?
|
|
96
|
+
|
|
97
|
+
# Issue #3156 — the BODY-LESS class or module the project declares that an external entry names, or nil: the
|
|
98
|
+
# first of its spellings that is a `discovered_classes` key, read in Ruby's lookup order. Such a declaration
|
|
99
|
+
# records no method, superclass or include (`module PMod; end`), which is why the chain holds it as external.
|
|
100
|
+
# The owner-qualified spelling (#1698) leads only on a mixin edge: Ruby evaluates a superclass expression
|
|
101
|
+
# outside the class's own body, so `class Leaf < Base` never names `Leaf::Base`. Any spelling up to the match
|
|
102
|
+
# that RBS alone knows, that a project constant write binds (`AMid = ::Other` in `NS`), or that names a class
|
|
103
|
+
# with a body leaves nil, since Ruby may resolve the constant there or the chain should have held it. It files
|
|
104
|
+
# the entry's negative class and constant edges, which a declaration or write of the name appearing or
|
|
105
|
+
# vanishing fires.
|
|
106
|
+
def declared_name(scope)
|
|
107
|
+
spelling = raw.to_s
|
|
108
|
+
Analysis::DependencyRecorder.read_last_segment(:class, spelling)
|
|
109
|
+
Analysis::DependencyRecorder.read_last_segment(:constant, spelling)
|
|
110
|
+
own = owner && !superclass_edge && !spelling.start_with?("::")
|
|
111
|
+
(own ? ["#{owner}::#{spelling}", *candidates] : candidates).uniq.each do |candidate|
|
|
112
|
+
return nil if constant_written?(scope, candidate)
|
|
113
|
+
return (scope.known_user_class?(candidate) ? nil : candidate) if scope.discovered_classes.key?(candidate)
|
|
114
|
+
return nil if Rigor::Reflection.rbs_class_known?(candidate, scope: scope)
|
|
115
|
+
end
|
|
116
|
+
nil
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
# Whether a project constant write other than a class declaration binds `name` exactly (the census keeps a
|
|
122
|
+
# write as spelled, a `*::` wildcard for one it cannot place), or a literal `const_set` in this file does
|
|
123
|
+
# (`LastLine::SelfEvidence#const_set_spellings`; the census files none).
|
|
124
|
+
def constant_written?(scope, name)
|
|
125
|
+
(scope.bound_constant_names(name) + scope.shadowing_constant_names(name)).any? do |written|
|
|
126
|
+
written == name || written.start_with?("*::")
|
|
127
|
+
end || scope.discovery.implicit_self_evidence&.const_set_spellings&.include?(name) || false
|
|
128
|
+
end
|
|
95
129
|
end
|
|
96
130
|
|
|
97
131
|
# How many project entries a chain may hold before it is cut (ADR-41 WD4). The breadth-first walks
|
|
@@ -104,9 +138,20 @@ module Rigor
|
|
|
104
138
|
# the multi-file rule fired (two or more files declare it and it has two or more edges on that side).
|
|
105
139
|
# Interned per `[node, kind]` in the flavor's bucket, so equal marks are one object. Kind `:extend_hook`
|
|
106
140
|
# (#1730, listing `"*"`) marks a node a module it extends may mix into through a hook; it is not an order mark
|
|
107
|
-
# ({#order_marks}).
|
|
141
|
+
# ({#order_marks}). Kind `:include_hook` (#1592, listing `"*"` and `"?"`) marks a class object a module it
|
|
142
|
+
# includes or prepends may extend, through a hook, with a module no table names; nor is it. Kind `:hook_order`
|
|
143
|
+
# (#1592, listing nothing) marks a class object whose own `extend`s and hook-extended modules have no known
|
|
144
|
+
# order between them: the fork it counts already settles the chain, and the mark tells the typing read why.
|
|
145
|
+
# Kind `:singleton_prepend` (#2998, listing nothing) marks a class object with singleton-side prepends of its
|
|
146
|
+
# own, which master's order does not place, and kind `:prepend_order` (#2998, listing nothing) one whose
|
|
147
|
+
# singleton-side prepends have no known order among themselves; neither is an order mark. Kind `:unnamed_prepend`
|
|
148
|
+
# (#3020, listing nothing) marks a class object with a singleton-side prepend of a module no table names, which
|
|
149
|
+
# is not an order mark either.
|
|
108
150
|
Mark = Data.define(:node, :kind, :listed, :multi_file)
|
|
109
151
|
|
|
152
|
+
# The mark kinds that are not order marks.
|
|
153
|
+
HOOK_MARK_KINDS = %i[extend_hook include_hook hook_order singleton_prepend prepend_order unnamed_prepend].freeze
|
|
154
|
+
|
|
110
155
|
NO_MARKS = [].freeze
|
|
111
156
|
|
|
112
157
|
attr_reader :root, :side, :entries, :level_starts, :level_classes, :flavor
|
|
@@ -133,6 +178,13 @@ module Rigor
|
|
|
133
178
|
# "no definer" from "a definer past the cut", which is budget uncertainty, not absence.
|
|
134
179
|
def truncated? = @truncated
|
|
135
180
|
|
|
181
|
+
# ADR-129 G2 — the chain's content, for the memo audit: a recomputation builds a new chain object, which
|
|
182
|
+
# equals the stored one field for field (its retro world included) when the memo's key holds every input.
|
|
183
|
+
def audit_signature
|
|
184
|
+
[@root, @side, @entries, @level_starts, @level_classes, @levels_end, @truncated, @forks, @marks, @flavor,
|
|
185
|
+
@retro&.audit_signature]
|
|
186
|
+
end
|
|
187
|
+
|
|
136
188
|
# How many forks the replay met while this chain was built, counted across every module sub-chain it drew
|
|
137
189
|
# on (a memoised sub-chain hands its own count up): each entry that reached the chain by a second route.
|
|
138
190
|
# An over-count only ever sends a reader to master's answer.
|
|
@@ -149,13 +201,57 @@ module Rigor
|
|
|
149
201
|
# those readers to master's order would answer a constant lookup breadth-first, which is wrong for the
|
|
150
202
|
# #1567 shape on every class that extends such a module.
|
|
151
203
|
def order_marks
|
|
152
|
-
@memo[:order_marks] ||= @marks.reject { |mark| mark.kind
|
|
204
|
+
@memo[:order_marks] ||= @marks.reject { |mark| HOOK_MARK_KINDS.include?(mark.kind) }.freeze
|
|
153
205
|
end
|
|
154
206
|
|
|
155
207
|
# True when a class on the chain records a mixin the tables cannot name (`"*"`: an `include` of a
|
|
156
208
|
# non-constant, or a call the walk cannot record), so the chain may hold a module it does not list.
|
|
157
209
|
def wildcard_mixin? = @marks.any? { |mark| mark.listed.include?(Relevance::WILDCARD) }
|
|
158
210
|
|
|
211
|
+
# Issue #2991 — true when a class on the chain mixes in a module the tables do not record under any name
|
|
212
|
+
# (`include(*MODS)`, `include helper`, a hook's `base.include M`, an unrecorded `self.include M`): the chain's SET
|
|
213
|
+
# of modules is unknown, not only its order. Every such mark is also a {#wildcard_mixin?} one; `include M;
|
|
214
|
+
# prepend M` taints the order alone.
|
|
215
|
+
def unnamed_mixin? = @marks.any? { |mark| mark.listed.include?(Relevance::UNNAMED) }
|
|
216
|
+
|
|
217
|
+
# Issue #1592 — whether a class object on the chain has hook-extended modules whose order against its own
|
|
218
|
+
# `extend`s, or among themselves, is not a fact ({Builder#singleton_mixins}).
|
|
219
|
+
def hook_order_marked? = @marks.any? { |mark| mark.kind == :hook_order }
|
|
220
|
+
|
|
221
|
+
# Issue #2998 — whether a class object on the chain has singleton-side prepends of its own
|
|
222
|
+
# ({Builder#own_singleton_prepends}), which master's order does not place.
|
|
223
|
+
def singleton_prepend_marked? = @marks.any? { |mark| mark.kind == :singleton_prepend }
|
|
224
|
+
|
|
225
|
+
# Issue #2998 — the class objects on the chain whose prepend region has no known order
|
|
226
|
+
# ({Builder#order_prepends}).
|
|
227
|
+
def prepend_order_nodes
|
|
228
|
+
@memo[:prepend_order_nodes] ||= @marks.filter_map { |mark| mark.node if mark.kind == :prepend_order }.freeze
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Issue #3020 — the class objects on the chain with a singleton-side prepend of a module no table names
|
|
232
|
+
# ({Builder#own_singleton_prepends}).
|
|
233
|
+
def unnamed_prepend_nodes
|
|
234
|
+
@memo[:unnamed_prepend_nodes] ||= @marks.filter_map { |mark| mark.node if mark.kind == :unnamed_prepend }.freeze
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Issue #3020 — the class objects on the chain whose own singleton side lists a module no table names
|
|
238
|
+
# (`extend(*MODS)`, `class << self; include(*MODS)`, `singleton_class.prepend(*MODS)`: the `:extend` order mark
|
|
239
|
+
# listing {Relevance::UNNAMED}).
|
|
240
|
+
def unnamed_extend_nodes
|
|
241
|
+
@memo[:unnamed_extend_nodes] ||= @marks.filter_map do |mark|
|
|
242
|
+
mark.node if mark.kind == :extend && mark.listed.include?(Relevance::UNNAMED)
|
|
243
|
+
end.freeze
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# The chain's retro world ({#settle}), or nil when it has none.
|
|
247
|
+
def retro_world = @retro
|
|
248
|
+
|
|
249
|
+
# Issue #1592 — the class objects on a singleton chain that a module they include or prepend may extend, through
|
|
250
|
+
# a hook, with a module no table names: that module sits right after the class object's own entry.
|
|
251
|
+
def hook_open_nodes
|
|
252
|
+
@memo[:hook_open_nodes] ||= @marks.filter_map { |mark| mark.node if mark.kind == :include_hook }.freeze
|
|
253
|
+
end
|
|
254
|
+
|
|
159
255
|
# The ONE decision every first-definer reader makes: does `answer`, read off this chain, stand, or does
|
|
160
256
|
# the reader answer what the walk this chain replaced answered ({MasterOrder})? Returns `:chain` or
|
|
161
257
|
# `:master`.
|
|
@@ -224,18 +320,30 @@ module Rigor
|
|
|
224
320
|
# it does not head the part searched (its prepends, or its own edges when the search starts past it,
|
|
225
321
|
# placed the rest). The entry that answers records none of its own: its method edge is the caller's
|
|
226
322
|
# `Scope#user_def_for`. A read that finds nothing has passed every entry.
|
|
323
|
+
#
|
|
324
|
+
# Issue #2998 — on a singleton chain a NEW file reopening a class object's class can prepend to its singleton
|
|
325
|
+
# (`class << self; prepend P`), ahead of everything the class object's level holds, which no class edge of the
|
|
326
|
+
# declaring files sees. So each class object the search passes, and one that answers, also files the name edge
|
|
327
|
+
# `mark:<last segment>` ({#record_prepend_regions}), which `IncrementalSession#mark_affected` fires when a
|
|
328
|
+
# `[ENVELOPE_SINGLETON_PREPEND_KIND, …]` entry (a diffed class mark) appears on or vanishes from the class, and
|
|
329
|
+
# not on a plain reopening. A class object behind the answer is filed through {#settle}'s filing, and so is every
|
|
330
|
+
# one on a read that finds nothing.
|
|
227
331
|
def search(scope, start = 0, stop = nil, side: nil)
|
|
228
332
|
recording = Analysis::DependencyRecorder.active?
|
|
229
333
|
stop ||= @entries.size
|
|
230
334
|
if recording
|
|
231
335
|
record_head(scope, start, side)
|
|
232
336
|
record_extend_hooks(scope)
|
|
337
|
+
record_include_hooks(scope)
|
|
233
338
|
end
|
|
234
339
|
index = start
|
|
235
340
|
while index < stop
|
|
236
341
|
entry = @entries[index]
|
|
237
342
|
answer = side.nil? || entry.side == side ? yield(entry) : nil
|
|
238
|
-
|
|
343
|
+
if answer
|
|
344
|
+
record_prepend_regions(start, index) if recording
|
|
345
|
+
return answer
|
|
346
|
+
end
|
|
239
347
|
|
|
240
348
|
ResolutionChain.record_entry(scope, entry, side) if recording
|
|
241
349
|
index += 1
|
|
@@ -243,6 +351,19 @@ module Rigor
|
|
|
243
351
|
nil
|
|
244
352
|
end
|
|
245
353
|
|
|
354
|
+
# The `mark:` name edges of the class objects on a singleton chain from `start` through `last`.
|
|
355
|
+
def record_prepend_regions(start, last)
|
|
356
|
+
return unless @side == :singleton
|
|
357
|
+
|
|
358
|
+
(start..last).each do |index|
|
|
359
|
+
entry = @entries[index]
|
|
360
|
+
next unless entry.side == :singleton && !entry.external?
|
|
361
|
+
|
|
362
|
+
Analysis::DependencyRecorder.read_last_segment(:mark, entry.name)
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
private :record_prepend_regions
|
|
366
|
+
|
|
246
367
|
# Records the ADR-46 class edge of the root and of every project entry ahead of `stop` (on `side`, when
|
|
247
368
|
# given; every entry when `stop` is nil) — the dependency of a read that consulted those positions.
|
|
248
369
|
def record(scope, stop = nil, side: nil)
|
|
@@ -251,6 +372,21 @@ module Rigor
|
|
|
251
372
|
ResolutionChain.record_class(scope, @root)
|
|
252
373
|
(stop || @entries.size).times { |index| ResolutionChain.record_entry(scope, @entries[index], side) }
|
|
253
374
|
record_extend_hooks(scope)
|
|
375
|
+
record_include_hooks(scope)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# Issue #2689 — the negative edge a method-walk MISS owes each EXTERNAL entry: `class:<last segment>` of the name
|
|
379
|
+
# as written. An external entry is an ancestor the project declares with no methods (the usual empty
|
|
380
|
+
# `ApplicationRecord`) or not at all yet, so the walk looked nothing up there; a later file that declares it
|
|
381
|
+
# with the method turns the miss, and the declaration appearing is the one event that file fires. A core or gem
|
|
382
|
+
# ancestor (`StandardError`, `Comparable`) files its key too, which fires only on a file that declares a
|
|
383
|
+
# constant of that name. Recorded only on a miss: a hit's answer cannot be turned by an entry gaining a method.
|
|
384
|
+
def record_external_absence
|
|
385
|
+
return unless Analysis::DependencyRecorder.active?
|
|
386
|
+
|
|
387
|
+
@entries.each do |entry|
|
|
388
|
+
Analysis::DependencyRecorder.read_last_segment(:class, entry.raw.to_s) if entry.external?
|
|
389
|
+
end
|
|
254
390
|
end
|
|
255
391
|
|
|
256
392
|
# What a {#settle} verdict adds to what the search already filed. The search files the root and every entry
|
|
@@ -279,6 +415,7 @@ module Rigor
|
|
|
279
415
|
Analysis::DependencyRecorder.file_chain_once(self, start, scope.discovery) { file_beyond(scope, start) }
|
|
280
416
|
record_hooks(scope)
|
|
281
417
|
record_extend_hooks(scope)
|
|
418
|
+
record_include_hooks(scope)
|
|
282
419
|
end
|
|
283
420
|
private :record_beyond
|
|
284
421
|
|
|
@@ -325,7 +462,7 @@ module Rigor
|
|
|
325
462
|
end
|
|
326
463
|
private :record_hooks
|
|
327
464
|
|
|
328
|
-
# #1730 — the edges of the {Builder#extend_hook_taint
|
|
465
|
+
# #1730 — the edges of the {Builder#extend_hook_taint} decision: an instance chain's project entries may carry
|
|
329
466
|
# a `"*"` mark because a module one of them `extend`s has a hook that mixes into the extender, and the
|
|
330
467
|
# module is often declared in a file the chain records nothing of. Each such module the decision read files
|
|
331
468
|
# its class edges (an edit to its hook moves its declaration signature) and the negative method edges of
|
|
@@ -352,6 +489,168 @@ module Rigor
|
|
|
352
489
|
end
|
|
353
490
|
private :record_extend_hooks
|
|
354
491
|
|
|
492
|
+
# #1592 — the edges of the {Builder#hook_extensions} read: a singleton chain draws, for every class on it, the
|
|
493
|
+
# modules that class's direct mixins' hooks extend it with, and the mixins are declared in files the chain's own
|
|
494
|
+
# entries record nothing of. Each resolved mixin files its class edges and the negative `mark:` edge on its name,
|
|
495
|
+
# which an edit adding or dropping a hook-extend entry fires; every mixin, and every module a hook extends by
|
|
496
|
+
# name, files the negative class edge on its last segment, which a NEW file declaring or reopening it fires.
|
|
497
|
+
# Filed once per consumer and chain.
|
|
498
|
+
def record_include_hooks(scope)
|
|
499
|
+
return unless @side == :singleton
|
|
500
|
+
|
|
501
|
+
edges = (@memo[:include_hook_edges] ||= ResolutionChain.include_hook_edges(scope, @flavor, @level_classes))
|
|
502
|
+
return if edges.empty?
|
|
503
|
+
|
|
504
|
+
Analysis::DependencyRecorder.file_chain_once(self, :include_hooks, scope.discovery) do
|
|
505
|
+
edges.each do |kind, name|
|
|
506
|
+
ResolutionChain.record_class(scope, name) if kind == :mixin
|
|
507
|
+
Analysis::DependencyRecorder.read_last_segment(:mark, name) if kind == :mixin
|
|
508
|
+
Analysis::DependencyRecorder.read_last_segment(:class, name)
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
private :record_include_hooks
|
|
513
|
+
|
|
514
|
+
# `[[:mixin, resolved name] | [:spelling, raw], …]`: the direct mixins of every class on a singleton chain, and
|
|
515
|
+
# the modules the resolved ones' hooks extend by name ({Builder#hook_extensions}).
|
|
516
|
+
def self.include_hook_edges(scope, flavor, classes)
|
|
517
|
+
resolver = resolver_for(scope, flavor)
|
|
518
|
+
discovery = scope.discovery
|
|
519
|
+
classes.each_with_object([]) do |klass, out|
|
|
520
|
+
next if klass.nil?
|
|
521
|
+
|
|
522
|
+
(discovery.discovered_includes[klass] || EMPTY_NAMES).uniq.each do |raw|
|
|
523
|
+
mixin = resolver.resolve_declared(klass, raw)
|
|
524
|
+
next out << [:spelling, raw].freeze if mixin.nil?
|
|
525
|
+
|
|
526
|
+
out << [:mixin, mixin].freeze
|
|
527
|
+
hook_extend_keys(discovery, mixin).each { |key| out << [:spelling, key[1]].freeze }
|
|
528
|
+
concern_dependency_edges(discovery, resolver, mixin, out)
|
|
529
|
+
end
|
|
530
|
+
end.uniq.freeze
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
# Issue #2992 — the edges of a concern mixin's dependency read ({.concern_dependencies}): every module its
|
|
534
|
+
# dependency entries name, a concern or not yet one (a file that adds `extend ActiveSupport::Concern` to it
|
|
535
|
+
# turns it into one), as a mixin, with the modules its hooks extend by name; a spelling no module answers files
|
|
536
|
+
# its negative class edge. A concern among them is followed further.
|
|
537
|
+
def self.concern_dependency_edges(discovery, resolver, mixin, out, seen = [mixin])
|
|
538
|
+
return unless concern_bucket?(discovery, mixin)
|
|
539
|
+
|
|
540
|
+
dependency_spellings(discovery, mixin).each do |raw|
|
|
541
|
+
dependency = resolver.resolve_declared(mixin, raw)
|
|
542
|
+
next out << [:spelling, raw].freeze if dependency.nil?
|
|
543
|
+
next if seen.include?(dependency)
|
|
544
|
+
|
|
545
|
+
seen << dependency
|
|
546
|
+
out << [:mixin, dependency].freeze
|
|
547
|
+
hook_extend_keys(discovery, dependency).each { |key| out << [:spelling, key[1]].freeze }
|
|
548
|
+
concern_dependency_edges(discovery, resolver, dependency, out, seen)
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
private_class_method :concern_dependency_edges
|
|
552
|
+
|
|
553
|
+
# The `[ENVELOPE_CONCERN_DEPENDENCY_KIND, written]` spellings of a module's bucket, nearest first (a later
|
|
554
|
+
# statement's ahead of an earlier one's).
|
|
555
|
+
def self.dependency_spellings(discovery, name)
|
|
556
|
+
bucket = discovery.discovered_parameter_envelopes[name]
|
|
557
|
+
return EMPTY_NAMES if bucket.nil?
|
|
558
|
+
|
|
559
|
+
bucket.each_key.filter_map do |key|
|
|
560
|
+
key[1] if key.is_a?(Array) && key.first == DiscoveryIndex::ENVELOPE_CONCERN_DEPENDENCY_KIND
|
|
561
|
+
end.reverse.uniq
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
# Issue #2992 — the concerns a concern `mixin` provably takes as dependencies (its
|
|
565
|
+
# `[ENVELOPE_CONCERN_DEPENDENCY_KIND, written]` entries: an `include` or `prepend` after its own `extend
|
|
566
|
+
# ActiveSupport::Concern`), at any depth through concerns, nearest first, without `mixin`:
|
|
567
|
+
# `Concern#append_features` records each as a dependency of `mixin` and includes it into `mixin`'s final
|
|
568
|
+
# includer, so the includer's singleton chain gains what each one's hooks extend it with. Empty when `mixin` is
|
|
569
|
+
# no concern. A dependency the walk meets twice is listed once, at its nearest place.
|
|
570
|
+
def self.concern_dependencies(discovery, resolver, mixin, seen = [mixin])
|
|
571
|
+
return EMPTY_NAMES unless concern_bucket?(discovery, mixin)
|
|
572
|
+
|
|
573
|
+
out = []
|
|
574
|
+
dependency_spellings(discovery, mixin).each do |raw|
|
|
575
|
+
dependency = resolver.resolve_declared(mixin, raw)
|
|
576
|
+
next if dependency.nil? || seen.include?(dependency) || !concern_bucket?(discovery, dependency)
|
|
577
|
+
|
|
578
|
+
seen << dependency
|
|
579
|
+
out << dependency
|
|
580
|
+
out.concat(concern_dependencies(discovery, resolver, dependency, seen))
|
|
581
|
+
end
|
|
582
|
+
out
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
# The entry an `extend ActiveSupport::Concern` module records ({Inference::ScopeIndexer}).
|
|
586
|
+
CONCERN_KEY = [DiscoveryIndex::ENVELOPE_HOOK_EXTEND_KIND, "ClassMethods", :const_get].freeze
|
|
587
|
+
private_constant :CONCERN_KEY
|
|
588
|
+
|
|
589
|
+
def self.concern_bucket?(discovery, name)
|
|
590
|
+
bucket = discovery.discovered_parameter_envelopes[name]
|
|
591
|
+
!bucket.nil? && bucket.key?(CONCERN_KEY)
|
|
592
|
+
end
|
|
593
|
+
private_class_method :concern_bucket?
|
|
594
|
+
|
|
595
|
+
# Issue #2997 — whether `owner.const_get(raw)` may find the project's `candidate` (`"#{owner}::#{raw}"`): a
|
|
596
|
+
# `class_methods do … end` block's `def`s are filed on `Owner::ClassMethods` in every body of the module, but
|
|
597
|
+
# Ruby has that module only when `owner` is a concern, so one that no `module` declaration writes counts only
|
|
598
|
+
# for a concern `owner`.
|
|
599
|
+
def self.const_get_candidate?(discovery, owner, candidate)
|
|
600
|
+
concern_bucket?(discovery, owner) || discovery.discovered_classes.key?(candidate)
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
# Issue #3005 — whether `name` is a module a concern block's `def self.x`s are filed on (`Owner::<included>`).
|
|
604
|
+
def self.hook_block_module?(name)
|
|
605
|
+
DiscoveryIndex::HOOK_BLOCK_SEGMENTS.any? { |segment| name.end_with?("::#{segment}") }
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
# Issue #3005 — whether a hook-block module on the level of the class object at `index` defines `method_name`:
|
|
609
|
+
# Ruby defines it on that class object itself when the block runs, so it and the class's own `def self.x`
|
|
610
|
+
# replace each other in an order the tables do not hold (the `include` against the `def`).
|
|
611
|
+
def hook_block_rival?(scope, index, method_name)
|
|
612
|
+
level = @level_starts.rindex { |start| start <= index }
|
|
613
|
+
return false if level.nil? || level >= level_count
|
|
614
|
+
|
|
615
|
+
level_entries(level).any? do |entry|
|
|
616
|
+
!entry.external? && entry.side == :instance && ResolutionChain.hook_block_module?(entry.name) &&
|
|
617
|
+
!scope.user_def_for(entry.name, method_name).nil?
|
|
618
|
+
end
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
GATED_HOOKS = DiscoveryIndex::CONCERN_BLOCK_HOOKS.invert.freeze
|
|
622
|
+
private_constant :GATED_HOOKS
|
|
623
|
+
|
|
624
|
+
# Issue #3005 — the hook an entry of `owner`'s bucket naming `hook` runs on: a gated concern-block hook
|
|
625
|
+
# ({DiscoveryIndex::CONCERN_BLOCK_HOOKS}) is its plain hook when `owner` is a concern and nil otherwise; any
|
|
626
|
+
# other hook is itself.
|
|
627
|
+
def self.effective_hook(discovery, owner, hook)
|
|
628
|
+
plain = GATED_HOOKS[hook]
|
|
629
|
+
return hook if plain.nil?
|
|
630
|
+
|
|
631
|
+
concern_bucket?(discovery, owner) ? plain : nil
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
HOOK_KEY_SLOTS = {
|
|
635
|
+
DiscoveryIndex::ENVELOPE_HOOK_EXTEND_KIND => 2, DiscoveryIndex::ENVELOPE_HOOK_EXTEND_OPEN_KIND => 1,
|
|
636
|
+
DiscoveryIndex::ENVELOPE_HOOK_DEF_KIND => 1
|
|
637
|
+
}.freeze
|
|
638
|
+
private_constant :HOOK_KEY_SLOTS
|
|
639
|
+
|
|
640
|
+
# The hook a hook-extend, open or def envelope key names (`:const_get` for a concern's), or nil for another key.
|
|
641
|
+
def self.hook_of(key)
|
|
642
|
+
slot = key.is_a?(Array) ? HOOK_KEY_SLOTS[key.first] : nil
|
|
643
|
+
slot && key[slot]
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
# The `[ENVELOPE_HOOK_EXTEND_KIND, written, hook]` keys of a module's envelope bucket, in recording order.
|
|
647
|
+
def self.hook_extend_keys(discovery, name)
|
|
648
|
+
bucket = discovery.discovered_parameter_envelopes[name]
|
|
649
|
+
return EMPTY_NAMES if bucket.nil?
|
|
650
|
+
|
|
651
|
+
bucket.each_key.select { |key| key.is_a?(Array) && key.first == DiscoveryIndex::ENVELOPE_HOOK_EXTEND_KIND }
|
|
652
|
+
end
|
|
653
|
+
|
|
355
654
|
# The hooks that let a module `extend`ed onto N reshape N's INSTANCE side (#1730): its singleton `extended` /
|
|
356
655
|
# `extend_object` run against N at the `extend` and may mix into N; its instance `included` /
|
|
357
656
|
# `append_features` / `prepended` / `prepend_features` / `inherited` become N's own singleton hooks and run
|
|
@@ -359,11 +658,44 @@ module Rigor
|
|
|
359
658
|
SINGLETON_EXTEND_HOOKS = %i[extended extend_object].freeze
|
|
360
659
|
INSTANCE_EXTEND_HOOKS = %i[included append_features prepended prepend_features inherited].freeze
|
|
361
660
|
|
|
362
|
-
|
|
661
|
+
# ADR-129 WD1 — three interning Memos, not run-scoped: a value is a frozen Array of frozen Strings built from the
|
|
662
|
+
# name and frozen constants alone, so an entry is fixed once written and holds nothing of a run. Each hit goes
|
|
663
|
+
# through `MemoAudit.check`, and an audit recomputation bypasses the table.
|
|
664
|
+
EXTEND_HOOK_KEYS = TypingState.memo(
|
|
665
|
+
:resolution_chain_extend_hook_keys,
|
|
666
|
+
audit: "extend hook keys",
|
|
667
|
+
reason: "keyed on the module name; a frozen Array of frozen Strings built from the name and the extend-hook " \
|
|
668
|
+
"constants alone"
|
|
669
|
+
)
|
|
670
|
+
HOOK_KEYS = TypingState.memo(
|
|
671
|
+
:resolution_chain_hook_keys,
|
|
672
|
+
audit: "hook keys",
|
|
673
|
+
reason: "keyed on the class name; a frozen Array of frozen Strings built from the name and " \
|
|
674
|
+
"Relevance::HOOKS alone"
|
|
675
|
+
)
|
|
676
|
+
EXTERNAL_KEYS = TypingState.memo(
|
|
677
|
+
:resolution_chain_external_keys,
|
|
678
|
+
audit: "external keys",
|
|
679
|
+
reason: "keyed on the class name; HOOK_KEYS' answer plus one frozen String built from the name"
|
|
680
|
+
)
|
|
681
|
+
private_constant :EXTEND_HOOK_KEYS, :HOOK_KEYS, :EXTERNAL_KEYS
|
|
363
682
|
|
|
364
683
|
# The frozen negative method keys of `name`'s {SINGLETON_EXTEND_HOOKS} and {INSTANCE_EXTEND_HOOKS}.
|
|
365
684
|
def self.extend_hook_keys(name)
|
|
366
|
-
|
|
685
|
+
return build_extend_hook_keys(name) if Inference::MemoAudit.enabled? && Inference::MemoAudit.suspended?
|
|
686
|
+
|
|
687
|
+
state = TypingState.current
|
|
688
|
+
table = (state[EXTEND_HOOK_KEYS] ||= {})
|
|
689
|
+
stored = table[name]
|
|
690
|
+
if stored.nil?
|
|
691
|
+
return table[name] = Inference::MemoAudit.key_audit("extend hook keys") { build_extend_hook_keys(name) }
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
Inference::MemoAudit.check("extend hook keys", stored, name) { build_extend_hook_keys(name) }
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
def self.build_extend_hook_keys(name)
|
|
698
|
+
(
|
|
367
699
|
SINGLETON_EXTEND_HOOKS.map { |hook| "method:#{name}.#{hook}".freeze } +
|
|
368
700
|
INSTANCE_EXTEND_HOOKS.map { |hook| "method:#{name}##{hook}".freeze }
|
|
369
701
|
).freeze
|
|
@@ -387,7 +719,7 @@ module Rigor
|
|
|
387
719
|
end
|
|
388
720
|
|
|
389
721
|
# `[[spelling, resolved name or nil, candidate names], …]`: every module the project entries of an instance chain
|
|
390
|
-
# `extend`, as {Builder#extend_hook_taint
|
|
722
|
+
# `extend`, as {Builder#extend_hook_taint} resolved them.
|
|
391
723
|
def self.extend_targets(scope, flavor, entries)
|
|
392
724
|
resolver = resolver_for(scope, flavor)
|
|
393
725
|
extends = scope.discovery.discovered_extends
|
|
@@ -401,19 +733,37 @@ module Rigor
|
|
|
401
733
|
end.uniq.freeze
|
|
402
734
|
end
|
|
403
735
|
|
|
404
|
-
@hook_keys = {}
|
|
405
|
-
@external_keys = {}
|
|
406
|
-
|
|
407
736
|
# The frozen `method:Name.hook` negative keys of `name`'s {Relevance::HOOKS}, built once per name.
|
|
408
737
|
def self.hook_keys(name)
|
|
409
|
-
|
|
738
|
+
return build_hook_keys(name) if Inference::MemoAudit.enabled? && Inference::MemoAudit.suspended?
|
|
739
|
+
|
|
740
|
+
state = TypingState.current
|
|
741
|
+
table = (state[HOOK_KEYS] ||= {})
|
|
742
|
+
stored = table[name]
|
|
743
|
+
return table[name] = Inference::MemoAudit.key_audit("hook keys") { build_hook_keys(name) } if stored.nil?
|
|
744
|
+
|
|
745
|
+
Inference::MemoAudit.check("hook keys", stored, name) { build_hook_keys(name) }
|
|
410
746
|
end
|
|
411
747
|
|
|
748
|
+
def self.build_hook_keys(name) = Relevance::HOOKS.map { |hook| "method:#{name}.#{hook}".freeze }.freeze
|
|
749
|
+
|
|
412
750
|
# {hook_keys} plus the `mixin:<name>` key an external entry files for its unpositioned mixins (#1661).
|
|
413
751
|
def self.external_keys(name)
|
|
414
|
-
|
|
752
|
+
return build_external_keys(name) if Inference::MemoAudit.enabled? && Inference::MemoAudit.suspended?
|
|
753
|
+
|
|
754
|
+
state = TypingState.current
|
|
755
|
+
table = (state[EXTERNAL_KEYS] ||= {})
|
|
756
|
+
stored = table[name]
|
|
757
|
+
if stored.nil?
|
|
758
|
+
return table[name] = Inference::MemoAudit.key_audit("external keys") { build_external_keys(name) }
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
Inference::MemoAudit.check("external keys", stored, name) { build_external_keys(name) }
|
|
415
762
|
end
|
|
416
763
|
|
|
764
|
+
def self.build_external_keys(name) = (hook_keys(name) + ["mixin:#{name}".freeze]).freeze
|
|
765
|
+
private_class_method :build_extend_hook_keys, :build_hook_keys, :build_external_keys
|
|
766
|
+
|
|
417
767
|
def record_head(scope, start, side)
|
|
418
768
|
ResolutionChain.record_class(scope, @root) unless start.zero? && @entries.first&.name == @root
|
|
419
769
|
start.times { |index| ResolutionChain.record_entry(scope, @entries[index], side) }
|
|
@@ -445,6 +795,25 @@ module Rigor
|
|
|
445
795
|
@entries.index { |entry| entry.name == name && entry.side == side }
|
|
446
796
|
end
|
|
447
797
|
|
|
798
|
+
# Issue #2979 — the position of the first entry after `name`'s own that is a true ancestor of it, or nil when
|
|
799
|
+
# `name` has no entry on `side`. The entries of the names bound to the same class object
|
|
800
|
+
# (`K = Struct.new("Named", …)`) follow `name`'s own directly, and a `def` under one of them is a redefinition
|
|
801
|
+
# of the class's method, not an override, so a reader of "the entries after the class" starts past them.
|
|
802
|
+
def ancestors_start(scope, name, side = @side)
|
|
803
|
+
own = index_of(name, side)
|
|
804
|
+
return nil if own.nil?
|
|
805
|
+
|
|
806
|
+
twins = DiscoveryIndex.same_class_names(scope.discovery.discovered_parameter_envelopes[name])
|
|
807
|
+
start = own + 1
|
|
808
|
+
start += 1 while twins_entry?(@entries[start], twins, side)
|
|
809
|
+
start
|
|
810
|
+
end
|
|
811
|
+
|
|
812
|
+
def twins_entry?(entry, twins, side)
|
|
813
|
+
!twins.empty? && !entry.nil? && entry.side == side && twins.include?(entry.name)
|
|
814
|
+
end
|
|
815
|
+
private :twins_entry?
|
|
816
|
+
|
|
448
817
|
# A LEVEL is one superclass step: the class (or its singleton) with the modules inserted around it,
|
|
449
818
|
# before the next superclass's entries begin. `level_classes[k]` is that class, or nil for a level that
|
|
450
819
|
# is an external superclass. Only COMPLETE levels are counted — a level the {LIMIT} cut in half is not.
|
|
@@ -496,8 +865,21 @@ module Rigor
|
|
|
496
865
|
end
|
|
497
866
|
private :compute_levels
|
|
498
867
|
|
|
499
|
-
|
|
500
|
-
|
|
868
|
+
# ADR-129 WD1 — a run-scoped Memo: one `[discovery, flavors]` slot keyed on the discovery index's identity, the
|
|
869
|
+
# only input a chain reads.
|
|
870
|
+
#
|
|
871
|
+
# ADR-129 G2 — every table of the slot's flavor buckets is audited on a hit under `RIGOR_MEMO_AUDIT=1`, and an
|
|
872
|
+
# audit recomputation reads a fresh, unstored bucket ({.flavor_bucket}), so it reads nothing stored.
|
|
873
|
+
MEMO = TypingState.memo(
|
|
874
|
+
:resolution_chain,
|
|
875
|
+
run_scoped: true,
|
|
876
|
+
audit: ["resolution chain", "resolution chain name", "resolution chain relevance",
|
|
877
|
+
"resolution chain master order", "resolution chain linearisation", "resolution chain mark",
|
|
878
|
+
"resolution chain entry", "resolution chain external entry", "singleton hook verdict"],
|
|
879
|
+
reason: "one slot keyed on the discovery index's identity; a chain is a pure function of that frozen index " \
|
|
880
|
+
"and the flavor"
|
|
881
|
+
)
|
|
882
|
+
private_constant :MEMO
|
|
501
883
|
|
|
502
884
|
FLAVORS = %i[methods constants arity].freeze
|
|
503
885
|
|
|
@@ -509,17 +891,40 @@ module Rigor
|
|
|
509
891
|
|
|
510
892
|
bucket = flavor_bucket(scope.discovery, flavor)
|
|
511
893
|
chains = bucket[side]
|
|
512
|
-
|
|
894
|
+
cached = chains[class_name]
|
|
895
|
+
if cached.nil?
|
|
896
|
+
return chains[class_name] = Inference::MemoAudit.key_audit("resolution chain") do
|
|
897
|
+
Builder.new(scope, flavor, bucket).chain(class_name, side)
|
|
898
|
+
end
|
|
899
|
+
end
|
|
900
|
+
return cached unless Inference::MemoAudit.enabled?
|
|
901
|
+
|
|
902
|
+
Inference::MemoAudit.check("resolution chain", cached.audit_signature, class_name, side) do
|
|
903
|
+
fresh = Analysis::DependencyRecorder.withhold do
|
|
904
|
+
Builder.new(scope, flavor, flavor_bucket(scope.discovery, flavor)).chain(class_name, side)
|
|
905
|
+
end
|
|
906
|
+
fresh.first.audit_signature
|
|
907
|
+
end
|
|
908
|
+
cached
|
|
513
909
|
end
|
|
514
910
|
|
|
911
|
+
# The flavor's bucket of memo tables. While an audit recomputation runs ({Inference::MemoAudit.suspended?})
|
|
912
|
+
# every call answers a fresh bucket that is never stored, so the recomputation neither reads nor writes the
|
|
913
|
+
# slot.
|
|
515
914
|
def self.flavor_bucket(discovery, flavor)
|
|
516
|
-
|
|
915
|
+
return new_bucket if Inference::MemoAudit.enabled? && Inference::MemoAudit.suspended?
|
|
916
|
+
|
|
917
|
+
state = TypingState.current
|
|
918
|
+
slot = state[MEMO]
|
|
517
919
|
unless slot && slot[0].equal?(discovery)
|
|
518
920
|
slot = [discovery, {}]
|
|
519
|
-
|
|
921
|
+
state[MEMO] = slot
|
|
520
922
|
end
|
|
521
|
-
slot[1][flavor] ||=
|
|
522
|
-
|
|
923
|
+
slot[1][flavor] ||= new_bucket
|
|
924
|
+
end
|
|
925
|
+
|
|
926
|
+
def self.new_bucket
|
|
927
|
+
{ instance: {}, singleton: {}, names: {}, interned: {}, externals: {}, master: {}, marks: {} }
|
|
523
928
|
end
|
|
524
929
|
|
|
525
930
|
# Which project class an as-written ancestor name denotes, under one flavor — shared by the chain and
|
|
@@ -536,9 +941,13 @@ module Rigor
|
|
|
536
941
|
# ambiguous spelling names (`:arity` only), or nil.
|
|
537
942
|
def resolve(owner, raw)
|
|
538
943
|
by_owner = (@names[owner] ||= {})
|
|
539
|
-
|
|
944
|
+
unless by_owner.key?(raw)
|
|
945
|
+
return by_owner[raw] = Inference::MemoAudit.key_audit("resolution chain name") { compute(owner, raw) }
|
|
946
|
+
end
|
|
540
947
|
|
|
541
|
-
by_owner[raw]
|
|
948
|
+
Inference::MemoAudit.check("resolution chain name", by_owner[raw], owner, raw) do
|
|
949
|
+
Analysis::DependencyRecorder.withhold { compute(owner, raw) }.first
|
|
950
|
+
end
|
|
542
951
|
end
|
|
543
952
|
|
|
544
953
|
# {#resolve}, with an ambiguous spelling read as unresolved.
|
|
@@ -547,8 +956,28 @@ module Rigor
|
|
|
547
956
|
resolved.is_a?(String) ? resolved : nil
|
|
548
957
|
end
|
|
549
958
|
|
|
959
|
+
# Issue #1592 — {#resolve_one}, or else the first candidate the project declares at all: a module holding only
|
|
960
|
+
# hooks (`included do extend M end`) is no project class under any flavor, yet its hooks run.
|
|
961
|
+
# The search stops at the first candidate that exists in any form: one RBS or a gem declares and the project's
|
|
962
|
+
# source does not has no hooks the tables know, so it answers nil.
|
|
963
|
+
#
|
|
964
|
+
# Issue #3015 — a module whose bucket holds the concern entry counts too: its body runs `extend
|
|
965
|
+
# ActiveSupport::Concern`, so the project declares it. A one-file index has no `discovered_class_sources`, and
|
|
966
|
+
# would otherwise drop a concern whose only defs sit in its blocks from an includer's chain.
|
|
967
|
+
def resolve_declared(owner, raw)
|
|
968
|
+
candidates(owner, raw).each do |candidate|
|
|
969
|
+
return candidate if project?(candidate) || @discovery.discovered_class_sources.key?(candidate) ||
|
|
970
|
+
@discovery.discovered_parameter_envelopes[candidate]&.key?(CONCERN_KEY)
|
|
971
|
+
return nil if rbs_known?(candidate)
|
|
972
|
+
end
|
|
973
|
+
nil
|
|
974
|
+
end
|
|
975
|
+
|
|
550
976
|
def candidates(owner, raw) = @scope.ancestor_name_candidates(owner, raw)
|
|
551
977
|
|
|
978
|
+
# Whether `name`, already qualified, is a project class under this flavor.
|
|
979
|
+
def project_name?(name) = project?(name)
|
|
980
|
+
|
|
552
981
|
private
|
|
553
982
|
|
|
554
983
|
def compute(owner, raw)
|
|
@@ -559,6 +988,12 @@ module Rigor
|
|
|
559
988
|
alternatives.empty? ? nil : alternatives
|
|
560
989
|
end
|
|
561
990
|
|
|
991
|
+
def rbs_known?(name)
|
|
992
|
+
Rigor::Reflection.rbs_class_known?(name, scope: @scope)
|
|
993
|
+
rescue StandardError
|
|
994
|
+
false
|
|
995
|
+
end
|
|
996
|
+
|
|
562
997
|
def project?(name)
|
|
563
998
|
case @flavor
|
|
564
999
|
when :constants
|
|
@@ -574,7 +1009,7 @@ module Rigor
|
|
|
574
1009
|
end
|
|
575
1010
|
private_constant :Resolver
|
|
576
1011
|
|
|
577
|
-
private_class_method :flavor_bucket
|
|
1012
|
+
private_class_method :flavor_bucket, :new_bucket
|
|
578
1013
|
|
|
579
1014
|
# {Relevance}'s verdict memo, one table per flavor bucket.
|
|
580
1015
|
def self.relevance_memo(scope, flavor)
|
|
@@ -718,9 +1153,16 @@ module Rigor
|
|
|
718
1153
|
def self.master_memo(scope, flavor, key)
|
|
719
1154
|
bucket = flavor_bucket(scope.discovery, flavor)
|
|
720
1155
|
memo = bucket[:master]
|
|
721
|
-
|
|
1156
|
+
unless memo.key?(key)
|
|
1157
|
+
return memo[key] = Inference::MemoAudit.key_audit("resolution chain master order") do
|
|
1158
|
+
yield(Resolver.new(scope, flavor, bucket))
|
|
1159
|
+
end
|
|
1160
|
+
end
|
|
722
1161
|
|
|
723
|
-
memo[key]
|
|
1162
|
+
Inference::MemoAudit.check("resolution chain master order", memo[key], flavor, key) do
|
|
1163
|
+
fresh = flavor_bucket(scope.discovery, flavor)
|
|
1164
|
+
Analysis::DependencyRecorder.withhold { yield(Resolver.new(scope, flavor, fresh)) }.first
|
|
1165
|
+
end
|
|
724
1166
|
end
|
|
725
1167
|
|
|
726
1168
|
# The linearisation itself, in one world: Ruby's skip rule (`retro: false`), or every skipped insertion
|
|
@@ -770,7 +1212,7 @@ module Rigor
|
|
|
770
1212
|
# The chain for `root`, carrying its retro world when the skip rule skipped anything on the way.
|
|
771
1213
|
def chain(root, side)
|
|
772
1214
|
entries, starts, classes, forks, marks, retro_ok = lin_for(root, side)
|
|
773
|
-
order_marked = marks.any? { |mark| mark.kind
|
|
1215
|
+
order_marked = marks.any? { |mark| !HOOK_MARK_KINDS.include?(mark.kind) }
|
|
774
1216
|
retro = build_retro(root, side) if forks == 1 && retro_ok && !order_marked && !@retro
|
|
775
1217
|
cut_at = cut_position(entries)
|
|
776
1218
|
return whole_chain(root, side, entries, starts, classes, [forks, marks], retro) if cut_at.nil?
|
|
@@ -829,14 +1271,10 @@ module Rigor
|
|
|
829
1271
|
memoised(@retro ? :lin_singleton_retro : :lin_singleton, name, depth) { compute_singleton(name, depth) }
|
|
830
1272
|
end
|
|
831
1273
|
|
|
832
|
-
def memoised(table, name, depth)
|
|
1274
|
+
def memoised(table, name, depth, &)
|
|
833
1275
|
memo = (@bucket[table] ||= {})
|
|
834
1276
|
cached = memo[name]
|
|
835
|
-
if cached
|
|
836
|
-
# A memoised chain's forks are the asker's too.
|
|
837
|
-
@frames.last&.absorb(cached[3], cached[4], cached[5])
|
|
838
|
-
return cached
|
|
839
|
-
end
|
|
1277
|
+
return absorb_cached(audited_lin(cached, table, name)) if cached
|
|
840
1278
|
|
|
841
1279
|
# A cycle (`class Foo < Foo` resolved inside its own namespace) or a hierarchy deeper than the budget
|
|
842
1280
|
# contributes nothing past that point, and the chains computed while it did are not memoised: they
|
|
@@ -851,7 +1289,7 @@ module Rigor
|
|
|
851
1289
|
frame = Frame.new(0, false, NO_MARKS, true)
|
|
852
1290
|
@frames.push(frame)
|
|
853
1291
|
stack.push(name)
|
|
854
|
-
entries, starts, classes =
|
|
1292
|
+
entries, starts, classes = Inference::MemoAudit.key_audit("resolution chain linearisation", &)
|
|
855
1293
|
stack.pop
|
|
856
1294
|
@frames.pop
|
|
857
1295
|
lin = [entries, starts, classes, frame.forks, frame.marks, frame.retro_ok].freeze
|
|
@@ -860,6 +1298,29 @@ module Rigor
|
|
|
860
1298
|
lin
|
|
861
1299
|
end
|
|
862
1300
|
|
|
1301
|
+
# A memoised linearisation rebuilt for the audit by a Builder over a fresh bucket. A memoised one met no
|
|
1302
|
+
# cycle and stayed inside the depth budget, so its depth does not change it; a retro world that fit
|
|
1303
|
+
# {LIMIT} once fits it again.
|
|
1304
|
+
# A memoised chain's forks are the asker's too.
|
|
1305
|
+
def absorb_cached(cached)
|
|
1306
|
+
@frames.last&.absorb(cached[3], cached[4], cached[5])
|
|
1307
|
+
cached
|
|
1308
|
+
end
|
|
1309
|
+
|
|
1310
|
+
def audited_lin(cached, table, name)
|
|
1311
|
+
Inference::MemoAudit.check("resolution chain linearisation", cached, table, name) { fresh_lin(table, name) }
|
|
1312
|
+
end
|
|
1313
|
+
|
|
1314
|
+
SINGLETON_LIN_TABLES = %i[lin_singleton lin_singleton_retro].freeze
|
|
1315
|
+
private_constant :SINGLETON_LIN_TABLES
|
|
1316
|
+
|
|
1317
|
+
def fresh_lin(table, name)
|
|
1318
|
+
side = SINGLETON_LIN_TABLES.include?(table) ? :singleton : :instance
|
|
1319
|
+
builder = Builder.new(@scope, @flavor, ResolutionChain.send(:flavor_bucket, @discovery, @flavor),
|
|
1320
|
+
retro: @retro)
|
|
1321
|
+
Analysis::DependencyRecorder.withhold { builder.send(:lin_for, name, side) }.first
|
|
1322
|
+
end
|
|
1323
|
+
|
|
863
1324
|
def hand_up(frame)
|
|
864
1325
|
parent = @frames.last
|
|
865
1326
|
return if parent.nil?
|
|
@@ -869,9 +1330,11 @@ module Rigor
|
|
|
869
1330
|
end
|
|
870
1331
|
|
|
871
1332
|
def compute_instance(name, depth)
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
1333
|
+
twins = same_class_twins(name)
|
|
1334
|
+
mark_instance_node(name, :include)
|
|
1335
|
+
twins.each { |owner| mark_instance_node(owner, :include) }
|
|
1336
|
+
return same_class_instance(name, twins, depth) unless twins.empty?
|
|
1337
|
+
|
|
875
1338
|
entries = [project_entry(name, :instance)]
|
|
876
1339
|
super_lin = superclass_lin(name, :instance, depth)
|
|
877
1340
|
entries.concat(super_lin[0])
|
|
@@ -883,12 +1346,333 @@ module Rigor
|
|
|
883
1346
|
end
|
|
884
1347
|
|
|
885
1348
|
def compute_singleton(name, depth)
|
|
886
|
-
|
|
1349
|
+
twins = same_class_twins(name)
|
|
1350
|
+
own = @discovery.discovered_extends[name] || EMPTY
|
|
1351
|
+
own_prepends = own_singleton_prepends(name, own)
|
|
1352
|
+
mark_unsettled(name, :extend, singleton_order_edges(own, own_prepends))
|
|
1353
|
+
twins.each { |owner| mark_unsettled(owner, :extend, (@discovery.discovered_extends[owner] || EMPTY).size) }
|
|
1354
|
+
return same_class_singleton(name, twins, depth) unless twins.empty?
|
|
1355
|
+
|
|
887
1356
|
entries = [project_entry(name, :singleton)]
|
|
888
1357
|
super_lin = superclass_lin(name, :singleton, depth)
|
|
889
1358
|
entries.concat(super_lin[0])
|
|
890
|
-
|
|
891
|
-
|
|
1359
|
+
own_extends = own_prepends.empty? ? own : own - own_prepends
|
|
1360
|
+
extends, prepends = singleton_mixins(name, own_extends, own_prepends)
|
|
1361
|
+
boundary = prepends.empty? ? CLASS_FIRST : prepend_all(entries, name, prepends, depth)
|
|
1362
|
+
super_start = include_all(entries, name, extends, EMPTY, depth, boundary)
|
|
1363
|
+
finish(entries, name, super_lin, super_start)
|
|
1364
|
+
end
|
|
1365
|
+
|
|
1366
|
+
# #1592 — the class's own `extend`s and the modules its direct mixins' hooks extend it with
|
|
1367
|
+
# ({#hook_extensions}), nearest first. Ruby inserts each where its statement ran, and the tables keep the
|
|
1368
|
+
# `extend` and `include` statements apart, so with both kinds present the two blocks' order is a fork: the
|
|
1369
|
+
# chain puts the own `extend`s nearer (the usual layout writes them after the `include`s) and the retro world
|
|
1370
|
+
# the hooks' modules, and a reader that the two worlds answer alike stands, since then only one block can
|
|
1371
|
+
# answer the name. An order the hooks do not fix, or an include whose own position is not a fact, is two
|
|
1372
|
+
# forks, which settle to master. Answers `[extends, prepends]`: a concern's `ClassMethods` on a prepend edge is
|
|
1373
|
+
# prepended to the class object (`Concern#prepend_features` runs `base.singleton_class.prepend`), so it sits
|
|
1374
|
+
# ahead of the class object's own entry.
|
|
1375
|
+
#
|
|
1376
|
+
# Issue #2998 — `own_prepends` are the class's own singleton-side prepends ({#own_singleton_prepends}), which
|
|
1377
|
+
# sit ahead of the class object too. An `extend` always sits behind the class object and a prepend always
|
|
1378
|
+
# ahead of it, so only two prepends can trade places ({#order_prepends}).
|
|
1379
|
+
def singleton_mixins(name, extends, own_prepends)
|
|
1380
|
+
plan = hook_extensions(name)
|
|
1381
|
+
if plan.open || unnamed_instance_mixin?(name)
|
|
1382
|
+
@frames.last.add_marks(mark_for(name, :include_hook, UNNAMED_LIST, false))
|
|
1383
|
+
end
|
|
1384
|
+
fork_hook_order(name, 2) unless plan.ordered || (plan.extends.empty? && plan.prepends.empty?)
|
|
1385
|
+
prepends = order_prepends(name, own_prepends, plan.prepends)
|
|
1386
|
+
hooked = plan.extends
|
|
1387
|
+
return [extends, prepends] if hooked.empty?
|
|
1388
|
+
return [hooked, prepends] if extends.empty?
|
|
1389
|
+
|
|
1390
|
+
fork_hook_order(name, 1)
|
|
1391
|
+
[@retro ? hooked + extends : extends + hooked, prepends]
|
|
1392
|
+
end
|
|
1393
|
+
|
|
1394
|
+
# The class object's prepends, nearest first, once its own are placed against its hooks'. Where their order
|
|
1395
|
+
# is not a fact (own and hook prepends both, or two own ones from two files or with an edge whose position is
|
|
1396
|
+
# not a fact), the chain counts two forks, which settle to master, and the class takes a `:prepend_order`
|
|
1397
|
+
# mark: the typing read declines a definer from its prepend region, since neither order is Ruby's for sure.
|
|
1398
|
+
def order_prepends(name, own, hooked)
|
|
1399
|
+
return hooked if own.empty?
|
|
1400
|
+
|
|
1401
|
+
if !hooked.empty? || (own.size > 1 && !own_prepends_ordered?(name))
|
|
1402
|
+
@frames.last.forks += 2
|
|
1403
|
+
@frames.last.add_marks(mark_for(name, :prepend_order, nil, false))
|
|
1404
|
+
end
|
|
1405
|
+
own + hooked
|
|
1406
|
+
end
|
|
1407
|
+
|
|
1408
|
+
def own_prepends_ordered?(name)
|
|
1409
|
+
!multi_file?(name) && (@discovery.unpositioned_mixins[name]&.dig(:extend) || EMPTY).empty?
|
|
1410
|
+
end
|
|
1411
|
+
|
|
1412
|
+
# Issue #2998 — the spellings of `name`'s singleton edges (`discovered_extends`) that are prepends: a
|
|
1413
|
+
# `class << self; prepend P` or a `singleton_class.prepend P` the walk filed as a
|
|
1414
|
+
# `[ENVELOPE_SINGLETON_PREPEND_KIND, "P"]` entry. They keep the table's order, nearest first, which is the
|
|
1415
|
+
# prepends' own: a later `prepend` sits ahead of an earlier one. A class with any takes the `:singleton_prepend`
|
|
1416
|
+
# mark, which tells the typing read that master's order, which knows no singleton prepend, may answer
|
|
1417
|
+
# otherwise.
|
|
1418
|
+
#
|
|
1419
|
+
# Issue #3020 — a prepend whose module no table names (`class << self; prepend(*MODS)`,
|
|
1420
|
+
# `singleton_class.prepend(*MODS)`) files the `"?"` entry instead, which no edge spells: the class takes the
|
|
1421
|
+
# `:unnamed_prepend` mark, and the typing read declines a definer at or behind the class object.
|
|
1422
|
+
def own_singleton_prepends(name, own)
|
|
1423
|
+
bucket = @discovery.discovered_parameter_envelopes[name]
|
|
1424
|
+
return EMPTY if bucket.nil?
|
|
1425
|
+
|
|
1426
|
+
@frames.last.add_marks(mark_for(name, :unnamed_prepend, nil, false)) if bucket.key?(UNNAMED_PREPEND_KEY)
|
|
1427
|
+
return EMPTY if own.empty?
|
|
1428
|
+
|
|
1429
|
+
prepends = own.select { |raw| bucket.key?([DiscoveryIndex::ENVELOPE_SINGLETON_PREPEND_KIND, raw]) }
|
|
1430
|
+
return EMPTY if prepends.empty?
|
|
1431
|
+
|
|
1432
|
+
@frames.last.add_marks(mark_for(name, :singleton_prepend, nil, false))
|
|
1433
|
+
prepends
|
|
1434
|
+
end
|
|
1435
|
+
|
|
1436
|
+
# The edge count the multi-file rule weighs: a prepend sits ahead of the class object and an `extend` behind
|
|
1437
|
+
# it whatever the load order, so only edges of one kind can trade places across files.
|
|
1438
|
+
def singleton_order_edges(own, own_prepends)
|
|
1439
|
+
return own.size if own_prepends.empty?
|
|
1440
|
+
|
|
1441
|
+
[own_prepends.size, own.size - own_prepends.size].max
|
|
1442
|
+
end
|
|
1443
|
+
|
|
1444
|
+
def fork_hook_order(name, weight)
|
|
1445
|
+
@frames.last.forks += weight
|
|
1446
|
+
@frames.last.add_marks(mark_for(name, :hook_order, nil, false))
|
|
1447
|
+
end
|
|
1448
|
+
|
|
1449
|
+
# The {HookPlan} for a class: the `[owner, spelling]` pairs of the modules the hooks of its direct
|
|
1450
|
+
# mixins extend it with, nearest first (a later include's ahead of an earlier one's, and within one mixin the
|
|
1451
|
+
# hook's own extends ahead of a concern's `ClassMethods`, which `append_features` extends before the
|
|
1452
|
+
# `included` block runs); the concern `ClassMethods` its prepend edges prepend to its singleton; whether one of
|
|
1453
|
+
# those hooks may extend it with a module no entry names; and whether the order is a fact. A concern includer
|
|
1454
|
+
# gets nothing from a concern: `Concern#append_features` records it as a dependency instead (#2992).
|
|
1455
|
+
def hook_extensions(name)
|
|
1456
|
+
includes = @discovery.discovered_includes[name] || EMPTY
|
|
1457
|
+
return NO_HOOKS if includes.empty?
|
|
1458
|
+
|
|
1459
|
+
listed = @discovery.unpositioned_mixins[name]&.dig(:include) || EMPTY
|
|
1460
|
+
plan = HookPlan.new([], [], false, true)
|
|
1461
|
+
each_hooked_mixin(name, includes) do |raw, mixin, hooks, loose|
|
|
1462
|
+
unpositioned = loose || listed.include?(raw) || listed.include?(WILDCARD_LIST.first)
|
|
1463
|
+
merge_targets(plan, hook_targets(mixin, hooks), unpositioned)
|
|
1464
|
+
plan.open ||= uncertain_dependencies?(mixin)
|
|
1465
|
+
end
|
|
1466
|
+
# Two files' includes run in load order.
|
|
1467
|
+
plan.ordered &&= plan.extends.size + plan.prepends.size < 2 || !multi_file?(name)
|
|
1468
|
+
plan
|
|
1469
|
+
end
|
|
1470
|
+
|
|
1471
|
+
# Adds one mixin's targets to the class's plan; an include whose own position is not a fact (`unpositioned`)
|
|
1472
|
+
# leaves its modules' order unknown.
|
|
1473
|
+
def merge_targets(plan, targets, unpositioned)
|
|
1474
|
+
plan.open ||= targets.open
|
|
1475
|
+
return if targets.extends.empty? && targets.prepends.empty?
|
|
1476
|
+
|
|
1477
|
+
plan.ordered &&= targets.ordered && !unpositioned
|
|
1478
|
+
plan.extends.concat(targets.extends)
|
|
1479
|
+
plan.prepends.concat(targets.prepends)
|
|
1480
|
+
end
|
|
1481
|
+
|
|
1482
|
+
# Issue #2994 — whether a module no table names may be mixed into `name`'s instance side by a call whose target
|
|
1483
|
+
# is `name` itself, so that its `included` / `prepended` hook runs with `name` as `base` and may extend the
|
|
1484
|
+
# class object with anything: `name` lists {Relevance::UNNAMED} on its own instance side (`include(*MODS)`,
|
|
1485
|
+
# `include helper`), or a module it includes does (a hook's `base.include(*MODS)`; that module's own
|
|
1486
|
+
# `include(*MODS)` is read so too). A superclass's own mark is read when its singleton chain is built. The
|
|
1487
|
+
# entries this reads are the class's own sites and its direct mixins', which a singleton chain's readers file
|
|
1488
|
+
# already. An extended module's hook that splats a module in already declines the read on another route.
|
|
1489
|
+
def unnamed_instance_mixin?(name)
|
|
1490
|
+
return true if lists_unnamed?(name)
|
|
1491
|
+
|
|
1492
|
+
(@discovery.discovered_includes[name] || EMPTY).uniq.any? do |raw|
|
|
1493
|
+
mixin = @resolver.resolve_declared(name, raw)
|
|
1494
|
+
!mixin.nil? && lists_unnamed?(mixin)
|
|
1495
|
+
end
|
|
1496
|
+
end
|
|
1497
|
+
|
|
1498
|
+
def lists_unnamed?(name)
|
|
1499
|
+
listed = @discovery.unpositioned_mixins[name]&.dig(:include)
|
|
1500
|
+
!listed.nil? && listed.include?(UNNAMED_LIST.last)
|
|
1501
|
+
end
|
|
1502
|
+
|
|
1503
|
+
HookPlan = Struct.new(:extends, :prepends, :open, :ordered)
|
|
1504
|
+
private_constant :HookPlan
|
|
1505
|
+
|
|
1506
|
+
# Yields each direct mixin of `name`, nearest first, as `[spelling, resolved, hooks]`: the hooks its edges
|
|
1507
|
+
# run, `:included` for an `include` and `:prepended` for a `prepend`.
|
|
1508
|
+
#
|
|
1509
|
+
# Issue #2992 — a concern mixin's dependencies follow it, each on the same edge
|
|
1510
|
+
# ({ResolutionChain.concern_dependencies}): `Concern#append_features` includes them into `name` ahead of the
|
|
1511
|
+
# concern itself, so their modules sit farther than the concern's. `spelling` is the mixin's for each, and
|
|
1512
|
+
# `loose` says a dependency's place is not a fact: `Concern` puts a prepended dependency ahead of the included
|
|
1513
|
+
# ones, which the tables list the other way, and two files of the concern add theirs in load order.
|
|
1514
|
+
def each_hooked_mixin(name, includes)
|
|
1515
|
+
prepends = @discovery.discovered_prepends[name] || EMPTY
|
|
1516
|
+
concern = concern?(name)
|
|
1517
|
+
includes.uniq.each do |raw|
|
|
1518
|
+
mixin = @resolver.resolve_declared(name, raw)
|
|
1519
|
+
next if mixin.nil? || (concern && concern?(mixin))
|
|
1520
|
+
|
|
1521
|
+
hooks = []
|
|
1522
|
+
hooks << :prepended if prepends.include?(raw)
|
|
1523
|
+
hooks << :included if includes.count(raw) > prepends.count(raw)
|
|
1524
|
+
yield raw, mixin, hooks, false
|
|
1525
|
+
dependencies = ResolutionChain.concern_dependencies(@discovery, @resolver, mixin)
|
|
1526
|
+
loose = dependencies.size > 1 && dependencies.any? { |member| loose_dependencies?(member, mixin) }
|
|
1527
|
+
dependencies.each { |dependency| yield raw, dependency, hooks, loose }
|
|
1528
|
+
end
|
|
1529
|
+
end
|
|
1530
|
+
|
|
1531
|
+
# Issue #2992 — whether a concern declared in two files includes a concern its dependency entries do not name:
|
|
1532
|
+
# written in a reopening without the `extend ActiveSupport::Concern`, that `include` is a dependency when the
|
|
1533
|
+
# reopening loads after the `extend` and an ordinary include of the concern itself when it loads first, so the
|
|
1534
|
+
# includer may or may not be extended with that concern's modules.
|
|
1535
|
+
def uncertain_dependencies?(mixin)
|
|
1536
|
+
return false unless concern?(mixin) && multi_file?(mixin)
|
|
1537
|
+
|
|
1538
|
+
spellings = ResolutionChain.dependency_spellings(@discovery, mixin)
|
|
1539
|
+
(@discovery.discovered_includes[mixin] || EMPTY).any? do |raw|
|
|
1540
|
+
next false if spellings.include?(raw)
|
|
1541
|
+
|
|
1542
|
+
dependency = @resolver.resolve_declared(mixin, raw)
|
|
1543
|
+
!dependency.nil? && concern?(dependency)
|
|
1544
|
+
end
|
|
1545
|
+
end
|
|
1546
|
+
|
|
1547
|
+
# Whether the concern `owner` or its dependency `member` prepends a module or is declared in two files.
|
|
1548
|
+
def loose_dependencies?(member, owner)
|
|
1549
|
+
[member, owner].any? { |name| multi_file?(name) || !(@discovery.discovered_prepends[name] || EMPTY).empty? }
|
|
1550
|
+
end
|
|
1551
|
+
|
|
1552
|
+
NO_HOOKS = HookPlan.new(EMPTY, EMPTY, false, true).freeze
|
|
1553
|
+
private_constant :NO_HOOKS
|
|
1554
|
+
|
|
1555
|
+
def concern?(name)
|
|
1556
|
+
bucket = @discovery.discovered_parameter_envelopes[name]
|
|
1557
|
+
!bucket.nil? && bucket.key?(CONCERN_KEY)
|
|
1558
|
+
end
|
|
1559
|
+
|
|
1560
|
+
# One mixin's hook-extended modules for the edges `hooks` runs, as {#hook_extensions} answers them.
|
|
1561
|
+
# A hook defined twice (`ENVELOPE_HOOK_DEF_KIND`) is open: which definition Ruby keeps is load order.
|
|
1562
|
+
#
|
|
1563
|
+
# Issue #3005 — a hook block's `def self.x`s (`<included>`) sit on the class object itself, ahead of every
|
|
1564
|
+
# module the block extends it with, so that module goes first and does not make the order unknown.
|
|
1565
|
+
def hook_targets(mixin, hooks)
|
|
1566
|
+
named, open = hook_keys(mixin, hooks)
|
|
1567
|
+
blocks, modules = named.partition { |raw| DiscoveryIndex::HOOK_BLOCK_SEGMENTS.include?(raw) }
|
|
1568
|
+
pairs = (blocks + modules).map { |raw| hook_target_pair(mixin, raw) }
|
|
1569
|
+
targets = HookPlan.new(pairs, [], open, modules.size <= 1)
|
|
1570
|
+
found = concern?(mixin) && const_get_target(mixin, "ClassMethods")
|
|
1571
|
+
(hooks.include?(:prepended) ? targets.prepends : targets.extends) << [mixin, "::#{found}"] if found
|
|
1572
|
+
targets
|
|
1573
|
+
end
|
|
1574
|
+
|
|
1575
|
+
# `[named, open]`: the spellings the mixin's hooks for `hooks` extend with, and whether one of them is open
|
|
1576
|
+
# or defined twice.
|
|
1577
|
+
def hook_keys(mixin, hooks)
|
|
1578
|
+
named = []
|
|
1579
|
+
open = false
|
|
1580
|
+
defs = Hash.new(0)
|
|
1581
|
+
(@discovery.discovered_parameter_envelopes[mixin] || EMPTY_BUCKET).each_key do |key|
|
|
1582
|
+
next unless hooks.include?(ResolutionChain.effective_hook(@discovery, mixin, ResolutionChain.hook_of(key)))
|
|
1583
|
+
|
|
1584
|
+
case key.first
|
|
1585
|
+
when DiscoveryIndex::ENVELOPE_HOOK_EXTEND_KIND then named << key[1]
|
|
1586
|
+
when DiscoveryIndex::ENVELOPE_HOOK_EXTEND_OPEN_KIND then open = true
|
|
1587
|
+
else defs[key[1]] += 1
|
|
1588
|
+
end
|
|
1589
|
+
end
|
|
1590
|
+
[named.uniq, open || defs.each_value.any? { |count| count > 1 }]
|
|
1591
|
+
end
|
|
1592
|
+
|
|
1593
|
+
EMPTY_BUCKET = {}.freeze
|
|
1594
|
+
private_constant :EMPTY_BUCKET
|
|
1595
|
+
|
|
1596
|
+
# The module a hook's constant names, written in the mixin's body: `Mixin::Raw` first (the mixin is the
|
|
1597
|
+
# innermost cref there), then the names its header's nesting gives. A name no project module answers stays
|
|
1598
|
+
# the mixin's spelling, an external entry.
|
|
1599
|
+
def hook_target_pair(mixin, raw)
|
|
1600
|
+
own = "#{mixin}::#{raw}"
|
|
1601
|
+
return [mixin, "::#{own}"] if !raw.start_with?("::") && @resolver.project_name?(own)
|
|
1602
|
+
|
|
1603
|
+
[mixin, raw]
|
|
1604
|
+
end
|
|
1605
|
+
|
|
1606
|
+
# `mixin.const_get(raw)`, as `Concern#append_features` looks `ClassMethods` up: `Name::Raw` for each entry of
|
|
1607
|
+
# the mixin's own instance chain, then a top-level `Raw`; nil when no project module answers, which Concern
|
|
1608
|
+
# then does not extend.
|
|
1609
|
+
#
|
|
1610
|
+
# Issue #2992 — a concern the mixin depends on is no ancestor of it ({ResolutionChain.concern_dependencies}),
|
|
1611
|
+
# so its `ClassMethods` is not found here: it reaches the includer as that dependency's own.
|
|
1612
|
+
def const_get_target(mixin, raw)
|
|
1613
|
+
chain = ResolutionChain.for(@scope, mixin, :instance, @flavor)
|
|
1614
|
+
dependencies = ResolutionChain.concern_dependencies(@discovery, @resolver, mixin)
|
|
1615
|
+
chain.entries.each do |entry|
|
|
1616
|
+
next if dependencies.include?(entry.name)
|
|
1617
|
+
|
|
1618
|
+
names = entry.external? ? entry.candidates : [entry.name]
|
|
1619
|
+
found = names.map { |owner| [owner, "#{owner}::#{raw}"] }.find do |owner, candidate|
|
|
1620
|
+
@resolver.project_name?(candidate) && ResolutionChain.const_get_candidate?(@discovery, owner, candidate)
|
|
1621
|
+
end&.last
|
|
1622
|
+
return found if found
|
|
1623
|
+
end
|
|
1624
|
+
@resolver.project_name?(raw) ? raw : nil
|
|
1625
|
+
end
|
|
1626
|
+
|
|
1627
|
+
# The instance-side marks of one node; `kind` is the side's mixin kind, `:include`. `discovered_includes`
|
|
1628
|
+
# already carries the prepended names, so it counts every instance-side edge once.
|
|
1629
|
+
def mark_instance_node(name, kind)
|
|
1630
|
+
mark_unsettled(name, kind, (@discovery.discovered_includes[name] || EMPTY).size)
|
|
1631
|
+
hook = extend_hook_taint(name)
|
|
1632
|
+
@frames.last.add_marks(mark_for(name, :extend_hook, hook, false)) if hook
|
|
1633
|
+
end
|
|
1634
|
+
|
|
1635
|
+
# Issue #2979 — the names `name`'s envelope bucket binds to the same class object (`K = Struct.new("Named",
|
|
1636
|
+
# …)` makes `K` and `Struct::Named` one class), without `name`; empty, and allocating nothing, for nearly every
|
|
1637
|
+
# class. The chain of either name heads with every name's entry, so a reader asks each name's own tables where
|
|
1638
|
+
# Ruby asks the one class's; nothing is copied between the names. Only the direct binding counts: two
|
|
1639
|
+
# `Struct.new("Named", …)` writes make two classes in Ruby, and neither constant reaches the other's tables
|
|
1640
|
+
# through `Struct::Named`. {ResolutionChain#ancestors_start} tells a reader where the true ancestors begin.
|
|
1641
|
+
def same_class_twins(name)
|
|
1642
|
+
twins = DiscoveryIndex.same_class_names(@discovery.discovered_parameter_envelopes[name])
|
|
1643
|
+
twins.include?(name) ? twins - [name] : twins
|
|
1644
|
+
end
|
|
1645
|
+
|
|
1646
|
+
# The instance side of a class with twins: every name's prepends ahead of the class, then every name's
|
|
1647
|
+
# includes after the LAST name, so the names stay ahead of every module either one includes, as the one class
|
|
1648
|
+
# heads its own ancestors. The superclass edge stays `name`'s own: a Struct's class has none the tables
|
|
1649
|
+
# record, under either name.
|
|
1650
|
+
def same_class_instance(name, twins, depth)
|
|
1651
|
+
names = [name, *twins].uniq
|
|
1652
|
+
entries = names.map { |owner| project_entry(owner, :instance) }
|
|
1653
|
+
super_lin = superclass_lin(name, :instance, depth)
|
|
1654
|
+
entries.concat(super_lin[0])
|
|
1655
|
+
boundary = [0, names.size]
|
|
1656
|
+
names.each do |owner|
|
|
1657
|
+
boundary = prepend_all(entries, owner, @discovery.discovered_prepends[owner] || EMPTY, depth, boundary)
|
|
1658
|
+
end
|
|
1659
|
+
super_start = names.reduce(boundary[1]) do |start, owner|
|
|
1660
|
+
include_all(entries, owner, @discovery.discovered_includes[owner] || EMPTY,
|
|
1661
|
+
@discovery.discovered_prepends[owner] || EMPTY, depth, [boundary[0] + names.size - 1, start])
|
|
1662
|
+
end
|
|
1663
|
+
finish(entries, name, super_lin, super_start)
|
|
1664
|
+
end
|
|
1665
|
+
|
|
1666
|
+
# The singleton twin of {#same_class_instance}: every name's class-object entry, then every name's extends.
|
|
1667
|
+
def same_class_singleton(name, twins, depth)
|
|
1668
|
+
names = [name, *twins].uniq
|
|
1669
|
+
entries = names.map { |owner| project_entry(owner, :singleton) }
|
|
1670
|
+
super_lin = superclass_lin(name, :singleton, depth)
|
|
1671
|
+
entries.concat(super_lin[0])
|
|
1672
|
+
super_start = names.reduce(names.size) do |start, owner|
|
|
1673
|
+
include_all(entries, owner, @discovery.discovered_extends[owner] || EMPTY, EMPTY, depth,
|
|
1674
|
+
[names.size - 1, start])
|
|
1675
|
+
end
|
|
892
1676
|
finish(entries, name, super_lin, super_start)
|
|
893
1677
|
end
|
|
894
1678
|
|
|
@@ -910,24 +1694,53 @@ module Rigor
|
|
|
910
1694
|
# The one-element frozen array carrying the {Mark} of `[name, kind]`, interned in the bucket.
|
|
911
1695
|
def mark_for(name, kind, listed, multi)
|
|
912
1696
|
by_node = (@bucket[:marks][kind] ||= {})
|
|
913
|
-
by_node[name]
|
|
914
|
-
|
|
1697
|
+
cached = by_node[name]
|
|
1698
|
+
if cached.nil?
|
|
1699
|
+
return by_node[name] = Inference::MemoAudit.key_audit("resolution chain mark") do
|
|
1700
|
+
new_mark(name, kind, listed, multi)
|
|
1701
|
+
end
|
|
1702
|
+
end
|
|
1703
|
+
return cached unless Inference::MemoAudit.enabled?
|
|
1704
|
+
|
|
1705
|
+
# The failure names the whole key: the bucket's flavor, the mark's kind, the node.
|
|
1706
|
+
Inference::MemoAudit.check("resolution chain mark", cached, [@flavor, kind], name) do
|
|
1707
|
+
new_mark(name, kind, listed, multi)
|
|
1708
|
+
end
|
|
915
1709
|
end
|
|
916
1710
|
|
|
917
|
-
|
|
918
|
-
|
|
1711
|
+
def new_mark(name, kind, listed, multi)
|
|
1712
|
+
[ResolutionChain::Mark.new(name, kind, listed.nil? ? EMPTY : listed.to_a.freeze, multi)].freeze
|
|
1713
|
+
end
|
|
1714
|
+
|
|
1715
|
+
# The `[origin, super_start]` of a one-name chain: the class at 0, its superclass's entries from 1.
|
|
1716
|
+
CLASS_FIRST = [0, 1].freeze
|
|
1717
|
+
private_constant :CLASS_FIRST
|
|
919
1718
|
|
|
920
1719
|
# #1730 — whether a module `name` extends has a hook that may mix into `name`'s instance side or its
|
|
921
1720
|
# includers' ({ResolutionChain.extend_reshapes_instance?}). Ruby runs it at the `extend`, so the mixin is
|
|
922
1721
|
# not a fact of the tables and `name`'s instance side is unpositioned. A spelling no project module
|
|
923
1722
|
# answers taints nothing: the project cannot see its hooks.
|
|
924
|
-
|
|
925
|
-
|
|
1723
|
+
#
|
|
1724
|
+
# The mark's list: nil when no extended module taints, else {UNNAMED_LIST} when one of those modules lists `"?"`
|
|
1725
|
+
# (#2991), {WILDCARD_LIST} otherwise. Only the marker carries over: a name such a module lists is spelled
|
|
1726
|
+
# relative to that module, not to the extender.
|
|
1727
|
+
def extend_hook_taint(name)
|
|
1728
|
+
found = nil
|
|
1729
|
+
(@discovery.discovered_extends[name] || EMPTY).each do |raw|
|
|
926
1730
|
resolved = @resolver.resolve_one(name, raw)
|
|
927
|
-
|
|
1731
|
+
next if resolved.nil? || !ResolutionChain.extend_reshapes_instance?(@scope, resolved)
|
|
1732
|
+
return UNNAMED_LIST if @discovery.unpositioned_mixins[resolved][:include].include?(UNNAMED_LIST.last)
|
|
1733
|
+
|
|
1734
|
+
found = WILDCARD_LIST
|
|
928
1735
|
end
|
|
1736
|
+
found
|
|
929
1737
|
end
|
|
930
1738
|
|
|
1739
|
+
WILDCARD_LIST = ["*"].freeze # Relevance::WILDCARD, which loads after this file
|
|
1740
|
+
UNNAMED_LIST = ["*", "?"].freeze # with Relevance::UNNAMED
|
|
1741
|
+
UNNAMED_PREPEND_KEY = [DiscoveryIndex::ENVELOPE_SINGLETON_PREPEND_KIND, "?"].freeze # with Relevance::UNNAMED
|
|
1742
|
+
private_constant :WILDCARD_LIST, :UNNAMED_LIST, :UNNAMED_PREPEND_KEY
|
|
1743
|
+
|
|
931
1744
|
def multi_file?(name)
|
|
932
1745
|
sites = @discovery.discovered_class_sources[name]
|
|
933
1746
|
!sites.nil? && sites.size >= 2
|
|
@@ -972,14 +1785,15 @@ module Rigor
|
|
|
972
1785
|
# module's own includes may have run before or after), and the tables record `include M; prepend M`
|
|
973
1786
|
# exactly as a plain `prepend M`. A direct prepend of the module itself is always inserted, in Ruby and
|
|
974
1787
|
# here, so it forks nothing.
|
|
975
|
-
def prepend_all(entries, owner, prepends, depth)
|
|
976
|
-
origin =
|
|
977
|
-
super_start = 1
|
|
1788
|
+
def prepend_all(entries, owner, prepends, depth, boundary = CLASS_FIRST)
|
|
1789
|
+
origin, super_start = boundary
|
|
978
1790
|
# The table holds every statement, nearest first: a same-owner repeat is a no-op in Ruby (the first
|
|
979
1791
|
# statement wins), so it is dropped here rather than met as a fork.
|
|
980
1792
|
prepends.reverse.uniq.each do |raw|
|
|
981
|
-
|
|
982
|
-
|
|
1793
|
+
# A hook's module comes as `[owner, spelling]`, as in {#include_all}.
|
|
1794
|
+
from, raw = raw.is_a?(Array) ? raw : [owner, raw]
|
|
1795
|
+
modules = Array(resolve(from, raw))
|
|
1796
|
+
each_mixin_chain(from, raw, depth) do |sub|
|
|
983
1797
|
point = -1
|
|
984
1798
|
sub.each do |entry|
|
|
985
1799
|
found = entries.index(entry)
|
|
@@ -1010,7 +1824,9 @@ module Rigor
|
|
|
1010
1824
|
mixins.reverse_each do |raw|
|
|
1011
1825
|
next if skip.include?(raw)
|
|
1012
1826
|
|
|
1013
|
-
|
|
1827
|
+
# A hook's module comes as `[owner, spelling]`: the mixin whose body wrote it resolves it.
|
|
1828
|
+
from, raw = raw.is_a?(Array) ? raw : [owner, raw]
|
|
1829
|
+
each_mixin_chain(from, raw, depth) do |sub|
|
|
1014
1830
|
point = origin
|
|
1015
1831
|
sub.each_with_index do |entry, index|
|
|
1016
1832
|
found = @retro ? nil : entries.index(entry)
|
|
@@ -1051,14 +1867,43 @@ module Rigor
|
|
|
1051
1867
|
|
|
1052
1868
|
def project_entry(name, side)
|
|
1053
1869
|
by_side = (@bucket[:interned][side] ||= {})
|
|
1054
|
-
by_side[name]
|
|
1870
|
+
cached = by_side[name]
|
|
1871
|
+
if cached.nil?
|
|
1872
|
+
return by_side[name] = Inference::MemoAudit.key_audit("resolution chain entry") do
|
|
1873
|
+
new_project_entry(name, side)
|
|
1874
|
+
end
|
|
1875
|
+
end
|
|
1876
|
+
return cached unless Inference::MemoAudit.enabled?
|
|
1877
|
+
|
|
1878
|
+
Inference::MemoAudit.check("resolution chain entry", cached, [@flavor, side], name) do
|
|
1879
|
+
new_project_entry(name, side)
|
|
1880
|
+
end
|
|
1881
|
+
end
|
|
1882
|
+
|
|
1883
|
+
def new_project_entry(name, side)
|
|
1884
|
+
Entry.new(name, side, nil, nil, false, name.to_s.split("::").last.freeze, nil)
|
|
1055
1885
|
end
|
|
1056
1886
|
|
|
1057
1887
|
def external_entry(owner, raw, side, superclass_edge)
|
|
1058
1888
|
by_edge = ((@bucket[:externals][side] ||= {})[superclass_edge] ||= {})
|
|
1059
1889
|
by_owner = (by_edge[owner] ||= {})
|
|
1060
|
-
by_owner[raw]
|
|
1061
|
-
|
|
1890
|
+
cached = by_owner[raw]
|
|
1891
|
+
if cached.nil?
|
|
1892
|
+
return by_owner[raw] = Inference::MemoAudit.key_audit("resolution chain external entry") do
|
|
1893
|
+
new_external_entry(owner, raw, side, superclass_edge)
|
|
1894
|
+
end
|
|
1895
|
+
end
|
|
1896
|
+
return cached unless Inference::MemoAudit.enabled?
|
|
1897
|
+
|
|
1898
|
+
# The failure names the whole key: flavor, side, superclass edge, owner and spelling.
|
|
1899
|
+
Inference::MemoAudit.check("resolution chain external entry", cached, [@flavor, side, superclass_edge],
|
|
1900
|
+
[owner, raw]) do
|
|
1901
|
+
Analysis::DependencyRecorder.withhold { new_external_entry(owner, raw, side, superclass_edge) }.first
|
|
1902
|
+
end
|
|
1903
|
+
end
|
|
1904
|
+
|
|
1905
|
+
def new_external_entry(owner, raw, side, superclass_edge)
|
|
1906
|
+
Entry.new(nil, side, raw, @scope.ancestor_name_candidates(owner, raw).freeze, superclass_edge, nil, owner)
|
|
1062
1907
|
end
|
|
1063
1908
|
end
|
|
1064
1909
|
private_constant :Builder
|