rigortype 0.4.4 → 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/docs/handbook/11-sig-gen.md +90 -17
- data/docs/manual/02-cli-reference.md +39 -9
- data/docs/manual/03-configuration.md +10 -8
- data/docs/manual/04-diagnostics.md +16 -5
- 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 +8 -1
- data/docs/manual/18-removing-dead-code.md +18 -7
- data/docs/manual/plugins/rigor-rspec.md +101 -1
- 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 +3 -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 +228 -15
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +49 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +166 -24
- data/lib/rigor/analysis/path_expansion.rb +3 -1
- data/lib/rigor/analysis/reachability/graph.rb +90 -13
- data/lib/rigor/analysis/reachability/scan.rb +279 -39
- data/lib/rigor/analysis/rule_catalog.rb +17 -4
- data/lib/rigor/analysis/runner/pool_coordinator.rb +69 -16
- data/lib/rigor/analysis/runner.rb +69 -65
- data/lib/rigor/analysis/severity_stamp.rb +11 -0
- data/lib/rigor/analysis/worker_session.rb +12 -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 +26 -8
- 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/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 +60 -15
- 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 +21 -1
- data/lib/rigor/inference/block_call_timing.rb +7 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/captured_locals.rb +44 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/definer_resolution.rb +87 -20
- data/lib/rigor/inference/expression_typer.rb +35 -5
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- 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 +137 -42
- data/lib/rigor/inference/narrowing.rb +54 -10
- 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 +6 -6
- data/lib/rigor/inference/return_barrier.rb +16 -0
- data/lib/rigor/inference/scope_indexer.rb +675 -105
- data/lib/rigor/inference/self_call_ivar_writes.rb +11 -6
- 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 +446 -139
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- 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 +2 -2
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -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 +45 -2
- 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 +5 -2
- data/lib/rigor/sig_gen/observation_collector.rb +137 -11
- 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 +5 -3
- 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-types/lib/rigor/plugin/dry_types.rb +1 -1
- 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-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 +13 -1
|
@@ -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,
|
|
@@ -29,6 +29,7 @@ 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/rbs_receiver_surface"
|
|
32
33
|
require_relative "check_rules/special_global_setters"
|
|
33
34
|
require_relative "check_rules/ruby_deprecations"
|
|
34
35
|
|
|
@@ -801,7 +802,7 @@ module Rigor
|
|
|
801
802
|
# rule must not surface a false `undefined-method`
|
|
802
803
|
# for them.
|
|
803
804
|
kind = receiver_type.is_a?(Type::Singleton) ? :singleton : :instance
|
|
804
|
-
return nil if source_declared_method?(scope, class_name, call_node.name, kind)
|
|
805
|
+
return nil if source_declared_method?(scope, class_name, call_node.name, kind, call_node: call_node)
|
|
805
806
|
|
|
806
807
|
return nil unless Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
|
|
807
808
|
|
|
@@ -817,6 +818,8 @@ module Rigor
|
|
|
817
818
|
return nil if method_def
|
|
818
819
|
|
|
819
820
|
return nil if last_resort_surface_answers?(receiver_type, class_name, call_node, scope, kind, lexical_sites)
|
|
821
|
+
return nil if subclass_instance_may_answer?(rendered_receiver, class_name, call_node.name, scope,
|
|
822
|
+
call_node.receiver)
|
|
820
823
|
|
|
821
824
|
definition_site = project_definition_site(scope, class_name, call_node.name, kind)
|
|
822
825
|
# Issue #735 — the site is evidence, and whose RBS this is decides what it is evidence OF. On a
|
|
@@ -951,12 +954,51 @@ module Rigor
|
|
|
951
954
|
# (see {#ancestry_declares_method?} for why that placement is load-bearing, not tidiness).
|
|
952
955
|
def last_resort_surface_answers?(receiver_type, class_name, call_node, scope, kind, lexical_sites = nil)
|
|
953
956
|
return true if lexically_defined_method?(class_name, call_node, scope, kind, lexical_sites)
|
|
957
|
+
return true if project_patched_through_ancestors?(scope, class_name, call_node.name, kind,
|
|
958
|
+
call_node: call_node)
|
|
954
959
|
return true if module_mixin_receiver?(receiver_type, scope)
|
|
955
960
|
return true if mixin_self_class_receiver?(call_node, scope)
|
|
956
961
|
return true if unknown_mixin_includer?(class_name, scope)
|
|
957
962
|
return true if object_mixin_may_define?(receiver_type, class_name, call_node.name, scope)
|
|
963
|
+
return true if ancestry_declares_method?(scope, class_name, call_node.name, kind)
|
|
958
964
|
|
|
959
|
-
|
|
965
|
+
rbs_ancestor_may_answer?(class_name, call_node.name, scope, kind)
|
|
966
|
+
end
|
|
967
|
+
|
|
968
|
+
# Issue #1722 — a `method_missing`, or a project mixin, on one of the class's RBS ancestors
|
|
969
|
+
# ({RbsReceiverSurface}). Last for {#ancestry_declares_method?}'s reason: it walks the class graph.
|
|
970
|
+
def rbs_ancestor_may_answer?(class_name, method_name, scope, kind)
|
|
971
|
+
if kind == :singleton
|
|
972
|
+
RbsReceiverSurface.singleton_hook?(scope, class_name)
|
|
973
|
+
else
|
|
974
|
+
RbsReceiverSurface.ancestor_may_answer?(scope, class_name, method_name)
|
|
975
|
+
end
|
|
976
|
+
end
|
|
977
|
+
|
|
978
|
+
# Issue #1722 — a receiver typed by its class's name, rather than by a literal, may hold an instance of a
|
|
979
|
+
# subclass (a `#: () -> String` signature admits a `MyStr < String`), so a method a project subclass
|
|
980
|
+
# defines, or a subclass's `method_missing`, may be the one that runs. A value the engine built from a
|
|
981
|
+
# literal is exactly its class and keeps firing: a literal type (`"a"`, `1`, a union of them), a shape
|
|
982
|
+
# carrier (`{a: 1}`, `[1, 2]`, `{}`, `[]`, and what folds to one), or a receiver written as a literal
|
|
983
|
+
# (`"a#{x}b"`, which types as plain `String`). A local holding an interpolated String types as plain
|
|
984
|
+
# `String` with no origin, so it still declines. Instance side only.
|
|
985
|
+
def subclass_instance_may_answer?(receiver_type, class_name, method_name, scope, receiver_node = nil)
|
|
986
|
+
return false if receiver_type.is_a?(Type::Singleton)
|
|
987
|
+
return false if exact_literal_receiver?(receiver_type) || LITERAL_RECEIVER_NODES.include?(receiver_node.class)
|
|
988
|
+
|
|
989
|
+
RbsReceiverSurface.subclass_may_define?(scope, class_name, method_name)
|
|
990
|
+
end
|
|
991
|
+
|
|
992
|
+
EXACT_TYPES = [Type::Constant, Type::Tuple, Type::HashShape].freeze
|
|
993
|
+
LITERAL_RECEIVER_NODES = [
|
|
994
|
+
Prism::StringNode, Prism::InterpolatedStringNode, Prism::XStringNode, Prism::InterpolatedXStringNode,
|
|
995
|
+
Prism::ArrayNode, Prism::HashNode, Prism::RegularExpressionNode, Prism::InterpolatedRegularExpressionNode
|
|
996
|
+
].to_set.freeze
|
|
997
|
+
private_constant :EXACT_TYPES, :LITERAL_RECEIVER_NODES
|
|
998
|
+
|
|
999
|
+
def exact_literal_receiver?(receiver_type)
|
|
1000
|
+
members = receiver_type.is_a?(Type::Union) ? receiver_type.members : [receiver_type]
|
|
1001
|
+
members.all? { |member| EXACT_TYPES.any? { |kind| member.is_a?(kind) } }
|
|
960
1002
|
end
|
|
961
1003
|
|
|
962
1004
|
# Issue #1697 — a module the project mixes into `Object` (a top-level `include M`, or `include M` in
|
|
@@ -1058,10 +1100,13 @@ module Rigor
|
|
|
1058
1100
|
# Both paths sit at the same dispatcher precedence; the
|
|
1059
1101
|
# check must hold them together so neither rule fires a
|
|
1060
1102
|
# false positive.
|
|
1061
|
-
|
|
1103
|
+
#
|
|
1104
|
+
# `call_node:` (issue #1708), when given, is the call asking: a private or protected `pre_eval:` patch
|
|
1105
|
+
# it may not reach does not count ({#patch_callable?}).
|
|
1106
|
+
def source_declared_method?(scope, class_name, method_name, kind, call_node: nil)
|
|
1062
1107
|
return true if scope.discovered_method?(class_name, method_name, kind)
|
|
1063
1108
|
|
|
1064
|
-
project_patched_method?(scope, class_name, method_name, kind)
|
|
1109
|
+
project_patched_method?(scope, class_name, method_name, kind, call_node: call_node)
|
|
1065
1110
|
end
|
|
1066
1111
|
|
|
1067
1112
|
# Issue #723 — the same question as {#source_declared_method?}, asked through the project's own
|
|
@@ -1087,12 +1132,42 @@ module Rigor
|
|
|
1087
1132
|
# inference (between plugins and dependency-source).
|
|
1088
1133
|
# Returns false when the environment carries no registry
|
|
1089
1134
|
# (legacy path) or the lookup misses.
|
|
1090
|
-
def project_patched_method?(scope, class_name, method_name, kind)
|
|
1135
|
+
def project_patched_method?(scope, class_name, method_name, kind, call_node: nil)
|
|
1091
1136
|
environment = scope.environment
|
|
1092
1137
|
registry = environment&.project_patched_methods
|
|
1093
1138
|
return false if registry.nil? || registry.empty?
|
|
1094
1139
|
|
|
1095
|
-
|
|
1140
|
+
entry = registry.lookup(class_name: class_name.to_s, method_name: method_name.to_sym, kind: kind)
|
|
1141
|
+
!entry.nil? && patch_callable?(registry, entry, scope, call_node)
|
|
1142
|
+
end
|
|
1143
|
+
|
|
1144
|
+
# Issue #1708 — {#project_patched_method?} asked through the receiver's ancestors: a `pre_eval:` patch on
|
|
1145
|
+
# `Object`, `Numeric` or `Enumerable` gives an `Integer` or `Array` the method. Asked only once the
|
|
1146
|
+
# receiver's RBS has come back without the method, so a nearer class's own declaration still decides arity
|
|
1147
|
+
# and argument types; {#source_declared_method?} stays exact for the rules that read it for those.
|
|
1148
|
+
#
|
|
1149
|
+
# `call_node:` as {#source_declared_method?} reads it.
|
|
1150
|
+
def project_patched_through_ancestors?(scope, class_name, method_name, kind, call_node: nil)
|
|
1151
|
+
environment = scope.environment
|
|
1152
|
+
registry = environment&.project_patched_methods
|
|
1153
|
+
return false if registry.nil? || registry.empty?
|
|
1154
|
+
|
|
1155
|
+
entry = registry.lookup_through_ancestors(class_name: class_name.to_s, method_name: method_name.to_sym,
|
|
1156
|
+
kind: kind, rbs_loader: environment.rbs_loader)
|
|
1157
|
+
!entry.nil? && patch_callable?(registry, entry, scope, call_node)
|
|
1158
|
+
end
|
|
1159
|
+
|
|
1160
|
+
# Issue #1708 — whether `call_node` may reach the patch `entry`: Ruby raises NoMethodError for a private
|
|
1161
|
+
# patch called with an explicit receiver other than `self` (`1.kp` on `module Kernel; private def kp`), and
|
|
1162
|
+
# for a protected one called from outside its owner's instances. No call node, and a declined verdict (another
|
|
1163
|
+
# definer the call may reach, {Inference::ProjectPatchedMethods#callable?}), are the permissive answer.
|
|
1164
|
+
def patch_callable?(registry, entry, scope, call_node)
|
|
1165
|
+
return true if call_node.nil?
|
|
1166
|
+
|
|
1167
|
+
receiver = call_node.receiver
|
|
1168
|
+
verdict = registry.callable?(entry, explicit: !receiver.nil? && !receiver.is_a?(Prism::SelfNode),
|
|
1169
|
+
self_type: scope.self_type, rbs_loader: scope.environment.rbs_loader)
|
|
1170
|
+
verdict != false
|
|
1096
1171
|
end
|
|
1097
1172
|
|
|
1098
1173
|
# ADR-34 — `call.unresolved-toplevel`. Fires on an
|
|
@@ -1141,6 +1216,8 @@ module Rigor
|
|
|
1141
1216
|
return nil if scope.top_level_def_for(name)
|
|
1142
1217
|
return nil if source_declared_method?(scope, "Object", name, :instance)
|
|
1143
1218
|
return nil if Rigor::Reflection.instance_method_definition("Object", name, scope: scope)
|
|
1219
|
+
# Issue #1708 — `main` is an Object, so a `pre_eval:` patch on `Kernel` or `BasicObject` defines the name too.
|
|
1220
|
+
return nil if project_patched_through_ancestors?(scope, "Object", name, :instance)
|
|
1144
1221
|
return nil if main_singleton_method?(name, scope)
|
|
1145
1222
|
return nil if Inference::ObjectMixins.may_define?(scope, name, receiver: :implicit)
|
|
1146
1223
|
# `Target.class_eval { def added = 1; def use_added = added }` files the defs on Target
|
|
@@ -2143,6 +2220,7 @@ module Rigor
|
|
|
2143
2220
|
return true unless Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
|
|
2144
2221
|
return true unless definition_available?(member, class_name, scope)
|
|
2145
2222
|
return true if lookup_method(member, class_name, method_name, scope)
|
|
2223
|
+
return true if project_patched_through_ancestors?(scope, class_name, method_name, :instance)
|
|
2146
2224
|
|
|
2147
2225
|
# Issue #723 — the arm's own RBS does not have it; its project ancestry still might. Last, for
|
|
2148
2226
|
# the reason {#ancestry_declares_method?} carries: this is the probe that walks the class graph.
|
|
@@ -2196,12 +2274,30 @@ module Rigor
|
|
|
2196
2274
|
# `compose_codepoints` typed `Hash | Hash` for an `Array`, flagging
|
|
2197
2275
|
# `.pack`). Require at least two distinct arm classes.
|
|
2198
2276
|
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)
|
|
2277
|
+
return nil if union_may_answer?(members, call_node, scope, lexical_sites)
|
|
2201
2278
|
|
|
2202
2279
|
build_undefined_method_diagnostic(path, call_node, receiver_type)
|
|
2203
2280
|
end
|
|
2204
2281
|
|
|
2282
|
+
# Whether some arm of a non-nil union of classes may answer the call, which keeps the union rule silent.
|
|
2283
|
+
def union_may_answer?(members, call_node, scope, lexical_sites)
|
|
2284
|
+
return true if members.any? { |member| method_present_anywhere?(member, call_node.name, scope) }
|
|
2285
|
+
return true if members.any? { |member| arm_surface_may_answer?(member, call_node.name, scope) }
|
|
2286
|
+
# Issue #1722 — `def o.m`, `o.extend(M)` and their kin give the one object the method, whatever its class.
|
|
2287
|
+
return true if lexical_sites&.singleton_local_def?(call_node)
|
|
2288
|
+
|
|
2289
|
+
refined_on_some_arm?(members, call_node, scope, lexical_sites)
|
|
2290
|
+
end
|
|
2291
|
+
|
|
2292
|
+
# Issue #1722 — the scalar rule's RBS-ancestor and subclass declines, asked of one arm. Kept out of
|
|
2293
|
+
# {#method_present_anywhere?}, which `call.possible-nil-receiver` also asks: there a "present" answer is
|
|
2294
|
+
# what lets the rule fire, and a `method_missing` on `Object` answers on the `nil` arm too.
|
|
2295
|
+
def arm_surface_may_answer?(member, method_name, scope)
|
|
2296
|
+
class_name = concrete_class_name(member)
|
|
2297
|
+
rbs_ancestor_may_answer?(class_name, method_name, scope, :instance) ||
|
|
2298
|
+
subclass_instance_may_answer?(member, class_name, method_name, scope)
|
|
2299
|
+
end
|
|
2300
|
+
|
|
2205
2301
|
# ADR-121 WD7 — a refinement in effect (or one Rigor cannot read) may define the name on one arm's class.
|
|
2206
2302
|
def refined_on_some_arm?(members, call_node, scope, lexical_sites)
|
|
2207
2303
|
members.any? do |member|
|
|
@@ -2865,6 +2961,13 @@ module Rigor
|
|
|
2865
2961
|
# - Issue #1568 — a module the class PREPENDS sits ahead of it in Ruby's order, so a definition there
|
|
2866
2962
|
# is what the call reaches; the class's own private `def` answers only when nothing prepended
|
|
2867
2963
|
# defines the name ({#reached_definer_visibility}).
|
|
2964
|
+
# - Issue #1984 — a class-object receiver (`Type::Singleton`, `K.x`) is checked against the singleton side:
|
|
2965
|
+
# the definition the call reaches on `K`'s singleton chain (its own singleton methods, its extended
|
|
2966
|
+
# modules' instance methods, then the superclass's singleton chain) must be recorded `:private`
|
|
2967
|
+
# (`private_class_method`, `class << self` with `private`, a private method of an extended module).
|
|
2968
|
+
# - Issue #1984 — Ruby hands a private call to `method_missing`, so a `method_missing` or
|
|
2969
|
+
# `respond_to_missing?` a project entry on the receiver's chain records, on the side the call reaches,
|
|
2970
|
+
# declines the rule ({#missing_hook_on_chain?}).
|
|
2868
2971
|
def visibility_mismatch_diagnostic(path, call_node, scope_index, lexical_sites = nil)
|
|
2869
2972
|
return nil unless explicit_non_self_receiver?(call_node.receiver)
|
|
2870
2973
|
|
|
@@ -2876,18 +2979,125 @@ module Rigor
|
|
|
2876
2979
|
return nil if inferred_param_receiver?(call_node, scope)
|
|
2877
2980
|
|
|
2878
2981
|
receiver_type = scope.type_of(call_node.receiver)
|
|
2879
|
-
|
|
2982
|
+
case receiver_type
|
|
2983
|
+
when Type::Nominal then instance_visibility_mismatch(path, call_node, scope, receiver_type, lexical_sites)
|
|
2984
|
+
when Type::Singleton then singleton_visibility_mismatch(path, call_node, scope, receiver_type, lexical_sites)
|
|
2985
|
+
end
|
|
2986
|
+
end
|
|
2880
2987
|
|
|
2988
|
+
def instance_visibility_mismatch(path, call_node, scope, receiver_type, lexical_sites)
|
|
2881
2989
|
visibility = scope.discovered_method_visibility(receiver_type.class_name, call_node.name)
|
|
2882
2990
|
return nil unless visibility == :private
|
|
2883
2991
|
|
|
2884
2992
|
reached = reached_definer_visibility(scope, receiver_type.class_name, call_node.name)
|
|
2885
2993
|
return nil if reached == :undecided
|
|
2886
2994
|
return nil unless reached.nil? || reached == :private
|
|
2995
|
+
return nil if missing_hook_on_chain?(scope, receiver_type.class_name, :instance)
|
|
2887
2996
|
# ADR-121 WD7 — a refinement in effect (or one Rigor cannot read) may redefine the method, public.
|
|
2888
2997
|
return nil if refined_method_in_effect?(receiver_type.class_name, call_node, scope, :instance, lexical_sites)
|
|
2889
2998
|
|
|
2890
|
-
build_visibility_mismatch_diagnostic(path, call_node, receiver_type)
|
|
2999
|
+
build_visibility_mismatch_diagnostic(path, call_node, receiver_type.class_name)
|
|
3000
|
+
end
|
|
3001
|
+
|
|
3002
|
+
# Issue #1984 — the singleton side reads only the reached definer: a private singleton method may be inherited
|
|
3003
|
+
# (`private_class_method :new` on a superclass) or come from an extended module, so there is no own-table
|
|
3004
|
+
# gate as on the instance side. It reports only:
|
|
3005
|
+
#
|
|
3006
|
+
# - a constant receiver (`K.x`, `A::K.x`): `self.class.x` or a local may hold a subclass that makes the name
|
|
3007
|
+
# public again (`public_class_method :new`, a template-method `def self.x`);
|
|
3008
|
+
# - a call inside a `def` ({#load_time_call?}): code that runs while the files load (a class body's
|
|
3009
|
+
# `ALL = [K.new]` before its `private_class_method :new`) may run before the call is private;
|
|
3010
|
+
# - no class on the chain may have had every singleton name made public (`K.public_class_method(*names)`,
|
|
3011
|
+
# `K.singleton_class.class_eval { … }`; {#singleton_wildcard_contested?}).
|
|
3012
|
+
def singleton_visibility_mismatch(path, call_node, scope, receiver_type, lexical_sites)
|
|
3013
|
+
class_name = receiver_type.class_name
|
|
3014
|
+
return nil unless call_node.receiver.is_a?(Prism::ConstantReadNode) ||
|
|
3015
|
+
call_node.receiver.is_a?(Prism::ConstantPathNode)
|
|
3016
|
+
return nil unless private_singleton_candidate?(scope, call_node.name)
|
|
3017
|
+
return nil if load_time_call?(scope, call_node)
|
|
3018
|
+
return nil unless reached_definer_visibility(scope, class_name, call_node.name, :singleton) == :private
|
|
3019
|
+
return nil if missing_hook_on_chain?(scope, class_name, :singleton)
|
|
3020
|
+
return nil if singleton_wildcard_contested?(scope, class_name)
|
|
3021
|
+
return nil if refined_method_in_effect?(class_name, call_node, scope, :singleton, lexical_sites)
|
|
3022
|
+
|
|
3023
|
+
build_visibility_mismatch_diagnostic(path, call_node, "singleton(#{class_name})")
|
|
3024
|
+
end
|
|
3025
|
+
|
|
3026
|
+
PRIVATE_NAMES_MEMO = :rigor_check_rules_private_singleton_names
|
|
3027
|
+
private_constant :PRIVATE_NAMES_MEMO
|
|
3028
|
+
|
|
3029
|
+
# Issue #1984 — the table-only gate the singleton side asks before it reads a chain, as the instance side asks
|
|
3030
|
+
# its own table first: whether any class records `name` private, on its singleton side or on its instance
|
|
3031
|
+
# side (an extended module's private method is private on the extender's singleton). Without it every
|
|
3032
|
+
# `Widget.new` would read, and file the dependency edges of, the whole singleton chain. The set is memoised per
|
|
3033
|
+
# discovery index.
|
|
3034
|
+
def private_singleton_candidate?(scope, name)
|
|
3035
|
+
discovery = scope.discovery
|
|
3036
|
+
slot = Thread.current[PRIVATE_NAMES_MEMO]
|
|
3037
|
+
unless slot && slot[0].equal?(discovery)
|
|
3038
|
+
slot = [discovery, private_recorded_names(discovery.discovered_method_visibilities)]
|
|
3039
|
+
Thread.current[PRIVATE_NAMES_MEMO] = slot
|
|
3040
|
+
end
|
|
3041
|
+
slot[1].include?(name.to_sym)
|
|
3042
|
+
end
|
|
3043
|
+
|
|
3044
|
+
def private_recorded_names(tables)
|
|
3045
|
+
tables.each_value.with_object(Set.new) do |table, names|
|
|
3046
|
+
table.each { |slot, visibility| names << (slot.is_a?(Array) ? slot.last : slot) if visibility == :private }
|
|
3047
|
+
end
|
|
3048
|
+
end
|
|
3049
|
+
|
|
3050
|
+
# Issue #1984 — whether the call is outside every `def` the file's deferred ranges name. A `def` is named
|
|
3051
|
+
# 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
|
|
3052
|
+
# load time and may be reported. The top level, class bodies, and blocks and lambdas that are not such a `def`
|
|
3053
|
+
# (a `map { … }` or `define_method` block, a `-> { }`) are load time and decline: such code may run while the
|
|
3054
|
+
# files load, before a later `private_class_method` or in a load order where the private declaration has not
|
|
3055
|
+
# run yet.
|
|
3056
|
+
def load_time_call?(scope, call_node)
|
|
3057
|
+
rows = scope.discovered_deferred_ranges[scope.source_path]
|
|
3058
|
+
return true if rows.nil?
|
|
3059
|
+
|
|
3060
|
+
offset = call_node.location.start_offset
|
|
3061
|
+
rows.none? { |(start, finish, name, _kind, _owner)| !name.nil? && start <= offset && offset < finish }
|
|
3062
|
+
end
|
|
3063
|
+
|
|
3064
|
+
SINGLETON_WILDCARD_SLOT = %i[singleton *].freeze
|
|
3065
|
+
private_constant :SINGLETON_WILDCARD_SLOT
|
|
3066
|
+
|
|
3067
|
+
# Issue #1984 — whether a project class on `class_name`'s singleton chain has its wildcard singleton slot
|
|
3068
|
+
# contested: a call the walk saw may have made any of its singleton methods public.
|
|
3069
|
+
def singleton_wildcard_contested?(scope, class_name)
|
|
3070
|
+
discovery = scope.discovery
|
|
3071
|
+
chain = Scope::ResolutionChain.for(scope, class_name.to_s, :singleton, :methods)
|
|
3072
|
+
chain.entries.any? do |entry|
|
|
3073
|
+
!entry.external? && discovery.contested?(:discovered_method_visibilities,
|
|
3074
|
+
[entry.name, SINGLETON_WILDCARD_SLOT])
|
|
3075
|
+
end
|
|
3076
|
+
end
|
|
3077
|
+
|
|
3078
|
+
VISIBILITY_MISSING_HOOKS = %i[method_missing respond_to_missing?].freeze
|
|
3079
|
+
private_constant :VISIBILITY_MISSING_HOOKS
|
|
3080
|
+
|
|
3081
|
+
# Issue #1984 — whether a project entry on `class_name`'s chain for `side` records a `method_missing` or
|
|
3082
|
+
# `respond_to_missing?` on the side that entry stands for (a singleton entry's singleton methods, a mixin's
|
|
3083
|
+
# instance methods), or the chain cannot be read whole. Ruby sends a private call to `method_missing`, so
|
|
3084
|
+
# such a hook may answer it.
|
|
3085
|
+
def missing_hook_on_chain?(scope, class_name, side)
|
|
3086
|
+
chain = Scope::ResolutionChain.for(scope, class_name.to_s, side, :methods)
|
|
3087
|
+
return true if chain.truncated?
|
|
3088
|
+
|
|
3089
|
+
chain.entries.any? do |entry|
|
|
3090
|
+
next false if entry.external?
|
|
3091
|
+
|
|
3092
|
+
kind = entry.side == :singleton ? :singleton : :instance
|
|
3093
|
+
VISIBILITY_MISSING_HOOKS.any? { |hook| records_hook?(scope, entry.name, hook, kind) }
|
|
3094
|
+
end
|
|
3095
|
+
end
|
|
3096
|
+
|
|
3097
|
+
def records_hook?(scope, owner, hook, kind)
|
|
3098
|
+
return true if scope.discovered_method?(owner, hook, kind)
|
|
3099
|
+
|
|
3100
|
+
!(kind == :singleton ? scope.singleton_def_for(owner, hook) : scope.user_def_for(owner, hook)).nil?
|
|
2891
3101
|
end
|
|
2892
3102
|
|
|
2893
3103
|
# Issue #1568, ADR-119 C1c — the recorded visibility of the definition a call on `class_name` reaches: the
|
|
@@ -2899,8 +3109,8 @@ module Rigor
|
|
|
2899
3109
|
# producers contest is not alone (`DefinerResolution` asks the next candidate too), so it is `:undecided`
|
|
2900
3110
|
# there without a read of the table here. nil is nothing recording the name at all, and the class's own
|
|
2901
3111
|
# 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,
|
|
3112
|
+
def reached_definer_visibility(scope, class_name, method_name, side = :instance)
|
|
3113
|
+
case Inference::DefinerResolution.resolve(scope, class_name, method_name, side, question: :visibility)
|
|
2904
3114
|
in Inference::DefinerResolution::Known(answer:)
|
|
2905
3115
|
answer
|
|
2906
3116
|
in Inference::DefinerResolution::UNKNOWN
|
|
@@ -2917,14 +3127,14 @@ module Rigor
|
|
|
2917
3127
|
true
|
|
2918
3128
|
end
|
|
2919
3129
|
|
|
2920
|
-
def build_visibility_mismatch_diagnostic(path, call_node,
|
|
3130
|
+
def build_visibility_mismatch_diagnostic(path, call_node, receiver_name)
|
|
2921
3131
|
Diagnostic.from_message_loc(
|
|
2922
3132
|
call_node,
|
|
2923
3133
|
rule: RULE_VISIBILITY_MISMATCH,
|
|
2924
3134
|
path: path,
|
|
2925
|
-
message: "private method `#{call_node.name}' called on #{
|
|
3135
|
+
message: "private method `#{call_node.name}' called on #{receiver_name} receiver",
|
|
2926
3136
|
severity: :error,
|
|
2927
|
-
receiver_type:
|
|
3137
|
+
receiver_type: receiver_name,
|
|
2928
3138
|
method_name: call_node.name.to_s
|
|
2929
3139
|
)
|
|
2930
3140
|
end
|
|
@@ -4001,6 +4211,9 @@ module Rigor
|
|
|
4001
4211
|
# prove a reduction, stay silent.
|
|
4002
4212
|
return nil if parent_visibility.nil?
|
|
4003
4213
|
return nil unless visibility_reduced?(parent_visibility, override_visibility)
|
|
4214
|
+
# Issue #1569 — a module function's instance copy is private because `module_function` made it so; the
|
|
4215
|
+
# author's intent is the public singleton copy, so that reduction is not reported.
|
|
4216
|
+
return nil if scope.module_function_def?(class_name, def_node)
|
|
4004
4217
|
|
|
4005
4218
|
build_override_visibility_diagnostic(
|
|
4006
4219
|
path, def_node, parent_class, parent_visibility, override_visibility
|
|
@@ -6,6 +6,7 @@ require_relative "return_type_heuristic"
|
|
|
6
6
|
require_relative "../../source/constant_path"
|
|
7
7
|
require_relative "../../source/node_children"
|
|
8
8
|
require_relative "../../inference/scope_indexer"
|
|
9
|
+
require_relative "../../rooted_glob"
|
|
9
10
|
|
|
10
11
|
module Rigor
|
|
11
12
|
module Analysis
|
|
@@ -127,7 +128,7 @@ module Rigor
|
|
|
127
128
|
def walk_root(root_dir, harvest, budget) # rubocop:disable Naming/PredicateMethod
|
|
128
129
|
return false unless File.directory?(root_dir)
|
|
129
130
|
|
|
130
|
-
|
|
131
|
+
RootedGlob.glob(root_dir, "**/*.rb").each do |path|
|
|
131
132
|
harvest_file(path, harvest, budget)
|
|
132
133
|
return true if harvest.catalog.size >= budget
|
|
133
134
|
end
|
|
@@ -194,6 +194,55 @@ module Rigor
|
|
|
194
194
|
moved.freeze
|
|
195
195
|
end
|
|
196
196
|
|
|
197
|
+
# Issue #1833 — the `[class name, mark]` pairs that appeared in or vanished from the class marks of the files an
|
|
198
|
+
# edit touched: a Symbol key of a `discovered_parameter_envelopes` bucket (the class-wide marks of
|
|
199
|
+
# `Scope::DiscoveryIndex`) or a `[ENVELOPE_REMOVED_KIND, name]` entry. A file may mark a class it does not declare
|
|
200
|
+
# (`x.singleton_class.prepend(Comparable)`, `o.extend(Helper)` in a method body), so neither the class-declaration
|
|
201
|
+
# diff nor a reader's edge to the declaring files sees such a mark appear or vanish.
|
|
202
|
+
#
|
|
203
|
+
# `before` and `after` are lists of envelope tables (`{class name => bucket}`): the touched files' seed-bundle
|
|
204
|
+
# tables before the edit, and the merged table of the re-parsed files after it. The diff is of their unions, which
|
|
205
|
+
# moves whenever the project's union of marks moves (the untouched files contribute the same marks to both).
|
|
206
|
+
# Returns a frozen Set of frozen `[class name, mark]` pairs.
|
|
207
|
+
def changed_class_marks(before, after)
|
|
208
|
+
(class_marks(before) ^ class_marks(after)).freeze
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def class_marks(tables)
|
|
212
|
+
marks = Set.new
|
|
213
|
+
tables.each do |table|
|
|
214
|
+
table&.each do |class_name, bucket|
|
|
215
|
+
bucket.each_key { |key| marks << [class_name, key].freeze if class_mark?(key) }
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
marks
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def class_mark?(key)
|
|
222
|
+
key.is_a?(Symbol) || (key.is_a?(Array) && key.first == Scope::DiscoveryIndex::ENVELOPE_REMOVED_KIND)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Issue #1661 — the names of the classes and modules whose unpositioned mixin edges
|
|
226
|
+
# (`Scope::DiscoveryIndex#unpositioned_mixins`, `"*"` included) appeared or vanished in the files an edit
|
|
227
|
+
# touched. A file can taint a module it does not otherwise change the chain of (`module Comparable;
|
|
228
|
+
# include(*MODS); end`, `Comparable.include(x) if c`), and an external chain entry has no declaring file
|
|
229
|
+
# for a reader's edge to name. `before` and `after` are lists of `{class => {side => [names]}}` tables,
|
|
230
|
+
# diffed as unions like {changed_class_marks}. Returns a frozen Set of class names.
|
|
231
|
+
def changed_unpositioned_owners(before, after)
|
|
232
|
+
moved = unpositioned_edges(before) ^ unpositioned_edges(after)
|
|
233
|
+
moved.to_set { |(owner, _side, _name)| owner }.freeze
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def unpositioned_edges(tables)
|
|
237
|
+
edges = Set.new
|
|
238
|
+
tables.each do |table|
|
|
239
|
+
table&.each do |owner, sides|
|
|
240
|
+
sides.each { |side, names| names.each { |name| edges << [owner, side, name].freeze } }
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
edges
|
|
244
|
+
end
|
|
245
|
+
|
|
197
246
|
# Issue #644 — the qualified constant names whose PUBLICATION could have moved in this edit: the names
|
|
198
247
|
# whose per-file census descriptor differs between the before- and after-state of any file in `paths`.
|
|
199
248
|
# The value-constant twin of {appeared_classes}, and deliberately NOT its analogue — a class either
|