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
|
@@ -5,6 +5,7 @@ require "prism"
|
|
|
5
5
|
require_relative "../../source/node_children"
|
|
6
6
|
require_relative "../../inference/narrowing"
|
|
7
7
|
require_relative "inferred_param_guard"
|
|
8
|
+
require_relative "clause_coverage"
|
|
8
9
|
|
|
9
10
|
module Rigor
|
|
10
11
|
module Analysis
|
|
@@ -36,6 +37,10 @@ module Rigor
|
|
|
36
37
|
# is code evidence that the subject can hold such an object (`io = STDOUT; case io when StringIO`), and Rigor's
|
|
37
38
|
# reading of the subject's type is what the evidence contradicts. The body scope stays `Bot`, so no call on the
|
|
38
39
|
# subject in it is checked. A subject that is a literal carrier (`x = 1; case x when String`) is still reported.
|
|
40
|
+
#
|
|
41
|
+
# Issue #1485 — the same holds for a `:prior_exhaustion` clause: on a subject that is not a literal carrier it is
|
|
42
|
+
# reported only when every class it names is at or below a class an earlier clause names ({ClauseCoverage}), so
|
|
43
|
+
# `io = STDOUT; case io when IO … when StringIO` is quiet and `when IO … when File` still reports.
|
|
39
44
|
class UnreachableClauseCollector
|
|
40
45
|
LOOP_OR_BLOCK_NODE_CLASSES = [
|
|
41
46
|
Prism::WhileNode, Prism::UntilNode, Prism::ForNode, Prism::BlockNode
|
|
@@ -48,6 +53,24 @@ module Rigor
|
|
|
48
53
|
# `else`-only.
|
|
49
54
|
DEFENSIVE_EXIT_CALLS = %i[raise fail throw abort exit].freeze
|
|
50
55
|
|
|
56
|
+
# Issue #2071 — core classes whose idiomatic stand-ins are not their subclasses: a `StringIO` answers the
|
|
57
|
+
# IO protocol without being an `IO`, and a `Tempfile` delegates to a `File` without being one (or an `IO`).
|
|
58
|
+
# A subject Rigor types as one of these classes (`$stdout` by idiom, ADR-117) may hold such a stand-in,
|
|
59
|
+
# which no `when IO` / `in File` matches, so the `else` still runs for it.
|
|
60
|
+
DUCK_STAND_INS = {
|
|
61
|
+
"IO" => %w[StringIO Tempfile].freeze,
|
|
62
|
+
"File" => %w[Tempfile].freeze
|
|
63
|
+
}.freeze
|
|
64
|
+
|
|
65
|
+
# Each stand-in's ancestors as Ruby 4.0.5 has them (`StringIO.ancestors`, `Tempfile.ancestors` without the
|
|
66
|
+
# anonymous `DelegateClass` entries): a clause naming one matches the stand-in. Written out rather than read
|
|
67
|
+
# from RBS, which declares `class Tempfile < File`; a `Tempfile` is a `Delegator`, so neither `Object` nor
|
|
68
|
+
# `Kernel` matches it.
|
|
69
|
+
STAND_IN_ANCESTORS = {
|
|
70
|
+
"StringIO" => %w[StringIO Enumerable Object Kernel BasicObject].freeze,
|
|
71
|
+
"Tempfile" => %w[Tempfile Delegator BasicObject].freeze
|
|
72
|
+
}.freeze
|
|
73
|
+
|
|
51
74
|
# @!attribute kind
|
|
52
75
|
# @return `:disjoint` (this clause's type is disjoint
|
|
53
76
|
# from the still-possible subject), `:prior_exhaustion` (an
|
|
@@ -124,21 +147,56 @@ module Rigor
|
|
|
124
147
|
return if entry_type.nil? || entry_type.is_a?(Type::Bot) || entry_type.is_a?(Type::Dynamic)
|
|
125
148
|
|
|
126
149
|
keyword = node.is_a?(Prism::CaseMatchNode) ? "in" : "when"
|
|
150
|
+
coverage = ClauseCoverage.new(scope, entry_type)
|
|
127
151
|
node.conditions.each do |clause|
|
|
128
152
|
case clause
|
|
129
|
-
when Prism::WhenNode then collect_when(clause, subject.name)
|
|
130
|
-
when Prism::InNode then collect_in(clause, subject.name)
|
|
153
|
+
when Prism::WhenNode then collect_when(clause, subject.name, coverage)
|
|
154
|
+
when Prism::InNode then collect_in(clause, subject.name, coverage)
|
|
131
155
|
end
|
|
156
|
+
coverage.record(clause)
|
|
132
157
|
end
|
|
158
|
+
return if stand_in_reaches_else?(node, entry_type)
|
|
159
|
+
|
|
133
160
|
collect_else(node.else_clause, subject.name, keyword)
|
|
134
161
|
end
|
|
135
162
|
|
|
163
|
+
# Issue #2071 — whether the subject, as the `case` binds it, has a `Nominal` member of a {DUCK_STAND_INS}
|
|
164
|
+
# class with a stand-in no clause names an ancestor of ({STAND_IN_ANCESTORS}). `io = STDOUT; case io when IO
|
|
165
|
+
# then … else … end` runs the `else` for a `StringIO`, which the type `IO` stands for in idiomatic Ruby; a
|
|
166
|
+
# literal subject is a `Constant`, not a `Nominal`, and keeps its verdict. Asked of the `else` only: a dead
|
|
167
|
+
# `when` has its own evidence rules (#1429, #1485).
|
|
168
|
+
def stand_in_reaches_else?(node, entry_type)
|
|
169
|
+
members = entry_type.is_a?(Type::Union) ? entry_type.members : [entry_type]
|
|
170
|
+
stand_ins = members.flat_map do |member|
|
|
171
|
+
member.is_a?(Type::Nominal) ? DUCK_STAND_INS.fetch(member.class_name.delete_prefix("::"), []) : []
|
|
172
|
+
end
|
|
173
|
+
return false if stand_ins.empty?
|
|
174
|
+
|
|
175
|
+
named = clause_class_names(node)
|
|
176
|
+
stand_ins.any? { |stand_in| named.none? { |name| STAND_IN_ANCESTORS.fetch(stand_in).include?(name) } }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# The classes the clauses name as bare constants (`when A, B`, `in A`, `in A => x`).
|
|
180
|
+
def clause_class_names(node)
|
|
181
|
+
node.conditions.flat_map do |clause|
|
|
182
|
+
patterns = clause.is_a?(Prism::WhenNode) ? clause.conditions : [bare_pattern(clause.pattern)]
|
|
183
|
+
patterns.filter_map do |pattern|
|
|
184
|
+
entry = @scope_index[pattern]
|
|
185
|
+
entry && Inference::Narrowing.lexical_class_name(pattern, entry)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def bare_pattern(pattern)
|
|
191
|
+
pattern.is_a?(Prism::CapturePatternNode) ? pattern.value : pattern
|
|
192
|
+
end
|
|
193
|
+
|
|
136
194
|
def entry_subject_type(case_node, subject_name)
|
|
137
195
|
scope = @scope_index[case_node]
|
|
138
196
|
scope&.local(subject_name)
|
|
139
197
|
end
|
|
140
198
|
|
|
141
|
-
def collect_when(clause, subject_name)
|
|
199
|
+
def collect_when(clause, subject_name, coverage)
|
|
142
200
|
return if clause.statements.nil? # empty body → no useful location
|
|
143
201
|
return unless all_constant_conditions?(clause)
|
|
144
202
|
|
|
@@ -149,6 +207,7 @@ module Rigor
|
|
|
149
207
|
kind = when_clause_kind(clause, subject_name)
|
|
150
208
|
return if kind == :disjoint &&
|
|
151
209
|
disjoint_nominal_subject?(@scope_index[clause.conditions.first], subject_name, clause.conditions)
|
|
210
|
+
return if kind == :prior_exhaustion && !coverage.proven?(clause)
|
|
152
211
|
|
|
153
212
|
@results << Result.new(
|
|
154
213
|
clause: clause, body: clause.statements, subject_name: subject_name,
|
|
@@ -187,7 +246,7 @@ module Rigor
|
|
|
187
246
|
# (prior-exhaustion — true for ANY pattern downstream of a covering set). Non-bare patterns are not
|
|
188
247
|
# narrowed, so their body is `bot` ONLY under prior-exhaustion; they never produce a spurious
|
|
189
248
|
# `:disjoint`.
|
|
190
|
-
def collect_in(clause, subject_name)
|
|
249
|
+
def collect_in(clause, subject_name, coverage)
|
|
191
250
|
return if clause.statements.nil?
|
|
192
251
|
|
|
193
252
|
scope = @scope_index[clause]
|
|
@@ -196,6 +255,7 @@ module Rigor
|
|
|
196
255
|
|
|
197
256
|
kind = in_clause_kind(clause, subject_name)
|
|
198
257
|
return if kind == :disjoint && disjoint_nominal_pattern?(clause, subject_name)
|
|
258
|
+
return if kind == :prior_exhaustion && !coverage.proven?(clause)
|
|
199
259
|
|
|
200
260
|
@results << Result.new(
|
|
201
261
|
clause: clause, body: clause.statements, subject_name: subject_name,
|
|
@@ -29,6 +29,8 @@ require_relative "check_rules/void_value_use_collector"
|
|
|
29
29
|
require_relative "check_rules/self_closedness_scanner"
|
|
30
30
|
require_relative "check_rules/source_arity"
|
|
31
31
|
require_relative "check_rules/lexical_method_sites"
|
|
32
|
+
require_relative "check_rules/rebinding_block_callees"
|
|
33
|
+
require_relative "check_rules/rbs_receiver_surface"
|
|
32
34
|
require_relative "check_rules/special_global_setters"
|
|
33
35
|
require_relative "check_rules/ruby_deprecations"
|
|
34
36
|
|
|
@@ -449,11 +451,31 @@ module Rigor
|
|
|
449
451
|
other_class = ivar_class_for(write[:type])
|
|
450
452
|
next nil if other_class.nil? || other_class == "NilClass" || other_class == first_class
|
|
451
453
|
next nil if declaration_admits?(write[:declared], canonical[:type], write[:type])
|
|
454
|
+
next nil if separate_invocations?(write, other_class, writes)
|
|
452
455
|
|
|
453
456
|
build_ivar_write_mismatch_diagnostic(path, write[:node], class_name, ivar_name, first_class, other_class)
|
|
454
457
|
end
|
|
455
458
|
end
|
|
456
459
|
|
|
460
|
+
# ADR-125 — a divergent write is never live together with the writes it conflicts with when the class
|
|
461
|
+
# serves one entry point per instance (a plugin-declared per-invocation class, e.g. a Rails controller:
|
|
462
|
+
# one action per request) and every conflicting write — non-nil, of a concrete class other than this
|
|
463
|
+
# write's — sits in a DIFFERENT action of the same class. An action is a public method no declared
|
|
464
|
+
# non-action macro names (`before_action :x`, `helper_method :y`); {IvarWriteCollector} decides it. A
|
|
465
|
+
# conflicting write in a private or protected helper, a callback target, a `helper_method`,
|
|
466
|
+
# `initialize`, or the same method keeps the finding. Which helper each action reaches is not proved
|
|
467
|
+
# here.
|
|
468
|
+
def separate_invocations?(write, write_class, writes)
|
|
469
|
+
return false unless write[:action]
|
|
470
|
+
|
|
471
|
+
writes.all? do |other|
|
|
472
|
+
other_class = ivar_class_for(other[:type])
|
|
473
|
+
next true if other_class.nil? || other_class == "NilClass" || other_class == write_class
|
|
474
|
+
|
|
475
|
+
other[:action] && other[:method_name] != write[:method_name]
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
|
|
457
479
|
def declaration_admits?(declared, *write_types)
|
|
458
480
|
return false if declared.nil?
|
|
459
481
|
|
|
@@ -801,7 +823,7 @@ module Rigor
|
|
|
801
823
|
# rule must not surface a false `undefined-method`
|
|
802
824
|
# for them.
|
|
803
825
|
kind = receiver_type.is_a?(Type::Singleton) ? :singleton : :instance
|
|
804
|
-
return nil if source_declared_method?(scope, class_name, call_node.name, kind)
|
|
826
|
+
return nil if source_declared_method?(scope, class_name, call_node.name, kind, call_node: call_node)
|
|
805
827
|
|
|
806
828
|
return nil unless Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
|
|
807
829
|
|
|
@@ -817,6 +839,8 @@ module Rigor
|
|
|
817
839
|
return nil if method_def
|
|
818
840
|
|
|
819
841
|
return nil if last_resort_surface_answers?(receiver_type, class_name, call_node, scope, kind, lexical_sites)
|
|
842
|
+
return nil if subclass_instance_may_answer?(rendered_receiver, class_name, call_node.name, scope,
|
|
843
|
+
call_node.receiver)
|
|
820
844
|
|
|
821
845
|
definition_site = project_definition_site(scope, class_name, call_node.name, kind)
|
|
822
846
|
# Issue #735 — the site is evidence, and whose RBS this is decides what it is evidence OF. On a
|
|
@@ -951,12 +975,52 @@ module Rigor
|
|
|
951
975
|
# (see {#ancestry_declares_method?} for why that placement is load-bearing, not tidiness).
|
|
952
976
|
def last_resort_surface_answers?(receiver_type, class_name, call_node, scope, kind, lexical_sites = nil)
|
|
953
977
|
return true if lexically_defined_method?(class_name, call_node, scope, kind, lexical_sites)
|
|
978
|
+
return true if project_patched_through_ancestors?(scope, class_name, call_node.name, kind,
|
|
979
|
+
call_node: call_node)
|
|
954
980
|
return true if module_mixin_receiver?(receiver_type, scope)
|
|
955
981
|
return true if mixin_self_class_receiver?(call_node, scope)
|
|
956
982
|
return true if unknown_mixin_includer?(class_name, scope)
|
|
957
983
|
return true if object_mixin_may_define?(receiver_type, class_name, call_node.name, scope)
|
|
984
|
+
return true if ancestry_declares_method?(scope, class_name, call_node.name, kind)
|
|
985
|
+
|
|
986
|
+
rbs_ancestor_may_answer?(class_name, call_node.name, scope, kind)
|
|
987
|
+
end
|
|
988
|
+
|
|
989
|
+
# Issue #1722 — a `method_missing`, or a project mixin, on one of the class's RBS ancestors
|
|
990
|
+
# ({RbsReceiverSurface}). Last for {#ancestry_declares_method?}'s reason: it walks the class graph.
|
|
991
|
+
def rbs_ancestor_may_answer?(class_name, method_name, scope, kind)
|
|
992
|
+
if kind == :singleton
|
|
993
|
+
RbsReceiverSurface.singleton_hook?(scope, class_name) ||
|
|
994
|
+
RbsReceiverSurface.singleton_mixin_may_answer?(scope, class_name, method_name)
|
|
995
|
+
else
|
|
996
|
+
RbsReceiverSurface.ancestor_may_answer?(scope, class_name, method_name)
|
|
997
|
+
end
|
|
998
|
+
end
|
|
958
999
|
|
|
959
|
-
|
|
1000
|
+
# Issue #1722 — a receiver typed by its class's name, rather than by a literal, may hold an instance of a
|
|
1001
|
+
# subclass (a `#: () -> String` signature admits a `MyStr < String`), so a method a project subclass
|
|
1002
|
+
# defines, or a subclass's `method_missing`, may be the one that runs. A value the engine built from a
|
|
1003
|
+
# literal is exactly its class and keeps firing: a literal type (`"a"`, `1`, a union of them), a shape
|
|
1004
|
+
# carrier (`{a: 1}`, `[1, 2]`, `{}`, `[]`, and what folds to one), or a receiver written as a literal
|
|
1005
|
+
# (`"a#{x}b"`, which types as plain `String`). A local holding an interpolated String types as plain
|
|
1006
|
+
# `String` with no origin, so it still declines. Instance side only.
|
|
1007
|
+
def subclass_instance_may_answer?(receiver_type, class_name, method_name, scope, receiver_node = nil)
|
|
1008
|
+
return false if receiver_type.is_a?(Type::Singleton)
|
|
1009
|
+
return false if exact_literal_receiver?(receiver_type) || LITERAL_RECEIVER_NODES.include?(receiver_node.class)
|
|
1010
|
+
|
|
1011
|
+
RbsReceiverSurface.subclass_may_define?(scope, class_name, method_name)
|
|
1012
|
+
end
|
|
1013
|
+
|
|
1014
|
+
EXACT_TYPES = [Type::Constant, Type::Tuple, Type::HashShape].freeze
|
|
1015
|
+
LITERAL_RECEIVER_NODES = [
|
|
1016
|
+
Prism::StringNode, Prism::InterpolatedStringNode, Prism::XStringNode, Prism::InterpolatedXStringNode,
|
|
1017
|
+
Prism::ArrayNode, Prism::HashNode, Prism::RegularExpressionNode, Prism::InterpolatedRegularExpressionNode
|
|
1018
|
+
].to_set.freeze
|
|
1019
|
+
private_constant :EXACT_TYPES, :LITERAL_RECEIVER_NODES
|
|
1020
|
+
|
|
1021
|
+
def exact_literal_receiver?(receiver_type)
|
|
1022
|
+
members = receiver_type.is_a?(Type::Union) ? receiver_type.members : [receiver_type]
|
|
1023
|
+
members.all? { |member| EXACT_TYPES.any? { |kind| member.is_a?(kind) } }
|
|
960
1024
|
end
|
|
961
1025
|
|
|
962
1026
|
# Issue #1697 — a module the project mixes into `Object` (a top-level `include M`, or `include M` in
|
|
@@ -1058,10 +1122,13 @@ module Rigor
|
|
|
1058
1122
|
# Both paths sit at the same dispatcher precedence; the
|
|
1059
1123
|
# check must hold them together so neither rule fires a
|
|
1060
1124
|
# false positive.
|
|
1061
|
-
|
|
1125
|
+
#
|
|
1126
|
+
# `call_node:` (issue #1708), when given, is the call asking: a private or protected `pre_eval:` patch
|
|
1127
|
+
# it may not reach does not count ({#patch_callable?}).
|
|
1128
|
+
def source_declared_method?(scope, class_name, method_name, kind, call_node: nil)
|
|
1062
1129
|
return true if scope.discovered_method?(class_name, method_name, kind)
|
|
1063
1130
|
|
|
1064
|
-
project_patched_method?(scope, class_name, method_name, kind)
|
|
1131
|
+
project_patched_method?(scope, class_name, method_name, kind, call_node: call_node)
|
|
1065
1132
|
end
|
|
1066
1133
|
|
|
1067
1134
|
# Issue #723 — the same question as {#source_declared_method?}, asked through the project's own
|
|
@@ -1087,12 +1154,42 @@ module Rigor
|
|
|
1087
1154
|
# inference (between plugins and dependency-source).
|
|
1088
1155
|
# Returns false when the environment carries no registry
|
|
1089
1156
|
# (legacy path) or the lookup misses.
|
|
1090
|
-
def project_patched_method?(scope, class_name, method_name, kind)
|
|
1157
|
+
def project_patched_method?(scope, class_name, method_name, kind, call_node: nil)
|
|
1158
|
+
environment = scope.environment
|
|
1159
|
+
registry = environment&.project_patched_methods
|
|
1160
|
+
return false if registry.nil? || registry.empty?
|
|
1161
|
+
|
|
1162
|
+
entry = registry.lookup(class_name: class_name.to_s, method_name: method_name.to_sym, kind: kind)
|
|
1163
|
+
!entry.nil? && patch_callable?(registry, entry, scope, call_node)
|
|
1164
|
+
end
|
|
1165
|
+
|
|
1166
|
+
# Issue #1708 — {#project_patched_method?} asked through the receiver's ancestors: a `pre_eval:` patch on
|
|
1167
|
+
# `Object`, `Numeric` or `Enumerable` gives an `Integer` or `Array` the method. Asked only once the
|
|
1168
|
+
# receiver's RBS has come back without the method, so a nearer class's own declaration still decides arity
|
|
1169
|
+
# and argument types; {#source_declared_method?} stays exact for the rules that read it for those.
|
|
1170
|
+
#
|
|
1171
|
+
# `call_node:` as {#source_declared_method?} reads it.
|
|
1172
|
+
def project_patched_through_ancestors?(scope, class_name, method_name, kind, call_node: nil)
|
|
1091
1173
|
environment = scope.environment
|
|
1092
1174
|
registry = environment&.project_patched_methods
|
|
1093
1175
|
return false if registry.nil? || registry.empty?
|
|
1094
1176
|
|
|
1095
|
-
|
|
1177
|
+
entry = registry.lookup_through_ancestors(class_name: class_name.to_s, method_name: method_name.to_sym,
|
|
1178
|
+
kind: kind, rbs_loader: environment.rbs_loader)
|
|
1179
|
+
!entry.nil? && patch_callable?(registry, entry, scope, call_node)
|
|
1180
|
+
end
|
|
1181
|
+
|
|
1182
|
+
# Issue #1708 — whether `call_node` may reach the patch `entry`: Ruby raises NoMethodError for a private
|
|
1183
|
+
# patch called with an explicit receiver other than `self` (`1.kp` on `module Kernel; private def kp`), and
|
|
1184
|
+
# for a protected one called from outside its owner's instances. No call node, and a declined verdict (another
|
|
1185
|
+
# definer the call may reach, {Inference::ProjectPatchedMethods#callable?}), are the permissive answer.
|
|
1186
|
+
def patch_callable?(registry, entry, scope, call_node)
|
|
1187
|
+
return true if call_node.nil?
|
|
1188
|
+
|
|
1189
|
+
receiver = call_node.receiver
|
|
1190
|
+
verdict = registry.callable?(entry, explicit: !receiver.nil? && !receiver.is_a?(Prism::SelfNode),
|
|
1191
|
+
self_type: scope.self_type, rbs_loader: scope.environment.rbs_loader)
|
|
1192
|
+
verdict != false
|
|
1096
1193
|
end
|
|
1097
1194
|
|
|
1098
1195
|
# ADR-34 — `call.unresolved-toplevel`. Fires on an
|
|
@@ -1141,6 +1238,8 @@ module Rigor
|
|
|
1141
1238
|
return nil if scope.top_level_def_for(name)
|
|
1142
1239
|
return nil if source_declared_method?(scope, "Object", name, :instance)
|
|
1143
1240
|
return nil if Rigor::Reflection.instance_method_definition("Object", name, scope: scope)
|
|
1241
|
+
# Issue #1708 — `main` is an Object, so a `pre_eval:` patch on `Kernel` or `BasicObject` defines the name too.
|
|
1242
|
+
return nil if project_patched_through_ancestors?(scope, "Object", name, :instance)
|
|
1144
1243
|
return nil if main_singleton_method?(name, scope)
|
|
1145
1244
|
return nil if Inference::ObjectMixins.may_define?(scope, name, receiver: :implicit)
|
|
1146
1245
|
# `Target.class_eval { def added = 1; def use_added = added }` files the defs on Target
|
|
@@ -1148,12 +1247,22 @@ module Rigor
|
|
|
1148
1247
|
# body is morally a class body, so ADR-34 stays silent there — including on a genuinely
|
|
1149
1248
|
# undefined name inside the block. Ranges are computed once per file.
|
|
1150
1249
|
return nil if call_inside_receiver_eval_ranges?(eval_ranges, call_node)
|
|
1151
|
-
|
|
1152
|
-
return nil if refined_method_in_effect?("Object", call_node, scope, :instance, lexical_sites)
|
|
1250
|
+
return nil if lexically_declined_toplevel_call?(call_node, scope, lexical_sites, scope_index)
|
|
1153
1251
|
|
|
1154
1252
|
build_unresolved_toplevel_diagnostic(path, call_node)
|
|
1155
1253
|
end
|
|
1156
1254
|
|
|
1255
|
+
# The `call.unresolved-toplevel` declines that read the file's syntax, asked last because they walk it.
|
|
1256
|
+
def lexically_declined_toplevel_call?(call_node, scope, lexical_sites, scope_index = nil)
|
|
1257
|
+
# ADR-121 WD7 — a refinement of `Object` (or one Rigor cannot read) in effect here may define the name.
|
|
1258
|
+
return true if refined_method_in_effect?("Object", call_node, scope, :instance, lexical_sites)
|
|
1259
|
+
return false if lexical_sites.nil?
|
|
1260
|
+
|
|
1261
|
+
# Issue #1817 — a block handed to a project method that may `module_eval` / `instance_exec` it runs under
|
|
1262
|
+
# another `self`, so a call in it is not a top-level one ({RebindingBlockCallees}).
|
|
1263
|
+
RebindingBlockCallees.inside_rebound_block?(call_node, scope, lexical_sites.block_calls, scope_index)
|
|
1264
|
+
end
|
|
1265
|
+
|
|
1157
1266
|
# Issue #1383 — RBS core declares `main`'s private singleton methods on
|
|
1158
1267
|
# `RBS::Unnamed::TopLevelSelfClass`, so the set follows RBS rather than a hand-kept
|
|
1159
1268
|
# list. CRuby 4.0's `main` also has `ruby2_keywords`, which RBS does not declare.
|
|
@@ -1410,12 +1519,27 @@ module Rigor
|
|
|
1410
1519
|
# overlay open receiver ({GEM_OVERLAY_OPEN_RECEIVERS}) whose
|
|
1411
1520
|
# bundled directory this run actually loaded, a core-overlay
|
|
1412
1521
|
# open receiver ({CORE_OVERLAY_OPEN_RECEIVERS}), or a
|
|
1413
|
-
# Rigor-synthesized stub type (see `RbsLoader#synthesized_type_names`)
|
|
1522
|
+
# Rigor-synthesized stub type (see `RbsLoader#synthesized_type_names`),
|
|
1523
|
+
# or a project-only reopen of a Ruby library class whose own RBS
|
|
1524
|
+
# did not load ({#ruby_library_reopen?}).
|
|
1414
1525
|
def unbounded_receiver_surface?(class_name, scope)
|
|
1415
1526
|
bare_name = class_name.to_s.sub(/\A::/, "")
|
|
1416
1527
|
open_receiver?(class_name, scope) || synthesized_stub_receiver?(class_name, scope) ||
|
|
1417
1528
|
CORE_OVERLAY_OPEN_RECEIVERS.include?(bare_name) ||
|
|
1418
|
-
(GEM_OVERLAY_OPEN_RECEIVERS.include?(bare_name) && gem_overlay_loaded?(scope))
|
|
1529
|
+
(GEM_OVERLAY_OPEN_RECEIVERS.include?(bare_name) && gem_overlay_loaded?(scope)) ||
|
|
1530
|
+
ruby_library_reopen?(class_name, scope)
|
|
1531
|
+
end
|
|
1532
|
+
|
|
1533
|
+
# Issue #2174 — `class Matrix` reopened with one `#:`-annotated method, and no `matrix` signatures
|
|
1534
|
+
# loaded, gives Matrix a declaration holding only that method ({RbsLoader#ruby_library_reopen?}).
|
|
1535
|
+
# `Matrix.identity(2)` is correct Ruby, so the class's surface is treated as open: `call.undefined-method`
|
|
1536
|
+
# declines on it, and a signature it only inherits (`Object#initialize` for `Matrix.new(rows)`) stops
|
|
1537
|
+
# deciding arity and argument types. Methods the project's own declaration lists stay checked.
|
|
1538
|
+
def ruby_library_reopen?(class_name, scope)
|
|
1539
|
+
loader = scope.environment&.rbs_loader
|
|
1540
|
+
return false if loader.nil? || !loader.respond_to?(:ruby_library_reopen?)
|
|
1541
|
+
|
|
1542
|
+
loader.ruby_library_reopen?(class_name)
|
|
1419
1543
|
end
|
|
1420
1544
|
|
|
1421
1545
|
def open_receiver?(class_name, scope)
|
|
@@ -2143,6 +2267,7 @@ module Rigor
|
|
|
2143
2267
|
return true unless Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
|
|
2144
2268
|
return true unless definition_available?(member, class_name, scope)
|
|
2145
2269
|
return true if lookup_method(member, class_name, method_name, scope)
|
|
2270
|
+
return true if project_patched_through_ancestors?(scope, class_name, method_name, :instance)
|
|
2146
2271
|
|
|
2147
2272
|
# Issue #723 — the arm's own RBS does not have it; its project ancestry still might. Last, for
|
|
2148
2273
|
# the reason {#ancestry_declares_method?} carries: this is the probe that walks the class graph.
|
|
@@ -2196,12 +2321,30 @@ module Rigor
|
|
|
2196
2321
|
# `compose_codepoints` typed `Hash | Hash` for an `Array`, flagging
|
|
2197
2322
|
# `.pack`). Require at least two distinct arm classes.
|
|
2198
2323
|
return nil if members.map { |member| concrete_class_name(member) }.uniq.size < 2
|
|
2199
|
-
return nil if
|
|
2200
|
-
return nil if refined_on_some_arm?(members, call_node, scope, lexical_sites)
|
|
2324
|
+
return nil if union_may_answer?(members, call_node, scope, lexical_sites)
|
|
2201
2325
|
|
|
2202
2326
|
build_undefined_method_diagnostic(path, call_node, receiver_type)
|
|
2203
2327
|
end
|
|
2204
2328
|
|
|
2329
|
+
# Whether some arm of a non-nil union of classes may answer the call, which keeps the union rule silent.
|
|
2330
|
+
def union_may_answer?(members, call_node, scope, lexical_sites)
|
|
2331
|
+
return true if members.any? { |member| method_present_anywhere?(member, call_node.name, scope) }
|
|
2332
|
+
return true if members.any? { |member| arm_surface_may_answer?(member, call_node.name, scope) }
|
|
2333
|
+
# Issue #1722 — `def o.m`, `o.extend(M)` and their kin give the one object the method, whatever its class.
|
|
2334
|
+
return true if lexical_sites&.singleton_local_def?(call_node)
|
|
2335
|
+
|
|
2336
|
+
refined_on_some_arm?(members, call_node, scope, lexical_sites)
|
|
2337
|
+
end
|
|
2338
|
+
|
|
2339
|
+
# Issue #1722 — the scalar rule's RBS-ancestor and subclass declines, asked of one arm. Kept out of
|
|
2340
|
+
# {#method_present_anywhere?}, which `call.possible-nil-receiver` also asks: there a "present" answer is
|
|
2341
|
+
# what lets the rule fire, and a `method_missing` on `Object` answers on the `nil` arm too.
|
|
2342
|
+
def arm_surface_may_answer?(member, method_name, scope)
|
|
2343
|
+
class_name = concrete_class_name(member)
|
|
2344
|
+
rbs_ancestor_may_answer?(class_name, method_name, scope, :instance) ||
|
|
2345
|
+
subclass_instance_may_answer?(member, class_name, method_name, scope)
|
|
2346
|
+
end
|
|
2347
|
+
|
|
2205
2348
|
# ADR-121 WD7 — a refinement in effect (or one Rigor cannot read) may define the name on one arm's class.
|
|
2206
2349
|
def refined_on_some_arm?(members, call_node, scope, lexical_sites)
|
|
2207
2350
|
members.any? do |member|
|
|
@@ -2684,9 +2827,37 @@ module Rigor
|
|
|
2684
2827
|
|
|
2685
2828
|
# Whether any project or `pre_eval:` file aliases `name`, on either side, which can make it another variable.
|
|
2686
2829
|
def global_aliased?(scope, name)
|
|
2830
|
+
record_global_census(Inference::GlobalWriteCensus.alias_entry(name))
|
|
2687
2831
|
Inference::GlobalWriteCensus.aliased?(global_write_census(scope), name)
|
|
2688
2832
|
end
|
|
2689
2833
|
|
|
2834
|
+
# Issue #2158 — a read of the whole-program census records the `global_census:` key of each entry it asks
|
|
2835
|
+
# about ({Inference::GlobalWriteCensus.dependency_key}): a file that starts or stops writing the entry (a `def
|
|
2836
|
+
# write`, an `alias $stdout $x`, a computed `define_method`) re-checks the write. A `pre_eval:` file moves the
|
|
2837
|
+
# snapshot's own fingerprint.
|
|
2838
|
+
def record_global_census(*entries)
|
|
2839
|
+
return unless DependencyRecorder.active?
|
|
2840
|
+
|
|
2841
|
+
entries.each do |entry|
|
|
2842
|
+
DependencyRecorder.read_name(:global_census, Inference::GlobalWriteCensus.dependency_key(entry))
|
|
2843
|
+
end
|
|
2844
|
+
end
|
|
2845
|
+
|
|
2846
|
+
# Whether the census holds any of `entries`, recording only what can flip the answer: the entries present
|
|
2847
|
+
# when it holds one (the answer stays true until every one of them vanishes, and another appearing changes
|
|
2848
|
+
# nothing), every entry when it holds none.
|
|
2849
|
+
#
|
|
2850
|
+
# This rests on an invariant: every census entry can only move a write toward being silenced, and a reported
|
|
2851
|
+
# write is always re-checked through `IncrementalSession#global_write_reporters`, so recording only the
|
|
2852
|
+
# present entries on a yes is sound. A future census entry that makes a write reported would break it, and
|
|
2853
|
+
# its reader would have to record every entry. Recording every entry on a no is defence in depth, since the
|
|
2854
|
+
# write that answer leaves reported re-checks through `global_write_reporters` anyway.
|
|
2855
|
+
def census_holds_any?(census, entries)
|
|
2856
|
+
present = entries.select { |entry| census.include?(entry) }
|
|
2857
|
+
record_global_census(*(present.empty? ? entries : present))
|
|
2858
|
+
!present.empty?
|
|
2859
|
+
end
|
|
2860
|
+
|
|
2690
2861
|
# A literal is rejected when its class is none the setter takes and, for a setter that also converts
|
|
2691
2862
|
# (`to_str`, `to_int`) or asks for `write`, the literal's object cannot answer that method.
|
|
2692
2863
|
def literal_rejected?(class_name, contract, node, scope, lexical_sites)
|
|
@@ -2734,11 +2905,15 @@ module Rigor
|
|
|
2734
2905
|
end
|
|
2735
2906
|
|
|
2736
2907
|
def program_may_answer?(ancestors, names, census, scope)
|
|
2737
|
-
|
|
2908
|
+
defines = [Inference::GlobalWriteCensus::DEFINES_ANY, *names.map { |name| [:defines, name] }]
|
|
2909
|
+
return true if census_holds_any?(census, defines)
|
|
2738
2910
|
|
|
2739
2911
|
dynamic_mark = Scope::DiscoveryIndex::ENVELOPE_DYNAMIC_MARK
|
|
2740
2912
|
return true if ancestors.any? { |owner| scope.parameter_envelopes_of(owner).key?(dynamic_mark) }
|
|
2741
2913
|
|
|
2914
|
+
# One key for every top-level mixin, a deliberate trade-off: the reader asks about each, so a mixin
|
|
2915
|
+
# appearing or vanishing anywhere re-checks every write that reached this step.
|
|
2916
|
+
record_global_census(%i[main_mixin *])
|
|
2742
2917
|
main_mixins = Inference::GlobalWriteCensus.main_mixins(census)
|
|
2743
2918
|
main_mixins.any? { |mixin| rbs_mixin_may_answer?(mixin, names, scope) } ||
|
|
2744
2919
|
mixin_may_answer?(ancestors, names, scope)
|
|
@@ -2780,7 +2955,7 @@ module Rigor
|
|
|
2780
2955
|
# active refinement on Ruby 4.0.5; a refined `respond_to_missing?` or `respond_to?`, and every implicit
|
|
2781
2956
|
# conversion, do not, so a refinement that adds only those leaves the write reported.
|
|
2782
2957
|
def refined_writer_in_effect?(census, node, scope, lexical_sites)
|
|
2783
|
-
Inference::GlobalWriteCensus
|
|
2958
|
+
census_holds_any?(census, [Inference::GlobalWriteCensus::REFINES_ANY, %i[refines write]]) &&
|
|
2784
2959
|
(lexical_sites.nil? || !scope.declared_refinements.empty? ||
|
|
2785
2960
|
lexical_sites.refinements.any_at?(node.location.start_offset))
|
|
2786
2961
|
end
|
|
@@ -2865,6 +3040,13 @@ module Rigor
|
|
|
2865
3040
|
# - Issue #1568 — a module the class PREPENDS sits ahead of it in Ruby's order, so a definition there
|
|
2866
3041
|
# is what the call reaches; the class's own private `def` answers only when nothing prepended
|
|
2867
3042
|
# defines the name ({#reached_definer_visibility}).
|
|
3043
|
+
# - Issue #1984 — a class-object receiver (`Type::Singleton`, `K.x`) is checked against the singleton side:
|
|
3044
|
+
# the definition the call reaches on `K`'s singleton chain (its own singleton methods, its extended
|
|
3045
|
+
# modules' instance methods, then the superclass's singleton chain) must be recorded `:private`
|
|
3046
|
+
# (`private_class_method`, `class << self` with `private`, a private method of an extended module).
|
|
3047
|
+
# - Issue #1984 — Ruby hands a private call to `method_missing`, so a `method_missing` or
|
|
3048
|
+
# `respond_to_missing?` a project entry on the receiver's chain records, on the side the call reaches,
|
|
3049
|
+
# declines the rule ({#missing_hook_on_chain?}).
|
|
2868
3050
|
def visibility_mismatch_diagnostic(path, call_node, scope_index, lexical_sites = nil)
|
|
2869
3051
|
return nil unless explicit_non_self_receiver?(call_node.receiver)
|
|
2870
3052
|
|
|
@@ -2876,18 +3058,128 @@ module Rigor
|
|
|
2876
3058
|
return nil if inferred_param_receiver?(call_node, scope)
|
|
2877
3059
|
|
|
2878
3060
|
receiver_type = scope.type_of(call_node.receiver)
|
|
2879
|
-
|
|
3061
|
+
case receiver_type
|
|
3062
|
+
when Type::Nominal then instance_visibility_mismatch(path, call_node, scope, receiver_type, lexical_sites)
|
|
3063
|
+
when Type::Singleton then singleton_visibility_mismatch(path, call_node, scope, receiver_type, lexical_sites)
|
|
3064
|
+
end
|
|
3065
|
+
end
|
|
2880
3066
|
|
|
3067
|
+
def instance_visibility_mismatch(path, call_node, scope, receiver_type, lexical_sites)
|
|
2881
3068
|
visibility = scope.discovered_method_visibility(receiver_type.class_name, call_node.name)
|
|
2882
3069
|
return nil unless visibility == :private
|
|
2883
3070
|
|
|
2884
3071
|
reached = reached_definer_visibility(scope, receiver_type.class_name, call_node.name)
|
|
2885
3072
|
return nil if reached == :undecided
|
|
2886
3073
|
return nil unless reached.nil? || reached == :private
|
|
3074
|
+
return nil if missing_hook_on_chain?(scope, receiver_type.class_name, :instance)
|
|
2887
3075
|
# ADR-121 WD7 — a refinement in effect (or one Rigor cannot read) may redefine the method, public.
|
|
2888
3076
|
return nil if refined_method_in_effect?(receiver_type.class_name, call_node, scope, :instance, lexical_sites)
|
|
2889
3077
|
|
|
2890
|
-
build_visibility_mismatch_diagnostic(path, call_node, receiver_type)
|
|
3078
|
+
build_visibility_mismatch_diagnostic(path, call_node, receiver_type.class_name)
|
|
3079
|
+
end
|
|
3080
|
+
|
|
3081
|
+
# Issue #1984 — the singleton side reads only the reached definer: a private singleton method may be inherited
|
|
3082
|
+
# (`private_class_method :new` on a superclass) or come from an extended module, so there is no own-table
|
|
3083
|
+
# gate as on the instance side. It reports only:
|
|
3084
|
+
#
|
|
3085
|
+
# - a constant receiver (`K.x`, `A::K.x`): `self.class.x` or a local may hold a subclass that makes the name
|
|
3086
|
+
# public again (`public_class_method :new`, a template-method `def self.x`);
|
|
3087
|
+
# - a call inside a `def` ({#load_time_call?}): code that runs while the files load (a class body's
|
|
3088
|
+
# `ALL = [K.new]` before its `private_class_method :new`) may run before the call is private;
|
|
3089
|
+
# - no class on the chain may have had every singleton name made public (`K.public_class_method(*names)`,
|
|
3090
|
+
# `K.singleton_class.class_eval { … }`; {#singleton_wildcard_contested?}).
|
|
3091
|
+
def singleton_visibility_mismatch(path, call_node, scope, receiver_type, lexical_sites)
|
|
3092
|
+
class_name = receiver_type.class_name
|
|
3093
|
+
return nil unless call_node.receiver.is_a?(Prism::ConstantReadNode) ||
|
|
3094
|
+
call_node.receiver.is_a?(Prism::ConstantPathNode)
|
|
3095
|
+
return nil unless private_singleton_candidate?(scope, call_node.name)
|
|
3096
|
+
return nil if load_time_call?(scope, call_node)
|
|
3097
|
+
return nil unless reached_definer_visibility(scope, class_name, call_node.name, :singleton) == :private
|
|
3098
|
+
return nil if missing_hook_on_chain?(scope, class_name, :singleton)
|
|
3099
|
+
return nil if singleton_wildcard_contested?(scope, class_name)
|
|
3100
|
+
return nil if refined_method_in_effect?(class_name, call_node, scope, :singleton, lexical_sites)
|
|
3101
|
+
|
|
3102
|
+
build_visibility_mismatch_diagnostic(path, call_node, "singleton(#{class_name})")
|
|
3103
|
+
end
|
|
3104
|
+
|
|
3105
|
+
PRIVATE_NAMES_MEMO = :rigor_check_rules_private_singleton_names
|
|
3106
|
+
private_constant :PRIVATE_NAMES_MEMO
|
|
3107
|
+
|
|
3108
|
+
# Issue #1984 — the table-only gate the singleton side asks before it reads a chain, as the instance side asks
|
|
3109
|
+
# its own table first: whether any class records `name` private, on its singleton side or on its instance
|
|
3110
|
+
# side (an extended module's private method is private on the extender's singleton). Without it every
|
|
3111
|
+
# `Widget.new` would read, and file the dependency edges of, the whole singleton chain. The set is memoised per
|
|
3112
|
+
# discovery index.
|
|
3113
|
+
def private_singleton_candidate?(scope, name)
|
|
3114
|
+
DependencyRecorder.read_visibility("*", name) if DependencyRecorder.active?
|
|
3115
|
+
discovery = scope.discovery
|
|
3116
|
+
slot = Thread.current[PRIVATE_NAMES_MEMO]
|
|
3117
|
+
unless slot && slot[0].equal?(discovery)
|
|
3118
|
+
slot = [discovery, private_recorded_names(discovery.discovered_method_visibilities)]
|
|
3119
|
+
Thread.current[PRIVATE_NAMES_MEMO] = slot
|
|
3120
|
+
end
|
|
3121
|
+
slot[1].include?(name.to_sym)
|
|
3122
|
+
end
|
|
3123
|
+
|
|
3124
|
+
def private_recorded_names(tables)
|
|
3125
|
+
tables.each_value.with_object(Set.new) do |table, names|
|
|
3126
|
+
table.each { |slot, visibility| names << (slot.is_a?(Array) ? slot.last : slot) if visibility == :private }
|
|
3127
|
+
end
|
|
3128
|
+
end
|
|
3129
|
+
|
|
3130
|
+
# Issue #1984 — whether the call is outside every `def` the file's deferred ranges name. A `def` is named
|
|
3131
|
+
# wherever it is written in a class body, at the top level or in a `class_eval` block, so a call in it is not
|
|
3132
|
+
# load time and may be reported. The top level, class bodies, and blocks and lambdas that are not such a `def`
|
|
3133
|
+
# (a `map { … }` or `define_method` block, a `-> { }`) are load time and decline: such code may run while the
|
|
3134
|
+
# files load, before a later `private_class_method` or in a load order where the private declaration has not
|
|
3135
|
+
# run yet.
|
|
3136
|
+
def load_time_call?(scope, call_node)
|
|
3137
|
+
rows = scope.discovered_deferred_ranges[scope.source_path]
|
|
3138
|
+
return true if rows.nil?
|
|
3139
|
+
|
|
3140
|
+
offset = call_node.location.start_offset
|
|
3141
|
+
# A row with no side (a `def` in a block, a `define_method` name) is a block's range or no range at all.
|
|
3142
|
+
rows.none? { |(start, finish, _name, kind, _owner)| !kind.nil? && start <= offset && offset < finish }
|
|
3143
|
+
end
|
|
3144
|
+
|
|
3145
|
+
SINGLETON_WILDCARD_SLOT = %i[singleton *].freeze
|
|
3146
|
+
private_constant :SINGLETON_WILDCARD_SLOT
|
|
3147
|
+
|
|
3148
|
+
# Issue #1984 — whether a project class on `class_name`'s singleton chain has its wildcard singleton slot
|
|
3149
|
+
# contested: a call the walk saw may have made any of its singleton methods public.
|
|
3150
|
+
def singleton_wildcard_contested?(scope, class_name)
|
|
3151
|
+
discovery = scope.discovery
|
|
3152
|
+
chain = Scope::ResolutionChain.for(scope, class_name.to_s, :singleton, :methods)
|
|
3153
|
+
chain.entries.any? do |entry|
|
|
3154
|
+
DependencyRecorder.read_visibility(entry.name, SINGLETON_WILDCARD_SLOT.last) unless entry.external?
|
|
3155
|
+
!entry.external? && discovery.contested?(:discovered_method_visibilities,
|
|
3156
|
+
[entry.name, SINGLETON_WILDCARD_SLOT])
|
|
3157
|
+
end
|
|
3158
|
+
end
|
|
3159
|
+
|
|
3160
|
+
VISIBILITY_MISSING_HOOKS = %i[method_missing respond_to_missing?].freeze
|
|
3161
|
+
private_constant :VISIBILITY_MISSING_HOOKS
|
|
3162
|
+
|
|
3163
|
+
# Issue #1984 — whether a project entry on `class_name`'s chain for `side` records a `method_missing` or
|
|
3164
|
+
# `respond_to_missing?` on the side that entry stands for (a singleton entry's singleton methods, a mixin's
|
|
3165
|
+
# instance methods), or the chain cannot be read whole. Ruby sends a private call to `method_missing`, so
|
|
3166
|
+
# such a hook may answer it.
|
|
3167
|
+
def missing_hook_on_chain?(scope, class_name, side)
|
|
3168
|
+
chain = Scope::ResolutionChain.for(scope, class_name.to_s, side, :methods)
|
|
3169
|
+
return true if chain.truncated?
|
|
3170
|
+
|
|
3171
|
+
chain.entries.any? do |entry|
|
|
3172
|
+
next false if entry.external?
|
|
3173
|
+
|
|
3174
|
+
kind = entry.side == :singleton ? :singleton : :instance
|
|
3175
|
+
VISIBILITY_MISSING_HOOKS.any? { |hook| records_hook?(scope, entry.name, hook, kind) }
|
|
3176
|
+
end
|
|
3177
|
+
end
|
|
3178
|
+
|
|
3179
|
+
def records_hook?(scope, owner, hook, kind)
|
|
3180
|
+
return true if scope.discovered_method?(owner, hook, kind)
|
|
3181
|
+
|
|
3182
|
+
!(kind == :singleton ? scope.singleton_def_for(owner, hook) : scope.user_def_for(owner, hook)).nil?
|
|
2891
3183
|
end
|
|
2892
3184
|
|
|
2893
3185
|
# Issue #1568, ADR-119 C1c — the recorded visibility of the definition a call on `class_name` reaches: the
|
|
@@ -2899,8 +3191,8 @@ module Rigor
|
|
|
2899
3191
|
# producers contest is not alone (`DefinerResolution` asks the next candidate too), so it is `:undecided`
|
|
2900
3192
|
# there without a read of the table here. nil is nothing recording the name at all, and the class's own
|
|
2901
3193
|
# visibility stands. An ancestor the project does not declare is passed over, as the method lookup passes it.
|
|
2902
|
-
def reached_definer_visibility(scope, class_name, method_name)
|
|
2903
|
-
case Inference::DefinerResolution.resolve(scope, class_name, method_name,
|
|
3194
|
+
def reached_definer_visibility(scope, class_name, method_name, side = :instance)
|
|
3195
|
+
case Inference::DefinerResolution.resolve(scope, class_name, method_name, side, question: :visibility)
|
|
2904
3196
|
in Inference::DefinerResolution::Known(answer:)
|
|
2905
3197
|
answer
|
|
2906
3198
|
in Inference::DefinerResolution::UNKNOWN
|
|
@@ -2917,14 +3209,14 @@ module Rigor
|
|
|
2917
3209
|
true
|
|
2918
3210
|
end
|
|
2919
3211
|
|
|
2920
|
-
def build_visibility_mismatch_diagnostic(path, call_node,
|
|
3212
|
+
def build_visibility_mismatch_diagnostic(path, call_node, receiver_name)
|
|
2921
3213
|
Diagnostic.from_message_loc(
|
|
2922
3214
|
call_node,
|
|
2923
3215
|
rule: RULE_VISIBILITY_MISMATCH,
|
|
2924
3216
|
path: path,
|
|
2925
|
-
message: "private method `#{call_node.name}' called on #{
|
|
3217
|
+
message: "private method `#{call_node.name}' called on #{receiver_name} receiver",
|
|
2926
3218
|
severity: :error,
|
|
2927
|
-
receiver_type:
|
|
3219
|
+
receiver_type: receiver_name,
|
|
2928
3220
|
method_name: call_node.name.to_s
|
|
2929
3221
|
)
|
|
2930
3222
|
end
|
|
@@ -3992,6 +4284,9 @@ module Rigor
|
|
|
3992
4284
|
|
|
3993
4285
|
override_visibility = scope.discovered_method_visibility(class_name, method_name)
|
|
3994
4286
|
return nil if override_visibility.nil?
|
|
4287
|
+
# Issue #2013 — a contested slot (a later `define_method` or alias gave the name another visibility, or a
|
|
4288
|
+
# write the walk cannot place) proves no reduction.
|
|
4289
|
+
return nil if scope.discovery.contested?(:discovered_method_visibilities, [class_name, method_name])
|
|
3995
4290
|
|
|
3996
4291
|
parent = nearest_ancestor_visibility(scope, class_name, method_name)
|
|
3997
4292
|
return nil if parent.nil?
|
|
@@ -4001,6 +4296,9 @@ module Rigor
|
|
|
4001
4296
|
# prove a reduction, stay silent.
|
|
4002
4297
|
return nil if parent_visibility.nil?
|
|
4003
4298
|
return nil unless visibility_reduced?(parent_visibility, override_visibility)
|
|
4299
|
+
# Issue #1569 — a module function's instance copy is private because `module_function` made it so; the
|
|
4300
|
+
# author's intent is the public singleton copy, so that reduction is not reported.
|
|
4301
|
+
return nil if scope.module_function_def?(class_name, def_node)
|
|
4004
4302
|
|
|
4005
4303
|
build_override_visibility_diagnostic(
|
|
4006
4304
|
path, def_node, parent_class, parent_visibility, override_visibility
|