rigortype 0.4.5 → 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 +1 -1
- data/docs/handbook/11-sig-gen.md +11 -2
- data/docs/manual/02-cli-reference.md +46 -7
- data/docs/manual/03-configuration.md +2 -1
- data/docs/manual/04-diagnostics.md +1 -1
- data/docs/manual/10-mcp-server.md +9 -1
- data/docs/manual/12-caching.md +8 -1
- 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 +2 -1
- data/lib/rigor/analysis/buffer_binding.rb +14 -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 +4 -0
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
- 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/unreachable_clause_collector.rb +51 -0
- data/lib/rigor/analysis/check_rules.rb +93 -8
- data/lib/rigor/analysis/dependency_recorder.rb +11 -0
- data/lib/rigor/analysis/edit_replay.rb +67 -0
- data/lib/rigor/analysis/incremental.rb +37 -0
- data/lib/rigor/analysis/incremental_session.rb +175 -27
- data/lib/rigor/analysis/path_expansion.rb +41 -0
- data/lib/rigor/analysis/require_relative_closure.rb +100 -0
- data/lib/rigor/analysis/rule_catalog.rb +5 -1
- 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 +17 -4
- 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 +156 -49
- data/lib/rigor/analysis/worker_session.rb +14 -2
- data/lib/rigor/cache/incremental_snapshot.rb +30 -7
- data/lib/rigor/cli/check_command.rb +93 -4
- data/lib/rigor/cli/diff_command.rb +2 -1
- 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/plugins_command.rb +5 -0
- 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 +46 -7
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/type_of_project_scope.rb +27 -0
- data/lib/rigor/config_audit.rb +16 -5
- data/lib/rigor/configuration.rb +14 -4
- data/lib/rigor/environment/rbs_loader.rb +115 -6
- 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/callee_mutation_chain.rb +115 -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 +17 -3
- data/lib/rigor/inference/error_info.rb +17 -3
- data/lib/rigor/inference/expression_typer.rb +62 -13
- data/lib/rigor/inference/global_write_census.rb +36 -1
- data/lib/rigor/inference/ivar_writer_census.rb +195 -0
- data/lib/rigor/inference/loop_value_record.rb +53 -0
- data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +110 -9
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
- data/lib/rigor/inference/method_dispatcher.rb +46 -16
- data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
- data/lib/rigor/inference/module_function_state.rb +12 -0
- data/lib/rigor/inference/mutation_widening.rb +5 -2
- data/lib/rigor/inference/narrowing.rb +66 -5
- data/lib/rigor/inference/object_mixins.rb +13 -1
- data/lib/rigor/inference/project_seed_tables.rb +2 -1
- data/lib/rigor/inference/project_yielder.rb +128 -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 +480 -103
- data/lib/rigor/inference/self_call_ivar_writes.rb +18 -6
- data/lib/rigor/inference/statement_evaluator.rb +658 -90
- 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/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/mcp/server.rb +9 -1
- data/lib/rigor/plugin/base.rb +26 -11
- data/lib/rigor/plugin/manifest.rb +29 -3
- data/lib/rigor/plugin/registry.rb +38 -0
- data/lib/rigor/protection/discovery_seed.rb +57 -2
- data/lib/rigor/rooted_glob.rb +10 -1
- data/lib/rigor/scope.rb +14 -5
- data/lib/rigor/sig_gen/generator.rb +308 -37
- data/lib/rigor/sig_gen/observation_collector.rb +4 -1
- data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +6 -3
- data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
- data/lib/rigor/type/combinator.rb +4 -3
- data/lib/rigor/type/constant.rb +45 -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-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +60 -43
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -2
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +6 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -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 +4 -4
- 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/scope.rbs +31 -50
- data/sig/rigor/type.rbs +0 -6
- metadata +20 -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
|
|
@@ -41,6 +42,9 @@ module Rigor
|
|
|
41
42
|
|
|
42
43
|
def refinements = (@refinements ||= Inference::InEffectRefinements.new(@root))
|
|
43
44
|
|
|
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
|
+
|
|
44
48
|
# Does a `def <local>.<name>` for this call's local receiver and method name — or a call that gives the
|
|
45
49
|
# local's object methods Rigor does not enumerate (issue #1722) — sit in the scope the call is in?
|
|
46
50
|
def singleton_local_def?(call_node)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "../../reflection"
|
|
4
4
|
require_relative "../dependency_recorder"
|
|
5
5
|
require_relative "../../inference/singleton_hook_decline"
|
|
6
|
+
require_relative "../../inference/object_mixins"
|
|
6
7
|
|
|
7
8
|
module Rigor
|
|
8
9
|
module Analysis
|
|
@@ -68,6 +69,43 @@ module Rigor
|
|
|
68
69
|
end
|
|
69
70
|
end
|
|
70
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
|
+
|
|
71
109
|
# Whether a project subclass of `class_name`, at any depth, defines `method_name` (through its own
|
|
72
110
|
# ancestry, which reaches its project mixins) or a `method_missing`. Its parent is read as written and
|
|
73
111
|
# resolved the way the class header resolves it: the first project class the spelling can denote, else the
|
|
@@ -132,19 +170,44 @@ module Rigor
|
|
|
132
170
|
if Rigor::Reflection.rbs_class_known?(owner, scope: scope)
|
|
133
171
|
scope.discovered_method?(owner, method_name, :instance)
|
|
134
172
|
else
|
|
135
|
-
project_defines?(scope, owner, method_name) ||
|
|
136
|
-
(open_mixins && Inference::SingletonHookDecline.entry_hook_capable?(scope, entry)
|
|
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)))
|
|
137
176
|
end
|
|
138
177
|
end
|
|
139
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
|
+
|
|
140
199
|
# An external the project declares (a module with no `def`, which the chain does not hold as a project entry)
|
|
141
|
-
# is known, and answers
|
|
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).
|
|
142
202
|
def external_may_answer?(scope, entry, method_name, open_mixins)
|
|
143
203
|
candidates = entry.candidates
|
|
144
204
|
return open_mixins if candidates.empty?
|
|
145
205
|
return true if candidates.any? do |candidate|
|
|
146
206
|
Rigor::Reflection.instance_method_definition(candidate, method_name, scope: scope)
|
|
147
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
|
|
148
211
|
|
|
149
212
|
open_mixins && candidates.none? { |candidate| declared?(scope, candidate) }
|
|
150
213
|
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Analysis
|
|
7
|
+
module CheckRules
|
|
8
|
+
module RebindingBlockCallees
|
|
9
|
+
# Which method a block call reaches, and on what receiver: `callee_may_rebind?` reads the callee name, a
|
|
10
|
+
# callable receiver, and whether Ruby can reach `method_missing` from here.
|
|
11
|
+
module Dispatch
|
|
12
|
+
# Issue #2186 — a block given to `call` / `bind_call` (or `yield` / `===` / `[]`) on a literal `method(:x)`
|
|
13
|
+
# chain goes to the method it names: `Ext.method(:me).call { … }` is `Ext.me { … }`. On a receiver typed as
|
|
14
|
+
# one of CALLABLE_CLASSES it goes to a callable Rigor cannot follow; on any other receiver it is a call by
|
|
15
|
+
# that name.
|
|
16
|
+
METHOD_OBJECT_CALLS = %i[call yield === [] bind_call].freeze
|
|
17
|
+
METHOD_READERS = %i[method public_method singleton_method instance_method public_instance_method].freeze
|
|
18
|
+
# The readers whose receiver is the object the named method runs on.
|
|
19
|
+
BOUND_METHOD_READERS = %i[method public_method singleton_method].freeze
|
|
20
|
+
METHOD_WRAPPERS = %i[bind unbind to_proc curry].freeze
|
|
21
|
+
CALLABLE_CLASSES = %w[Method UnboundMethod Proc].freeze
|
|
22
|
+
private_constant :METHOD_READERS, :BOUND_METHOD_READERS, :METHOD_WRAPPERS, :CALLABLE_CLASSES
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# `[name, receiver]` of the method the block reaches: the call's own name, the literal name a `send` family
|
|
27
|
+
# call dispatches to, or the method a literal `method(:name)` chain names. The receiver is a node, `:implicit`
|
|
28
|
+
# for a receiverless call, or nil where Rigor cannot say (an `instance_method` chain). The name is nil when a
|
|
29
|
+
# `send` names its method with anything but a valid literal.
|
|
30
|
+
def dispatch(call)
|
|
31
|
+
reader = method_reader(call.receiver) if METHOD_OBJECT_CALLS.include?(call.name)
|
|
32
|
+
if reader
|
|
33
|
+
receiver = reader.receiver || :implicit if BOUND_METHOD_READERS.include?(reader.name)
|
|
34
|
+
return [RebindingBlockCallees.literal_name(reader.arguments&.arguments&.first), receiver]
|
|
35
|
+
end
|
|
36
|
+
[RebindingBlockCallees.send_target(call), call.receiver || :implicit]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def method_reader(receiver)
|
|
40
|
+
receiver = receiver.receiver while wrapper?(receiver)
|
|
41
|
+
receiver if receiver.is_a?(Prism::CallNode) && METHOD_READERS.include?(receiver.name)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Whether `receiver` may be a Method, UnboundMethod or Proc, whose `call` runs a callable Rigor cannot follow.
|
|
45
|
+
def callable?(receiver, scope, scope_index)
|
|
46
|
+
return false unless receiver.is_a?(Prism::Node)
|
|
47
|
+
|
|
48
|
+
type = receiver_type(receiver, scope, scope_index)
|
|
49
|
+
members = type.is_a?(Type::Union) ? type.members : [type]
|
|
50
|
+
members.any? do |member|
|
|
51
|
+
member.is_a?(Type::BoundMethod) ||
|
|
52
|
+
(member.is_a?(Type::Nominal) && CALLABLE_CLASSES.include?(member.class_name.to_s))
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def wrapper?(node) = node.is_a?(Prism::CallNode) && METHOD_WRAPPERS.include?(node.name)
|
|
57
|
+
|
|
58
|
+
# A `call` (or the like) on a receiver that may be a Method, UnboundMethod or Proc.
|
|
59
|
+
def callable_call?(name, receiver, scope, scope_index)
|
|
60
|
+
METHOD_OBJECT_CALLS.include?(name) && callable?(receiver, scope, scope_index)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def receiver_type(receiver, scope, scope_index)
|
|
64
|
+
(scope_index&.[](receiver) || scope).type_of(receiver)
|
|
65
|
+
rescue StandardError
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Whether the receiver's type names a class whose RBS or project table gives it `name`, on every member of a
|
|
70
|
+
# union. A receiverless call at the top level asks `Object`, `main`'s class. Any other type cannot say.
|
|
71
|
+
def resolves_on_receiver?(receiver, name, scope, scope_index)
|
|
72
|
+
return scope.toplevel? && class_defines?("Object", :instance, name, scope) if receiver == :implicit
|
|
73
|
+
return false if receiver.nil?
|
|
74
|
+
|
|
75
|
+
type = receiver_type(receiver, scope, scope_index)
|
|
76
|
+
!type.nil? && type_resolves?(type, name, scope)
|
|
77
|
+
rescue StandardError
|
|
78
|
+
false
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def type_resolves?(type, name, scope)
|
|
82
|
+
case type
|
|
83
|
+
when Type::Union then type.members.all? { |member| type_resolves?(member, name, scope) }
|
|
84
|
+
when Type::Singleton then class_defines?(type.class_name, :singleton, name, scope)
|
|
85
|
+
else
|
|
86
|
+
class_name = receiver_class_name(type)
|
|
87
|
+
!class_name.nil? && class_defines?(class_name, :instance, name, scope)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def receiver_class_name(type)
|
|
92
|
+
case type
|
|
93
|
+
when Type::Nominal, Type::StructInstance, Type::DataInstance then type.class_name&.to_s
|
|
94
|
+
when Type::Tuple then "Array"
|
|
95
|
+
when Type::HashShape then "Hash"
|
|
96
|
+
when Type::Constant then type.value.class.name
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def class_defines?(class_name, side, name, scope)
|
|
101
|
+
rbs = if side == :singleton
|
|
102
|
+
Rigor::Reflection.singleton_method_definition(class_name, name, scope: scope)
|
|
103
|
+
else
|
|
104
|
+
Rigor::Reflection.instance_method_definition(class_name, name, scope: scope)
|
|
105
|
+
end
|
|
106
|
+
!rbs.nil? || scope.discovered_method?(class_name, name, side)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|