rigortype 0.4.4 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +5 -5
- data/docs/handbook/11-sig-gen.md +100 -18
- data/docs/manual/02-cli-reference.md +85 -16
- data/docs/manual/03-configuration.md +12 -9
- data/docs/manual/04-diagnostics.md +17 -6
- data/docs/manual/05-inspecting-types.md +2 -2
- data/docs/manual/06-baseline.md +1 -1
- data/docs/manual/07-plugins.md +2 -1
- data/docs/manual/08-skills.md +7 -0
- data/docs/manual/10-mcp-server.md +17 -2
- data/docs/manual/12-caching.md +8 -1
- data/docs/manual/18-removing-dead-code.md +18 -7
- data/docs/manual/plugins/rigor-actionmailer.md +30 -0
- data/docs/manual/plugins/rigor-actionpack.md +30 -0
- data/docs/manual/plugins/rigor-devise.md +15 -0
- data/docs/manual/plugins/rigor-rspec.md +102 -1
- data/lib/rigor/analysis/buffer_binding.rb +14 -0
- data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +30 -9
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +142 -15
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +115 -7
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +253 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +3 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +64 -4
- data/lib/rigor/analysis/check_rules.rb +319 -21
- data/lib/rigor/analysis/dependency_recorder.rb +11 -0
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/edit_replay.rb +67 -0
- data/lib/rigor/analysis/incremental.rb +86 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +336 -46
- data/lib/rigor/analysis/path_expansion.rb +44 -1
- data/lib/rigor/analysis/reachability/graph.rb +90 -13
- data/lib/rigor/analysis/reachability/scan.rb +279 -39
- data/lib/rigor/analysis/require_relative_closure.rb +100 -0
- data/lib/rigor/analysis/rule_catalog.rb +22 -5
- data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
- data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +11 -3
- data/lib/rigor/analysis/runner/pool_coordinator.rb +82 -16
- data/lib/rigor/analysis/runner/project_pre_passes.rb +45 -3
- data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
- data/lib/rigor/analysis/runner.rb +222 -111
- data/lib/rigor/analysis/severity_stamp.rb +11 -0
- data/lib/rigor/analysis/worker_session.rb +24 -1
- data/lib/rigor/cache/descriptor.rb +40 -7
- data/lib/rigor/cache/engine_source.rb +3 -2
- data/lib/rigor/cache/incremental_snapshot.rb +49 -8
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +4 -3
- data/lib/rigor/cli/check_command.rb +93 -4
- data/lib/rigor/cli/command.rb +3 -1
- data/lib/rigor/cli/diff_command.rb +2 -1
- data/lib/rigor/cli/docs_command.rb +3 -2
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/explain_command.rb +3 -2
- data/lib/rigor/cli/lens_command.rb +5 -5
- data/lib/rigor/cli/mcp_command.rb +2 -1
- data/lib/rigor/cli/options.rb +20 -3
- data/lib/rigor/cli/plugin_command.rb +2 -1
- data/lib/rigor/cli/plugins_command.rb +7 -1
- data/lib/rigor/cli/plugins_renderer.rb +24 -2
- data/lib/rigor/cli/show_bleedingedge_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +73 -13
- data/lib/rigor/cli/skill_command.rb +2 -1
- data/lib/rigor/cli/skill_describe.rb +3 -2
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/type_of_project_scope.rb +27 -0
- data/lib/rigor/cli/unused_command.rb +10 -7
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +72 -16
- data/lib/rigor/configuration/severity_profile.rb +20 -2
- data/lib/rigor/configuration.rb +43 -7
- data/lib/rigor/effects/signature_sources.rb +3 -1
- data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
- data/lib/rigor/environment/gated_signature_guard.rb +2 -1
- data/lib/rigor/environment/gated_signature_plan.rb +2 -1
- data/lib/rigor/environment/installed_gem_set.rb +2 -1
- data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
- data/lib/rigor/environment/rbs_loader.rb +136 -7
- data/lib/rigor/environment/required_features.rb +136 -29
- data/lib/rigor/environment/ruby_library_roots.rb +122 -0
- data/lib/rigor/environment.rb +4 -2
- data/lib/rigor/inference/block_call_timing.rb +7 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/callee_mutation_chain.rb +115 -0
- data/lib/rigor/inference/captured_locals.rb +44 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/deep_erasure.rb +138 -0
- data/lib/rigor/inference/def_return_typer.rb +159 -9
- data/lib/rigor/inference/definer_resolution.rb +101 -20
- data/lib/rigor/inference/error_info.rb +17 -3
- data/lib/rigor/inference/expression_typer.rb +97 -18
- data/lib/rigor/inference/global_write_census.rb +36 -1
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- data/lib/rigor/inference/ivar_writer_census.rb +195 -0
- data/lib/rigor/inference/loop_value_record.rb +53 -0
- data/lib/rigor/inference/macro_block_self_type.rb +20 -6
- data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +278 -42
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
- data/lib/rigor/inference/method_dispatcher.rb +113 -19
- data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
- data/lib/rigor/inference/module_function_state.rb +149 -42
- data/lib/rigor/inference/mutation_widening.rb +5 -2
- data/lib/rigor/inference/narrowing.rb +116 -11
- data/lib/rigor/inference/object_mixins.rb +13 -1
- data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
- data/lib/rigor/inference/project_patched_methods.rb +168 -4
- data/lib/rigor/inference/project_patched_mixins.rb +162 -0
- data/lib/rigor/inference/project_patched_scanner.rb +51 -12
- data/lib/rigor/inference/project_patched_visibility.rb +214 -0
- data/lib/rigor/inference/project_seed_tables.rb +121 -0
- data/lib/rigor/inference/project_yielder.rb +128 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
- data/lib/rigor/inference/reflective_ivar_write.rb +6 -6
- data/lib/rigor/inference/return_barrier.rb +16 -0
- data/lib/rigor/inference/scope_indexer/certainty.rb +9 -0
- data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +162 -0
- data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
- data/lib/rigor/inference/scope_indexer.rb +1144 -197
- data/lib/rigor/inference/self_call_ivar_writes.rb +29 -12
- data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
- data/lib/rigor/inference/statement_evaluator.rb +1088 -213
- data/lib/rigor/inference/synthetic_method_index.rb +23 -0
- data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +72 -12
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- data/lib/rigor/language_server/buffer_table.rb +6 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +20 -6
- data/lib/rigor/language_server/project_context.rb +59 -3
- data/lib/rigor/language_server/server.rb +5 -3
- data/lib/rigor/lens/contract_reader.rb +45 -2
- data/lib/rigor/lens/document.rb +2 -1
- data/lib/rigor/lens/file_reader.rb +12 -2
- data/lib/rigor/lens/members.rb +16 -2
- data/lib/rigor/lens/run.rb +34 -7
- data/lib/rigor/mcp/server.rb +11 -3
- data/lib/rigor/plugin/base.rb +29 -13
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -3
- data/lib/rigor/plugin/manifest.rb +29 -3
- data/lib/rigor/plugin/registry.rb +38 -0
- data/lib/rigor/protection/discovery_seed.rb +64 -36
- data/lib/rigor/protection/mutation_cache.rb +8 -2
- data/lib/rigor/rooted_glob.rb +36 -0
- data/lib/rigor/scope/discovery_index.rb +5 -0
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
- data/lib/rigor/scope/resolution_chain.rb +40 -5
- data/lib/rigor/scope.rb +59 -7
- data/lib/rigor/sig_gen/alias_index.rb +2 -1
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +465 -49
- data/lib/rigor/sig_gen/layout_index.rb +5 -2
- data/lib/rigor/sig_gen/observation_collector.rb +141 -12
- data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +16 -155
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
- data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
- data/lib/rigor/sig_gen/write_reporter.rb +203 -0
- data/lib/rigor/sig_gen/write_result.rb +3 -1
- data/lib/rigor/sig_gen/writer.rb +5 -3
- data/lib/rigor/signature_path_audit.rb +4 -2
- data/lib/rigor/type/combinator.rb +4 -3
- data/lib/rigor/type/constant.rb +54 -3
- data/lib/rigor/type/hash_shape.rb +14 -9
- data/lib/rigor/type/struct_instance.rb +3 -2
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +61 -44
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +38 -9
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -2
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
- data/sig/rigor/analysis/baseline.rbs +3 -2
- data/sig/rigor/effects/file_collection.rbs +5 -6
- data/sig/rigor/effects/registry.rbs +8 -6
- data/sig/rigor/effects/summary.rbs +6 -5
- data/sig/rigor/environment.rbs +5 -3
- data/sig/rigor/inference/optimistic_origin.rbs +11 -0
- data/sig/rigor/inference.rbs +6 -0
- data/sig/rigor/plugin/additional_initializer.rbs +5 -5
- data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
- data/sig/rigor/plugin/effect_attribution.rbs +25 -10
- data/sig/rigor/plugin/effect_edge.rbs +12 -5
- data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
- data/sig/rigor/plugin/io_boundary.rbs +5 -2
- data/sig/rigor/plugin/manifest.rbs +16 -18
- data/sig/rigor/plugin/protocol_contract.rbs +10 -9
- data/sig/rigor/plugin/registry.rbs +1 -1
- data/sig/rigor/rooted_glob.rbs +7 -0
- data/sig/rigor/scope.rbs +34 -52
- data/sig/rigor/type.rbs +0 -6
- data/skills/rigor-doctor/references/01-checks.md +23 -11
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
- metadata +32 -1
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
5
|
require_relative "rule_walk"
|
|
6
|
+
require_relative "../../scope/resolution_chain"
|
|
6
7
|
require_relative "../../source/constant_path"
|
|
7
8
|
require_relative "../../source/node_children"
|
|
8
9
|
|
|
@@ -37,36 +38,54 @@ module Rigor
|
|
|
37
38
|
# `def` that sits directly in a class / module body, never one nested in another `def`. On the
|
|
38
39
|
# shared full DFS that prune becomes the `:inside_def` gate; the enclosing class / module name stack
|
|
39
40
|
# the legacy walk threaded as `qualified_prefix` is now `context.qualified_prefix`.
|
|
40
|
-
|
|
41
|
+
#
|
|
42
|
+
# ADR-125 — the walk also visits class-body `CallNode`s (a `def` body is gated off), to record the
|
|
43
|
+
# macro calls whose literal arguments name a method (`before_action :load_roles`).
|
|
44
|
+
NODE_CLASSES = [Prism::DefNode, Prism::CallNode].freeze
|
|
41
45
|
RULE_WALK_GATES = %i[inside_def detached_ivar_facet].freeze
|
|
42
46
|
|
|
43
|
-
# Returns `Hash[class_name (String) => Hash[ivar_name (Symbol) => Array<{node:, type:, declared
|
|
44
|
-
# Empty when the tree has no qualifying writes.
|
|
45
|
-
# declaration of the ivar — an `@x: T` member or
|
|
46
|
-
# inherited — and nil when the RBS declares none.
|
|
47
|
+
# Returns `Hash[class_name (String) => Hash[ivar_name (Symbol) => Array<{node:, type:, declared:,
|
|
48
|
+
# method_name:, visibility:, per_invocation:, action:}>]]`. Empty when the tree has no qualifying writes.
|
|
49
|
+
# `declared` is the translated type of the class's RBS declaration of the ivar — an `@x: T` member or
|
|
50
|
+
# the slot an `attr_*` member implies, own or inherited — and nil when the RBS declares none.
|
|
51
|
+
#
|
|
52
|
+
# ADR-125 — `method_name` is the enclosing `def`'s name and `visibility` its instance visibility
|
|
53
|
+
# (`:public` / `:protected` / `:private`, or nil when the discovery table has no settled answer).
|
|
54
|
+
# `per_invocation` is whether the class is, or descends from, a class a loaded plugin declares in
|
|
55
|
+
# `per_invocation_classes:`. `action` is whether the enclosing method is one of that class's entry
|
|
56
|
+
# points: public, and named by none of the declaring plugin's `per_invocation_non_action_macros:` in a
|
|
57
|
+
# class body of this file (`before_action :load_roles`, `helper_method :current_roles`).
|
|
47
58
|
def initialize(scope_index)
|
|
48
59
|
@scope_index = scope_index
|
|
49
60
|
@accumulator = {}
|
|
61
|
+
@per_invocation = {}
|
|
62
|
+
@macro_arguments = {}
|
|
50
63
|
end
|
|
51
64
|
|
|
52
65
|
# Legacy single-collector walk — kept as the oracle the ADR-53 Track B equivalence harness compares
|
|
53
66
|
# {RuleWalk} against; deleted when Track B completes.
|
|
54
67
|
def collect(root)
|
|
55
68
|
walk(root, [])
|
|
56
|
-
|
|
69
|
+
results
|
|
57
70
|
end
|
|
58
71
|
|
|
59
72
|
# {RuleWalk} entry point: the legacy walk's per-`def` logic, invoked at each class-/module-body `def`
|
|
60
73
|
# under the shared traversal contract. `collect_def_writes`' verbatim body reads the enclosing class
|
|
61
74
|
# prefix from `context.qualified_prefix`.
|
|
62
|
-
def visit(
|
|
63
|
-
|
|
75
|
+
def visit(node, context)
|
|
76
|
+
if node.is_a?(Prism::CallNode)
|
|
77
|
+
record_macro_call(node, context.qualified_prefix)
|
|
78
|
+
else
|
|
79
|
+
collect_def_writes(node, context.qualified_prefix)
|
|
80
|
+
end
|
|
64
81
|
end
|
|
65
82
|
|
|
66
83
|
# The accumulated result, frozen the same way `#collect` returns it — used by {RuleWalk}-driven
|
|
67
84
|
# callers after the walk completes.
|
|
68
85
|
def results
|
|
69
|
-
@accumulator.
|
|
86
|
+
@accumulator.to_h do |class_name, writes_by_ivar|
|
|
87
|
+
[class_name, writes_by_ivar.transform_values { |writes| with_actions(class_name, writes) }.freeze]
|
|
88
|
+
end.freeze
|
|
70
89
|
end
|
|
71
90
|
|
|
72
91
|
private
|
|
@@ -87,29 +106,71 @@ module Rigor
|
|
|
87
106
|
when Prism::DefNode
|
|
88
107
|
collect_def_writes(node, qualified_prefix)
|
|
89
108
|
return
|
|
109
|
+
when Prism::CallNode
|
|
110
|
+
record_macro_call(node, qualified_prefix)
|
|
90
111
|
end
|
|
91
112
|
|
|
92
113
|
node.rigor_each_child { |child| walk(child, qualified_prefix) }
|
|
93
114
|
end
|
|
94
115
|
|
|
116
|
+
# A receiverless (or `self.`) call in a class body, with the method names its literal Symbol / String
|
|
117
|
+
# arguments spell. Kept for every such call: which macro names count is the registry's answer, read
|
|
118
|
+
# once per class when the writes are finalised.
|
|
119
|
+
def record_macro_call(call_node, qualified_prefix)
|
|
120
|
+
return if qualified_prefix.empty?
|
|
121
|
+
return unless call_node.receiver.nil? || call_node.receiver.is_a?(Prism::SelfNode)
|
|
122
|
+
|
|
123
|
+
names = (call_node.arguments&.arguments || []).filter_map { |argument| literal_method_name(argument) }
|
|
124
|
+
return if names.empty?
|
|
125
|
+
|
|
126
|
+
((@macro_arguments[qualified_prefix.join("::")] ||= {})[call_node.name] ||= []).concat(names)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# A Symbol / String literal's name, or the name of a `def` passed inline (`helper_method def
|
|
130
|
+
# current_user … end`). A literal with bytes no Symbol can hold (`before_action "\xff"`, #1821) names no
|
|
131
|
+
# method, so it answers nil.
|
|
132
|
+
def literal_method_name(argument)
|
|
133
|
+
return argument.name if argument.is_a?(Prism::DefNode) && argument.receiver.nil?
|
|
134
|
+
return nil unless argument.is_a?(Prism::SymbolNode) || argument.is_a?(Prism::StringNode)
|
|
135
|
+
|
|
136
|
+
text = argument.unescaped
|
|
137
|
+
text.valid_encoding? ? text.to_sym : nil
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Each write with `action` set (see {#initialize}). A frozen copy, so `#results` stays repeatable.
|
|
141
|
+
def with_actions(class_name, writes)
|
|
142
|
+
macros = @per_invocation[class_name]
|
|
143
|
+
excluded = macros ? non_action_names(class_name, macros) : nil
|
|
144
|
+
writes.map do |write|
|
|
145
|
+
action = !excluded.nil? && write[:visibility] == :public && !excluded.include?(write[:method_name])
|
|
146
|
+
write.merge(action: action).freeze
|
|
147
|
+
end.freeze
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def non_action_names(class_name, macros)
|
|
151
|
+
(@macro_arguments[class_name] || {}).each_with_object(Set.new) do |(macro, names), set|
|
|
152
|
+
set.merge(names) if macros.include?(macro)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
95
156
|
def collect_def_writes(def_node, qualified_prefix)
|
|
96
157
|
return if def_node.body.nil? || qualified_prefix.empty?
|
|
97
158
|
return if def_node.receiver.is_a?(Prism::SelfNode)
|
|
98
159
|
|
|
99
160
|
class_name = qualified_prefix.join("::")
|
|
100
|
-
gather_writes(def_node.body, class_name)
|
|
161
|
+
gather_writes(def_node.body, class_name, def_node.name)
|
|
101
162
|
end
|
|
102
163
|
|
|
103
|
-
def gather_writes(node, class_name)
|
|
164
|
+
def gather_writes(node, class_name, method_name)
|
|
104
165
|
return unless node.is_a?(Prism::Node)
|
|
105
166
|
|
|
106
|
-
record_write(node, class_name) if node.is_a?(Prism::InstanceVariableWriteNode)
|
|
167
|
+
record_write(node, class_name, method_name) if node.is_a?(Prism::InstanceVariableWriteNode)
|
|
107
168
|
return if BARRIER_NODES.any? { |klass| node.is_a?(klass) }
|
|
108
169
|
|
|
109
|
-
node.rigor_each_child { |child| gather_writes(child, class_name) }
|
|
170
|
+
node.rigor_each_child { |child| gather_writes(child, class_name, method_name) }
|
|
110
171
|
end
|
|
111
172
|
|
|
112
|
-
def record_write(node, class_name)
|
|
173
|
+
def record_write(node, class_name, method_name)
|
|
113
174
|
scope = @scope_index[node]
|
|
114
175
|
return if scope.nil?
|
|
115
176
|
|
|
@@ -117,7 +178,73 @@ module Rigor
|
|
|
117
178
|
declared = declared_ivar(scope, class_name, node.name)
|
|
118
179
|
@accumulator[class_name] ||= {}
|
|
119
180
|
@accumulator[class_name][node.name] ||= []
|
|
120
|
-
@accumulator[class_name][node.name] << {
|
|
181
|
+
@accumulator[class_name][node.name] << {
|
|
182
|
+
node: node, type: rvalue_type, declared: declared, method_name: method_name,
|
|
183
|
+
visibility: method_visibility(scope, class_name, method_name),
|
|
184
|
+
per_invocation: !per_invocation_macros(scope, class_name).nil?
|
|
185
|
+
}
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# The enclosing `def`'s instance visibility, or nil when it is not a settled fact: a slot the
|
|
189
|
+
# discovery table does not hold, or one a later `define_method` / alias / unplaceable write contests
|
|
190
|
+
# (#2013). `initialize` and the other lifecycle hooks are private whatever the table says.
|
|
191
|
+
def method_visibility(scope, class_name, method_name)
|
|
192
|
+
return :private if CheckRules::IMPLICITLY_PRIVATE_METHODS.include?(method_name)
|
|
193
|
+
return nil if scope.discovery.contested?(:discovered_method_visibilities, [class_name, method_name])
|
|
194
|
+
|
|
195
|
+
scope.discovered_method_visibility(class_name, method_name)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# ADR-125 — when `class_name` is, or descends from, a class a loaded plugin declares per-invocation, the
|
|
199
|
+
# non-action macros its declaring manifests list (a Set, possibly empty); nil otherwise. Read off the
|
|
200
|
+
# class's instance {Scope::ResolutionChain}, so a project superclass chain
|
|
201
|
+
# (`UsersController < ApplicationController < ActionController::Base`) is followed as Ruby resolves it.
|
|
202
|
+
# A project entry matches by name. An external superclass matches through the first of its candidate
|
|
203
|
+
# spellings the environment knows, by name or by an RBS-known `:equal` / `:subclass` ordering. When the
|
|
204
|
+
# environment knows none of them (rigor-actionpack leaves `ActionController::Base` undeclared on
|
|
205
|
+
# purpose), a candidate that is literally a declared name matches, unless some candidate names a class
|
|
206
|
+
# the project itself declares: then the spelling may resolve there instead. An ambiguous spelling (no
|
|
207
|
+
# candidates) and a truncated chain answer nil: the carve-out only ever withholds a finding, so an
|
|
208
|
+
# unproved descent keeps the rule firing. Memoised per class.
|
|
209
|
+
def per_invocation_macros(scope, class_name)
|
|
210
|
+
return @per_invocation[class_name] if @per_invocation.key?(class_name)
|
|
211
|
+
|
|
212
|
+
@per_invocation[class_name] = matched_per_invocation_macros(scope, class_name)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def matched_per_invocation_macros(scope, class_name)
|
|
216
|
+
environment = scope.environment
|
|
217
|
+
registry = environment&.plugin_registry
|
|
218
|
+
return nil if registry.nil? || registry.per_invocation_classes.empty?
|
|
219
|
+
|
|
220
|
+
chain = Scope::ResolutionChain.for(scope, class_name, :instance, :methods)
|
|
221
|
+
return nil if chain.truncated?
|
|
222
|
+
|
|
223
|
+
declared = chain.search(scope) { |entry| matched_declarations(entry, registry, scope) }
|
|
224
|
+
declared&.each_with_object(Set.new) { |name, set| set.merge(registry.per_invocation_non_action_macros(name)) }
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# The declared per-invocation names `entry` matches, or nil (so {ResolutionChain#search} moves on).
|
|
228
|
+
def matched_declarations(entry, registry, scope)
|
|
229
|
+
return (registry.per_invocation_class?(entry.name.to_s) ? [entry.name.to_s] : nil) unless entry.external?
|
|
230
|
+
return nil unless entry.superclass_edge
|
|
231
|
+
|
|
232
|
+
environment = scope.environment
|
|
233
|
+
names = entry.candidates.map { |name| name.to_s.delete_prefix("::") }
|
|
234
|
+
known = names.find { |name| environment.class_known?(name) }
|
|
235
|
+
return literal_declarations(names, registry, scope) if known.nil?
|
|
236
|
+
|
|
237
|
+
matched = registry.per_invocation_classes.select do |declared|
|
|
238
|
+
%i[equal subclass].include?(environment.class_ordering(known, declared))
|
|
239
|
+
end
|
|
240
|
+
matched.empty? ? nil : matched
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def literal_declarations(names, registry, scope)
|
|
244
|
+
return nil if names.any? { |name| scope.discovered_classes.key?(name) }
|
|
245
|
+
|
|
246
|
+
matched = names.select { |name| registry.per_invocation_class?(name) }
|
|
247
|
+
matched.empty? ? nil : matched
|
|
121
248
|
end
|
|
122
249
|
|
|
123
250
|
def declared_ivar(scope, class_name, ivar_name)
|
|
@@ -4,6 +4,7 @@ require "prism"
|
|
|
4
4
|
|
|
5
5
|
require_relative "../../inference/scope_indexer"
|
|
6
6
|
require_relative "../../source/node_children"
|
|
7
|
+
require_relative "rebinding_block_callees"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
9
10
|
module Analysis
|
|
@@ -17,6 +18,13 @@ module Rigor
|
|
|
17
18
|
# - **Singleton defs on locals.** `def o.m` defines `m` on the one object `o` holds. The local's type
|
|
18
19
|
# is not changed; `o.m` is simply not reported within the scope the `def` is written in (the
|
|
19
20
|
# enclosing `def`, `class` / `module` body, or file — a block shares its enclosing scope's locals).
|
|
21
|
+
# Issue #1722 — the other spellings that give that one object methods make the local opaque for every
|
|
22
|
+
# name in the same scope: `o.extend(M)`, `o.define_singleton_method(...)` (only the literal name it
|
|
23
|
+
# defines, when it names one), any call on `o.singleton_class` (`class_eval { def m }`, `define_method`,
|
|
24
|
+
# `include`), `class << o`, and an `o.instance_eval` / `o.instance_exec` that is given a String or whose
|
|
25
|
+
# block writes a `def` or calls `extend` / `define_singleton_method`. Where in the scope the call sits,
|
|
26
|
+
# and whether it runs, are not read: a call under an `if` may run. A local assigned `o.clone` shares
|
|
27
|
+
# what `o` has (`clone` copies the singleton class, `dup` does not).
|
|
20
28
|
#
|
|
21
29
|
# Built lazily: the walk runs the first time a would-fire call asks, so a file with no such call pays
|
|
22
30
|
# one small object and nothing else.
|
|
@@ -34,22 +42,48 @@ module Rigor
|
|
|
34
42
|
|
|
35
43
|
def refinements = (@refinements ||= Inference::InEffectRefinements.new(@root))
|
|
36
44
|
|
|
37
|
-
#
|
|
38
|
-
|
|
45
|
+
# Issue #1817 — the file's block-passing calls, for `call.unresolved-toplevel`'s rebinding-callee decline.
|
|
46
|
+
def block_calls = (@block_calls ||= RebindingBlockCallees::Sites.new(@root))
|
|
47
|
+
|
|
48
|
+
# Does a `def <local>.<name>` for this call's local receiver and method name — or a call that gives the
|
|
49
|
+
# local's object methods Rigor does not enumerate (issue #1722) — sit in the scope the call is in?
|
|
39
50
|
def singleton_local_def?(call_node)
|
|
40
51
|
receiver = call_node.receiver
|
|
41
52
|
return false unless receiver.is_a?(Prism::LocalVariableReadNode)
|
|
42
53
|
|
|
43
54
|
build
|
|
44
|
-
|
|
45
|
-
@singleton_defs.any? do |local, method_name, start, stop|
|
|
46
|
-
local == receiver.name && method_name == call_node.name && offset >= start && offset < stop &&
|
|
47
|
-
!nested_scope_holds?(offset, start, stop)
|
|
48
|
-
end
|
|
55
|
+
singleton_local_holds?(receiver.name, call_node.name, call_node.location.start_offset, CLONE_DEPTH)
|
|
49
56
|
end
|
|
50
57
|
|
|
58
|
+
# The name an opening records when it may define any method on the local's object.
|
|
59
|
+
ANY_METHOD = Object.new.freeze
|
|
60
|
+
# How many `t = s.clone` links a lookup follows.
|
|
61
|
+
CLONE_DEPTH = 4
|
|
62
|
+
EVAL_CALLS = %i[instance_eval instance_exec].freeze
|
|
63
|
+
EVAL_BLOCK_CALLS = %i[extend define_singleton_method].freeze
|
|
64
|
+
private_constant :ANY_METHOD, :CLONE_DEPTH, :EVAL_CALLS, :EVAL_BLOCK_CALLS
|
|
65
|
+
|
|
51
66
|
private
|
|
52
67
|
|
|
68
|
+
def singleton_local_holds?(local_name, method_name, offset, depth)
|
|
69
|
+
opened = in_scope?(@singleton_defs, offset) do |local, name|
|
|
70
|
+
local == local_name && (name.equal?(ANY_METHOD) || name == method_name)
|
|
71
|
+
end
|
|
72
|
+
return true if opened
|
|
73
|
+
return false if depth.zero?
|
|
74
|
+
|
|
75
|
+
@clones.any? do |target, source, start, stop|
|
|
76
|
+
target == local_name && offset >= start && offset < stop && !nested_scope_holds?(offset, start, stop) &&
|
|
77
|
+
singleton_local_holds?(source, method_name, offset, depth - 1)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def in_scope?(rows, offset)
|
|
82
|
+
rows.any? do |local, name, start, stop|
|
|
83
|
+
yield(local, name) && offset >= start && offset < stop && !nested_scope_holds?(offset, start, stop)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
53
87
|
# Is `offset` inside a local scope nested in the one `[start, stop)` spans? A `def` in the file's top level
|
|
54
88
|
# does not see the top level's `o`.
|
|
55
89
|
def nested_scope_holds?(offset, start, stop)
|
|
@@ -64,6 +98,7 @@ module Rigor
|
|
|
64
98
|
|
|
65
99
|
@built = true
|
|
66
100
|
@singleton_defs = []
|
|
101
|
+
@clones = []
|
|
67
102
|
@scopes = []
|
|
68
103
|
return if @root.nil?
|
|
69
104
|
|
|
@@ -78,11 +113,16 @@ module Rigor
|
|
|
78
113
|
when Prism::ClassNode, Prism::ModuleNode
|
|
79
114
|
return walk_children(node.body, scope_span(node))
|
|
80
115
|
when Prism::SingletonClassNode
|
|
116
|
+
record_opened_local(node.expression, ANY_METHOD, locals)
|
|
81
117
|
walk(node.expression, locals)
|
|
82
118
|
return walk_children(node.body, scope_span(node))
|
|
83
119
|
when Prism::DefNode
|
|
84
120
|
record_singleton_def(node, locals)
|
|
85
121
|
return walk_children(node.body, scope_span(node))
|
|
122
|
+
when Prism::CallNode
|
|
123
|
+
record_singleton_opening(node, locals)
|
|
124
|
+
when Prism::LocalVariableWriteNode
|
|
125
|
+
record_clone(node, locals)
|
|
86
126
|
end
|
|
87
127
|
|
|
88
128
|
walk_children(node, locals)
|
|
@@ -101,6 +141,74 @@ module Rigor
|
|
|
101
141
|
@singleton_defs << [receiver.name, node.name, *locals]
|
|
102
142
|
end
|
|
103
143
|
|
|
144
|
+
# Issue #1722 — a call that gives the object its receiver local holds singleton methods.
|
|
145
|
+
def record_singleton_opening(node, locals)
|
|
146
|
+
receiver = node.receiver
|
|
147
|
+
if singleton_class_of_local?(receiver)
|
|
148
|
+
record_opened_local(receiver.receiver, ANY_METHOD, locals)
|
|
149
|
+
elsif receiver.is_a?(Prism::LocalVariableReadNode)
|
|
150
|
+
name = opened_method_name(node)
|
|
151
|
+
record_opened_local(receiver, name, locals) if name
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# `o.singleton_class`.
|
|
156
|
+
def singleton_class_of_local?(node)
|
|
157
|
+
node.is_a?(Prism::CallNode) && node.name == :singleton_class && node.arguments.nil? &&
|
|
158
|
+
node.receiver.is_a?(Prism::LocalVariableReadNode)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# The method name a call on a local defines on the local's object: the literal name given to
|
|
162
|
+
# `define_singleton_method`, {ANY_METHOD} where it may be any name, or nil when the call defines none.
|
|
163
|
+
def opened_method_name(node)
|
|
164
|
+
case node.name
|
|
165
|
+
when :extend then ANY_METHOD
|
|
166
|
+
when :define_singleton_method then literal_name(node.arguments&.arguments&.first) || ANY_METHOD
|
|
167
|
+
when *EVAL_CALLS then ANY_METHOD if eval_defines?(node)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def literal_name(node)
|
|
172
|
+
node.unescaped.to_sym if node.is_a?(Prism::SymbolNode) || node.is_a?(Prism::StringNode)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# An `instance_eval` given code as a String, or whose block writes a `def` or calls `extend` /
|
|
176
|
+
# `define_singleton_method` on its `self`.
|
|
177
|
+
def eval_defines?(node)
|
|
178
|
+
return true unless node.arguments.nil?
|
|
179
|
+
|
|
180
|
+
block = node.block
|
|
181
|
+
block.is_a?(Prism::BlockNode) && defines_on_self?(block.body)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def defines_on_self?(node)
|
|
185
|
+
return false if node.nil?
|
|
186
|
+
return true if node.is_a?(Prism::DefNode)
|
|
187
|
+
if node.is_a?(Prism::CallNode) && EVAL_BLOCK_CALLS.include?(node.name) &&
|
|
188
|
+
(node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
|
|
189
|
+
return true
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
found = false
|
|
193
|
+
node.rigor_each_child { |child| found ||= defines_on_self?(child) }
|
|
194
|
+
found
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def record_opened_local(node, method_name, locals)
|
|
198
|
+
return unless node.is_a?(Prism::LocalVariableReadNode)
|
|
199
|
+
|
|
200
|
+
@singleton_defs << [node.name, method_name, *locals]
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# `t = s.clone`: `clone` copies the singleton class, so `t` holds what `s` was given.
|
|
204
|
+
def record_clone(node, locals)
|
|
205
|
+
value = node.value
|
|
206
|
+
return unless value.is_a?(Prism::CallNode) && value.name == :clone
|
|
207
|
+
return unless value.receiver.is_a?(Prism::LocalVariableReadNode)
|
|
208
|
+
|
|
209
|
+
@clones << [node.name, value.receiver.name, *locals]
|
|
210
|
+
end
|
|
211
|
+
|
|
104
212
|
# The span of a node that opens a local scope, recorded for {#nested_scope_holds?}.
|
|
105
213
|
def scope_span(node)
|
|
106
214
|
span = span_of(node)
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../reflection"
|
|
4
|
+
require_relative "../dependency_recorder"
|
|
5
|
+
require_relative "../../inference/singleton_hook_decline"
|
|
6
|
+
require_relative "../../inference/object_mixins"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module Analysis
|
|
10
|
+
module CheckRules
|
|
11
|
+
# Issue #1722 — what the project may give an instance of an RBS-declared class beyond the class's RBS, asked
|
|
12
|
+
# by `call.undefined-method` once the RBS lookup and the project's own tables for the class itself have come
|
|
13
|
+
# back "absent". Three places the earlier probes do not look:
|
|
14
|
+
#
|
|
15
|
+
# - **A `method_missing` on an RBS ancestor.** `class Integer; def method_missing(*)` (or one on `Numeric`,
|
|
16
|
+
# `Comparable`, `Object`, `Kernel`, `BasicObject`, or a project module mixed into any of them) answers
|
|
17
|
+
# every name Ruby dispatches to an Integer. `respond_to_missing?` is not read: Ruby dispatches to
|
|
18
|
+
# `method_missing` whether or not it is defined.
|
|
19
|
+
# - **A project mixin of the class or an RBS ancestor.** `Integer.include(Ext)` in another file, or
|
|
20
|
+
# `Comparable.include(Ext)` / `module Comparable; def zzz`, gives every Integer `zzz`. The earlier probes
|
|
21
|
+
# read the class's own chain through `discovered_methods`, which holds no plain `def` written in another
|
|
22
|
+
# file than the one being checked; this reads the def table too for a project module, and walks the
|
|
23
|
+
# ancestors RBS lists after the class. A cross-file `def` on an RBS-declared class or module itself stays
|
|
24
|
+
# ADR-17's monkey-patch report ({.mixin_defines?}). A mixin with an open surface — a project module with a
|
|
25
|
+
# recorded `included` / `extended` / … hook, a `"*"` mixin or `ActiveSupport::Concern`, or one neither RBS nor
|
|
26
|
+
# the project declares — answers any name, except on `Object`, `Kernel` and `BasicObject`: there only a
|
|
27
|
+
# module that defines the name answers, #1697's reading (`Inference::ObjectMixins`) of a module mixed into
|
|
28
|
+
# every object.
|
|
29
|
+
# - **A project subclass** ({.subclass_may_define?}). A value typed by its class's name through a signature
|
|
30
|
+
# (`#: () -> String`) may be an instance of any subclass, so a method a project subclass defines may be
|
|
31
|
+
# the one that runs.
|
|
32
|
+
#
|
|
33
|
+
# Each ancestor's name is filed as an ADR-46 class dependency, so a file that later reopens one re-checks
|
|
34
|
+
# the call, and the subclass walk files a `subclass_of` edge per class it reads.
|
|
35
|
+
module RbsReceiverSurface
|
|
36
|
+
MISSING_HOOK = :method_missing
|
|
37
|
+
# The roots whose project mixins `Inference::ObjectMixins` answers for.
|
|
38
|
+
OBJECT_ROOTS = %w[Object Kernel BasicObject].to_set.freeze
|
|
39
|
+
private_constant :MISSING_HOOK, :OBJECT_ROOTS
|
|
40
|
+
|
|
41
|
+
module_function
|
|
42
|
+
|
|
43
|
+
# Whether the project may answer `method_name` on an instance of `class_name` through one of its RBS
|
|
44
|
+
# ancestors.
|
|
45
|
+
def ancestor_may_answer?(scope, class_name, method_name)
|
|
46
|
+
ancestor_names(scope, class_name).any? do |name|
|
|
47
|
+
DependencyRecorder.read_last_segment(:class, name) if DependencyRecorder.active?
|
|
48
|
+
next false unless scope.known_user_class?(name)
|
|
49
|
+
|
|
50
|
+
chain_answers?(scope, name, method_name, open_mixins: !OBJECT_ROOTS.include?(name))
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Whether the singleton side of `class_name` may reach a project `method_missing`: a `def self.method_missing`
|
|
55
|
+
# on the class or an RBS superclass, or an instance `method_missing` on `Class`, `Module`, `Object`,
|
|
56
|
+
# `Kernel` or `BasicObject` (a class object is an instance of each).
|
|
57
|
+
def singleton_hook?(scope, class_name)
|
|
58
|
+
loader = scope.environment&.rbs_loader
|
|
59
|
+
pairs = loader ? loader.singleton_ancestors_for(class_name.to_s) : []
|
|
60
|
+
pairs = [[:singleton, class_name.to_s]] if pairs.empty?
|
|
61
|
+
pairs.any? do |kind, name|
|
|
62
|
+
DependencyRecorder.read_last_segment(:class, name) if DependencyRecorder.active?
|
|
63
|
+
if kind == :singleton
|
|
64
|
+
!scope.singleton_def_for(name, MISSING_HOOK).nil? ||
|
|
65
|
+
scope.discovered_method?(name, MISSING_HOOK, :singleton)
|
|
66
|
+
else
|
|
67
|
+
scope.known_user_class?(name) && hook_on_chain?(scope, name)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Issue #2094 — the singleton side's twin of {.ancestor_may_answer?}: a module the class or an RBS superclass
|
|
73
|
+
# `extend`s (in its body, as `Float.extend(Ext)`, or as `Float.singleton_class.include(Ext)`) may answer
|
|
74
|
+
# `method_name` on the class object, read as {.mixin_defines?} / {.external_may_answer?} read an instance-side
|
|
75
|
+
# mixin. Only each level's own singleton segment is read: the class's further singleton ancestors are the
|
|
76
|
+
# RBS superclasses this walk visits in turn, and `Class` / `Module` are {.singleton_hook?}'s.
|
|
77
|
+
def singleton_mixin_may_answer?(scope, class_name, method_name)
|
|
78
|
+
singleton_class_names(scope, class_name).any? do |name|
|
|
79
|
+
DependencyRecorder.read_last_segment(:class, name) if DependencyRecorder.active?
|
|
80
|
+
|
|
81
|
+
extends_answer?(scope, name, method_name, open_mixins: !OBJECT_ROOTS.include?(name))
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def singleton_class_names(scope, class_name)
|
|
86
|
+
loader = scope.environment&.rbs_loader
|
|
87
|
+
pairs = loader ? loader.singleton_ancestors_for(class_name.to_s) : []
|
|
88
|
+
names = pairs.filter_map { |kind, name| name if kind == :singleton }
|
|
89
|
+
names.empty? ? [class_name.to_s] : names
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def extends_answer?(scope, name, method_name, open_mixins:)
|
|
93
|
+
chain = Scope::ResolutionChain.for(scope, name, :singleton, :methods)
|
|
94
|
+
return true if chain.truncated?
|
|
95
|
+
# A mixin call the indexer could not record (`Integer.extend(m)`, `Symbol.singleton_class.send(:include,
|
|
96
|
+
# M)`) leaves the side tainted: some module the tables do not name may answer any name.
|
|
97
|
+
return true if open_mixins && chain.wildcard_mixin?
|
|
98
|
+
|
|
99
|
+
entries = chain.level_count.zero? ? chain.entries : chain.level_entries(0)
|
|
100
|
+
entries.any? do |entry|
|
|
101
|
+
if entry.external?
|
|
102
|
+
external_may_answer?(scope, entry, method_name, open_mixins)
|
|
103
|
+
else
|
|
104
|
+
entry.name != name && mixin_defines?(scope, entry, method_name, open_mixins)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Whether a project subclass of `class_name`, at any depth, defines `method_name` (through its own
|
|
110
|
+
# ancestry, which reaches its project mixins) or a `method_missing`. Its parent is read as written and
|
|
111
|
+
# resolved the way the class header resolves it: the first project class the spelling can denote, else the
|
|
112
|
+
# top-level name, which is the RBS class.
|
|
113
|
+
def subclass_may_define?(scope, class_name, method_name)
|
|
114
|
+
each_subclass(scope, class_name.to_s) do |subclass|
|
|
115
|
+
return true if project_defines?(scope, subclass, method_name) ||
|
|
116
|
+
project_defines?(scope, subclass, MISSING_HOOK) ||
|
|
117
|
+
!scope.user_def_through_ancestors(subclass, method_name).first.nil? ||
|
|
118
|
+
scope.discovered_method_through_ancestors?(subclass, method_name, :instance)
|
|
119
|
+
end
|
|
120
|
+
false
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def ancestor_names(scope, class_name)
|
|
124
|
+
loader = scope.environment&.rbs_loader
|
|
125
|
+
names = loader ? loader.ancestor_names_for(class_name.to_s) : []
|
|
126
|
+
names.empty? ? [class_name.to_s] : names
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# A search of `name`'s project chain for an entry that may answer: a project entry that defines
|
|
130
|
+
# `method_name` or a `method_missing`, or an external mixin RBS declares `method_name` on. When
|
|
131
|
+
# `open_mixins` (every ancestor but `Object`, `Kernel` and `BasicObject`, #1697), also a mixin whose surface
|
|
132
|
+
# is open: a project module SingletonHookDecline reads as hook-capable, or an external neither RBS nor the
|
|
133
|
+
# project declares. A chain cut at its budget may hold an answer past the cut.
|
|
134
|
+
def chain_answers?(scope, name, method_name, open_mixins:)
|
|
135
|
+
chain = Scope::ResolutionChain.for(scope, name, :instance, :methods)
|
|
136
|
+
found = chain.search(scope, side: :instance) do |entry|
|
|
137
|
+
if entry.external?
|
|
138
|
+
external_may_answer?(scope, entry, method_name, open_mixins)
|
|
139
|
+
else
|
|
140
|
+
project_defines?(scope, entry.name, MISSING_HOOK) ||
|
|
141
|
+
mixin_defines?(scope, entry, method_name, open_mixins)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
found || chain.truncated?
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# The singleton side's question of an instance chain: does a project entry on it define `method_missing`?
|
|
148
|
+
def hook_on_chain?(scope, name)
|
|
149
|
+
chain = Scope::ResolutionChain.for(scope, name, :instance, :methods)
|
|
150
|
+
found = chain.search(scope, side: :instance) do |entry|
|
|
151
|
+
!entry.external? && project_defines?(scope, entry.name, MISSING_HOOK)
|
|
152
|
+
end
|
|
153
|
+
found || chain.truncated?
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def project_defines?(scope, owner, method_name)
|
|
157
|
+
!scope.user_def_for(owner, method_name).nil? || scope.discovered_method?(owner, method_name, :instance)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# ADR-17 — a `def` the project writes on a class or module RBS declares (`class String; def x`, `module
|
|
161
|
+
# Comparable; def x`) in another file than the call is a monkey-patch Rigor does not apply cross-file, and the
|
|
162
|
+
# rule reports it with the `pre_eval:` advice, so on such an entry only what `discovered_methods` carries
|
|
163
|
+
# answers. A project module's own `def` answers from any file.
|
|
164
|
+
#
|
|
165
|
+
# When `open_mixins`, a project module {Inference::SingletonHookDecline} reads as hook-capable — one that
|
|
166
|
+
# records an `included` / `extended` / … hook (`def self.included(base) = base.class_eval { def x }`), lists
|
|
167
|
+
# a `"*"` mixin, or extends `ActiveSupport::Concern` — may answer any name.
|
|
168
|
+
def mixin_defines?(scope, entry, method_name, open_mixins)
|
|
169
|
+
owner = entry.name
|
|
170
|
+
if Rigor::Reflection.rbs_class_known?(owner, scope: scope)
|
|
171
|
+
scope.discovered_method?(owner, method_name, :instance)
|
|
172
|
+
else
|
|
173
|
+
project_defines?(scope, owner, method_name) || macro_names?(scope, owner, method_name) ||
|
|
174
|
+
(open_mixins && (Inference::SingletonHookDecline.entry_hook_capable?(scope, entry) ||
|
|
175
|
+
open_extend?(scope, owner)))
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Issue #2094 — a class-body macro of the module names the method ({Inference::ObjectMixins.macro_named?}).
|
|
180
|
+
# Answers the named method only, so it holds on `Object`, `Kernel` and `BasicObject` too, where a module
|
|
181
|
+
# that defines the name answers.
|
|
182
|
+
def macro_names?(scope, owner, method_name)
|
|
183
|
+
Inference::ObjectMixins.macro_named?(scope, owner, method_name)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Issue #2094 — the module `extend`s a module whose surface is open: one neither RBS nor the project declares
|
|
187
|
+
# (a gem's DSL with no signatures), or a project module {Inference::SingletonHookDecline} reads as
|
|
188
|
+
# hook-capable. Any macro the module's body calls may come from it and define any name, and its `extended`
|
|
189
|
+
# hook may too, so the module answers any name, the reading an undeclared `include` already gets. A chain cut
|
|
190
|
+
# at its budget may hold such a module past the cut.
|
|
191
|
+
def open_extend?(scope, owner)
|
|
192
|
+
chain = Scope::ResolutionChain.for(scope, owner, :singleton, :methods)
|
|
193
|
+
return true if chain.truncated?
|
|
194
|
+
|
|
195
|
+
entries = chain.level_count.zero? ? chain.entries : chain.level_entries(0)
|
|
196
|
+
entries.any? { |extended| Inference::SingletonHookDecline.entry_hook_capable?(scope, extended) }
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# An external the project declares (a module with no `def`, which the chain does not hold as a project entry)
|
|
200
|
+
# is known, and answers through RBS, or through what its own body's macros and `extend`s may define (issue
|
|
201
|
+
# #2094: {.macro_names?} and {.open_extend?}, asked of each candidate the project declares).
|
|
202
|
+
def external_may_answer?(scope, entry, method_name, open_mixins)
|
|
203
|
+
candidates = entry.candidates
|
|
204
|
+
return open_mixins if candidates.empty?
|
|
205
|
+
return true if candidates.any? do |candidate|
|
|
206
|
+
Rigor::Reflection.instance_method_definition(candidate, method_name, scope: scope)
|
|
207
|
+
end
|
|
208
|
+
return true if candidates.any? do |candidate|
|
|
209
|
+
macro_names?(scope, candidate, method_name) || (open_mixins && open_extend?(scope, candidate))
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
open_mixins && candidates.none? { |candidate| declared?(scope, candidate) }
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def declared?(scope, name)
|
|
216
|
+
scope.discovered_classes.key?(name) || Rigor::Reflection.rbs_class_known?(name, scope: scope)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def each_subclass(scope, class_name)
|
|
220
|
+
children = children_by_parent(scope)
|
|
221
|
+
queue = children.fetch(class_name, []).dup
|
|
222
|
+
seen = {}
|
|
223
|
+
record_subclasses_read(class_name)
|
|
224
|
+
until queue.empty?
|
|
225
|
+
subclass = queue.shift
|
|
226
|
+
next if seen[subclass]
|
|
227
|
+
|
|
228
|
+
seen[subclass] = true
|
|
229
|
+
record_subclasses_read(subclass)
|
|
230
|
+
yield subclass
|
|
231
|
+
queue.concat(children.fetch(subclass, []))
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def children_by_parent(scope)
|
|
236
|
+
scope.discovered_superclasses.each_with_object({}) do |(child, raw_parent), children|
|
|
237
|
+
next if raw_parent.nil?
|
|
238
|
+
|
|
239
|
+
candidates = scope.ancestor_name_candidates(child, raw_parent.to_s)
|
|
240
|
+
parent = candidates.find { |name| scope.known_user_class?(name) } || candidates.last
|
|
241
|
+
(children[parent] ||= []) << child if parent
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def record_subclasses_read(class_name)
|
|
246
|
+
return unless DependencyRecorder.active?
|
|
247
|
+
|
|
248
|
+
DependencyRecorder.read_last_segment(:subclass_of, class_name)
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|