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
|
@@ -62,9 +62,14 @@ module Rigor
|
|
|
62
62
|
# may be one of its descendants (#1878): a `def self.x = const_get(:X)` runs on whichever subclass it
|
|
63
63
|
# is called on, and a module's method on whichever class includes, prepends or extends it. {Graph}
|
|
64
64
|
# then looks the literal name up in those descendants as well.
|
|
65
|
+
#
|
|
66
|
+
# `anywhere` is true when the receiver is a `self` the walk cannot name at all — the block of an eval call
|
|
67
|
+
# on a local or another computed receiver (`k.class_eval { const_get(:E) }`, #1946). {Graph} then keeps
|
|
68
|
+
# every declaration the literal could name.
|
|
65
69
|
DynamicUse = Data.define(:name, :prefix, :reason, :site, :path, :line, :scope, :within, :inherit, :role,
|
|
66
|
-
:receiver_open) do
|
|
67
|
-
def initialize(scope: :namespace, within: nil, inherit: true, role: :production, receiver_open: false,
|
|
70
|
+
:receiver_open, :anywhere) do
|
|
71
|
+
def initialize(scope: :namespace, within: nil, inherit: true, role: :production, receiver_open: false,
|
|
72
|
+
anywhere: false, **)
|
|
68
73
|
super
|
|
69
74
|
end
|
|
70
75
|
|
|
@@ -102,7 +107,18 @@ module Rigor
|
|
|
102
107
|
end
|
|
103
108
|
end
|
|
104
109
|
|
|
105
|
-
|
|
110
|
+
# An ancestry edge written outside a declaration's header and body statements (#1946): `K.include(Mix)`,
|
|
111
|
+
# `K.send(:include, Mix)` and the `prepend` / `extend` / `public_send` / `__send__` forms, an `include`
|
|
112
|
+
# in an eval block on `K` or nested in a conditional, and the superclass of `K = Class.new(Base)`. `target`
|
|
113
|
+
# is the class the module joins, `ancestor` the module (or superclass); each is a {Reference} resolved in
|
|
114
|
+
# the call's nesting, or nil when it is not a constant the walk can name — a local receiver, an
|
|
115
|
+
# `include` whose `self` is unknown, or a computed argument (`K.include(mods.first)`). {Graph} reads
|
|
116
|
+
# these only to widen a `const_get` on `self`'s reach, never for ordinary constant resolution.
|
|
117
|
+
Ancestry = Data.define(:target, :ancestor, :path, :line)
|
|
118
|
+
|
|
119
|
+
Result = Data.define(:declarations, :references, :dynamic_uses, :ancestries) do
|
|
120
|
+
def initialize(ancestries: [].freeze, **) = super
|
|
121
|
+
end
|
|
106
122
|
|
|
107
123
|
# Roles a referring file can have (ADR-102 WD8). A reference edge carries its referrer's role so
|
|
108
124
|
# "used only by its own test" is a reportable category rather than a bucket boundary.
|
|
@@ -163,7 +179,7 @@ module Rigor
|
|
|
163
179
|
walker = Walker.new(path: path, role: role || role_for(path, root: root))
|
|
164
180
|
walker.walk(parsed.value, [])
|
|
165
181
|
Result.new(declarations: walker.declarations.freeze, references: walker.references.freeze,
|
|
166
|
-
dynamic_uses: walker.dynamic_uses.freeze)
|
|
182
|
+
dynamic_uses: walker.dynamic_uses.freeze, ancestries: walker.ancestries.freeze)
|
|
167
183
|
end
|
|
168
184
|
|
|
169
185
|
# The values a local variable can hold, read off its assignments in one local scope, for a
|
|
@@ -263,17 +279,47 @@ module Rigor
|
|
|
263
279
|
EVAL_FAMILY = %i[class_eval module_eval class_exec module_exec instance_eval instance_exec].freeze
|
|
264
280
|
private_constant :EVAL_FAMILY
|
|
265
281
|
|
|
282
|
+
# Calls that define a method from their block: `define_method`'s block is an instance method's body,
|
|
283
|
+
# `define_singleton_method`'s a singleton method's (#1946).
|
|
284
|
+
METHOD_DEFINERS = { define_method: :instance, define_singleton_method: :singleton }.freeze
|
|
285
|
+
private_constant :METHOD_DEFINERS
|
|
286
|
+
|
|
266
287
|
# A call is a dynamic-resolution site, and its block may run with another `self` ({#rebinds_self?}).
|
|
267
288
|
def record_call(node, nesting)
|
|
268
289
|
record_dynamic_use(node, nesting)
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
290
|
+
record_mixin(node, nesting)
|
|
291
|
+
return unless node.block.is_a?(Prism::BlockNode)
|
|
292
|
+
|
|
293
|
+
classify_block(node, nesting)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def classify_block(node, nesting)
|
|
297
|
+
block = node.block
|
|
298
|
+
if rebinds_self?(node)
|
|
299
|
+
@rebinding_blocks << block
|
|
300
|
+
frame = eval_frame(node, nesting)
|
|
301
|
+
@eval_frames[block] = frame if frame
|
|
302
|
+
elsif meta_new_call?(node)
|
|
303
|
+
@meta_new_blocks << block
|
|
304
|
+
elsif METHOD_DEFINERS.key?(node.name) && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
|
|
305
|
+
@definer_blocks[block] = METHOD_DEFINERS.fetch(node.name)
|
|
306
|
+
end
|
|
273
307
|
end
|
|
274
308
|
|
|
275
|
-
|
|
276
|
-
|
|
309
|
+
# The `self` of an eval call's block (#1878, #1946): the receiver when it is a constant, `:unknown` when
|
|
310
|
+
# it is a local or another computed value, and nil — the reading the walk had before — on `self`,
|
|
311
|
+
# `self.class`, an implicit receiver or `Object`. `singleton_defs` is whether a plain `def` in the block
|
|
312
|
+
# defines a singleton method of the receiver (`instance_eval`) rather than an instance method.
|
|
313
|
+
def eval_frame(call, nesting)
|
|
314
|
+
return anonymous_subclass_frame(call, nesting) if call.name == :new
|
|
315
|
+
return nil unless EVAL_FAMILY.include?(call.name)
|
|
316
|
+
|
|
317
|
+
singleton_defs = %i[instance_eval instance_exec].include?(call.name)
|
|
318
|
+
receiver = call.receiver
|
|
319
|
+
return EvalFrame.new(receiver_reference(receiver, nesting), singleton_defs) if constant_receiver?(receiver)
|
|
320
|
+
return nil if receiver.nil? || self_receiver?(receiver) || constant_path?(receiver)
|
|
321
|
+
|
|
322
|
+
EvalFrame.new(:unknown, singleton_defs)
|
|
277
323
|
end
|
|
278
324
|
|
|
279
325
|
# Rigor knows the argument's shape, which is the whole reason this can be tiered rather than treated
|
|
@@ -306,6 +352,7 @@ module Rigor
|
|
|
306
352
|
else []
|
|
307
353
|
end
|
|
308
354
|
bounds = [[nil, :namespace, nil]] if bounds.empty?
|
|
355
|
+
record_anywhere(node, name, reason)
|
|
309
356
|
bounds.each do |prefix, scope, within|
|
|
310
357
|
@dynamic_uses << DynamicUse.new(name: name, prefix: prefix, scope: scope, within: within,
|
|
311
358
|
inherit: anchors.include?(TOP_LEVEL), role: @role,
|
|
@@ -338,21 +385,55 @@ module Rigor
|
|
|
338
385
|
end
|
|
339
386
|
end
|
|
340
387
|
|
|
388
|
+
# A literal `const_get` on a `self` the walk cannot name at all reaches any declaration of that name.
|
|
389
|
+
def record_anywhere(node, name, reason)
|
|
390
|
+
return unless name && !name.start_with?("::") && unknown_self?(node)
|
|
391
|
+
|
|
392
|
+
@dynamic_uses << DynamicUse.new(name: name, prefix: nil, anywhere: true, role: @role, site: site(node),
|
|
393
|
+
path: @path, line: node.location.start_line, reason: reason)
|
|
394
|
+
end
|
|
395
|
+
|
|
341
396
|
# Whether a `const_get` on `self` can run on a descendant of the enclosing declaration (#1878): in a
|
|
342
397
|
# method, or a block that rebinds `self`, `self` is whichever subclass or includer the method was
|
|
343
398
|
# called on. Directly in a class body, or inside `Other.class_eval { }`, it is the declaration itself.
|
|
344
399
|
def runtime_self?(node)
|
|
345
|
-
node.name == :const_get && self_receiver?(node.receiver) && @
|
|
400
|
+
node.name == :const_get && self_receiver?(node.receiver) && @eval_frame.nil? &&
|
|
346
401
|
!(@self_kind == :declaration && !@in_block && !@self_rebound)
|
|
347
402
|
end
|
|
348
403
|
|
|
404
|
+
# Whether a `const_get` on `self` runs on a receiver the walk cannot name at all (#1946): directly in the
|
|
405
|
+
# block of an eval call on a computed receiver, or in a method that block defines when the receiver form
|
|
406
|
+
# names the eval receiver ({#method_receiver?}).
|
|
407
|
+
def unknown_self?(node)
|
|
408
|
+
return false unless node.name == :const_get && self_receiver?(node.receiver)
|
|
409
|
+
return @eval_frame.receiver == :unknown && !node.receiver.is_a?(Prism::CallNode) if @eval_frame
|
|
410
|
+
|
|
411
|
+
!@eval_method.nil? && @eval_method.receiver == :unknown && method_receiver?(node.receiver)
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
# Whether `receiver` names the eval receiver in a method an eval block defines: `self.class` in an
|
|
415
|
+
# instance method (`self` is an instance of the receiver or of a descendant), an implicit receiver or
|
|
416
|
+
# `self` in a singleton method. An implicit `const_get` in an instance method raises `NoMethodError`.
|
|
417
|
+
def method_receiver?(receiver)
|
|
418
|
+
class_of_self = receiver.is_a?(Prism::CallNode)
|
|
419
|
+
@eval_method.kind == :instance ? class_of_self : !class_of_self
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# Inside a method an eval block on a constant defines, the receiver form that names that constant reads
|
|
423
|
+
# it as a runtime `self` that may be any of its descendants (#1946).
|
|
424
|
+
def eval_method_references(receiver)
|
|
425
|
+
return [] unless @eval_method&.receiver.is_a?(Reference) && method_receiver?(receiver)
|
|
426
|
+
|
|
427
|
+
[@eval_method.receiver]
|
|
428
|
+
end
|
|
429
|
+
|
|
349
430
|
# Where a `const_get` on `self` (implicit, `self`, or `self.class`) looks its name up: `:declaration`
|
|
350
431
|
# when `self` is the enclosing class or module, `:top_level` when it is `Class` or a singleton class,
|
|
351
432
|
# `:unknown` otherwise. A block's `self` is unknown — `included do`, `class_eval` and an instance-
|
|
352
433
|
# evaluated DSL block (`configure { }`, an RSpec `describe` body) all rebind it, and the walk cannot
|
|
353
434
|
# tell those from an `each` block. So is the `self` of a `def Other.x` or of a `class << Other` body.
|
|
354
435
|
def self_lookup(receiver)
|
|
355
|
-
return :declaration if @
|
|
436
|
+
return :declaration if @eval_frame&.receiver.is_a?(Reference) && !receiver.is_a?(Prism::CallNode)
|
|
356
437
|
return :unknown if @in_block || @self_rebound
|
|
357
438
|
|
|
358
439
|
class_of_self = receiver.is_a?(Prism::CallNode)
|
|
@@ -374,7 +455,7 @@ module Rigor
|
|
|
374
455
|
receiver = node.receiver
|
|
375
456
|
namespaces =
|
|
376
457
|
if self_receiver?(receiver)
|
|
377
|
-
eval_self_references(receiver) || self_references(node, nesting)
|
|
458
|
+
eval_self_references(receiver) || (self_references(node, nesting) + eval_method_references(receiver))
|
|
378
459
|
elsif constant_receiver?(receiver)
|
|
379
460
|
[receiver_reference(receiver, nesting)]
|
|
380
461
|
else
|
|
@@ -388,7 +469,8 @@ module Rigor
|
|
|
388
469
|
|
|
389
470
|
# Inside `Other.class_eval { }` an implicit or `self` receiver is `Other` (#1878).
|
|
390
471
|
def eval_self_references(receiver)
|
|
391
|
-
|
|
472
|
+
frame = @eval_frame
|
|
473
|
+
[frame.receiver] if frame&.receiver.is_a?(Reference) && !receiver.is_a?(Prism::CallNode)
|
|
392
474
|
end
|
|
393
475
|
|
|
394
476
|
# The enclosing declaration as a rooted {Reference}, so {Graph} anchors it as it anchors a constant
|
|
@@ -407,9 +489,13 @@ module Rigor
|
|
|
407
489
|
def rebinds_self?(call)
|
|
408
490
|
return true if SELF_REBINDING.include?(call.name)
|
|
409
491
|
|
|
492
|
+
@owner.nil? && meta_new_call?(call)
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def meta_new_call?(call)
|
|
410
496
|
recv = call.receiver
|
|
411
497
|
name = recv && Source::ConstantPath.qualified_name_or_nil(recv)&.delete_prefix("::")
|
|
412
|
-
|
|
498
|
+
!name.nil? && META_NEW[name] == call.name
|
|
413
499
|
end
|
|
414
500
|
|
|
415
501
|
def self_receiver?(receiver)
|
|
@@ -490,11 +576,127 @@ module Rigor
|
|
|
490
576
|
end
|
|
491
577
|
end
|
|
492
578
|
|
|
579
|
+
# #1946 — the {Walker}'s reading of ancestry written outside a declaration's own statements, as
|
|
580
|
+
# {Ancestry} edges: receiver-form and `send`-spelled mixin calls, a mixin call whose `self` is not the
|
|
581
|
+
# enclosing declaration body, and the superclass of `K = Class.new(Base)`.
|
|
582
|
+
module AncestrySites
|
|
583
|
+
private
|
|
584
|
+
|
|
585
|
+
# An anonymous `Class.new(Base) { }` block's `self` is a subclass of `Base` that declares no constant of
|
|
586
|
+
# its own, so a `const_get` on it is read at `Base` (#1946).
|
|
587
|
+
def anonymous_subclass_frame(call, nesting)
|
|
588
|
+
superclass = call.arguments&.arguments&.first
|
|
589
|
+
return nil unless call.receiver.is_a?(Prism::ConstantReadNode) && call.receiver.name == :Class &&
|
|
590
|
+
constant_receiver?(superclass)
|
|
591
|
+
|
|
592
|
+
EvalFrame.new(receiver_reference(superclass, nesting), false)
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
def constant_path?(node)
|
|
596
|
+
node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
# Mixin calls: the ones a class body writes, and their `send` spellings.
|
|
600
|
+
MIXINS = %i[include prepend extend].freeze
|
|
601
|
+
private_constant :MIXINS
|
|
602
|
+
|
|
603
|
+
SENDS = %i[send public_send __send__].freeze
|
|
604
|
+
private_constant :SENDS
|
|
605
|
+
|
|
606
|
+
# Records an {Ancestry} for a mixin call the declaration's own statements do not carry (#1946). A
|
|
607
|
+
# receiver-less `include` directly in a class body is already one of its `includes` when every argument
|
|
608
|
+
# is a constant; a computed one (`include Mods.pick`) is an ancestor the walk cannot name. A `self`
|
|
609
|
+
# argument is the `self` the call is written in (`Foo.include(self)` in `module Mix`), not the receiver.
|
|
610
|
+
def record_mixin(node, nesting)
|
|
611
|
+
modules = mixin_arguments(node)
|
|
612
|
+
return if modules.nil? || modules.empty?
|
|
613
|
+
return if node.receiver.nil? && !SENDS.include?(node.name) && @body_statements.include?(node) &&
|
|
614
|
+
modules.all? { |arg| mixin_reference(arg, nesting) }
|
|
615
|
+
|
|
616
|
+
target = mixin_target(node, nesting)
|
|
617
|
+
modules.each do |arg|
|
|
618
|
+
ancestor = arg.is_a?(Prism::SelfNode) ? self_target(node, nesting) : mixin_reference(arg, nesting)
|
|
619
|
+
next if target.nil? && ancestor.nil?
|
|
620
|
+
|
|
621
|
+
@ancestries << Ancestry.new(target: target, ancestor: ancestor, path: @path,
|
|
622
|
+
line: node.location.start_line)
|
|
623
|
+
end
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
# The module arguments of a mixin call, or nil when it is not one.
|
|
627
|
+
def mixin_arguments(node)
|
|
628
|
+
args = node.arguments&.arguments || []
|
|
629
|
+
return args if MIXINS.include?(node.name)
|
|
630
|
+
return nil unless SENDS.include?(node.name)
|
|
631
|
+
|
|
632
|
+
verb = args.first
|
|
633
|
+
name = verb.unescaped if verb.is_a?(Prism::SymbolNode) || verb.is_a?(Prism::StringNode)
|
|
634
|
+
# Compared as strings: `to_sym` raises on a literal that is not valid in its encoding (`send("\xFF")`).
|
|
635
|
+
MIXINS.any? { |mixin| mixin.name == name } ? args.drop(1) : nil
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
def mixin_reference(arg, nesting)
|
|
639
|
+
return nil unless constant_path?(arg) && Source::ConstantPath.qualified_name_or_nil(arg)
|
|
640
|
+
|
|
641
|
+
receiver_reference(arg, nesting)
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
# The class a mixin call adds to: a constant receiver, or for `self` and an implicit receiver whatever
|
|
645
|
+
# `self` is — the eval receiver in an eval block, the enclosing declaration in its body. Anything else,
|
|
646
|
+
# the top level (`Object`, whose descendants are every class) included, is unknown (nil).
|
|
647
|
+
def mixin_target(node, nesting)
|
|
648
|
+
receiver = node.receiver
|
|
649
|
+
return mixin_reference(receiver, nesting) unless receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
650
|
+
|
|
651
|
+
self_target(node, nesting)
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
# What `self` is at `node` as a {Reference}, where the walk can name it; nil otherwise.
|
|
655
|
+
def self_target(node, nesting)
|
|
656
|
+
frame = @eval_frame
|
|
657
|
+
return frame&.receiver.is_a?(Reference) ? frame.receiver : nil if frame || @eval_method
|
|
658
|
+
return nil if @in_block || @self_rebound || @self_kind != :declaration || nesting.empty?
|
|
659
|
+
|
|
660
|
+
rooted_reference(nesting.join("::"), node)
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
def rooted_reference(fqn, node)
|
|
664
|
+
Reference.new(as_written: fqn, nesting: [].freeze, from: nil, role: @role, path: @path,
|
|
665
|
+
line: node.location.start_line, rooted: true)
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
# `K = Class.new(Base)` makes `Base` an ancestor of `K` (#1946). It stays out of the declaration's
|
|
669
|
+
# `superclass`, which ordinary constant resolution reads: the engine does not resolve a constant
|
|
670
|
+
# through a `Class.new` superclass either, and the two are pinned to agree.
|
|
671
|
+
def record_class_new_superclass(call, fqn, nesting)
|
|
672
|
+
superclass = call.arguments&.arguments&.first
|
|
673
|
+
return unless call.receiver.name == :Class && superclass
|
|
674
|
+
|
|
675
|
+
ancestor = if superclass.is_a?(Prism::SelfNode) then self_target(call, nesting)
|
|
676
|
+
else mixin_reference(superclass, nesting)
|
|
677
|
+
end
|
|
678
|
+
@ancestries << Ancestry.new(target: rooted_reference(fqn, call), ancestor: ancestor,
|
|
679
|
+
path: @path, line: call.location.start_line)
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
# The receiver-less statements directly in a declaration body.
|
|
683
|
+
def body_statements(body)
|
|
684
|
+
body.child_nodes.select { |stmt| stmt.is_a?(Prism::CallNode) && stmt.receiver.nil? }
|
|
685
|
+
.to_set.compare_by_identity
|
|
686
|
+
end
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
# The `self` of an eval block ({DynamicSites#eval_frame}): a {Reference}, or `:unknown`.
|
|
690
|
+
EvalFrame = Data.define(:receiver, :singleton_defs)
|
|
691
|
+
# A method an eval block defines on its receiver, and whether it is an `:instance` or `:singleton` method.
|
|
692
|
+
EvalMethod = Data.define(:receiver, :kind)
|
|
693
|
+
|
|
493
694
|
# Single-pass walker. Tracks `nesting` as the stack of enclosing declaration names.
|
|
494
695
|
class Walker
|
|
495
696
|
include DynamicSites
|
|
697
|
+
include AncestrySites
|
|
496
698
|
|
|
497
|
-
attr_reader :declarations, :references, :dynamic_uses
|
|
699
|
+
attr_reader :declarations, :references, :dynamic_uses, :ancestries
|
|
498
700
|
|
|
499
701
|
def initialize(path:, role:)
|
|
500
702
|
@path = path
|
|
@@ -502,6 +704,10 @@ module Rigor
|
|
|
502
704
|
@declarations = []
|
|
503
705
|
@references = []
|
|
504
706
|
@dynamic_uses = []
|
|
707
|
+
@ancestries = []
|
|
708
|
+
# The receiver-less statements directly in the declaration body being walked, which its own
|
|
709
|
+
# `includes` already carry. See {#record_mixin}.
|
|
710
|
+
@body_statements = Set.new.compare_by_identity
|
|
505
711
|
# The declaration a meta-new rvalue is being walked for, or nil. See {#walk_constant_write}.
|
|
506
712
|
@owner = nil
|
|
507
713
|
# The node whose subtree holds the local variables in scope: the file, a method, or a class body.
|
|
@@ -512,10 +718,15 @@ module Rigor
|
|
|
512
718
|
# `class_eval` or anonymous `Class.new` block. See {#rebinds_self?}.
|
|
513
719
|
@self_rebound = false
|
|
514
720
|
@rebinding_blocks = Set.new.compare_by_identity
|
|
515
|
-
# The
|
|
516
|
-
#
|
|
517
|
-
@
|
|
518
|
-
@
|
|
721
|
+
# The {EvalFrame} of each `*_eval` block, and the one whose `self` the walk is directly in (nil when
|
|
722
|
+
# `self` is not an eval receiver). See {#eval_frame}.
|
|
723
|
+
@eval_frames = {}.compare_by_identity
|
|
724
|
+
@eval_frame = nil
|
|
725
|
+
# Inside a method an eval block defines (a `def`, or a `define_method` / `define_singleton_method`
|
|
726
|
+
# block), the eval receiver and the method's kind; nil elsewhere. See {#eval_method_references}.
|
|
727
|
+
@eval_method = nil
|
|
728
|
+
@meta_new_blocks = Set.new.compare_by_identity
|
|
729
|
+
@definer_blocks = {}.compare_by_identity
|
|
519
730
|
# What `self` is here, for {#self_lookup}: `:declaration` in a class or module body (and at the top
|
|
520
731
|
# level), `:singleton_method` in a `def self.x` or a def in a `class << self` body,
|
|
521
732
|
# `:instance_method` in any other def, `:singleton_class` in a `class << self` body outside its
|
|
@@ -583,14 +794,18 @@ module Rigor
|
|
|
583
794
|
|
|
584
795
|
# The body's nesting names the declaration itself, so a credit inherited from a meta-new rvalue
|
|
585
796
|
# this declaration sits in must not leak into it.
|
|
586
|
-
|
|
587
|
-
|
|
797
|
+
walk_body(node.body, nesting + [name], module_body)
|
|
798
|
+
end
|
|
799
|
+
|
|
800
|
+
def walk_body(body, nesting, module_body)
|
|
801
|
+
saved = [@owner, @self_rebound, @in_block, @module_body, @body_statements]
|
|
588
802
|
@owner = nil
|
|
589
803
|
@self_rebound = @in_block = false
|
|
590
804
|
@module_body = module_body
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
805
|
+
@body_statements = body_statements(body)
|
|
806
|
+
in_local_scope(body, :declaration) { walk(body, nesting) }
|
|
807
|
+
ensure
|
|
808
|
+
@owner, @self_rebound, @in_block, @module_body, @body_statements = saved
|
|
594
809
|
end
|
|
595
810
|
|
|
596
811
|
# A meta-new rvalue — `Class.new(Base) { ... }`, `Struct.new(:a) do ... end` — is the declaration's
|
|
@@ -623,6 +838,7 @@ module Rigor
|
|
|
623
838
|
fqn = (nesting + [node.name.to_s]).join("::")
|
|
624
839
|
@declarations << Declaration.new(fqn: fqn, path: @path,
|
|
625
840
|
line: node.location.start_line, superclass: nil, includes: [].freeze)
|
|
841
|
+
record_class_new_superclass(call, fqn, nesting)
|
|
626
842
|
fqn
|
|
627
843
|
end
|
|
628
844
|
|
|
@@ -630,24 +846,35 @@ module Rigor
|
|
|
630
846
|
# top level of the body is inspected: a mixin applied inside a conditional or a nested def is not a
|
|
631
847
|
# static ancestor edge.
|
|
632
848
|
def mixin_names(body)
|
|
633
|
-
body.child_nodes.
|
|
634
|
-
next unless stmt.is_a?(Prism::CallNode) && %i[include prepend extend].include?(stmt.name)
|
|
635
|
-
next if stmt.receiver
|
|
849
|
+
body.child_nodes.flat_map do |stmt|
|
|
850
|
+
next [] unless stmt.is_a?(Prism::CallNode) && %i[include prepend extend].include?(stmt.name)
|
|
851
|
+
next [] if stmt.receiver
|
|
636
852
|
|
|
637
|
-
|
|
638
|
-
arg && Source::ConstantPath.qualified_name_or_nil(arg)
|
|
853
|
+
(stmt.arguments&.arguments || []).filter_map { |arg| Source::ConstantPath.qualified_name_or_nil(arg) }
|
|
639
854
|
end
|
|
640
855
|
end
|
|
641
856
|
|
|
642
857
|
def in_local_scope(scope, self_kind = self_kind_in(scope))
|
|
643
|
-
saved = [@local_scope, @block_params, @self_kind, @
|
|
644
|
-
@
|
|
858
|
+
saved = [@local_scope, @block_params, @self_kind, @eval_frame, @eval_method]
|
|
859
|
+
@eval_method = eval_def(scope)
|
|
860
|
+
@eval_frame = nil
|
|
645
861
|
@local_scope = scope
|
|
646
862
|
@block_params = Set.new.freeze
|
|
647
863
|
@self_kind = self_kind
|
|
648
864
|
yield
|
|
649
865
|
ensure
|
|
650
|
-
@local_scope, @block_params, @self_kind, @
|
|
866
|
+
@local_scope, @block_params, @self_kind, @eval_frame, @eval_method = saved
|
|
867
|
+
end
|
|
868
|
+
|
|
869
|
+
# The method a `def` directly in an eval block defines on the eval receiver (#1946): a plain `def` is an
|
|
870
|
+
# instance method, or a singleton one under `instance_eval`; a `def self.x` is a singleton method.
|
|
871
|
+
def eval_def(scope)
|
|
872
|
+
frame = @eval_frame
|
|
873
|
+
return nil unless frame && scope.is_a?(Prism::DefNode)
|
|
874
|
+
|
|
875
|
+
if scope.receiver.nil? then EvalMethod.new(frame.receiver, frame.singleton_defs ? :singleton : :instance)
|
|
876
|
+
elsif scope.receiver.is_a?(Prism::SelfNode) then EvalMethod.new(frame.receiver, :singleton)
|
|
877
|
+
end
|
|
651
878
|
end
|
|
652
879
|
|
|
653
880
|
# The {#self_lookup} kind inside `scope`, from the kind around it. Only a receiver-less def or a `def
|
|
@@ -681,17 +908,30 @@ module Rigor
|
|
|
681
908
|
end
|
|
682
909
|
|
|
683
910
|
def in_block(block)
|
|
684
|
-
saved = [@block_params, @self_rebound, @in_block, @
|
|
911
|
+
saved = [@block_params, @self_rebound, @in_block, @eval_frame, @eval_method]
|
|
685
912
|
names = block.parameters ? LocalValues.bindings(block.parameters) : []
|
|
686
913
|
@block_params = (@block_params | names).freeze unless names.empty?
|
|
687
|
-
|
|
688
|
-
@self_rebound = true
|
|
689
|
-
@eval_receiver = @eval_receivers[block]
|
|
690
|
-
end
|
|
914
|
+
enter_block_self(block)
|
|
691
915
|
@in_block = true
|
|
692
916
|
yield
|
|
693
917
|
ensure
|
|
694
|
-
@block_params, @self_rebound, @in_block, @
|
|
918
|
+
@block_params, @self_rebound, @in_block, @eval_frame, @eval_method = saved
|
|
919
|
+
end
|
|
920
|
+
|
|
921
|
+
# A block whose `self` is another object drops the eval reading around it: an eval block sets its own
|
|
922
|
+
# (#1878), and a `Class.new` / `Struct.new` block's `self` is the class it builds. A `define_method` /
|
|
923
|
+
# `define_singleton_method` block directly in an eval block is a method of the eval receiver (#1946).
|
|
924
|
+
def enter_block_self(block)
|
|
925
|
+
if @rebinding_blocks.include?(block)
|
|
926
|
+
@self_rebound = true
|
|
927
|
+
@eval_frame = @eval_frames[block]
|
|
928
|
+
@eval_method = nil
|
|
929
|
+
elsif @meta_new_blocks.include?(block)
|
|
930
|
+
@eval_frame = @eval_method = nil
|
|
931
|
+
elsif @eval_frame && @definer_blocks.key?(block)
|
|
932
|
+
@eval_method = EvalMethod.new(@eval_frame.receiver, @definer_blocks.fetch(block))
|
|
933
|
+
@eval_frame = nil
|
|
934
|
+
end
|
|
695
935
|
end
|
|
696
936
|
|
|
697
937
|
def site(node)
|
|
@@ -99,7 +99,12 @@ module Rigor
|
|
|
99
99
|
"Rigor's bundled overlays name (`OpenStruct`, whose fields are per-instance singleton methods; " \
|
|
100
100
|
"`ActiveSupport::Duration` and `ActiveSupport::Concern` when their gem overlay loads). Both sides " \
|
|
101
101
|
"of the class are open, and an inherited Object / Kernel signature no longer decides " \
|
|
102
|
-
"`call.wrong-arity` or `call.argument-type-mismatch` on it."
|
|
102
|
+
"`call.wrong-arity` or `call.argument-type-mismatch` on it.",
|
|
103
|
+
"The program may answer the call another way (#1722): a `method_missing` on the receiver's class or an " \
|
|
104
|
+
"ancestor; a module mixed into either that defines the method (`Integer.include(Ext)` counts wherever " \
|
|
105
|
+
"it is written); singleton methods given to the receiver local (`s.extend(M)`, " \
|
|
106
|
+
"`define_singleton_method`, `class << s`); or, for a receiver typed by its class's name rather than a " \
|
|
107
|
+
"literal, a project subclass that defines the method."
|
|
103
108
|
],
|
|
104
109
|
suppression: "`# rigor:disable call.undefined-method` on the call line, " \
|
|
105
110
|
"or `disable: [\"call.undefined-method\"]` in `.rigor.yml`.",
|
|
@@ -561,14 +566,22 @@ module Rigor
|
|
|
561
566
|
summary: "Explicit-receiver call to a method declared `private` in source.",
|
|
562
567
|
fires_when: [
|
|
563
568
|
"Call is `receiver.method(...)` with explicit non-self receiver.",
|
|
564
|
-
"Receiver type resolves to `Type::Nominal[X]`.",
|
|
565
|
-
"X is a user-defined class whose source carries the method under `private
|
|
569
|
+
"Receiver type resolves to `Type::Nominal[X]`, or to the class object `singleton(X)`.",
|
|
570
|
+
"X is a user-defined class whose source carries the method under `private`; for a class object, " \
|
|
571
|
+
"the singleton method the call reaches is private (`private_class_method`, `class << self` with " \
|
|
572
|
+
"`private`, or a private method of a module X extends)."
|
|
566
573
|
],
|
|
567
574
|
does_not_fire_when: [
|
|
568
575
|
"Implicit-self call (no receiver) — always allowed for private.",
|
|
569
576
|
"Receiver is `self` (Ruby 2.7+ permits `self.private_method`).",
|
|
570
577
|
"Receiver class is RBS-known but not user-source-defined (RBS-side visibility is deferred).",
|
|
571
|
-
"Method is `:protected` (subclass tracking is deferred)."
|
|
578
|
+
"Method is `:protected` (subclass tracking is deferred).",
|
|
579
|
+
"A `method_missing` or `respond_to_missing?` on the receiver's chain, on the side the call reaches, " \
|
|
580
|
+
"may answer the call (Ruby sends a private call to `method_missing`).",
|
|
581
|
+
"The visibility rests on a statement that may not run, or on files that disagree about it.",
|
|
582
|
+
"On a class object: the receiver is not a constant (`self.class.x` may be a subclass that " \
|
|
583
|
+
"re-publicises the name), or the call is not inside a `def` (load-time code may run before the " \
|
|
584
|
+
"method is made private)."
|
|
572
585
|
],
|
|
573
586
|
suppression: "`# rigor:disable method-visibility-mismatch`.",
|
|
574
587
|
severity_authored: :error,
|