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
|
@@ -8,6 +8,7 @@ require_relative "../project_environment"
|
|
|
8
8
|
require_relative "../scope"
|
|
9
9
|
require_relative "../reflection"
|
|
10
10
|
require_relative "../type"
|
|
11
|
+
require_relative "../inference/reflective_ivar_write"
|
|
11
12
|
require_relative "../source/constant_path"
|
|
12
13
|
require_relative "../source/literals"
|
|
13
14
|
require_relative "../source/node_children"
|
|
@@ -25,6 +26,7 @@ require_relative "inline_declarations"
|
|
|
25
26
|
require_relative "layout_index"
|
|
26
27
|
require_relative "method_candidate"
|
|
27
28
|
require_relative "writer"
|
|
29
|
+
require_relative "../rooted_glob"
|
|
28
30
|
|
|
29
31
|
module Rigor
|
|
30
32
|
module SigGen
|
|
@@ -98,6 +100,7 @@ module Rigor
|
|
|
98
100
|
# (`build_candidate`, `method_def_prefix`) a never-nil invariant without per-call-site defensive guards.
|
|
99
101
|
@namespace_kinds = {}
|
|
100
102
|
@module_function_methods = Set.new
|
|
103
|
+
@uncertain_module_function_defs = Set.new.compare_by_identity
|
|
101
104
|
@class_shells = Set.new
|
|
102
105
|
@class_superclasses = {}
|
|
103
106
|
@meta_layouts = {}
|
|
@@ -446,7 +449,7 @@ module Rigor
|
|
|
446
449
|
def resolve_paths(args)
|
|
447
450
|
args.flat_map do |arg|
|
|
448
451
|
if File.directory?(arg)
|
|
449
|
-
|
|
452
|
+
RootedGlob.glob(arg, "**/*.rb")
|
|
450
453
|
elsif File.file?(arg) && arg.end_with?(".rb")
|
|
451
454
|
[arg]
|
|
452
455
|
else
|
|
@@ -463,10 +466,7 @@ module Rigor
|
|
|
463
466
|
base_scope = Scope.empty(environment: environment)
|
|
464
467
|
scope_index = Inference::ScopeIndexer.index(parse_result.value, default_scope: base_scope)
|
|
465
468
|
|
|
466
|
-
|
|
467
|
-
@module_function_methods = Set.new
|
|
468
|
-
@class_shells = Set.new
|
|
469
|
-
@class_superclasses = {}
|
|
469
|
+
reset_file_maps
|
|
470
470
|
@meta_layouts = collect_meta_layouts(scope_index)
|
|
471
471
|
register_meta_classes
|
|
472
472
|
defs = collect_method_definitions(parse_result.value, scope_index)
|
|
@@ -488,6 +488,15 @@ module Rigor
|
|
|
488
488
|
collect_attr_candidates(parse_result.value, path, scope_index, obs_ivar_map)
|
|
489
489
|
end
|
|
490
490
|
|
|
491
|
+
# The per-file scratch state {#analyse_file} starts each file with.
|
|
492
|
+
def reset_file_maps
|
|
493
|
+
@namespace_kinds = {}
|
|
494
|
+
@module_function_methods = Set.new
|
|
495
|
+
@uncertain_module_function_defs = Set.new.compare_by_identity
|
|
496
|
+
@class_shells = Set.new
|
|
497
|
+
@class_superclasses = {}
|
|
498
|
+
end
|
|
499
|
+
|
|
491
500
|
# Walks the AST collecting `(def_node, class_name, kind)` tuples for every `def` Rigor can re-type. Slice 1
|
|
492
501
|
# covered instance `def foo` methods inside a nameable `class` / `module` body. Slice 4 extends this to
|
|
493
502
|
# singleton-side methods via `def self.foo` and `class << self; def foo; end`. A plain top-level `def` is
|
|
@@ -732,6 +741,8 @@ module Rigor
|
|
|
732
741
|
class_name = prefix.join("::")
|
|
733
742
|
if Inference::ModuleFunctionState.sig_gen_module_function?(module_function_active, kind)
|
|
734
743
|
@module_function_methods << [class_name, node.name]
|
|
744
|
+
elsif Inference::ModuleFunctionState.sig_gen_uncertain?(module_function_active, kind)
|
|
745
|
+
@uncertain_module_function_defs << node
|
|
735
746
|
end
|
|
736
747
|
out << [node, class_name, kind]
|
|
737
748
|
end
|
|
@@ -783,9 +794,13 @@ module Rigor
|
|
|
783
794
|
# private methods are implementation details, not part of the type contract downstream consumers (Steep,
|
|
784
795
|
# IDE, gem users) read. The default now skips private and protected methods; the `:include_private` flag
|
|
785
796
|
# restores the slice-4 behaviour for callers that want every method.
|
|
797
|
+
#
|
|
798
|
+
# A module function renders as `def self?.name`, which RBS reads as a public singleton method and a private
|
|
799
|
+
# instance method, so a `private` written before the `module_function` toggle does not exclude it (#1968).
|
|
786
800
|
def visibility_excludes?(def_node, class_name, kind, scope_index)
|
|
787
801
|
return false if kind == :singleton
|
|
788
802
|
return false if @include_private
|
|
803
|
+
return false if @module_function_methods.include?([class_name, def_node.name])
|
|
789
804
|
|
|
790
805
|
scope = scope_index[def_node] || scope_index.each_value.first
|
|
791
806
|
return false if scope.nil?
|
|
@@ -960,6 +975,9 @@ module Rigor
|
|
|
960
975
|
# #1676 — a top-level `def` is collected with no class; it is reported, never typed.
|
|
961
976
|
def classify_collected_def(path, def_node, class_name, kind, scope_index)
|
|
962
977
|
return skipped(path, def_node, class_name, kind, :top_level_def) if class_name.nil?
|
|
978
|
+
if @uncertain_module_function_defs.include?(def_node)
|
|
979
|
+
return skipped(path, def_node, class_name, kind, :uncertain_module_function)
|
|
980
|
+
end
|
|
963
981
|
|
|
964
982
|
classify_def(path, def_node, class_name, kind, scope_index)
|
|
965
983
|
end
|
|
@@ -1421,12 +1439,17 @@ module Rigor
|
|
|
1421
1439
|
# same position a method with no declaration at all is in. Over a declaration the author wrote, though, a
|
|
1422
1440
|
# VALUE literal the body proves (`-> 1`, `-> "x"`, `-> :k`) is an implementation detail the `untyped` was
|
|
1423
1441
|
# abstracting over, so it is widened to its class (`1 | 2` to `Integer`, `"S" | nil` to `(String | nil)`; #1489).
|
|
1424
|
-
# `
|
|
1425
|
-
#
|
|
1426
|
-
#
|
|
1427
|
-
#
|
|
1442
|
+
# `true` and `false` widen to `bool` (#1891): a predicate's base `-> true` made every subclass override
|
|
1443
|
+
# returning `false` a `def.override-return-widened`. A return that is only `nil` has no such widening — it is
|
|
1444
|
+
# the shape of a template method a subclass fills in — so it is declined and the `untyped` stays. `nil` in a
|
|
1445
|
+
# union (`T?`) and a computed `Float` are honest types and stay. An `untyped` that is only the inline reader's
|
|
1446
|
+
# synthesis ({#synthesized_declaration?}) is no word of the author's and keeps the strictest carrier.
|
|
1447
|
+
# `declared_return_rbs` carries `"untyped"` so the `[tighter, was: untyped]` print tag says a declaration
|
|
1448
|
+
# existed.
|
|
1428
1449
|
def declared_untyped_candidate(path, def_node, class_name, kind, inferred, method_def, rendered: nil)
|
|
1429
1450
|
unless synthesized_declaration?(method_def)
|
|
1451
|
+
return equivalent(path, def_node, class_name, kind, inferred, "untyped") if bare_nil?(inferred)
|
|
1452
|
+
|
|
1430
1453
|
widened = widen_value_literals(inferred)
|
|
1431
1454
|
unless widened.equal?(inferred)
|
|
1432
1455
|
# A `rendered` line was spelled from the unwidened type; there is nothing to rebuild it from.
|
|
@@ -1446,6 +1469,7 @@ module Rigor
|
|
|
1446
1469
|
def widen_value_literals(type)
|
|
1447
1470
|
case type
|
|
1448
1471
|
when Type::Constant
|
|
1472
|
+
return widen_boolean_literal if [true, false].include?(type.value)
|
|
1449
1473
|
return type unless VALUE_LITERAL_CLASSES.any? { |klass| type.value.instance_of?(klass) }
|
|
1450
1474
|
|
|
1451
1475
|
Type::Combinator.nominal_of(type.value.class.name)
|
|
@@ -1457,6 +1481,14 @@ module Rigor
|
|
|
1457
1481
|
end
|
|
1458
1482
|
end
|
|
1459
1483
|
|
|
1484
|
+
def widen_boolean_literal
|
|
1485
|
+
Type::Combinator.union(Type::Combinator.constant_of(true), Type::Combinator.constant_of(false))
|
|
1486
|
+
end
|
|
1487
|
+
|
|
1488
|
+
def bare_nil?(type)
|
|
1489
|
+
type.is_a?(Type::Constant) && type.value.nil?
|
|
1490
|
+
end
|
|
1491
|
+
|
|
1460
1492
|
# A declaration only the inline reader's synthesis (a `virtual:` buffer) supplies — ADR-93 gives every `def` an
|
|
1461
1493
|
# `untyped` member, annotated or not — is no word of the author's about the return, so it keeps the proposal
|
|
1462
1494
|
# a method with no declaration at all gets: the strictest carrier the body proves.
|
|
@@ -1590,11 +1622,13 @@ module Rigor
|
|
|
1590
1622
|
end
|
|
1591
1623
|
|
|
1592
1624
|
# `Type::Constant#erase_to_rbs` spells the values RBS writes as literal types (`true` / `false` /
|
|
1593
|
-
# `nil`, an Integer, a Symbol, a String) as that literal and every other carrier as
|
|
1594
|
-
#
|
|
1595
|
-
#
|
|
1625
|
+
# `nil`, an Integer, a Symbol, a String) as that literal and every other carrier as a nominal. Asking
|
|
1626
|
+
# which of the two branches it took keeps the list of literal-spellable classes from forking a second
|
|
1627
|
+
# copy into this file. A Range's nominal carries its element type (`Range[Integer]`, #1891), so it is
|
|
1628
|
+
# excluded by class rather than by comparing the erasure with the class name: like a Float it is a
|
|
1629
|
+
# nominal, not a pinned literal.
|
|
1596
1630
|
def rbs_literal?(type)
|
|
1597
|
-
type.is_a?(Type::Constant) && type.erase_to_rbs != type.value.class.name
|
|
1631
|
+
type.is_a?(Type::Constant) && !type.value.is_a?(Range) && type.erase_to_rbs != type.value.class.name
|
|
1598
1632
|
end
|
|
1599
1633
|
|
|
1600
1634
|
# Heuristic added after the third-round self-dogfood: `FallbackTracer#size` body is `@events.size`, where
|
|
@@ -1837,7 +1871,8 @@ module Rigor
|
|
|
1837
1871
|
# pre-pass resolved the type to `nil` or `Dynamic[top]` — typically because `@ivar = param` inside
|
|
1838
1872
|
# `initialize` typed the param as `untyped` — the observation-derived type is substituted. This lets
|
|
1839
1873
|
# `attr_reader :name` emit a concrete type when `ClassName.new("alice")` call sites are visible to the
|
|
1840
|
-
# observation scan.
|
|
1874
|
+
# observation scan. {#build_observed_ivar_map} leaves out an ivar anything else writes, so a writer-backed
|
|
1875
|
+
# field is skipped (issue #1410).
|
|
1841
1876
|
def ivar_type_lookup(scope_index, class_name, obs_ivar_map = {})
|
|
1842
1877
|
any_scope = scope_index.each_value.first
|
|
1843
1878
|
return ->(_) {} if any_scope.nil?
|
|
@@ -1854,12 +1889,107 @@ module Rigor
|
|
|
1854
1889
|
# ivars assigned directly from `def initialize` parameters. Only populated when `@observations` is
|
|
1855
1890
|
# non-empty (i.e. `--params=observed` was supplied). Matches the pattern `@ivar_name = param_name` where
|
|
1856
1891
|
# `param_name` is a required / optional positional or keyword parameter of `initialize`.
|
|
1892
|
+
#
|
|
1893
|
+
# Issue #1410: the observed arguments describe the field only while those `@ivar = param_name` writes are
|
|
1894
|
+
# all that writes it. An `attr_writer` / `attr_accessor`, a write in another instance method or block, or
|
|
1895
|
+
# any other write in `initialize` lets the field hold values the observation never saw, so such an ivar is
|
|
1896
|
+
# left out and its accessor is skipped as `untyped_return`, as a hand-written setter is.
|
|
1857
1897
|
def build_observed_ivar_map(root)
|
|
1858
1898
|
return {} if @observations.empty?
|
|
1859
1899
|
|
|
1860
1900
|
result = {}
|
|
1861
1901
|
collect_init_ivar_obs(root, [], result)
|
|
1862
|
-
|
|
1902
|
+
other_writes = Hash.new { |h, k| h[k] = Set.new }
|
|
1903
|
+
collect_other_ivar_writes(root, [], false, other_writes)
|
|
1904
|
+
result.to_h do |class_name, map|
|
|
1905
|
+
others = other_writes[class_name]
|
|
1906
|
+
[class_name, others.include?(ANY_IVAR) ? {} : map.reject { |attr_name, _| others.include?(:"@#{attr_name}") }]
|
|
1907
|
+
end
|
|
1908
|
+
end
|
|
1909
|
+
|
|
1910
|
+
IVAR_WRITE_NODES = [
|
|
1911
|
+
Prism::InstanceVariableWriteNode, Prism::InstanceVariableOperatorWriteNode,
|
|
1912
|
+
Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode, Prism::InstanceVariableTargetNode
|
|
1913
|
+
].freeze
|
|
1914
|
+
ANY_IVAR = :*
|
|
1915
|
+
private_constant :IVAR_WRITE_NODES, :ANY_IVAR
|
|
1916
|
+
|
|
1917
|
+
# Records into `other_writes[class_name]` every instance ivar a class writes other than through an
|
|
1918
|
+
# `initialize`'s `@ivar = param_name`: the ivars an implicit-self `attr_writer` / `attr_accessor` names,
|
|
1919
|
+
# and every write inside an instance `def` or a block, an `instance_variable_set` on `self` included
|
|
1920
|
+
# ({#written_ivar}). `def self.x`, a `class << self` body and a bare class-body write set the class
|
|
1921
|
+
# object's ivars and do not count.
|
|
1922
|
+
def collect_other_ivar_writes(node, prefix, in_block, other_writes)
|
|
1923
|
+
return unless node.is_a?(Prism::Node)
|
|
1924
|
+
|
|
1925
|
+
case node
|
|
1926
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
1927
|
+
child_prefix = Source::ConstantPath.declaration_prefix(prefix, node.constant_path)
|
|
1928
|
+
return collect_other_ivar_writes(node.body, child_prefix, false, other_writes) if child_prefix
|
|
1929
|
+
when Prism::SingletonClassNode then return
|
|
1930
|
+
when Prism::DefNode then return record_def_ivar_writes(node, prefix, other_writes)
|
|
1931
|
+
when Prism::BlockNode, Prism::LambdaNode then in_block = true
|
|
1932
|
+
end
|
|
1933
|
+
record_class_level_write(node, prefix, in_block, other_writes) unless prefix.empty?
|
|
1934
|
+
|
|
1935
|
+
node.rigor_each_child { |child| collect_other_ivar_writes(child, prefix, in_block, other_writes) }
|
|
1936
|
+
end
|
|
1937
|
+
|
|
1938
|
+
# The ivars an `attr_writer` / `attr_accessor` on `self` names, or the ivar a write in a block sets.
|
|
1939
|
+
def record_class_level_write(node, prefix, in_block, other_writes)
|
|
1940
|
+
writes = other_writes[prefix.join("::")]
|
|
1941
|
+
attr_writer_names(node).each { |name| writes << :"@#{name}" }
|
|
1942
|
+
name = written_ivar(node) if in_block
|
|
1943
|
+
writes << name if name
|
|
1944
|
+
end
|
|
1945
|
+
|
|
1946
|
+
ATTR_WRITER_DECLARATIONS = %i[attr_writer attr_accessor].freeze
|
|
1947
|
+
SEND_NAMES = %i[send public_send __send__].freeze
|
|
1948
|
+
private_constant :ATTR_WRITER_DECLARATIONS, :SEND_NAMES
|
|
1949
|
+
|
|
1950
|
+
# The literal names `attr_writer :x` / `attr_accessor :x` declares when called on `self`, implicitly or as
|
|
1951
|
+
# `self.`, or through `send` / `public_send` / `__send__` with the declaration's name as a literal first
|
|
1952
|
+
# argument (`send(:attr_writer, :x)`). Any other call declares none this check sees.
|
|
1953
|
+
def attr_writer_names(node)
|
|
1954
|
+
return [] unless node.is_a?(Prism::CallNode) && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
|
|
1955
|
+
|
|
1956
|
+
names = Source::Literals.symbol_arguments(node)
|
|
1957
|
+
return names if ATTR_WRITER_DECLARATIONS.include?(node.name)
|
|
1958
|
+
return [] unless SEND_NAMES.include?(node.name)
|
|
1959
|
+
|
|
1960
|
+
ATTR_WRITER_DECLARATIONS.include?(Source::Literals.symbol_arg(node, 0)) ? names.drop(1) : []
|
|
1961
|
+
end
|
|
1962
|
+
|
|
1963
|
+
# Every ivar write in an instance `def` counts, except, in `initialize`, the `@ivar = param_name` write
|
|
1964
|
+
# {#scan_ivar_param_assignments} keys the observation on.
|
|
1965
|
+
def record_def_ivar_writes(def_node, prefix, other_writes)
|
|
1966
|
+
return if def_node.body.nil? || def_node.receiver || prefix.empty?
|
|
1967
|
+
|
|
1968
|
+
credited = Set.new.compare_by_identity
|
|
1969
|
+
if def_node.name == :initialize && def_node.parameters
|
|
1970
|
+
params = build_init_param_index(def_node.parameters).keys.to_set
|
|
1971
|
+
scan_ivar_param_assignments(def_node.body, params, {}, credited)
|
|
1972
|
+
end
|
|
1973
|
+
each_node(def_node.body) do |node|
|
|
1974
|
+
name = written_ivar(node)
|
|
1975
|
+
other_writes[prefix.join("::")] << name if name && !credited.include?(node)
|
|
1976
|
+
end
|
|
1977
|
+
end
|
|
1978
|
+
|
|
1979
|
+
# The ivar `node` writes: a write node's name, an `instance_variable_set` on `self`'s literal name, or
|
|
1980
|
+
# `ANY_IVAR` for one whose name the analysis cannot read ({Inference::ReflectiveIvarWrite}).
|
|
1981
|
+
def written_ivar(node)
|
|
1982
|
+
return node.name if IVAR_WRITE_NODES.any? { |klass| node.is_a?(klass) }
|
|
1983
|
+
return nil unless Inference::ReflectiveIvarWrite.self_write?(node)
|
|
1984
|
+
|
|
1985
|
+
Inference::ReflectiveIvarWrite.literal_name(node) || ANY_IVAR
|
|
1986
|
+
end
|
|
1987
|
+
|
|
1988
|
+
def each_node(node, &)
|
|
1989
|
+
return unless node.is_a?(Prism::Node)
|
|
1990
|
+
|
|
1991
|
+
yield node
|
|
1992
|
+
node.rigor_each_child { |child| each_node(child, &) }
|
|
1863
1993
|
end
|
|
1864
1994
|
|
|
1865
1995
|
def collect_init_ivar_obs(node, prefix, result)
|
|
@@ -1945,21 +2075,36 @@ module Rigor
|
|
|
1945
2075
|
|
|
1946
2076
|
# Walk a def body for direct `@ivar = local_var` assignments where `local_var` is one of the listed
|
|
1947
2077
|
# parameter names. Records ivar_name (Symbol with `@` prefix) → param_name. Does not recurse into nested
|
|
1948
|
-
# defs / classes / modules.
|
|
1949
|
-
|
|
2078
|
+
# defs / classes / modules. A read counts only when it reaches the method's own parameter: its
|
|
2079
|
+
# `LocalVariableReadNode#depth` equals the number of blocks / lambdas between it and the method, so a
|
|
2080
|
+
# block parameter of the same name (`each { |count| @count = count }`) is not the constructor argument
|
|
2081
|
+
# (#1410). `credited`, when given, collects every write node the result is keyed on.
|
|
2082
|
+
def scan_ivar_param_assignments(node, param_names, result, credited = nil, scopes = 0)
|
|
1950
2083
|
return unless node.is_a?(Prism::Node)
|
|
1951
2084
|
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
result[
|
|
2085
|
+
ivar, param = param_assignment(node, param_names, scopes)
|
|
2086
|
+
if ivar
|
|
2087
|
+
result[ivar] ||= param
|
|
2088
|
+
credited << node if credited && result[ivar] == param
|
|
1956
2089
|
end
|
|
1957
2090
|
|
|
1958
2091
|
return if node.is_a?(Prism::DefNode) ||
|
|
1959
2092
|
node.is_a?(Prism::ClassNode) ||
|
|
1960
2093
|
node.is_a?(Prism::ModuleNode)
|
|
1961
2094
|
|
|
1962
|
-
|
|
2095
|
+
scopes += 1 if node.is_a?(Prism::BlockNode) || node.is_a?(Prism::LambdaNode)
|
|
2096
|
+
node.rigor_each_child { |c| scan_ivar_param_assignments(c, param_names, result, credited, scopes) }
|
|
2097
|
+
end
|
|
2098
|
+
|
|
2099
|
+
# `[ivar, param]` for `@ivar = param` whose read reaches the method's own parameter `param`, else nil.
|
|
2100
|
+
def param_assignment(node, param_names, scopes)
|
|
2101
|
+
return nil unless node.is_a?(Prism::InstanceVariableWriteNode)
|
|
2102
|
+
|
|
2103
|
+
read = node.value
|
|
2104
|
+
return nil unless read.is_a?(Prism::LocalVariableReadNode) && read.depth == scopes
|
|
2105
|
+
return nil unless param_names.include?(read.name)
|
|
2106
|
+
|
|
2107
|
+
[node.name, read.name]
|
|
1963
2108
|
end
|
|
1964
2109
|
|
|
1965
2110
|
def build_attr_candidates(call_name, class_name, attr_name, ivar_type, ctx)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "rbs"
|
|
4
|
+
require_relative "../rooted_glob"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
module SigGen
|
|
@@ -20,7 +21,28 @@ module Rigor
|
|
|
20
21
|
# First-found wins on duplicate declarations across files; RBS itself allows the same class to be declared
|
|
21
22
|
# in multiple files for additive member contributions, but the writer only needs one canonical target per
|
|
22
23
|
# class.
|
|
24
|
+
#
|
|
25
|
+
# ADR-122 WD2 adds the member level: `(class_name, method_name, kind) → sig_file_path`, so a member the project
|
|
26
|
+
# already declares in one file is updated there rather than inserted a second time into the class's
|
|
27
|
+
# first-found file (#1488).
|
|
23
28
|
class LayoutIndex
|
|
29
|
+
# The `[method_name, kind]` keys a member declaration answers for, `kind` being `:instance` or
|
|
30
|
+
# `:singleton`. A `def self?.f` answers for both kinds; an `attr_accessor` answers for its reader and its
|
|
31
|
+
# writer; an `alias new old` (`alias self.new self.old`) answers for its new name only, under its own kind
|
|
32
|
+
# (#1975). Shared with {Writer}, which asks the same question of the target file it is merging into.
|
|
33
|
+
def self.member_keys(member)
|
|
34
|
+
case member
|
|
35
|
+
when RBS::AST::Members::MethodDefinition
|
|
36
|
+
kinds = member.kind == :singleton_instance ? %i[instance singleton] : [member.kind]
|
|
37
|
+
kinds.map { |kind| [member.name, kind] }
|
|
38
|
+
when RBS::AST::Members::Alias then [[member.new_name, member.kind]]
|
|
39
|
+
when RBS::AST::Members::AttrReader then [[member.name, member.kind]]
|
|
40
|
+
when RBS::AST::Members::AttrWriter then [[:"#{member.name}=", member.kind]]
|
|
41
|
+
when RBS::AST::Members::AttrAccessor then [[member.name, member.kind], [:"#{member.name}=", member.kind]]
|
|
42
|
+
else []
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
24
46
|
# @param signature_paths —
|
|
25
47
|
# the `.rigor.yml`-configured signature directories.
|
|
26
48
|
# When `nil` or empty, falls back to `<project_root>/sig`
|
|
@@ -29,6 +51,7 @@ module Rigor
|
|
|
29
51
|
def initialize(signature_paths:, project_root: Dir.pwd)
|
|
30
52
|
@signature_paths = resolve_paths(signature_paths, project_root)
|
|
31
53
|
@index = nil
|
|
54
|
+
@members = nil
|
|
32
55
|
end
|
|
33
56
|
|
|
34
57
|
# @param class_name — fully-qualified Ruby class
|
|
@@ -40,6 +63,13 @@ module Rigor
|
|
|
40
63
|
index[class_name]
|
|
41
64
|
end
|
|
42
65
|
|
|
66
|
+
# @return absolute path of the first sig file (in the same order as {#file_for}) that declares
|
|
67
|
+
# `method_name` of `kind` on `class_name`, or `nil` when no file does.
|
|
68
|
+
def member_file_for(class_name, method_name, kind)
|
|
69
|
+
index
|
|
70
|
+
@members[[class_name, method_name, kind]]
|
|
71
|
+
end
|
|
72
|
+
|
|
43
73
|
def empty?
|
|
44
74
|
index.empty?
|
|
45
75
|
end
|
|
@@ -60,14 +90,16 @@ module Rigor
|
|
|
60
90
|
|
|
61
91
|
def build_index
|
|
62
92
|
accumulator = {}
|
|
93
|
+
@members = {}
|
|
63
94
|
Array(@signature_paths).each do |dir|
|
|
64
95
|
base = Pathname(dir)
|
|
65
96
|
next unless base.directory?
|
|
66
97
|
|
|
67
|
-
|
|
98
|
+
RootedGlob.glob(base, "**/*.rbs").each do |rbs_file|
|
|
68
99
|
index_file(Pathname(rbs_file), accumulator)
|
|
69
100
|
end
|
|
70
101
|
end
|
|
102
|
+
@members.freeze
|
|
71
103
|
accumulator.freeze
|
|
72
104
|
end
|
|
73
105
|
|
|
@@ -97,6 +129,13 @@ module Rigor
|
|
|
97
129
|
local_name = decl.name.to_s.sub(/\A::/, "")
|
|
98
130
|
full = prefix.empty? ? local_name : "#{prefix.join('::')}::#{local_name}"
|
|
99
131
|
accumulator[full] ||= rbs_path
|
|
132
|
+
decl.members.each do |member|
|
|
133
|
+
# An `overloading?` member (`def f: ... | ...`) adds to a declaration elsewhere; routing a candidate to
|
|
134
|
+
# it would replace the user's overload.
|
|
135
|
+
next if member.is_a?(RBS::AST::Members::MethodDefinition) && member.overloading?
|
|
136
|
+
|
|
137
|
+
self.class.member_keys(member).each { |name, kind| @members[[full, name, kind]] ||= rbs_path }
|
|
138
|
+
end
|
|
100
139
|
|
|
101
140
|
record_decls(decl.members, prefix + [local_name], rbs_path, accumulator)
|
|
102
141
|
end
|
|
@@ -68,6 +68,14 @@ module Rigor
|
|
|
68
68
|
@declared_annotations + @annotations + [@rbs].compact
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
+
# The RBS member kind `rbs` declares: `:singleton_instance` for a `def self?.` line (a `module_function`
|
|
72
|
+
# method, whose `kind` stays `:instance`), `kind` otherwise. The writer replaces only a member of this
|
|
73
|
+
# kind, so an overwrite neither drops a `self?.` member's singleton half nor adds one beside a split
|
|
74
|
+
# `def self.f` / `def f` pair (#1488).
|
|
75
|
+
def rbs_kind
|
|
76
|
+
kind == :instance && rbs.to_s.start_with?("def self?.") ? :singleton_instance : kind
|
|
77
|
+
end
|
|
78
|
+
|
|
71
79
|
# A copy carrying a different annotation decision. The generator's effect pass rebuilds rather than
|
|
72
80
|
# mutates, because a candidate is frozen the moment it is built.
|
|
73
81
|
def with_effect_annotation(annotations, reason)
|
|
@@ -10,6 +10,7 @@ require_relative "../source/constant_path"
|
|
|
10
10
|
require_relative "../source/literals"
|
|
11
11
|
require_relative "../source/node_children"
|
|
12
12
|
require_relative "../inference/scope_indexer"
|
|
13
|
+
require_relative "../rooted_glob"
|
|
13
14
|
|
|
14
15
|
module Rigor
|
|
15
16
|
module SigGen
|
|
@@ -32,6 +33,7 @@ module Rigor
|
|
|
32
33
|
# `let` / `subject` bindings ride on slice 5's optional `rigor-rspec` integration.
|
|
33
34
|
# - Calls whose receiver does not type as a `Type::Nominal` (e.g. `(some_dynamic).bar(...)`) are skipped —
|
|
34
35
|
# the collector cannot attribute them to a specific class.
|
|
36
|
+
# - A `T | nil` receiver with one `Nominal` / `Singleton` non-nil member counts as that member (#1389).
|
|
35
37
|
# - Zero-argument calls give no observation; methods are matched by `(class_name, method_name)` only.
|
|
36
38
|
class ObservationCollector # rubocop:disable Metrics/ClassLength
|
|
37
39
|
# @param paths — observe paths (files /
|
|
@@ -62,6 +64,12 @@ module Rigor
|
|
|
62
64
|
observations.transform_values(&:freeze).freeze
|
|
63
65
|
end
|
|
64
66
|
|
|
67
|
+
# Whether `arg` names something {#collect} reads: a directory, or a `.rb` file. The CLI's
|
|
68
|
+
# `observation` report splits roots on this, so "observed" means what the collector does (#1398).
|
|
69
|
+
def self.readable_root?(arg)
|
|
70
|
+
File.directory?(arg) || (File.file?(arg) && arg.end_with?(".rb"))
|
|
71
|
+
end
|
|
72
|
+
|
|
65
73
|
private
|
|
66
74
|
|
|
67
75
|
# Issue #821 — same universe as the generator and as `rigor check`; see
|
|
@@ -74,8 +82,8 @@ module Rigor
|
|
|
74
82
|
def resolve_paths(args)
|
|
75
83
|
args.flat_map do |arg|
|
|
76
84
|
if File.directory?(arg)
|
|
77
|
-
|
|
78
|
-
elsif
|
|
85
|
+
RootedGlob.glob(arg, "**/*.rb")
|
|
86
|
+
elsif self.class.readable_root?(arg)
|
|
79
87
|
[arg]
|
|
80
88
|
else
|
|
81
89
|
[]
|
|
@@ -92,8 +100,9 @@ module Rigor
|
|
|
92
100
|
base_scope = base_scope.with_discovery(base_scope.discovery.with(discovered_classes: discovered_classes))
|
|
93
101
|
scope_index = Inference::ScopeIndexer.index(parse_result.value, default_scope: base_scope)
|
|
94
102
|
bindings = collect_rspec_bindings(parse_result.value, scope_index)
|
|
103
|
+
@before_ivars = collect_before_ivars(parse_result.value, scope_index)
|
|
95
104
|
|
|
96
|
-
walk_calls(parse_result.value, scope_index, bindings, observations)
|
|
105
|
+
walk_calls(parse_result.value, scope_index, bindings, observations, [])
|
|
97
106
|
end
|
|
98
107
|
|
|
99
108
|
# Pre-walks `@source_paths` to collect every qualified class / module declaration. The result seeds the
|
|
@@ -145,22 +154,64 @@ module Rigor
|
|
|
145
154
|
end
|
|
146
155
|
end
|
|
147
156
|
|
|
148
|
-
|
|
157
|
+
# `blocks` is the chain of enclosing RSpec group / example / hook blocks, innermost last — the blocks a
|
|
158
|
+
# `before` ivar can reach the call from ({#before_ivar_type}). See {#enclosing_blocks} for where it is cut.
|
|
159
|
+
def walk_calls(node, scope_index, bindings, observations, blocks)
|
|
149
160
|
return unless node.is_a?(Prism::Node)
|
|
150
161
|
|
|
151
|
-
record_call(node, scope_index, bindings, observations) if node.is_a?(Prism::CallNode)
|
|
152
|
-
node.rigor_each_child
|
|
162
|
+
record_call(node, scope_index, bindings, observations, blocks) if node.is_a?(Prism::CallNode)
|
|
163
|
+
node.rigor_each_child do |child|
|
|
164
|
+
walk_calls(child, scope_index, bindings, observations, enclosing_blocks(node, child, blocks))
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Group, example and hook calls whose block runs with the example's ivars. `RSpec.` may prefix a group call.
|
|
169
|
+
RSPEC_GROUP_CALLS = %i[
|
|
170
|
+
describe context feature shared_examples shared_examples_for shared_context
|
|
171
|
+
].freeze
|
|
172
|
+
RSPEC_EXAMPLE_OR_HOOK_CALLS = %i[
|
|
173
|
+
it specify example scenario its before prepend_before append_before
|
|
174
|
+
].freeze
|
|
175
|
+
private_constant :RSPEC_GROUP_CALLS, :RSPEC_EXAMPLE_OR_HOOK_CALLS
|
|
176
|
+
|
|
177
|
+
# The chain `child` of `parent` sees. A group / example / hook call's block extends it; every other block or
|
|
178
|
+
# lambda (`Class.new { … }`, `instance_eval { … }`, `define_method(:m) { … }`, `each { … }`), a class / module /
|
|
179
|
+
# singleton-class body, and a `def` that is not a direct statement of a group block cut it, since their `self`
|
|
180
|
+
# may be another object.
|
|
181
|
+
def enclosing_blocks(parent, child, blocks)
|
|
182
|
+
case child
|
|
183
|
+
when Prism::BlockNode then rspec_scope_call?(parent) ? [*blocks, child] : []
|
|
184
|
+
when Prism::LambdaNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode then []
|
|
185
|
+
when Prism::DefNode then group_statement?(child, blocks.last) ? blocks : []
|
|
186
|
+
else blocks
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def rspec_scope_call?(node)
|
|
191
|
+
return false unless node.is_a?(Prism::CallNode)
|
|
192
|
+
return rspec_group_receiver?(node.receiver) if RSPEC_GROUP_CALLS.include?(node.name)
|
|
193
|
+
|
|
194
|
+
node.receiver.nil? && RSPEC_EXAMPLE_OR_HOOK_CALLS.include?(node.name)
|
|
153
195
|
end
|
|
154
196
|
|
|
155
|
-
def
|
|
197
|
+
def rspec_group_receiver?(receiver)
|
|
198
|
+
receiver.nil? || (receiver.is_a?(Prism::ConstantReadNode) && receiver.name == :RSpec)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def group_statement?(node, block)
|
|
202
|
+
body = block&.body
|
|
203
|
+
body.is_a?(Prism::StatementsNode) && body.body.any? { |statement| statement.equal?(node) }
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def record_call(call_node, scope_index, bindings, observations, blocks)
|
|
156
207
|
receiver = call_node.receiver
|
|
157
208
|
return if receiver.nil?
|
|
158
209
|
|
|
159
210
|
scope = scope_index[call_node] || scope_index[receiver]
|
|
160
211
|
return if scope.nil?
|
|
161
212
|
|
|
162
|
-
receiver_type = resolve_receiver_type(receiver, scope, bindings)
|
|
163
|
-
key = observation_key(call_node, receiver_type)
|
|
213
|
+
receiver_type = resolve_receiver_type(receiver, scope, bindings, blocks)
|
|
214
|
+
key = observation_key(call_node, observed_receiver(receiver_type, call_node, scope))
|
|
164
215
|
return if key.nil?
|
|
165
216
|
|
|
166
217
|
observation = collect_args(call_node, scope)
|
|
@@ -181,6 +232,36 @@ module Rigor
|
|
|
181
232
|
end
|
|
182
233
|
end
|
|
183
234
|
|
|
235
|
+
# Issue #1389 — a receiver typed `T | nil` whose one non-nil member is a `Nominal` or `Singleton` is observed as
|
|
236
|
+
# that member. A Minitest `setup` ivar read in a test method types as `Foo?` (setup is not `initialize`), and a
|
|
237
|
+
# call of a method `nil` does not answer can only have reached `Foo#bar` with its arguments: on `nil` it raises
|
|
238
|
+
# `NoMethodError`, and `&.` skips the call. A method `NilClass`'s RBS declares (`==`, `respond_to?`, `is_a?`)
|
|
239
|
+
# may have run on `nil` instead, so a plain `.` call of one keeps the union unobserved. The sole member still
|
|
240
|
+
# has to pass {#observation_key}'s `Nominal` / `Singleton` checks, so `untyped | nil` stays unobserved, and a
|
|
241
|
+
# union with two non-nil members (`Foo | Bar | nil`) passes through unchanged. Observation only: the
|
|
242
|
+
# receiver's inferred type elsewhere is untouched.
|
|
243
|
+
def observed_receiver(receiver_type, call_node, scope)
|
|
244
|
+
return receiver_type unless receiver_type.is_a?(Type::Union)
|
|
245
|
+
|
|
246
|
+
non_nil = receiver_type.members.reject { |member| nil_type?(member) }
|
|
247
|
+
return receiver_type unless non_nil.size == 1
|
|
248
|
+
return receiver_type if !call_node.safe_navigation? && nil_answers?(call_node.name, scope)
|
|
249
|
+
|
|
250
|
+
non_nil.first
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Whether `nil` may answer `method_name`: `NilClass`'s RBS declares it, or that RBS is not available to ask.
|
|
254
|
+
def nil_answers?(method_name, scope)
|
|
255
|
+
loader = scope.environment.rbs_loader
|
|
256
|
+
return true if loader.nil? || loader.instance_definition("NilClass").nil?
|
|
257
|
+
|
|
258
|
+
!loader.instance_method(class_name: "NilClass", method_name: method_name).nil?
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def nil_type?(type)
|
|
262
|
+
(type.is_a?(Type::Constant) && type.value.nil?) || (type.is_a?(Type::Nominal) && type.class_name == "NilClass")
|
|
263
|
+
end
|
|
264
|
+
|
|
184
265
|
# ADR-14 slice 5 — RSpec-aware receiver typing. Resolves a CallNode receiver against the collected
|
|
185
266
|
# `bindings` map (built by {#collect_rspec_bindings}) before falling back to ordinary `scope.type_of`. The
|
|
186
267
|
# three RSpec-shaped receivers we recognise:
|
|
@@ -190,11 +271,23 @@ module Rigor
|
|
|
190
271
|
# - `described_class.new(...)` chain — when the surrounding `describe Foo do … end` resolved `Foo`,
|
|
191
272
|
# return `Type::Nominal[Foo]`.
|
|
192
273
|
# - Anything else — pass through to `scope.type_of`, matching slice-3 behaviour.
|
|
193
|
-
def resolve_receiver_type(receiver, scope, bindings)
|
|
274
|
+
def resolve_receiver_type(receiver, scope, bindings, blocks)
|
|
194
275
|
return resolve_described_class_new(bindings) if described_class_new?(receiver)
|
|
195
276
|
return bindings[receiver.name] if bound_call?(receiver, bindings)
|
|
196
277
|
|
|
197
|
-
safe_type_of(scope, receiver)
|
|
278
|
+
type = safe_type_of(scope, receiver)
|
|
279
|
+
return type unless receiver.is_a?(Prism::InstanceVariableReadNode) && (type.nil? || type.is_a?(Type::Dynamic))
|
|
280
|
+
|
|
281
|
+
before_ivar_type(receiver.name, blocks) || type
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Issue #1389 — an ivar read inside an `it` block types as `Dynamic` (the block's ivars are not a class's), so
|
|
285
|
+
# a `before { @foo = Foo.new }` write stands in for it. Only a `before` whose example-group block encloses the
|
|
286
|
+
# read counts — a sibling `describe`, an un-included `shared_context` or a class body never reaches it — and
|
|
287
|
+
# when those `before`s write the name with more than one type, no type is answered.
|
|
288
|
+
def before_ivar_type(name, blocks)
|
|
289
|
+
types = blocks.flat_map { |block| @before_ivars.dig(block, name) || [] }.uniq
|
|
290
|
+
types.size == 1 ? types.first : nil
|
|
198
291
|
end
|
|
199
292
|
|
|
200
293
|
def bound_call?(receiver, bindings)
|
|
@@ -279,6 +372,39 @@ module Rigor
|
|
|
279
372
|
bindings[name] = body_type if body_type
|
|
280
373
|
end
|
|
281
374
|
|
|
375
|
+
# `before { @foo = Foo.new }` / `before(:each) do … end` inside an example-group block: records each ivar the
|
|
376
|
+
# `before` block's top-level statements write, with the written value's type, under that enclosing group
|
|
377
|
+
# block — `{ BlockNode => { :@foo => [Type] } }`, keyed by node identity. A `before` outside every group
|
|
378
|
+
# block ({#enclosing_blocks}), and `config.before`, record nothing.
|
|
379
|
+
def collect_before_ivars(root, scope_index)
|
|
380
|
+
index = {}.compare_by_identity
|
|
381
|
+
walk_before_ivars(root, [], index, scope_index)
|
|
382
|
+
index
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def walk_before_ivars(node, blocks, index, scope_index)
|
|
386
|
+
return unless node.is_a?(Prism::Node)
|
|
387
|
+
|
|
388
|
+
record_before_ivars(node, blocks.last, index, scope_index) if blocks.last
|
|
389
|
+
node.rigor_each_child do |child|
|
|
390
|
+
walk_before_ivars(child, enclosing_blocks(node, child, blocks), index, scope_index)
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def record_before_ivars(node, group, index, scope_index)
|
|
395
|
+
return unless node.is_a?(Prism::CallNode) && node.name == :before && node.receiver.nil?
|
|
396
|
+
|
|
397
|
+
body = block_body_node(node.block)
|
|
398
|
+
statements = body.is_a?(Prism::StatementsNode) ? body.body : [body].compact
|
|
399
|
+
statements.each do |statement|
|
|
400
|
+
next unless statement.is_a?(Prism::InstanceVariableWriteNode)
|
|
401
|
+
|
|
402
|
+
scope = scope_index[statement.value] || scope_index[statement]
|
|
403
|
+
type = scope && safe_type_of(scope, statement.value)
|
|
404
|
+
((index[group] ||= {})[statement.name] ||= []) << type if type
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
|
|
282
408
|
def binding_name_for(call_node)
|
|
283
409
|
first_arg = call_node.arguments&.arguments&.first
|
|
284
410
|
return call_node.name == :subject ? :subject : nil if first_arg.nil?
|