rigortype 0.4.3 → 0.4.5
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/data/core_overlay/ostruct.rbs +40 -0
- data/docs/handbook/11-sig-gen.md +104 -15
- data/docs/manual/02-cli-reference.md +43 -12
- data/docs/manual/03-configuration.md +20 -7
- data/docs/manual/04-diagnostics.md +56 -3
- data/docs/manual/05-inspecting-types.md +2 -2
- data/docs/manual/06-baseline.md +1 -1
- data/docs/manual/07-plugins.md +3 -2
- data/docs/manual/08-skills.md +7 -0
- data/docs/manual/10-mcp-server.md +40 -4
- data/docs/manual/18-removing-dead-code.md +23 -2
- data/docs/manual/plugins/rigor-rails-routes.md +2 -2
- data/docs/manual/plugins/rigor-rspec.md +101 -1
- data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
- data/exe/rigor +12 -17
- data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +111 -7
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +190 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +60 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +13 -4
- data/lib/rigor/analysis/check_rules.rb +274 -51
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +61 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +270 -28
- data/lib/rigor/analysis/path_expansion.rb +3 -1
- data/lib/rigor/analysis/reachability/graph.rb +111 -14
- data/lib/rigor/analysis/reachability/scan.rb +305 -23
- data/lib/rigor/analysis/rule_catalog.rb +22 -4
- data/lib/rigor/analysis/run_cache_key.rb +31 -7
- data/lib/rigor/analysis/run_cache_probe.rb +5 -3
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +48 -12
- data/lib/rigor/analysis/runner/pool_coordinator.rb +179 -31
- data/lib/rigor/analysis/runner/project_pre_passes.rb +29 -4
- data/lib/rigor/analysis/runner.rb +102 -72
- data/lib/rigor/analysis/severity_stamp.rb +45 -1
- data/lib/rigor/analysis/worker_session.rb +69 -2
- data/lib/rigor/cache/descriptor.rb +79 -12
- data/lib/rigor/cache/engine_source.rb +3 -2
- data/lib/rigor/cache/incremental_snapshot.rb +39 -5
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +4 -3
- data/lib/rigor/cli/command.rb +3 -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/mcp_command.rb +1 -1
- data/lib/rigor/cli/plugin_command.rb +2 -1
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +32 -11
- data/lib/rigor/cli/skill_command.rb +2 -1
- data/lib/rigor/cli/skill_describe.rb +3 -2
- data/lib/rigor/cli/unused_command.rb +10 -7
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +163 -22
- data/lib/rigor/configuration/dependencies.rb +19 -11
- data/lib/rigor/configuration/severity_profile.rb +20 -2
- data/lib/rigor/configuration.rb +29 -3
- 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 +67 -11
- data/lib/rigor/inference/block_call_timing.rb +12 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/captured_locals.rb +67 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/definer_resolution.rb +101 -25
- data/lib/rigor/inference/expression_typer.rb +94 -9
- data/lib/rigor/inference/guard_rebinding.rb +7 -2
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
- data/lib/rigor/inference/macro_block_self_type.rb +20 -6
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +170 -35
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
- data/lib/rigor/inference/method_dispatcher.rb +67 -3
- data/lib/rigor/inference/module_function_state.rb +213 -51
- data/lib/rigor/inference/narrowing.rb +235 -75
- 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 +120 -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 +116 -0
- data/lib/rigor/inference/repeated_or_writes.rb +414 -1
- data/lib/rigor/inference/return_barrier.rb +16 -0
- data/lib/rigor/inference/scope_indexer.rb +1061 -122
- data/lib/rigor/inference/self_call_ivar_writes.rb +346 -0
- 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 +512 -150
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- data/lib/rigor/lens/anchor.rb +3 -3
- data/lib/rigor/lens/contract_reader.rb +60 -2
- data/lib/rigor/lens/document.rb +4 -3
- data/lib/rigor/lens/file_reader.rb +452 -73
- data/lib/rigor/lens/members.rb +81 -17
- data/lib/rigor/lens/run.rb +73 -4
- data/lib/rigor/lens.rb +2 -1
- data/lib/rigor/mcp/server.rb +63 -22
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/box_probe.rb +154 -7
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -3
- data/lib/rigor/project_environment.rb +6 -3
- data/lib/rigor/protection/discovery_seed.rb +9 -36
- data/lib/rigor/protection/mutation_cache.rb +8 -2
- data/lib/rigor/rooted_glob.rb +27 -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 +56 -8
- 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 +167 -22
- data/lib/rigor/sig_gen/layout_index.rb +40 -1
- data/lib/rigor/sig_gen/method_candidate.rb +8 -0
- data/lib/rigor/sig_gen/observation_collector.rb +137 -11
- data/lib/rigor/sig_gen/path_mapper.rb +18 -2
- data/lib/rigor/sig_gen/renderer.rb +13 -155
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
- 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 +50 -35
- data/lib/rigor/signature_path_audit.rb +4 -2
- data/lib/rigor/type/constant.rb +10 -1
- 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 +1 -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-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-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-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +57 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +1 -1
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +5 -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 +155 -12
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
- 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/sig/rigor/plugin/io_boundary.rbs +5 -2
- data/sig/rigor/rooted_glob.rbs +7 -0
- data/sig/rigor/scope.rbs +4 -3
- data/skills/rigor-doctor/references/01-checks.md +23 -11
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
- metadata +16 -1
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../reflection"
|
|
4
|
+
require_relative "../dependency_recorder"
|
|
5
|
+
require_relative "../../inference/singleton_hook_decline"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Analysis
|
|
9
|
+
module CheckRules
|
|
10
|
+
# Issue #1722 — what the project may give an instance of an RBS-declared class beyond the class's RBS, asked
|
|
11
|
+
# by `call.undefined-method` once the RBS lookup and the project's own tables for the class itself have come
|
|
12
|
+
# back "absent". Three places the earlier probes do not look:
|
|
13
|
+
#
|
|
14
|
+
# - **A `method_missing` on an RBS ancestor.** `class Integer; def method_missing(*)` (or one on `Numeric`,
|
|
15
|
+
# `Comparable`, `Object`, `Kernel`, `BasicObject`, or a project module mixed into any of them) answers
|
|
16
|
+
# every name Ruby dispatches to an Integer. `respond_to_missing?` is not read: Ruby dispatches to
|
|
17
|
+
# `method_missing` whether or not it is defined.
|
|
18
|
+
# - **A project mixin of the class or an RBS ancestor.** `Integer.include(Ext)` in another file, or
|
|
19
|
+
# `Comparable.include(Ext)` / `module Comparable; def zzz`, gives every Integer `zzz`. The earlier probes
|
|
20
|
+
# read the class's own chain through `discovered_methods`, which holds no plain `def` written in another
|
|
21
|
+
# file than the one being checked; this reads the def table too for a project module, and walks the
|
|
22
|
+
# ancestors RBS lists after the class. A cross-file `def` on an RBS-declared class or module itself stays
|
|
23
|
+
# ADR-17's monkey-patch report ({.mixin_defines?}). A mixin with an open surface — a project module with a
|
|
24
|
+
# recorded `included` / `extended` / … hook, a `"*"` mixin or `ActiveSupport::Concern`, or one neither RBS nor
|
|
25
|
+
# the project declares — answers any name, except on `Object`, `Kernel` and `BasicObject`: there only a
|
|
26
|
+
# module that defines the name answers, #1697's reading (`Inference::ObjectMixins`) of a module mixed into
|
|
27
|
+
# every object.
|
|
28
|
+
# - **A project subclass** ({.subclass_may_define?}). A value typed by its class's name through a signature
|
|
29
|
+
# (`#: () -> String`) may be an instance of any subclass, so a method a project subclass defines may be
|
|
30
|
+
# the one that runs.
|
|
31
|
+
#
|
|
32
|
+
# Each ancestor's name is filed as an ADR-46 class dependency, so a file that later reopens one re-checks
|
|
33
|
+
# the call, and the subclass walk files a `subclass_of` edge per class it reads.
|
|
34
|
+
module RbsReceiverSurface
|
|
35
|
+
MISSING_HOOK = :method_missing
|
|
36
|
+
# The roots whose project mixins `Inference::ObjectMixins` answers for.
|
|
37
|
+
OBJECT_ROOTS = %w[Object Kernel BasicObject].to_set.freeze
|
|
38
|
+
private_constant :MISSING_HOOK, :OBJECT_ROOTS
|
|
39
|
+
|
|
40
|
+
module_function
|
|
41
|
+
|
|
42
|
+
# Whether the project may answer `method_name` on an instance of `class_name` through one of its RBS
|
|
43
|
+
# ancestors.
|
|
44
|
+
def ancestor_may_answer?(scope, class_name, method_name)
|
|
45
|
+
ancestor_names(scope, class_name).any? do |name|
|
|
46
|
+
DependencyRecorder.read_last_segment(:class, name) if DependencyRecorder.active?
|
|
47
|
+
next false unless scope.known_user_class?(name)
|
|
48
|
+
|
|
49
|
+
chain_answers?(scope, name, method_name, open_mixins: !OBJECT_ROOTS.include?(name))
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Whether the singleton side of `class_name` may reach a project `method_missing`: a `def self.method_missing`
|
|
54
|
+
# on the class or an RBS superclass, or an instance `method_missing` on `Class`, `Module`, `Object`,
|
|
55
|
+
# `Kernel` or `BasicObject` (a class object is an instance of each).
|
|
56
|
+
def singleton_hook?(scope, class_name)
|
|
57
|
+
loader = scope.environment&.rbs_loader
|
|
58
|
+
pairs = loader ? loader.singleton_ancestors_for(class_name.to_s) : []
|
|
59
|
+
pairs = [[:singleton, class_name.to_s]] if pairs.empty?
|
|
60
|
+
pairs.any? do |kind, name|
|
|
61
|
+
DependencyRecorder.read_last_segment(:class, name) if DependencyRecorder.active?
|
|
62
|
+
if kind == :singleton
|
|
63
|
+
!scope.singleton_def_for(name, MISSING_HOOK).nil? ||
|
|
64
|
+
scope.discovered_method?(name, MISSING_HOOK, :singleton)
|
|
65
|
+
else
|
|
66
|
+
scope.known_user_class?(name) && hook_on_chain?(scope, name)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Whether a project subclass of `class_name`, at any depth, defines `method_name` (through its own
|
|
72
|
+
# ancestry, which reaches its project mixins) or a `method_missing`. Its parent is read as written and
|
|
73
|
+
# resolved the way the class header resolves it: the first project class the spelling can denote, else the
|
|
74
|
+
# top-level name, which is the RBS class.
|
|
75
|
+
def subclass_may_define?(scope, class_name, method_name)
|
|
76
|
+
each_subclass(scope, class_name.to_s) do |subclass|
|
|
77
|
+
return true if project_defines?(scope, subclass, method_name) ||
|
|
78
|
+
project_defines?(scope, subclass, MISSING_HOOK) ||
|
|
79
|
+
!scope.user_def_through_ancestors(subclass, method_name).first.nil? ||
|
|
80
|
+
scope.discovered_method_through_ancestors?(subclass, method_name, :instance)
|
|
81
|
+
end
|
|
82
|
+
false
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def ancestor_names(scope, class_name)
|
|
86
|
+
loader = scope.environment&.rbs_loader
|
|
87
|
+
names = loader ? loader.ancestor_names_for(class_name.to_s) : []
|
|
88
|
+
names.empty? ? [class_name.to_s] : names
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# A search of `name`'s project chain for an entry that may answer: a project entry that defines
|
|
92
|
+
# `method_name` or a `method_missing`, or an external mixin RBS declares `method_name` on. When
|
|
93
|
+
# `open_mixins` (every ancestor but `Object`, `Kernel` and `BasicObject`, #1697), also a mixin whose surface
|
|
94
|
+
# is open: a project module SingletonHookDecline reads as hook-capable, or an external neither RBS nor the
|
|
95
|
+
# project declares. A chain cut at its budget may hold an answer past the cut.
|
|
96
|
+
def chain_answers?(scope, name, method_name, open_mixins:)
|
|
97
|
+
chain = Scope::ResolutionChain.for(scope, name, :instance, :methods)
|
|
98
|
+
found = chain.search(scope, side: :instance) do |entry|
|
|
99
|
+
if entry.external?
|
|
100
|
+
external_may_answer?(scope, entry, method_name, open_mixins)
|
|
101
|
+
else
|
|
102
|
+
project_defines?(scope, entry.name, MISSING_HOOK) ||
|
|
103
|
+
mixin_defines?(scope, entry, method_name, open_mixins)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
found || chain.truncated?
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# The singleton side's question of an instance chain: does a project entry on it define `method_missing`?
|
|
110
|
+
def hook_on_chain?(scope, name)
|
|
111
|
+
chain = Scope::ResolutionChain.for(scope, name, :instance, :methods)
|
|
112
|
+
found = chain.search(scope, side: :instance) do |entry|
|
|
113
|
+
!entry.external? && project_defines?(scope, entry.name, MISSING_HOOK)
|
|
114
|
+
end
|
|
115
|
+
found || chain.truncated?
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def project_defines?(scope, owner, method_name)
|
|
119
|
+
!scope.user_def_for(owner, method_name).nil? || scope.discovered_method?(owner, method_name, :instance)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# ADR-17 — a `def` the project writes on a class or module RBS declares (`class String; def x`, `module
|
|
123
|
+
# Comparable; def x`) in another file than the call is a monkey-patch Rigor does not apply cross-file, and the
|
|
124
|
+
# rule reports it with the `pre_eval:` advice, so on such an entry only what `discovered_methods` carries
|
|
125
|
+
# answers. A project module's own `def` answers from any file.
|
|
126
|
+
#
|
|
127
|
+
# When `open_mixins`, a project module {Inference::SingletonHookDecline} reads as hook-capable — one that
|
|
128
|
+
# records an `included` / `extended` / … hook (`def self.included(base) = base.class_eval { def x }`), lists
|
|
129
|
+
# a `"*"` mixin, or extends `ActiveSupport::Concern` — may answer any name.
|
|
130
|
+
def mixin_defines?(scope, entry, method_name, open_mixins)
|
|
131
|
+
owner = entry.name
|
|
132
|
+
if Rigor::Reflection.rbs_class_known?(owner, scope: scope)
|
|
133
|
+
scope.discovered_method?(owner, method_name, :instance)
|
|
134
|
+
else
|
|
135
|
+
project_defines?(scope, owner, method_name) ||
|
|
136
|
+
(open_mixins && Inference::SingletonHookDecline.entry_hook_capable?(scope, entry))
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# 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 only through RBS.
|
|
142
|
+
def external_may_answer?(scope, entry, method_name, open_mixins)
|
|
143
|
+
candidates = entry.candidates
|
|
144
|
+
return open_mixins if candidates.empty?
|
|
145
|
+
return true if candidates.any? do |candidate|
|
|
146
|
+
Rigor::Reflection.instance_method_definition(candidate, method_name, scope: scope)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
open_mixins && candidates.none? { |candidate| declared?(scope, candidate) }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def declared?(scope, name)
|
|
153
|
+
scope.discovered_classes.key?(name) || Rigor::Reflection.rbs_class_known?(name, scope: scope)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def each_subclass(scope, class_name)
|
|
157
|
+
children = children_by_parent(scope)
|
|
158
|
+
queue = children.fetch(class_name, []).dup
|
|
159
|
+
seen = {}
|
|
160
|
+
record_subclasses_read(class_name)
|
|
161
|
+
until queue.empty?
|
|
162
|
+
subclass = queue.shift
|
|
163
|
+
next if seen[subclass]
|
|
164
|
+
|
|
165
|
+
seen[subclass] = true
|
|
166
|
+
record_subclasses_read(subclass)
|
|
167
|
+
yield subclass
|
|
168
|
+
queue.concat(children.fetch(subclass, []))
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def children_by_parent(scope)
|
|
173
|
+
scope.discovered_superclasses.each_with_object({}) do |(child, raw_parent), children|
|
|
174
|
+
next if raw_parent.nil?
|
|
175
|
+
|
|
176
|
+
candidates = scope.ancestor_name_candidates(child, raw_parent.to_s)
|
|
177
|
+
parent = candidates.find { |name| scope.known_user_class?(name) } || candidates.last
|
|
178
|
+
(children[parent] ||= []) << child if parent
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def record_subclasses_read(class_name)
|
|
183
|
+
return unless DependencyRecorder.active?
|
|
184
|
+
|
|
185
|
+
DependencyRecorder.read_last_segment(:subclass_of, class_name)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -168,11 +168,70 @@ module Rigor
|
|
|
168
168
|
# Bare (dot-less) diagnostic ids the engine emits outside the catalogue (see the `rule:` literals in
|
|
169
169
|
# `Analysis::Runner` / `Runner::DiagnosticAggregator`). A token equal to one of these is treated as
|
|
170
170
|
# known by `suppression.unknown-rule` even though it carries no family prefix; extend the list when
|
|
171
|
-
# the runner grows a new bare id.
|
|
171
|
+
# the runner grows a new bare id. Ids a marker or `disable:` cannot suppress (the `--explain`
|
|
172
|
+
# `fallback` row, the `contribution_merge` conflict reasons) stay out: they are
|
|
173
|
+
# `ConfigAudit::SEVERITY_ONLY_IDS`, known to the `severity_overrides:` audit alone.
|
|
172
174
|
NON_CHECK_DIAGNOSTIC_IDS = %w[
|
|
173
175
|
configuration-error load-error pool-degraded runtime-error source-rbs-synthesis-failed
|
|
174
176
|
source-rbs-annotation-not-honoured
|
|
175
177
|
].freeze
|
|
178
|
+
|
|
179
|
+
# Resolves a user-supplied rule token (`undefined-method`, `call.undefined-method`, or the family wildcard
|
|
180
|
+
# `call`) to the set of canonical rule identifiers it disables. Returns `nil` for `"all"` (the existing
|
|
181
|
+
# wildcard meaning "every rule"), or for unknown tokens.
|
|
182
|
+
def self.resolve_rule_token(token)
|
|
183
|
+
return nil if token == "all"
|
|
184
|
+
return [LEGACY_RULE_ALIASES.fetch(token)] if LEGACY_RULE_ALIASES.key?(token)
|
|
185
|
+
return PRE_EVAL_RULES if token == "pre-eval"
|
|
186
|
+
return ALL_RULES.select { |r| r.start_with?("#{token}.") } if RULE_FAMILIES.include?(token)
|
|
187
|
+
|
|
188
|
+
ALL_RULES.include?(token) ? [token] : []
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Expands a list of user-supplied rule tokens into the canonical-id set per ADR-8 § "Backward
|
|
192
|
+
# compatibility". `disabled_rules` accepts unprefixed legacy names (`undefined-method`), canonical names
|
|
193
|
+
# (`call.undefined-method`), family wildcards (`call`) and the `all` sentinel.
|
|
194
|
+
def self.expand_rule_tokens(tokens)
|
|
195
|
+
Array(tokens).each_with_object(Set.new) do |token, set|
|
|
196
|
+
set.merge(expand_token(token.to_s))
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def self.expand_token(token)
|
|
201
|
+
return ["all"] if token == "all"
|
|
202
|
+
|
|
203
|
+
resolved = resolve_rule_token(token)
|
|
204
|
+
resolved.nil? || resolved.empty? ? [token] : resolved
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Whether the `.rigor.yml` `disable:` list, expanded by {.expand_rule_tokens}, drops a diagnostic whose rule
|
|
208
|
+
# id is `rule`: the `all` sentinel drops every rule-bearing row, any other entry its exact id. A nil rule (a
|
|
209
|
+
# parse, path or internal error) is never dropped.
|
|
210
|
+
def self.config_disabled?(rule, disabled)
|
|
211
|
+
return false if rule.nil?
|
|
212
|
+
|
|
213
|
+
disabled.include?("all") || disabled.include?(rule)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# The source-family prefix the runner stamps on every plugin-emitted row (`plugin.<id>`).
|
|
217
|
+
PLUGIN_FAMILY_PREFIX = "plugin."
|
|
218
|
+
|
|
219
|
+
# Issue #1953 — whether a suppression token set drops a plugin's own row. Such a row answers only to its
|
|
220
|
+
# qualified id: the exact `plugin.<id>.<rule>` (its `qualified_rule`) or the whole plugin's `plugin.<id>`
|
|
221
|
+
# (its `source_family`). Both compare whole strings against the row's own fields, so `plugin.<id>` never
|
|
222
|
+
# reaches a plugin whose id merely starts with `<id>.`. Ids and rules may both hold dots, so plugin `a` with
|
|
223
|
+
# rule `b.c` and plugin `a.b` with rule `c` share `plugin.a.b.c`, and that exact token drops both. A bare rule
|
|
224
|
+
# and `all` do not reach these rows: many plugins share a bare rule (`load-error`, `unknown-column`), and a
|
|
225
|
+
# bare match would hide another plugin's findings. Every other row (built-in, `plugin_loader`, `plugin_trust`)
|
|
226
|
+
# answers false.
|
|
227
|
+
def self.plugin_row_disabled?(diagnostic, tokens)
|
|
228
|
+
return false if diagnostic.rule.nil?
|
|
229
|
+
|
|
230
|
+
family = diagnostic.source_family.to_s
|
|
231
|
+
return false unless family.start_with?(PLUGIN_FAMILY_PREFIX)
|
|
232
|
+
|
|
233
|
+
tokens.include?(family) || tokens.include?(diagnostic.qualified_rule)
|
|
234
|
+
end
|
|
176
235
|
end
|
|
177
236
|
end
|
|
178
237
|
end
|
|
@@ -101,6 +101,19 @@ module Rigor
|
|
|
101
101
|
# {#decline_unless_recorded_nowhere}): it settles by definitions, as an `ABSENT` read does.
|
|
102
102
|
return settle_by_walk if @unknown
|
|
103
103
|
|
|
104
|
+
# Issue #1833 — a level the `:arity` resolver took to be the project's on a mark alone (a namespace
|
|
105
|
+
# candidate a marks-only file rewrites) stands only while that mark does, and no definition appearing or
|
|
106
|
+
# vanishing can overturn it. Only such levels file the edge: the walk's other mark reads would re-check
|
|
107
|
+
# the call whenever an unrelated namespace reuses an ancestor's last segment.
|
|
108
|
+
@levels.each do |level|
|
|
109
|
+
([level.class_name] + level.modules).each do |name|
|
|
110
|
+
next if @scope.known_user_class?(name)
|
|
111
|
+
next unless Scope::DiscoveryIndex.declares_surface?(@scope.discovered_parameter_envelopes[name])
|
|
112
|
+
|
|
113
|
+
Analysis::DependencyRecorder.read_last_segment(:mark, name)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
104
117
|
@owner_entries.each do |name, kind|
|
|
105
118
|
if kind == :singleton
|
|
106
119
|
@scope.user_singleton_def_site_for(name, @method_name)
|
|
@@ -121,7 +134,8 @@ module Rigor
|
|
|
121
134
|
# Stage 2, for the class {#owner_envelope} last answered.
|
|
122
135
|
def authoritative?(class_name)
|
|
123
136
|
!load_order_dependent?(class_name) && !object_extension_may_shadow? &&
|
|
124
|
-
@levels.all? { |level| clean_level?(level) && public_at?(level.class_name) } &&
|
|
137
|
+
@levels.all? { |level| clean_level?(level) && (@kind == :singleton || public_at?(level.class_name)) } &&
|
|
138
|
+
!(@kind == :singleton && singleton_private?(class_name)) &&
|
|
125
139
|
chain_free_of_hooks?(class_name) &&
|
|
126
140
|
subclasses_agree?(class_name, @envelope)
|
|
127
141
|
end
|
|
@@ -245,8 +259,10 @@ module Rigor
|
|
|
245
259
|
end
|
|
246
260
|
|
|
247
261
|
# A module some method body hands to `obj.extend` sits ahead of the object's class, so if it defines
|
|
248
|
-
# the name at all, that object answers with the module's definition.
|
|
262
|
+
# the name at all, that object answers with the module's definition. Every bucket is read, so the answer
|
|
263
|
+
# records `mark:*`, which an object-extended mark appearing or vanishing anywhere fires (issue #1833).
|
|
249
264
|
def object_extension_may_shadow?
|
|
265
|
+
Analysis::DependencyRecorder.read_name(:mark, "*") if Analysis::DependencyRecorder.active?
|
|
250
266
|
@scope.discovered_parameter_envelopes.any? do |_name, bucket|
|
|
251
267
|
bucket.key?(Scope::DiscoveryIndex::ENVELOPE_OBJECT_EXTENDED_MARK) && bucket.key?([:instance, @method_name])
|
|
252
268
|
end
|
|
@@ -291,7 +307,9 @@ module Rigor
|
|
|
291
307
|
end
|
|
292
308
|
|
|
293
309
|
# A bucket of class marks alone (issue #1820: `x.singleton_class.prepend(Comparable)`) does not make a name
|
|
294
|
-
# the project's.
|
|
310
|
+
# the project's. The bucket is read directly rather than through `parameter_envelopes_of`: a `mark:<last>`
|
|
311
|
+
# edge recorded here fires on marks a correctly-fitting call does not depend on, so the dependency on
|
|
312
|
+
# a mark that could flip this resolution is kept at `settle_by_definitions` instead (issue #1833).
|
|
295
313
|
def project_class?(name)
|
|
296
314
|
@scope.known_user_class?(name) ||
|
|
297
315
|
Scope::DiscoveryIndex.declares_surface?(@scope.discovered_parameter_envelopes[name])
|
|
@@ -348,6 +366,17 @@ module Rigor
|
|
|
348
366
|
false
|
|
349
367
|
end
|
|
350
368
|
|
|
369
|
+
# Issue #2059 — a call Ruby rejects as private on the singleton side (`private_class_method :two`, a private
|
|
370
|
+
# method of a module that extends itself) is `def.method-visibility-mismatch`'s, which reads the same definer;
|
|
371
|
+
# the arity is not checked. A read that declines leaves the arity to the other conditions.
|
|
372
|
+
def singleton_private?(class_name)
|
|
373
|
+
case Inference::DefinerResolution.resolve(@scope, class_name, @method_name, :singleton, question: :visibility)
|
|
374
|
+
in Inference::DefinerResolution::Known(answer:, owner: _owner) then answer == :private
|
|
375
|
+
in Inference::DefinerResolution::UNKNOWN then false
|
|
376
|
+
in Inference::DefinerResolution::ABSENT then false # rubocop:disable Lint/DuplicateBranch
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
351
380
|
def public_at?(class_name)
|
|
352
381
|
visibility = @scope.discovered_method_visibility(class_name, @method_name)
|
|
353
382
|
visibility.nil? || visibility == :public
|
|
@@ -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
|
|
@@ -124,11 +129,13 @@ module Rigor
|
|
|
124
129
|
return if entry_type.nil? || entry_type.is_a?(Type::Bot) || entry_type.is_a?(Type::Dynamic)
|
|
125
130
|
|
|
126
131
|
keyword = node.is_a?(Prism::CaseMatchNode) ? "in" : "when"
|
|
132
|
+
coverage = ClauseCoverage.new(scope, entry_type)
|
|
127
133
|
node.conditions.each do |clause|
|
|
128
134
|
case clause
|
|
129
|
-
when Prism::WhenNode then collect_when(clause, subject.name)
|
|
130
|
-
when Prism::InNode then collect_in(clause, subject.name)
|
|
135
|
+
when Prism::WhenNode then collect_when(clause, subject.name, coverage)
|
|
136
|
+
when Prism::InNode then collect_in(clause, subject.name, coverage)
|
|
131
137
|
end
|
|
138
|
+
coverage.record(clause)
|
|
132
139
|
end
|
|
133
140
|
collect_else(node.else_clause, subject.name, keyword)
|
|
134
141
|
end
|
|
@@ -138,7 +145,7 @@ module Rigor
|
|
|
138
145
|
scope&.local(subject_name)
|
|
139
146
|
end
|
|
140
147
|
|
|
141
|
-
def collect_when(clause, subject_name)
|
|
148
|
+
def collect_when(clause, subject_name, coverage)
|
|
142
149
|
return if clause.statements.nil? # empty body → no useful location
|
|
143
150
|
return unless all_constant_conditions?(clause)
|
|
144
151
|
|
|
@@ -149,6 +156,7 @@ module Rigor
|
|
|
149
156
|
kind = when_clause_kind(clause, subject_name)
|
|
150
157
|
return if kind == :disjoint &&
|
|
151
158
|
disjoint_nominal_subject?(@scope_index[clause.conditions.first], subject_name, clause.conditions)
|
|
159
|
+
return if kind == :prior_exhaustion && !coverage.proven?(clause)
|
|
152
160
|
|
|
153
161
|
@results << Result.new(
|
|
154
162
|
clause: clause, body: clause.statements, subject_name: subject_name,
|
|
@@ -187,7 +195,7 @@ module Rigor
|
|
|
187
195
|
# (prior-exhaustion — true for ANY pattern downstream of a covering set). Non-bare patterns are not
|
|
188
196
|
# narrowed, so their body is `bot` ONLY under prior-exhaustion; they never produce a spurious
|
|
189
197
|
# `:disjoint`.
|
|
190
|
-
def collect_in(clause, subject_name)
|
|
198
|
+
def collect_in(clause, subject_name, coverage)
|
|
191
199
|
return if clause.statements.nil?
|
|
192
200
|
|
|
193
201
|
scope = @scope_index[clause]
|
|
@@ -196,6 +204,7 @@ module Rigor
|
|
|
196
204
|
|
|
197
205
|
kind = in_clause_kind(clause, subject_name)
|
|
198
206
|
return if kind == :disjoint && disjoint_nominal_pattern?(clause, subject_name)
|
|
207
|
+
return if kind == :prior_exhaustion && !coverage.proven?(clause)
|
|
199
208
|
|
|
200
209
|
@results << Result.new(
|
|
201
210
|
clause: clause, body: clause.statements, subject_name: subject_name,
|