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
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
# Not required on its own: `rigor/inference/declaration_walk` loads it once the walk exists, and
|
|
6
|
+
# {Reflection.class_declarations_in} loads that entry point on first use. Keeping the walk out of `Reflection`'s own
|
|
7
|
+
# load keeps the engine-free paths engine-free (ADR-104).
|
|
8
|
+
module Rigor
|
|
9
|
+
module Reflection
|
|
10
|
+
# The walk's collector behind {.class_declarations_in}; the plugin never sees it.
|
|
11
|
+
class DeclarationCollector
|
|
12
|
+
include Inference::DeclarationWalk::Collector
|
|
13
|
+
|
|
14
|
+
EMPTY_NESTING = [].freeze
|
|
15
|
+
|
|
16
|
+
attr_reader :classes
|
|
17
|
+
|
|
18
|
+
def initialize
|
|
19
|
+
@classes = []
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def on_declaration(node, context, body_context)
|
|
23
|
+
if node.is_a?(Prism::ClassNode)
|
|
24
|
+
name = body_context.prefix.join("::")
|
|
25
|
+
@classes << ClassDeclaration.new(name: name, nesting: frozen_nesting(context), node: node) unless name.empty?
|
|
26
|
+
end
|
|
27
|
+
Inference::DeclarationWalk::DESCEND
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# The walk's chain, frozen: the root's `[]` is one array shared by every top-level declaration.
|
|
33
|
+
def frozen_nesting(context)
|
|
34
|
+
nesting = context.nesting || EMPTY_NESTING
|
|
35
|
+
nesting.frozen? ? nesting : nesting.dup.freeze
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# {DeclarationCollector} that also forwards every call. A subclass, because the walk dispatches only the events a
|
|
40
|
+
# collector's class overrides, so a classes-only run pays for no call event.
|
|
41
|
+
class DeclarationCallCollector < DeclarationCollector
|
|
42
|
+
def initialize(on_call)
|
|
43
|
+
super()
|
|
44
|
+
@on_call = on_call
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def on_call(node, context)
|
|
48
|
+
owner = context.prefix.join("::")
|
|
49
|
+
@on_call.call(DeclaredCall.new(node: node, owner: owner.empty? ? nil : owner))
|
|
50
|
+
Inference::DeclarationWalk::DESCEND
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
private_constant :DeclarationCollector, :DeclarationCallCollector
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
# {Reflection}'s per-file declaration walk for plugins (ADR-128, #2708): {.class_declarations_in} answers which
|
|
7
|
+
# classes a parsed file declares under their `Module.nesting`-correct names, and hands each call the walk visits
|
|
8
|
+
# to a plugin together with the class or module its position reaches. It replaces a plugin's own
|
|
9
|
+
# `DeclarationWalk::Collector`, so the walk's context, its control constants and its collector protocol stay
|
|
10
|
+
# internal.
|
|
11
|
+
module Reflection
|
|
12
|
+
# One `class` header of a file, as {.class_declarations_in} reports it.
|
|
13
|
+
#
|
|
14
|
+
# - `name` — the qualified name the header's body is keyed under (`"Admin::User"` for `class User` in
|
|
15
|
+
# `module Admin`, and for `class Admin::User`); never empty.
|
|
16
|
+
# - `nesting` — `Module.nesting` where the header is written, innermost first, not counting the class itself
|
|
17
|
+
# (`["Admin"]` for `class User` in `module Admin`; `[]` at the top level). Frozen.
|
|
18
|
+
# - `node` — the `Prism::ClassNode`, for its `superclass` and `body`.
|
|
19
|
+
ClassDeclaration = Data.define(:name, :nesting, :node)
|
|
20
|
+
|
|
21
|
+
# One call the walk visited, as {.class_declarations_in} yields it.
|
|
22
|
+
#
|
|
23
|
+
# - `node` — the `Prism::CallNode`.
|
|
24
|
+
# - `owner` — the qualified name of the innermost `class` / `module` the call is lexically inside, or nil where
|
|
25
|
+
# none is (the top level, or below a bare header in a `class << self` body).
|
|
26
|
+
DeclaredCall = Data.define(:node, :owner)
|
|
27
|
+
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
# ADR-128, #2708 — the classes a parsed file declares, in the order their headers are written. A class
|
|
31
|
+
# reopened in the file appears once per header. A `module`, a `class << self` and a class no name reaches
|
|
32
|
+
# (`Class.new { … }`, a header below `class << self`) are not reported.
|
|
33
|
+
#
|
|
34
|
+
# With a block, the walk also yields a {DeclaredCall} for the calls the engine's declaration walk visits, in
|
|
35
|
+
# source order, outer calls before the calls inside them. Those are the calls in statements and in the bodies of
|
|
36
|
+
# `class` / `module` / `class << self` / blocks, at any depth. The owner is lexical, not `self`: a call in
|
|
37
|
+
# `class << self` or in a `Class.new { … }` block reports the class around it.
|
|
38
|
+
#
|
|
39
|
+
# The block does not see every call node of the file. The walk does not enter, so the block never sees:
|
|
40
|
+
#
|
|
41
|
+
# - a class or module header's own expressions: the superclass (`class Foo < Struct.new(:a)`) and the constant
|
|
42
|
+
# path (`class foo::Bar`);
|
|
43
|
+
# - the factory call a constant write binds (`K = Class.new { … }`), whose receiver, arguments and block body the
|
|
44
|
+
# walk does enter;
|
|
45
|
+
# - the tail of such a write (`K = Class.new { … }.freeze` yields what is inside the block, not `freeze`), and
|
|
46
|
+
# the constant-path parent of a write target (`P` in `P::K = …`).
|
|
47
|
+
#
|
|
48
|
+
# `root` is a `Prism::Node`, typically `Prism.parse(source).value`.
|
|
49
|
+
def class_declarations_in(root, &on_call)
|
|
50
|
+
# Loaded on first use, not with `Reflection`: the walk heads the analysis engine, which the engine-free
|
|
51
|
+
# paths (a warm `rigor effects`) load `Reflection` without. The walk's entry point loads the collectors.
|
|
52
|
+
require_relative "../inference/declaration_walk"
|
|
53
|
+
collector = on_call ? DeclarationCallCollector.new(on_call) : DeclarationCollector.new
|
|
54
|
+
Inference::DeclarationWalk.run(root, [collector], Inference::DeclarationWalk::Context.root(nesting: []))
|
|
55
|
+
collector.classes
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
# {Reflection}'s ancestor facade for plugins (ADR-128 WD1), split out for the reader rather than for the
|
|
5
|
+
# loader: {.project_method_defined} turns the two `Scope` ancestor walks into one `Trinary` answer. Reopening
|
|
6
|
+
# the module keeps it behind the facade its callers already read.
|
|
7
|
+
module Reflection
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
PROJECT_METHOD_KINDS = %i[instance singleton].freeze
|
|
11
|
+
PROJECT_METHOD_SOURCES = %i[def discovered either].freeze
|
|
12
|
+
private_constant :PROJECT_METHOD_KINDS, :PROJECT_METHOD_SOURCES
|
|
13
|
+
|
|
14
|
+
# ADR-128 WD1 — does the project define `method_name` on `class_name` or on an ancestor the project itself
|
|
15
|
+
# declares? Answers a {Trinary}: `yes` where the walk found a definer, `no` where it found none and its
|
|
16
|
+
# `ResolutionChain` was not truncated, and `maybe` where the chain was truncated at its budget before it found one
|
|
17
|
+
# (ADR-41 WD4), so a definer past the cut cannot be ruled out. It replaces a plugin's direct call to the
|
|
18
|
+
# two `Scope` walks, whose budget answers differ and are not a promise to plugin authors.
|
|
19
|
+
#
|
|
20
|
+
# - `kind:` is `:instance` or `:singleton`; each walks its own chain.
|
|
21
|
+
# - `source:` names the walk outright, with that walk's order and settling:
|
|
22
|
+
# - `:def` — the first project `def` (`Scope#user_def_through_ancestors`, or `#singleton_def_through_ancestors`
|
|
23
|
+
# for `kind: :singleton`). It does not count `attr_*` or `define_method`.
|
|
24
|
+
# - `:discovered` — the discovery table (`Scope#discovered_method_through_ancestors?`), which also counts
|
|
25
|
+
# `attr_*` and `define_method`.
|
|
26
|
+
# - `:either` — their union as a three-valued OR: `yes` wins, then `maybe`, then `no`.
|
|
27
|
+
#
|
|
28
|
+
# A site that tested the `:def` walk's old answer tests `.yes?` (that walk read a cut as absent); a site that
|
|
29
|
+
# tested the `:discovered` walk's tests `!no?` (that walk read a cut as present).
|
|
30
|
+
def project_method_defined(class_name, method_name, kind:, source:, scope:)
|
|
31
|
+
raise ArgumentError, "unknown kind #{kind.inspect}" unless PROJECT_METHOD_KINDS.include?(kind)
|
|
32
|
+
raise ArgumentError, "unknown source #{source.inspect}" unless PROJECT_METHOD_SOURCES.include?(source)
|
|
33
|
+
|
|
34
|
+
case source
|
|
35
|
+
when :def then project_def_defined(class_name, method_name, kind, scope)
|
|
36
|
+
when :discovered then project_discovered_defined(class_name, method_name, kind, scope)
|
|
37
|
+
else project_either_defined(class_name, method_name, kind, scope)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Does RBS declare `method_name` on `class_name` or on an ancestor the project's own ancestry reaches? A
|
|
42
|
+
# project class (`class ArticlesController < ActionController::Base`) is rarely in RBS itself; its RBS-known
|
|
43
|
+
# ancestors are the EXTERNAL entries of its `ResolutionChain`, which the RBS-only reads
|
|
44
|
+
# ({.instance_method_definition}) cannot reach from the project class's name. This walks the chain and asks RBS
|
|
45
|
+
# of each entry (every candidate spelling of an external one), on the side the entry sits.
|
|
46
|
+
#
|
|
47
|
+
# Answers a {Trinary}: `yes` when an entry's RBS declares the method, `no` when none does and the chain was
|
|
48
|
+
# not truncated, `maybe` when the chain was cut at its budget (ADR-41 WD4) before one did. `kind:` is
|
|
49
|
+
# `:instance` or `:singleton`; a `nil` `class_name` answers `no`.
|
|
50
|
+
def rbs_method_defined(class_name, method_name, kind:, scope:)
|
|
51
|
+
chain = ancestry_chain(class_name, kind, scope)
|
|
52
|
+
return Trinary.no if chain.nil?
|
|
53
|
+
|
|
54
|
+
found = chain.search(scope) do |entry|
|
|
55
|
+
chain_entry_names(entry).any? { |name| !rbs_method_on_side(name, method_name, entry.side, scope).nil? }
|
|
56
|
+
end
|
|
57
|
+
chain_answer(found, chain)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Does the project ancestry of `class_name` reach any of `ancestor_names` (qualified names, a leading `::`
|
|
61
|
+
# ignored)? An external ancestor counts by any candidate spelling, so a superclass the project names but
|
|
62
|
+
# does not declare (`ActionController::Base`, with no RBS loaded) is found. Same `Trinary` and `kind:` as
|
|
63
|
+
# {.rbs_method_defined}.
|
|
64
|
+
def ancestry_reaches(class_name, ancestor_names, kind:, scope:)
|
|
65
|
+
chain = ancestry_chain(class_name, kind, scope)
|
|
66
|
+
return Trinary.no if chain.nil?
|
|
67
|
+
|
|
68
|
+
wanted = ancestor_names.map { |name| name.to_s.delete_prefix("::") }
|
|
69
|
+
found = chain.search(scope) do |entry|
|
|
70
|
+
chain_entry_names(entry).any? { |name| wanted.include?(name.to_s.delete_prefix("::")) }
|
|
71
|
+
end
|
|
72
|
+
chain_answer(found, chain)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def ancestry_chain(class_name, kind, scope)
|
|
76
|
+
raise ArgumentError, "unknown kind #{kind.inspect}" unless PROJECT_METHOD_KINDS.include?(kind)
|
|
77
|
+
return nil if class_name.nil?
|
|
78
|
+
|
|
79
|
+
Scope::ResolutionChain.for(scope, class_name.to_s, kind, :methods)
|
|
80
|
+
end
|
|
81
|
+
private_class_method :ancestry_chain
|
|
82
|
+
|
|
83
|
+
def chain_answer(found, chain)
|
|
84
|
+
return Trinary.yes if found
|
|
85
|
+
|
|
86
|
+
chain.truncated? ? Trinary.maybe : Trinary.no
|
|
87
|
+
end
|
|
88
|
+
private_class_method :chain_answer
|
|
89
|
+
|
|
90
|
+
def chain_entry_names(entry)
|
|
91
|
+
entry.external? ? entry.candidates : [entry.name]
|
|
92
|
+
end
|
|
93
|
+
private_class_method :chain_entry_names
|
|
94
|
+
|
|
95
|
+
def rbs_method_on_side(class_name, method_name, side, scope)
|
|
96
|
+
if side == :singleton
|
|
97
|
+
singleton_method_definition(class_name, method_name, scope: scope)
|
|
98
|
+
else
|
|
99
|
+
instance_method_definition(class_name, method_name, scope: scope)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
private_class_method :rbs_method_on_side
|
|
103
|
+
|
|
104
|
+
# `yes` wins, then `maybe`, then `no`. The early return is a short-circuit and carries no answer of its own:
|
|
105
|
+
# `yes.or(x)` is `yes`, so removing it changes only which walks run, and so which dependencies they read.
|
|
106
|
+
def project_either_defined(class_name, method_name, kind, scope)
|
|
107
|
+
by_def = project_def_defined(class_name, method_name, kind, scope)
|
|
108
|
+
return by_def if by_def.yes?
|
|
109
|
+
|
|
110
|
+
by_def.or(project_discovered_defined(class_name, method_name, kind, scope))
|
|
111
|
+
end
|
|
112
|
+
private_class_method :project_either_defined
|
|
113
|
+
|
|
114
|
+
# The def walk answers `[node, owner]` or `[nil, nil]`, and `[nil, nil]` both when the chain holds no definer
|
|
115
|
+
# and when it was cut; the chain (memoised, so this is the one the walk read) tells the two apart.
|
|
116
|
+
def project_def_defined(class_name, method_name, kind, scope)
|
|
117
|
+
found = if kind == :singleton
|
|
118
|
+
scope.singleton_def_through_ancestors(class_name, method_name)
|
|
119
|
+
else
|
|
120
|
+
scope.user_def_through_ancestors(class_name, method_name)
|
|
121
|
+
end
|
|
122
|
+
return Trinary.yes if found.first
|
|
123
|
+
|
|
124
|
+
project_chain_truncated?(class_name, kind, scope) ? Trinary.maybe : Trinary.no
|
|
125
|
+
end
|
|
126
|
+
private_class_method :project_def_defined
|
|
127
|
+
|
|
128
|
+
# The discovery walk answers `true` both for a definer and for a cut chain, so a `true` on a cut chain is
|
|
129
|
+
# settled by searching the chain's entries for a definer, as the walk does.
|
|
130
|
+
#
|
|
131
|
+
# Issue #2968 — where the walk finds none, a class on the chain whose side is open (an open-names mark: a
|
|
132
|
+
# splatted `Struct.new(*FIELDS)`, `attr_accessor(*NAMES)`, a `define_method` under a computed name) answers
|
|
133
|
+
# `maybe`: it defines methods under names no table holds. Issue #2994 — so does an instance side whose chain mixes
|
|
134
|
+
# in a module no table names (`include(*MODS)`, `ResolutionChain#unnamed_mixin?`); the #618 top-level-`def` veto
|
|
135
|
+
# (`ExpressionTyper#instance_self_answers?`) reads the same mark, so a plugin that stands down here does not hand
|
|
136
|
+
# the call to a top-level `def`.
|
|
137
|
+
def project_discovered_defined(class_name, method_name, kind, scope)
|
|
138
|
+
unless scope.discovered_method_through_ancestors?(class_name, method_name, kind)
|
|
139
|
+
return open_side?(class_name, kind, scope) ? Trinary.maybe : Trinary.no
|
|
140
|
+
end
|
|
141
|
+
# A shortcut, not a decision: on an untruncated chain the search below finds the same definer.
|
|
142
|
+
return Trinary.yes unless project_chain_truncated?(class_name, kind, scope)
|
|
143
|
+
|
|
144
|
+
chain = Scope::ResolutionChain.for(scope, class_name.to_s, kind, :methods)
|
|
145
|
+
found = chain.search(scope) do |entry|
|
|
146
|
+
# A guard, not a decision: it mirrors `Scope#discovered_method_through_ancestors?`, whose search skips
|
|
147
|
+
# an external entry (no project name to look up). `discovered_method?(nil, ...)` is false anyway.
|
|
148
|
+
next false if entry.external?
|
|
149
|
+
|
|
150
|
+
scope.discovered_method?(entry.name, method_name, discovered_entry_kind(entry, kind))
|
|
151
|
+
end
|
|
152
|
+
found ? Trinary.yes : Trinary.maybe
|
|
153
|
+
end
|
|
154
|
+
private_class_method :project_discovered_defined
|
|
155
|
+
|
|
156
|
+
def open_side?(class_name, kind, scope)
|
|
157
|
+
return true if Scope::DiscoveryIndex.open_names_on_chain?(scope, class_name, kind)
|
|
158
|
+
|
|
159
|
+
kind == :instance && Scope::DiscoveryIndex.unnamed_mixin_on_chain?(scope, class_name)
|
|
160
|
+
end
|
|
161
|
+
private_class_method :open_side?
|
|
162
|
+
|
|
163
|
+
# A singleton-side entry reads the singleton table; a module on the singleton chain answers with its
|
|
164
|
+
# INSTANCE methods; on the instance chain the caller's kind stands (`Scope#kind_for`).
|
|
165
|
+
def discovered_entry_kind(entry, kind)
|
|
166
|
+
return :singleton if entry.side == :singleton
|
|
167
|
+
|
|
168
|
+
kind == :singleton ? :instance : kind
|
|
169
|
+
end
|
|
170
|
+
private_class_method :discovered_entry_kind
|
|
171
|
+
|
|
172
|
+
def project_chain_truncated?(class_name, kind, scope)
|
|
173
|
+
Scope::ResolutionChain.for(scope, class_name.to_s, kind, :methods).truncated?
|
|
174
|
+
end
|
|
175
|
+
private_class_method :project_chain_truncated?
|
|
176
|
+
end
|
|
177
|
+
end
|
data/lib/rigor/reflection.rb
CHANGED
|
@@ -511,3 +511,5 @@ end
|
|
|
511
511
|
# private candidate lookups, so the facade has to exist first.
|
|
512
512
|
require_relative "reflection/constant_ancestors"
|
|
513
513
|
require_relative "reflection/constant_path"
|
|
514
|
+
require_relative "reflection/project_method"
|
|
515
|
+
require_relative "reflection/declarations"
|
data/lib/rigor/rooted_glob.rb
CHANGED
|
@@ -38,13 +38,30 @@ module Rigor
|
|
|
38
38
|
# it always did. An absolute one is split into its own directory and a literal basename: `File.join(".", "/abs")`
|
|
39
39
|
# reads `./abs`, so an absolute path joined under `"."` matched nothing.
|
|
40
40
|
#
|
|
41
|
+
# A `~`-prefixed path is expanded before the split (#2586): the readers apply `File.expand_path`, so `~/x.yml`
|
|
42
|
+
# reads `$HOME/x.yml` and the watch must be split from that spelling — `[".", "~/x.yml"]` would watch
|
|
43
|
+
# `./~/x.yml` and never fire. Only the `~` form is expanded; expanding every path would change how relative
|
|
44
|
+
# entries are watched.
|
|
45
|
+
#
|
|
41
46
|
# @param path — a file path, taken literally
|
|
42
47
|
# @return `[root, pattern]`: `"."` and the literal path when relative, else the directory and the literal basename
|
|
43
48
|
def split_file(path)
|
|
44
|
-
path = path.to_s
|
|
49
|
+
path = expand_home(path.to_s)
|
|
45
50
|
return [".", literal(path)] unless File.absolute_path?(path)
|
|
46
51
|
|
|
47
52
|
[File.dirname(path), literal(File.basename(path))]
|
|
48
53
|
end
|
|
54
|
+
|
|
55
|
+
# `File.expand_path`'s `~` handling, kept literal on failure: `~nouser/x` (or an unset HOME) names no home
|
|
56
|
+
# directory and raises `ArgumentError`; the readers fail on the same path, so the unexpanded spelling —
|
|
57
|
+
# watched under `"."` like any other relative entry — is the consistent answer.
|
|
58
|
+
def expand_home(path)
|
|
59
|
+
return path unless path.start_with?("~")
|
|
60
|
+
|
|
61
|
+
File.expand_path(path)
|
|
62
|
+
rescue ArgumentError
|
|
63
|
+
path
|
|
64
|
+
end
|
|
65
|
+
private_class_method :expand_home
|
|
49
66
|
end
|
|
50
67
|
end
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "English"
|
|
4
|
+
require_relative "typing_state"
|
|
5
|
+
require_relative "memo_audit"
|
|
6
|
+
|
|
3
7
|
module Rigor
|
|
4
|
-
# Issues #2412 and #2399 — the
|
|
5
|
-
# owner
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
+
# Issues #2412 and #2399 — the run-end hook for per-thread state whose contents belong to one analysis run. An
|
|
9
|
+
# owner declares such a slot in {TypingState} with `run_scoped: true` where it names it, and each entry point that
|
|
10
|
+
# runs an analysis calls {.release} in its `ensure`, so a finished run, or one that raised, leaves none of its
|
|
11
|
+
# scopes, discovery tables or environment behind on the thread.
|
|
8
12
|
#
|
|
9
13
|
# A slot that holds its run until the next run overwrites it retains one run per thread. A slot whose value
|
|
10
14
|
# reaches its own key retains every run, as `LoopValueRecord`'s `ObjectSpace::WeakKeyMap` did: the map holds
|
|
@@ -12,26 +16,22 @@ module Rigor
|
|
|
12
16
|
# bounds both. Inside a run nothing changes: every memo keeps its key and its hit rate, and a slot read outside
|
|
13
17
|
# a run (a language-server hover on the main thread) refills on its first miss.
|
|
14
18
|
#
|
|
15
|
-
# Only
|
|
19
|
+
# Only Tables, Memos and Resources are run-scoped. A Frame a `with_*` block installs and restores is already empty
|
|
16
20
|
# when the frame returns, and clearing it from inside a frame would break the frame.
|
|
17
21
|
#
|
|
18
|
-
#
|
|
22
|
+
# ADR-129 — run-scoped state is a `run_scoped: true` {TypingState} slot, and nothing else registers: this module
|
|
23
|
+
# keeps no key list of its own.
|
|
19
24
|
module RunScopedSlots
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
#
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
key
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# Every registered key, in registration order.
|
|
29
|
-
def self.keys = @keys.dup.freeze
|
|
30
|
-
|
|
31
|
-
# Empties every registered slot on the calling thread.
|
|
25
|
+
# Empties every run-scoped {TypingState} slot on the calling thread.
|
|
26
|
+
#
|
|
27
|
+
# ADR-129 WD5 (#2785) — with the memo audit on, a mismatch no file drained (one raised by typing outside the
|
|
28
|
+
# per-file loop: return summaries, return re-evaluation, sig-gen) would be released here unreported, so it is
|
|
29
|
+
# raised after the release, unless an exception is already leaving the run.
|
|
32
30
|
def self.release
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
undrained = Inference::MemoAudit.enabled? ? Inference::MemoAudit.drain : nil
|
|
32
|
+
TypingState.release_run_scoped
|
|
33
|
+
raise undrained.first if undrained && !undrained.empty? && $ERROR_INFO.nil?
|
|
34
|
+
|
|
35
35
|
nil
|
|
36
36
|
end
|
|
37
37
|
end
|
|
@@ -60,7 +60,7 @@ module Rigor
|
|
|
60
60
|
:contested_discovered_parameter_envelopes
|
|
61
61
|
)
|
|
62
62
|
|
|
63
|
-
class DiscoveryIndex
|
|
63
|
+
class DiscoveryIndex # rubocop:disable Metrics/ClassLength -- the envelope kinds and marks are one constant each
|
|
64
64
|
# Issue #2330 — identity as a hash key. `Data` hashes and compares structurally, which over these project-sized
|
|
65
65
|
# tables is both costly and wrong for a per-thread memo keyed by "this very index" (an `ObjectSpace::WeakKeyMap`
|
|
66
66
|
# holds the memo while the index lives and drops it with it). `==` stays structural.
|
|
@@ -247,6 +247,100 @@ module Rigor
|
|
|
247
247
|
ENVELOPE_DYNAMIC_MARK = :"<dynamic>"
|
|
248
248
|
ENVELOPE_OBJECT_EXTENDED_MARK = :"<object-extended>"
|
|
249
249
|
ENVELOPE_REFINED_MARK = :"<refined>"
|
|
250
|
+
# Issue #2793 — a class-body macro defines methods under names its syntax does not spell
|
|
251
|
+
# (`def_delegators :@x, *NAMES`), so no table can enumerate the class's members: `call.undefined-method`
|
|
252
|
+
# treats the class, and every project class that inherits or mixes it in, as open, as it does a source
|
|
253
|
+
# `method_missing`. Not a {.rewritten_surface?} mark: the macro adds names and replaces no method body
|
|
254
|
+
# Rigor reads.
|
|
255
|
+
ENVELOPE_OPEN_NAMES_MARK = :"<open-names>"
|
|
256
|
+
# Issue #2793 — the sided twins of {ENVELOPE_OPEN_NAMES_MARK}: a `define_method` whose name is not a literal
|
|
257
|
+
# (`%i[a b].each { |m| define_method(m) { } }`) where `self` is the class defines instance methods no table can
|
|
258
|
+
# name, and one in a `class << self` body singleton methods. `call.undefined-method` reads the instance mark
|
|
259
|
+
# for an instance receiver and the singleton mark for the class object, each beside the sideless mark.
|
|
260
|
+
ENVELOPE_OPEN_INSTANCE_NAMES_MARK = :"<open-instance-names>"
|
|
261
|
+
ENVELOPE_OPEN_SINGLETON_NAMES_MARK = :"<open-singleton-names>"
|
|
262
|
+
# Issue #2901 — the two halves of a class macro that defines methods under computed names. A
|
|
263
|
+
# `[ENVELOPE_COMPUTED_DEFINER_KIND, side, name]` entry says the class or module's own method `name`, on `side`
|
|
264
|
+
# (`:singleton` or `:instance`, #2975), runs a `define_method` on its `self` whose name is not a literal
|
|
265
|
+
# (`def self.my_attr(*n) = n.each { |x| define_method(x) {} }`, or a module's instance method an `extend` makes a
|
|
266
|
+
# class macro). A
|
|
267
|
+
# `[ENVELOPE_BODY_CALL_KIND, name]` entry says the class body calls `name` on `self` (`my_attr :foo`).
|
|
268
|
+
# `call.undefined-method` reads a class as instance-open when a body call of it resolves, on its singleton
|
|
269
|
+
# chain, to a computed definer. Neither entry names a member, so neither declares a surface.
|
|
270
|
+
ENVELOPE_COMPUTED_DEFINER_KIND = :computed_definer
|
|
271
|
+
ENVELOPE_BODY_CALL_KIND = :body_call
|
|
272
|
+
# Issue #2975 — `[ENVELOPE_HOOK_EXTEND_KIND, "ClassMethods"]` says the module's `self.included(base)` (or
|
|
273
|
+
# `self.prepended(base)`) hook runs `base.extend(ClassMethods)`, the constant as written: an includer's
|
|
274
|
+
# singleton chain then reaches `ClassMethods`, whose computed definers its body calls may resolve to.
|
|
275
|
+
#
|
|
276
|
+
# Issue #2986 — an `extend ActiveSupport::Concern` module records `[ENVELOPE_HOOK_EXTEND_KIND, "ClassMethods",
|
|
277
|
+
# :const_get]`: `Concern#append_features` extends its includer with `const_get(:ClassMethods)`, which searches the
|
|
278
|
+
# module, its ancestors and `Object`, not the lexical scope. A `[ENVELOPE_HOOK_BODY_CALL_KIND, hook, name]` entry
|
|
279
|
+
# says its `included do … end` (`hook` `:included`) or `prepended do … end` (`:prepended`) block calls `name` on
|
|
280
|
+
# `self`, which Ruby runs in the class body of a class that includes (or prepends) the module directly. It is
|
|
281
|
+
# filed only on the module whose body the walk is in, so it is not one of `Incremental`'s diffed class-mark kinds.
|
|
282
|
+
#
|
|
283
|
+
# Issue #1592 — a hook's own entry names the hook as its third element (`[ENVELOPE_HOOK_EXTEND_KIND, "M",
|
|
284
|
+
# :included]`, `:prepended` for `self.prepended` or a `prepended do … end` block), and an `extend` on `self` in a
|
|
285
|
+
# concern's `included do … end` / `prepended do … end` block records one too. The includer's singleton resolution
|
|
286
|
+
# chain inserts each such module for the edge its hook runs on. A `[ENVELOPE_HOOK_EXTEND_OPEN_KIND, hook]` entry
|
|
287
|
+
# says that hook may extend its includer with a module no entry names, or one it may not extend at all (an
|
|
288
|
+
# `extend` that is not a direct statement of the hook body, or whose argument is not a constant).
|
|
289
|
+
ENVELOPE_HOOK_EXTEND_KIND = :hook_extend
|
|
290
|
+
ENVELOPE_HOOK_EXTEND_OPEN_KIND = :hook_extend_open
|
|
291
|
+
# Issue #1592 — `[ENVELOPE_HOOK_DEF_KIND, hook, source]`: one definition of the module's `self.included` /
|
|
292
|
+
# `self.prepended` hook, by its source text. Two entries for one hook mean the hook is defined twice, and which
|
|
293
|
+
# definition Ruby keeps is load order.
|
|
294
|
+
ENVELOPE_HOOK_DEF_KIND = :hook_def
|
|
295
|
+
ENVELOPE_HOOK_BODY_CALL_KIND = :hook_body_call
|
|
296
|
+
# Issue #2992 — `[ENVELOPE_CONCERN_DEPENDENCY_KIND, "Dep"]`: the module's body runs `include Dep` (or `prepend
|
|
297
|
+
# Dep`) after its own `extend ActiveSupport::Concern`, so a concern `Dep` is a dependency of the module, which
|
|
298
|
+
# ActiveSupport includes into the module's final includer instead. Filed only on the module whose body the walk
|
|
299
|
+
# is in, as the hook-body-call entry is.
|
|
300
|
+
ENVELOPE_CONCERN_DEPENDENCY_KIND = :concern_dependency
|
|
301
|
+
# Issue #3005 — the hook a hook-extend, open or hook-body-call entry names when it comes from an `included do …
|
|
302
|
+
# end` / `prepended do … end` block in a body that does not run `extend ActiveSupport::Concern` itself. The
|
|
303
|
+
# block is ActiveSupport's only when some body of the module records the concern entry; a reader takes the
|
|
304
|
+
# gated hook as the plain one then, and as no hook otherwise ({ResolutionChain.effective_hook}).
|
|
305
|
+
CONCERN_BLOCK_HOOKS = { included: :concern_included, prepended: :concern_prepended }.freeze
|
|
306
|
+
# Issue #3005 — the last segment of the module a concern block's `def self.x`s are filed on (`Owner::<included>`),
|
|
307
|
+
# and the spelling its hook-extend entry names.
|
|
308
|
+
HOOK_BLOCK_SEGMENTS = %w[<included> <prepended>].freeze
|
|
309
|
+
# Issue #2979 — `[ENVELOPE_SAME_CLASS_KIND, "Struct::Named"]` on `K`'s bucket, and its mirror on
|
|
310
|
+
# `Struct::Named`'s, say that `K = Struct.new("Named", …)` binds both constants to one class object: the method
|
|
311
|
+
# chain of either name reads the other's tables at its own position ({.same_class_names}). It names no member.
|
|
312
|
+
ENVELOPE_SAME_CLASS_KIND = :same_class
|
|
313
|
+
# Issue #2998 — `[ENVELOPE_SINGLETON_PREPEND_KIND, "P"]` on `K`'s bucket says one of `K`'s `discovered_extends`
|
|
314
|
+
# entries spelt `"P"` is a prepend to `K`'s singleton class (`class << self; prepend P; end`,
|
|
315
|
+
# `singleton_class.prepend P`, `singleton_class.send(:prepend, P)` in `K`'s body), which sits ahead of the class
|
|
316
|
+
# object's own methods where an `extend` sits behind them. The singleton resolution chain reads it.
|
|
317
|
+
ENVELOPE_SINGLETON_PREPEND_KIND = :singleton_prepend
|
|
318
|
+
# Issue #3018 — `[ENVELOPE_OBJECT_SINGLETON_KIND, name]` says a definer written on the constant's object from
|
|
319
|
+
# outside its body defines `name` there: `def K.name`, a literal `K.define_singleton_method(:name)`, or a `def
|
|
320
|
+
# name` in a `K.singleton_class.class_eval` block. Each also files the opaque `[:singleton, name]` entry, which
|
|
321
|
+
# name-rewriting class-body macros (`memoize :name`) file too; this entry is the one the class-object path of
|
|
322
|
+
# `call.undefined-method` reads as evidence the singleton method exists.
|
|
323
|
+
ENVELOPE_OBJECT_SINGLETON_KIND = :object_singleton
|
|
324
|
+
MACRO_KINDS = [ENVELOPE_BODY_CALL_KIND, ENVELOPE_COMPUTED_DEFINER_KIND, ENVELOPE_HOOK_EXTEND_KIND,
|
|
325
|
+
ENVELOPE_HOOK_EXTEND_OPEN_KIND, ENVELOPE_HOOK_DEF_KIND, ENVELOPE_HOOK_BODY_CALL_KIND,
|
|
326
|
+
ENVELOPE_CONCERN_DEPENDENCY_KIND, ENVELOPE_SAME_CLASS_KIND,
|
|
327
|
+
ENVELOPE_SINGLETON_PREPEND_KIND].freeze
|
|
328
|
+
private_constant :MACRO_KINDS
|
|
329
|
+
|
|
330
|
+
NO_NAMES = [].freeze
|
|
331
|
+
private_constant :NO_NAMES
|
|
332
|
+
|
|
333
|
+
# Issue #2979 — the other names an envelope bucket says its class object is bound to
|
|
334
|
+
# ({ENVELOPE_SAME_CLASS_KIND}), in recording order; empty for nearly every bucket, and allocates nothing then.
|
|
335
|
+
def self.same_class_names(bucket)
|
|
336
|
+
return NO_NAMES if bucket.nil?
|
|
337
|
+
|
|
338
|
+
names = nil
|
|
339
|
+
bucket.each_key do |key|
|
|
340
|
+
(names ||= []) << key.last if key.is_a?(Array) && key.first == ENVELOPE_SAME_CLASS_KIND
|
|
341
|
+
end
|
|
342
|
+
names || NO_NAMES
|
|
343
|
+
end
|
|
250
344
|
|
|
251
345
|
# Issue #1820 — whether an envelope bucket says the project declares, or rewrites, the name's method table: a
|
|
252
346
|
# per-method entry, the {ENVELOPE_MODULE_MARK} a `module` declaration records, or the {ENVELOPE_DYNAMIC_MARK}
|
|
@@ -258,7 +352,60 @@ module Rigor
|
|
|
258
352
|
private_constant :DECLARING_MARKS
|
|
259
353
|
|
|
260
354
|
def self.declares_surface?(bucket)
|
|
261
|
-
!bucket.nil? && bucket.any? { |key, _|
|
|
355
|
+
!bucket.nil? && bucket.any? { |key, _| declaring_key?(key) }
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Issue #2901 — a per-method entry declares a member; the two macro entries ({ENVELOPE_BODY_CALL_KIND},
|
|
359
|
+
# {ENVELOPE_COMPUTED_DEFINER_KIND}) do not.
|
|
360
|
+
def self.declaring_key?(key)
|
|
361
|
+
return DECLARING_MARKS.include?(key) if key.is_a?(Symbol)
|
|
362
|
+
|
|
363
|
+
!(key.is_a?(Array) && MACRO_KINDS.include?(key.first))
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Issue #2968 — the open-names marks of one side: {ENVELOPE_OPEN_NAMES_MARK} and the side's own twin.
|
|
367
|
+
OPEN_NAMES_MARKS = {
|
|
368
|
+
instance: [ENVELOPE_OPEN_NAMES_MARK, ENVELOPE_OPEN_INSTANCE_NAMES_MARK].freeze,
|
|
369
|
+
singleton: [ENVELOPE_OPEN_NAMES_MARK, ENVELOPE_OPEN_SINGLETON_NAMES_MARK].freeze
|
|
370
|
+
}.freeze
|
|
371
|
+
private_constant :OPEN_NAMES_MARKS
|
|
372
|
+
|
|
373
|
+
# Issue #2968 — whether an entry of `class_name`'s `kind` chain carries that side's open-names mark: a splatted
|
|
374
|
+
# `Struct.new(*FIELDS)`, `attr_accessor(*NAMES)`, a `define_method` under a computed name, so the side defines
|
|
375
|
+
# methods no table names. A project class with no `def` of its own is an external entry (#2094), so its candidate
|
|
376
|
+
# names are asked too. Read by `Reflection.project_method_defined` and by the implicit-self top-level-`def` veto
|
|
377
|
+
# (`ExpressionTyper#instance_self_answers?`), beside `call.undefined-method`'s own reader.
|
|
378
|
+
def self.open_names_on_chain?(scope, class_name, kind)
|
|
379
|
+
marks = OPEN_NAMES_MARKS.fetch(kind)
|
|
380
|
+
chain = ResolutionChain.for(scope, class_name.to_s, kind, :methods)
|
|
381
|
+
found = chain.search(scope) do |entry|
|
|
382
|
+
names = entry.external? ? entry.candidates : [entry.name]
|
|
383
|
+
names.any? do |name|
|
|
384
|
+
bucket = scope.parameter_envelopes_of(name)
|
|
385
|
+
marks.any? { |mark| bucket.key?(mark) }
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
!found.nil?
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# Issue #2994 — whether `class_name`'s instance-side chain mixes in a module no table names
|
|
392
|
+
# (`ResolutionChain#unnamed_mixin?`: `include(*MODS)`, `include helper`, a hook's `base.include`). A project class
|
|
393
|
+
# with no `def` of its own is an external entry (#2094) whose ancestry the chain does not walk, so each of its
|
|
394
|
+
# candidate names that the project declares is asked on its own chain. Read beside {.open_names_on_chain?} by
|
|
395
|
+
# `Reflection.project_method_defined` and by the implicit-self top-level-`def` veto.
|
|
396
|
+
def self.unnamed_mixin_on_chain?(scope, class_name)
|
|
397
|
+
chain = ResolutionChain.for(scope, class_name.to_s, :instance, :methods)
|
|
398
|
+
return true if chain.unnamed_mixin?
|
|
399
|
+
|
|
400
|
+
found = chain.search(scope) do |entry|
|
|
401
|
+
next false unless entry.external?
|
|
402
|
+
|
|
403
|
+
entry.candidates.any? do |name|
|
|
404
|
+
scope.discovered_classes.key?(name) &&
|
|
405
|
+
ResolutionChain.for(scope, name, :instance, :methods).unnamed_mixin?
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
!found.nil?
|
|
262
409
|
end
|
|
263
410
|
|
|
264
411
|
# Whether an envelope bucket says the project rewrites the class's method table beyond its literal names: a
|
|
@@ -27,6 +27,8 @@ module Rigor
|
|
|
27
27
|
# segment of its spelling, so a new file declaring the module re-checks the consumer.
|
|
28
28
|
module Relevance
|
|
29
29
|
WILDCARD = "*"
|
|
30
|
+
# Issue #2991 — listed beside {WILDCARD} when the side mixes in a module no name spells.
|
|
31
|
+
UNNAMED = "?"
|
|
30
32
|
|
|
31
33
|
# The singleton hooks Ruby calls while a module enters (or is asked to enter) another's ancestry. A module
|
|
32
34
|
# that defines one can add methods to the includer, extender or prepender with nothing the tables record
|
|
@@ -39,7 +41,18 @@ module Rigor
|
|
|
39
41
|
def discharged?(scope, chain, mark, name)
|
|
40
42
|
name = name.to_sym
|
|
41
43
|
memo = ResolutionChain.relevance_memo(scope, chain.flavor)
|
|
42
|
-
|
|
44
|
+
key = [mark, name]
|
|
45
|
+
entry = memo[key]
|
|
46
|
+
verdict, edges =
|
|
47
|
+
if entry.nil?
|
|
48
|
+
memo[key] = Inference::MemoAudit.key_audit("resolution chain relevance") do
|
|
49
|
+
compute(scope, chain.flavor, mark, name)
|
|
50
|
+
end
|
|
51
|
+
else
|
|
52
|
+
Inference::MemoAudit.check("resolution chain relevance", entry, mark, name) do
|
|
53
|
+
Analysis::DependencyRecorder.withhold { compute(scope, chain.flavor, mark, name) }.first
|
|
54
|
+
end
|
|
55
|
+
end
|
|
43
56
|
replay(scope, edges) if Analysis::DependencyRecorder.active?
|
|
44
57
|
verdict
|
|
45
58
|
end
|