rigortype 0.4.4 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +5 -5
- data/docs/handbook/11-sig-gen.md +100 -18
- data/docs/manual/02-cli-reference.md +85 -16
- data/docs/manual/03-configuration.md +12 -9
- data/docs/manual/04-diagnostics.md +17 -6
- data/docs/manual/05-inspecting-types.md +2 -2
- data/docs/manual/06-baseline.md +1 -1
- data/docs/manual/07-plugins.md +2 -1
- data/docs/manual/08-skills.md +7 -0
- data/docs/manual/10-mcp-server.md +17 -2
- data/docs/manual/12-caching.md +8 -1
- data/docs/manual/18-removing-dead-code.md +18 -7
- data/docs/manual/plugins/rigor-actionmailer.md +30 -0
- data/docs/manual/plugins/rigor-actionpack.md +30 -0
- data/docs/manual/plugins/rigor-devise.md +15 -0
- data/docs/manual/plugins/rigor-rspec.md +102 -1
- data/lib/rigor/analysis/buffer_binding.rb +14 -0
- data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +30 -9
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +142 -15
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +115 -7
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +253 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +3 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +64 -4
- data/lib/rigor/analysis/check_rules.rb +319 -21
- data/lib/rigor/analysis/dependency_recorder.rb +11 -0
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/edit_replay.rb +67 -0
- data/lib/rigor/analysis/incremental.rb +86 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +336 -46
- data/lib/rigor/analysis/path_expansion.rb +44 -1
- data/lib/rigor/analysis/reachability/graph.rb +90 -13
- data/lib/rigor/analysis/reachability/scan.rb +279 -39
- data/lib/rigor/analysis/require_relative_closure.rb +100 -0
- data/lib/rigor/analysis/rule_catalog.rb +22 -5
- data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
- data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +11 -3
- data/lib/rigor/analysis/runner/pool_coordinator.rb +82 -16
- data/lib/rigor/analysis/runner/project_pre_passes.rb +45 -3
- data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
- data/lib/rigor/analysis/runner.rb +222 -111
- data/lib/rigor/analysis/severity_stamp.rb +11 -0
- data/lib/rigor/analysis/worker_session.rb +24 -1
- data/lib/rigor/cache/descriptor.rb +40 -7
- data/lib/rigor/cache/engine_source.rb +3 -2
- data/lib/rigor/cache/incremental_snapshot.rb +49 -8
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +4 -3
- data/lib/rigor/cli/check_command.rb +93 -4
- data/lib/rigor/cli/command.rb +3 -1
- data/lib/rigor/cli/diff_command.rb +2 -1
- data/lib/rigor/cli/docs_command.rb +3 -2
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/explain_command.rb +3 -2
- data/lib/rigor/cli/lens_command.rb +5 -5
- data/lib/rigor/cli/mcp_command.rb +2 -1
- data/lib/rigor/cli/options.rb +20 -3
- data/lib/rigor/cli/plugin_command.rb +2 -1
- data/lib/rigor/cli/plugins_command.rb +7 -1
- data/lib/rigor/cli/plugins_renderer.rb +24 -2
- data/lib/rigor/cli/show_bleedingedge_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +73 -13
- data/lib/rigor/cli/skill_command.rb +2 -1
- data/lib/rigor/cli/skill_describe.rb +3 -2
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/type_of_project_scope.rb +27 -0
- data/lib/rigor/cli/unused_command.rb +10 -7
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +72 -16
- data/lib/rigor/configuration/severity_profile.rb +20 -2
- data/lib/rigor/configuration.rb +43 -7
- data/lib/rigor/effects/signature_sources.rb +3 -1
- data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
- data/lib/rigor/environment/gated_signature_guard.rb +2 -1
- data/lib/rigor/environment/gated_signature_plan.rb +2 -1
- data/lib/rigor/environment/installed_gem_set.rb +2 -1
- data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
- data/lib/rigor/environment/rbs_loader.rb +136 -7
- data/lib/rigor/environment/required_features.rb +136 -29
- data/lib/rigor/environment/ruby_library_roots.rb +122 -0
- data/lib/rigor/environment.rb +4 -2
- data/lib/rigor/inference/block_call_timing.rb +7 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/callee_mutation_chain.rb +115 -0
- data/lib/rigor/inference/captured_locals.rb +44 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/deep_erasure.rb +138 -0
- data/lib/rigor/inference/def_return_typer.rb +159 -9
- data/lib/rigor/inference/definer_resolution.rb +101 -20
- data/lib/rigor/inference/error_info.rb +17 -3
- data/lib/rigor/inference/expression_typer.rb +97 -18
- data/lib/rigor/inference/global_write_census.rb +36 -1
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- data/lib/rigor/inference/ivar_writer_census.rb +195 -0
- data/lib/rigor/inference/loop_value_record.rb +53 -0
- data/lib/rigor/inference/macro_block_self_type.rb +20 -6
- data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +278 -42
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
- data/lib/rigor/inference/method_dispatcher.rb +113 -19
- data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
- data/lib/rigor/inference/module_function_state.rb +149 -42
- data/lib/rigor/inference/mutation_widening.rb +5 -2
- data/lib/rigor/inference/narrowing.rb +116 -11
- data/lib/rigor/inference/object_mixins.rb +13 -1
- data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
- data/lib/rigor/inference/project_patched_methods.rb +168 -4
- data/lib/rigor/inference/project_patched_mixins.rb +162 -0
- data/lib/rigor/inference/project_patched_scanner.rb +51 -12
- data/lib/rigor/inference/project_patched_visibility.rb +214 -0
- data/lib/rigor/inference/project_seed_tables.rb +121 -0
- data/lib/rigor/inference/project_yielder.rb +128 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
- data/lib/rigor/inference/reflective_ivar_write.rb +6 -6
- data/lib/rigor/inference/return_barrier.rb +16 -0
- data/lib/rigor/inference/scope_indexer/certainty.rb +9 -0
- data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +162 -0
- data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
- data/lib/rigor/inference/scope_indexer.rb +1144 -197
- data/lib/rigor/inference/self_call_ivar_writes.rb +29 -12
- data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
- data/lib/rigor/inference/statement_evaluator.rb +1088 -213
- data/lib/rigor/inference/synthetic_method_index.rb +23 -0
- data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +72 -12
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- data/lib/rigor/language_server/buffer_table.rb +6 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +20 -6
- data/lib/rigor/language_server/project_context.rb +59 -3
- data/lib/rigor/language_server/server.rb +5 -3
- data/lib/rigor/lens/contract_reader.rb +45 -2
- data/lib/rigor/lens/document.rb +2 -1
- data/lib/rigor/lens/file_reader.rb +12 -2
- data/lib/rigor/lens/members.rb +16 -2
- data/lib/rigor/lens/run.rb +34 -7
- data/lib/rigor/mcp/server.rb +11 -3
- data/lib/rigor/plugin/base.rb +29 -13
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -3
- data/lib/rigor/plugin/manifest.rb +29 -3
- data/lib/rigor/plugin/registry.rb +38 -0
- data/lib/rigor/protection/discovery_seed.rb +64 -36
- data/lib/rigor/protection/mutation_cache.rb +8 -2
- data/lib/rigor/rooted_glob.rb +36 -0
- data/lib/rigor/scope/discovery_index.rb +5 -0
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
- data/lib/rigor/scope/resolution_chain.rb +40 -5
- data/lib/rigor/scope.rb +59 -7
- data/lib/rigor/sig_gen/alias_index.rb +2 -1
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +465 -49
- data/lib/rigor/sig_gen/layout_index.rb +5 -2
- data/lib/rigor/sig_gen/observation_collector.rb +141 -12
- data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +16 -155
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
- data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
- data/lib/rigor/sig_gen/write_reporter.rb +203 -0
- data/lib/rigor/sig_gen/write_result.rb +3 -1
- data/lib/rigor/sig_gen/writer.rb +5 -3
- data/lib/rigor/signature_path_audit.rb +4 -2
- data/lib/rigor/type/combinator.rb +4 -3
- data/lib/rigor/type/constant.rb +54 -3
- data/lib/rigor/type/hash_shape.rb +14 -9
- data/lib/rigor/type/struct_instance.rb +3 -2
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +61 -44
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +38 -9
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -2
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
- data/sig/rigor/analysis/baseline.rbs +3 -2
- data/sig/rigor/effects/file_collection.rbs +5 -6
- data/sig/rigor/effects/registry.rbs +8 -6
- data/sig/rigor/effects/summary.rbs +6 -5
- data/sig/rigor/environment.rbs +5 -3
- data/sig/rigor/inference/optimistic_origin.rbs +11 -0
- data/sig/rigor/inference.rbs +6 -0
- data/sig/rigor/plugin/additional_initializer.rbs +5 -5
- data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
- data/sig/rigor/plugin/effect_attribution.rbs +25 -10
- data/sig/rigor/plugin/effect_edge.rbs +12 -5
- data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
- data/sig/rigor/plugin/io_boundary.rbs +5 -2
- data/sig/rigor/plugin/manifest.rbs +16 -18
- data/sig/rigor/plugin/protocol_contract.rbs +10 -9
- data/sig/rigor/plugin/registry.rbs +1 -1
- data/sig/rigor/rooted_glob.rbs +7 -0
- data/sig/rigor/scope.rbs +34 -52
- data/sig/rigor/type.rbs +0 -6
- data/skills/rigor-doctor/references/01-checks.md +23 -11
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
- metadata +32 -1
|
@@ -8,11 +8,13 @@ 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"
|
|
14
15
|
require_relative "../inference/def_return_typer"
|
|
15
16
|
require_relative "../inference/scope_indexer"
|
|
17
|
+
require_relative "../inference/project_seed_tables"
|
|
16
18
|
require_relative "../inference/module_function_state"
|
|
17
19
|
require_relative "../inference/rbs_type_translator"
|
|
18
20
|
require_relative "alias_index"
|
|
@@ -25,6 +27,7 @@ require_relative "inline_declarations"
|
|
|
25
27
|
require_relative "layout_index"
|
|
26
28
|
require_relative "method_candidate"
|
|
27
29
|
require_relative "writer"
|
|
30
|
+
require_relative "../rooted_glob"
|
|
28
31
|
|
|
29
32
|
module Rigor
|
|
30
33
|
module SigGen
|
|
@@ -97,7 +100,8 @@ module Rigor
|
|
|
97
100
|
# candidates from one file don't leak into another; initialising empty here gives downstream consumers
|
|
98
101
|
# (`build_candidate`, `method_def_prefix`) a never-nil invariant without per-call-site defensive guards.
|
|
99
102
|
@namespace_kinds = {}
|
|
100
|
-
@module_function_methods = Set.new
|
|
103
|
+
@module_function_methods = Set.new.compare_by_identity
|
|
104
|
+
@uncertain_module_function_defs = Set.new.compare_by_identity
|
|
101
105
|
@class_shells = Set.new
|
|
102
106
|
@class_superclasses = {}
|
|
103
107
|
@meta_layouts = {}
|
|
@@ -123,6 +127,7 @@ module Rigor
|
|
|
123
127
|
|
|
124
128
|
def run
|
|
125
129
|
resolved = resolve_paths(@paths)
|
|
130
|
+
@resolved_paths = resolved
|
|
126
131
|
@environment = build_environment(resolved)
|
|
127
132
|
candidates = resolved.flat_map { |path| analyse_file(path, @environment) }
|
|
128
133
|
annotate_effects(
|
|
@@ -446,7 +451,7 @@ module Rigor
|
|
|
446
451
|
def resolve_paths(args)
|
|
447
452
|
args.flat_map do |arg|
|
|
448
453
|
if File.directory?(arg)
|
|
449
|
-
|
|
454
|
+
RootedGlob.glob(arg, "**/*.rb")
|
|
450
455
|
elsif File.file?(arg) && arg.end_with?(".rb")
|
|
451
456
|
[arg]
|
|
452
457
|
else
|
|
@@ -455,25 +460,41 @@ module Rigor
|
|
|
455
460
|
end.uniq
|
|
456
461
|
end
|
|
457
462
|
|
|
463
|
+
# #1154 — the project's discovery tables, read only by the declared-parameter ivar credit: it must see the ivar
|
|
464
|
+
# writers in other files (an ancestor, another object's `instance_variable_set`, a reopening) as `rigor check`
|
|
465
|
+
# does. Built once per run over the configured project paths as well as the files sig-gen reads, so a run over
|
|
466
|
+
# one file declines what a `check` of it declines; every other part of the generator keeps its per-file scope.
|
|
467
|
+
def ivar_writer_scope(base_scope)
|
|
468
|
+
@ivar_writer_tables ||= Inference::ProjectSeedTables.for_paths(
|
|
469
|
+
((@resolved_paths || []) + resolve_paths(Array(@configuration.paths))).uniq { |path| File.expand_path(path) }
|
|
470
|
+
)
|
|
471
|
+
base_scope.with_discovery(base_scope.discovery.with(**@ivar_writer_tables))
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
# #1406 — sig-gen keeps the census's own answer: a declared ivar type read back would echo the declaration.
|
|
475
|
+
def index_file(root, base_scope)
|
|
476
|
+
Inference::ScopeIndexer.index(
|
|
477
|
+
root, default_scope: base_scope, ivar_writer_scope: ivar_writer_scope(base_scope), declared_ivar_types: false
|
|
478
|
+
)
|
|
479
|
+
end
|
|
480
|
+
|
|
458
481
|
def analyse_file(path, environment)
|
|
459
482
|
source = File.read(path)
|
|
460
483
|
parse_result = Prism.parse(source, filepath: path, version: @configuration.target_ruby)
|
|
461
484
|
return [] if parse_result.errors.any?
|
|
462
485
|
|
|
463
|
-
|
|
464
|
-
scope_index = Inference::ScopeIndexer.index(parse_result.value, default_scope: base_scope)
|
|
486
|
+
scope_index = index_file(parse_result.value, Scope.empty(environment: environment))
|
|
465
487
|
|
|
466
|
-
|
|
467
|
-
@module_function_methods = Set.new
|
|
468
|
-
@class_shells = Set.new
|
|
469
|
-
@class_superclasses = {}
|
|
488
|
+
reset_file_maps
|
|
470
489
|
@meta_layouts = collect_meta_layouts(scope_index)
|
|
471
490
|
register_meta_classes
|
|
472
491
|
defs = collect_method_definitions(parse_result.value, scope_index)
|
|
492
|
+
definers = definer_table(parse_result.value, defs)
|
|
493
|
+
defs = live_definitions(defs, definers)
|
|
473
494
|
# Candidate construction freezes the per-file maps above (see {#build_candidate}), so every registration
|
|
474
495
|
# pass has to be finished before the first `build_candidate` call. Meta members lead the RETURNED order —
|
|
475
496
|
# a value class's members and constructors read first, ahead of the methods its block body defines.
|
|
476
|
-
meta_candidates =
|
|
497
|
+
meta_candidates = live_meta_candidates(path, scope_index, definers)
|
|
477
498
|
candidates_from_defs = defs.filter_map do |def_node, class_name, kind|
|
|
478
499
|
# An analyzer bug typing one def's body must cost only that def's candidate, never the whole
|
|
479
500
|
# `rigor sig-gen` run. The `check` path recovers each *file* this way (worker_session.rb); sig-gen
|
|
@@ -485,7 +506,111 @@ module Rigor
|
|
|
485
506
|
end
|
|
486
507
|
obs_ivar_map = build_observed_ivar_map(parse_result.value)
|
|
487
508
|
meta_candidates + candidates_from_defs +
|
|
488
|
-
collect_attr_candidates(parse_result.value, path, scope_index, obs_ivar_map)
|
|
509
|
+
collect_attr_candidates(parse_result.value, path, scope_index, obs_ivar_map, definers)
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
# Ruby keeps the LAST definition of a name: a `def x` followed by `attr_reader :x` (or the reverse) defines
|
|
513
|
+
# `x` once, and a second `def x` after a `module_function` reset replaces only the instance method. Writing
|
|
514
|
+
# one entry per definition gave RBS a duplicate method definition (#2008, #2023), so every definer of a
|
|
515
|
+
# `(class, kind, name)` slot is recorded with its source offset, and only the last one in a file is written.
|
|
516
|
+
# A module function defines two slots, its singleton copy and its instance method. A `Data.define` or
|
|
517
|
+
# `Struct.new` member is defined before any body that could redefine it, so it ranks first.
|
|
518
|
+
#
|
|
519
|
+
# Only a definer whose statement certainly runs can be last: a direct statement of a `class`, `module` or
|
|
520
|
+
# `class << self` body, of a `Data.define` / `Struct.new` block or of the file ({#walk_statements} records
|
|
521
|
+
# them). One under a condition, in a block or a lambda, passed to a call (`private def x`), or after an
|
|
522
|
+
# uncertain `module_function` reset may not run, as far as this reading tells. A slot such a definer could
|
|
523
|
+
# still take over from the last certain one is contested, and nothing is written for it.
|
|
524
|
+
#
|
|
525
|
+
# @return `{ [class_name, kind, method_name] => [[offset, certain], ...] }`.
|
|
526
|
+
MEMBER_OFFSET = -1
|
|
527
|
+
private_constant :MEMBER_OFFSET
|
|
528
|
+
|
|
529
|
+
def definer_table(root, defs)
|
|
530
|
+
certain = @certain_definers
|
|
531
|
+
table = Hash.new { |h, k| h[k] = [] }
|
|
532
|
+
record_def_definers(defs, certain, table)
|
|
533
|
+
meta_member_slots.each { |slot| table[slot] << [MEMBER_OFFSET, true] }
|
|
534
|
+
ctx = AttrWalkContext.new(definers: [])
|
|
535
|
+
walk_attr_calls(root, [], false, ctx)
|
|
536
|
+
ctx.definers.each do |slot, call_node|
|
|
537
|
+
table[slot] << [call_node.location.start_offset, certain.include?(call_node)]
|
|
538
|
+
end
|
|
539
|
+
table
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def record_def_definers(defs, certain, table)
|
|
543
|
+
defs.each do |def_node, class_name, kind|
|
|
544
|
+
next if class_name.nil?
|
|
545
|
+
|
|
546
|
+
definer_slots(def_node, class_name, kind).each do |slot, possible_only|
|
|
547
|
+
table[slot] << [def_node.location.start_offset, !possible_only && certain.include?(def_node)]
|
|
548
|
+
end
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
def live_meta_candidates(path, scope_index, table)
|
|
553
|
+
collect_meta_member_candidates(path, scope_index).select do |c|
|
|
554
|
+
live_definer?(table, [c.class_name, c.kind, c.method_name], MEMBER_OFFSET)
|
|
555
|
+
end
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
# `[slot, possible_only]` pairs. A `def` after an uncertain `module_function` reset may or may not install a
|
|
559
|
+
# singleton copy, so it is a possible definer of that slot only.
|
|
560
|
+
def definer_slots(def_node, class_name, kind)
|
|
561
|
+
slots = [[[class_name, kind, def_node.name], false]]
|
|
562
|
+
if @module_function_methods.include?(def_node)
|
|
563
|
+
slots << [[class_name, :singleton, def_node.name], false]
|
|
564
|
+
elsif @uncertain_module_function_defs.include?(def_node)
|
|
565
|
+
slots << [[class_name, :singleton, def_node.name], true]
|
|
566
|
+
end
|
|
567
|
+
slots
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
def meta_member_slots
|
|
571
|
+
@meta_layouts.flat_map do |class_name, layout|
|
|
572
|
+
MetaClassShape.of(kind: layout.kind, members: layout.member_names, keyword_init: layout.keyword_init)
|
|
573
|
+
.member_decls.map { |member| [class_name, member.kind, member.method_name] }
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
# Whether the definer at `offset` is the one Ruby keeps for `slot`. A slot with a single definer keeps it,
|
|
578
|
+
# certain or not, as before.
|
|
579
|
+
def live_definer?(table, slot, offset)
|
|
580
|
+
entries = table.fetch(slot, [])
|
|
581
|
+
return true if entries.size <= 1
|
|
582
|
+
|
|
583
|
+
last_certain = entries.select { |_, certain| certain }.map(&:first).max
|
|
584
|
+
return false if last_certain.nil?
|
|
585
|
+
return false if entries.any? { |entry_offset, certain| !certain && entry_offset > last_certain }
|
|
586
|
+
|
|
587
|
+
offset == last_certain
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
# The `defs` whose definition Ruby keeps. A module function whose instance method a later definition
|
|
591
|
+
# replaced keeps only its singleton copy (`M.label` still returns the first body), so it is written as
|
|
592
|
+
# `def self.label`. One whose singleton copy was replaced keeps only a private instance method, which
|
|
593
|
+
# sig-gen does not write, so it is dropped.
|
|
594
|
+
def live_definitions(defs, table)
|
|
595
|
+
defs.filter_map do |def_node, class_name, kind|
|
|
596
|
+
next [def_node, class_name, kind] if class_name.nil?
|
|
597
|
+
|
|
598
|
+
slots = definer_slots(def_node, class_name, kind).reject(&:last).map(&:first)
|
|
599
|
+
live = slots.select { |slot| live_definer?(table, slot, def_node.location.start_offset) }
|
|
600
|
+
next [def_node, class_name, kind] if live.size == slots.size
|
|
601
|
+
next nil unless live == [[class_name, :singleton, def_node.name]]
|
|
602
|
+
|
|
603
|
+
[def_node, class_name, :singleton]
|
|
604
|
+
end
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
# The per-file scratch state {#analyse_file} starts each file with.
|
|
608
|
+
def reset_file_maps
|
|
609
|
+
@namespace_kinds = {}
|
|
610
|
+
@module_function_methods = Set.new.compare_by_identity
|
|
611
|
+
@uncertain_module_function_defs = Set.new.compare_by_identity
|
|
612
|
+
@class_shells = Set.new
|
|
613
|
+
@class_superclasses = {}
|
|
489
614
|
end
|
|
490
615
|
|
|
491
616
|
# Walks the AST collecting `(def_node, class_name, kind)` tuples for every `def` Rigor can re-type. Slice 1
|
|
@@ -500,15 +625,19 @@ module Rigor
|
|
|
500
625
|
# - `@namespace_kinds[qualified_name]` records whether each segment came from `class Foo` (`:class`) or
|
|
501
626
|
# `module Foo` (`:module`). Used by the writer's `wrap_in_modules` step to emit the right keyword for
|
|
502
627
|
# each intermediate segment AND the leaf.
|
|
503
|
-
# - `@module_function_methods` records
|
|
504
|
-
#
|
|
505
|
-
#
|
|
628
|
+
# - `@module_function_methods` records the `def` nodes a `module_function` (no args) call preceded inside
|
|
629
|
+
# a module body. The renderer emits `def self?.name` for these, the RBS spelling that matches the dual
|
|
630
|
+
# instance + singleton dispatch the runtime produces. It is keyed by node, not by name: a later `def` of
|
|
631
|
+
# the same name after a reset is an ordinary method (#2023).
|
|
506
632
|
def collect_method_definitions(root, scope_index)
|
|
507
633
|
@declared_types = scope_index[root].declared_types
|
|
508
634
|
@walk_root = root
|
|
509
635
|
@in_effect_refinements = nil
|
|
510
636
|
out = []
|
|
511
637
|
@block_depth = 0
|
|
638
|
+
@certain_bodies = Set.new.compare_by_identity
|
|
639
|
+
@certain_definers = Set.new.compare_by_identity
|
|
640
|
+
mark_certain_body(root.statements)
|
|
512
641
|
walk_defs(root, [], false, false, out)
|
|
513
642
|
out
|
|
514
643
|
end
|
|
@@ -521,6 +650,7 @@ module Rigor
|
|
|
521
650
|
return if descend_into_namespace?(node, prefix, out)
|
|
522
651
|
when Prism::SingletonClassNode
|
|
523
652
|
if node.expression.is_a?(Prism::SelfNode) && node.body
|
|
653
|
+
mark_certain_body(node.body)
|
|
524
654
|
walk_defs(node.body, prefix, true, false, out)
|
|
525
655
|
return
|
|
526
656
|
end
|
|
@@ -530,6 +660,7 @@ module Rigor
|
|
|
530
660
|
when Prism::ConstantWriteNode
|
|
531
661
|
body = meta_block_body(node, prefix)
|
|
532
662
|
if body
|
|
663
|
+
mark_certain_body(body)
|
|
533
664
|
walk_defs(body, prefix + [node.name.to_s], false, false, out)
|
|
534
665
|
return
|
|
535
666
|
end
|
|
@@ -708,13 +839,30 @@ module Rigor
|
|
|
708
839
|
def walk_namespace_body(namespace_node, prefix, out)
|
|
709
840
|
return if namespace_node.body.nil?
|
|
710
841
|
|
|
842
|
+
mark_certain_body(namespace_node.body)
|
|
711
843
|
walk_defs(namespace_node.body, prefix, false, false, out)
|
|
712
844
|
end
|
|
713
845
|
|
|
846
|
+
# A body whose direct statements certainly run when it does ({#definer_table}): its statement list unless the
|
|
847
|
+
# body has a `rescue` clause, and its `ensure` clause. A `rescue` or `else` clause may not run.
|
|
848
|
+
def mark_certain_body(body)
|
|
849
|
+
return @certain_bodies << body unless body.is_a?(Prism::BeginNode)
|
|
850
|
+
|
|
851
|
+
# With a `rescue`, a statement before a definer can raise and skip it, so the main list is not certain.
|
|
852
|
+
@certain_bodies << body.statements if body.statements && body.rescue_clause.nil?
|
|
853
|
+
@certain_bodies << body.ensure_clause.statements if body.ensure_clause&.statements
|
|
854
|
+
end
|
|
855
|
+
|
|
714
856
|
# The directive's reach is {Inference::ModuleFunctionState.each_sig_gen_statement}.
|
|
715
857
|
def walk_statements(stmts_node, prefix, in_singleton_class, module_function_active, out)
|
|
858
|
+
certain_body = @certain_bodies.include?(stmts_node)
|
|
716
859
|
Inference::ModuleFunctionState.each_sig_gen_statement(stmts_node.body,
|
|
717
860
|
module_function_active) do |stmt, active|
|
|
861
|
+
if certain_body
|
|
862
|
+
@certain_definers << stmt
|
|
863
|
+
# A `begin … end` statement's body (and `ensure`) runs whenever the statement does.
|
|
864
|
+
mark_certain_body(stmt) if stmt.is_a?(Prism::BeginNode)
|
|
865
|
+
end
|
|
718
866
|
walk_defs(stmt, prefix, in_singleton_class, active, out)
|
|
719
867
|
end
|
|
720
868
|
end
|
|
@@ -731,7 +879,9 @@ module Rigor
|
|
|
731
879
|
kind = node.receiver.is_a?(Prism::SelfNode) || in_singleton_class ? :singleton : :instance
|
|
732
880
|
class_name = prefix.join("::")
|
|
733
881
|
if Inference::ModuleFunctionState.sig_gen_module_function?(module_function_active, kind)
|
|
734
|
-
@module_function_methods <<
|
|
882
|
+
@module_function_methods << node
|
|
883
|
+
elsif Inference::ModuleFunctionState.sig_gen_uncertain?(module_function_active, kind)
|
|
884
|
+
@uncertain_module_function_defs << node
|
|
735
885
|
end
|
|
736
886
|
out << [node, class_name, kind]
|
|
737
887
|
end
|
|
@@ -771,9 +921,9 @@ module Rigor
|
|
|
771
921
|
|
|
772
922
|
# Returns "def self." (kind: :singleton), "def self?." (instance method declared inside a
|
|
773
923
|
# `module_function` region — both instance + singleton dispatch at runtime), or "def " (plain instance).
|
|
774
|
-
def method_def_prefix(
|
|
924
|
+
def method_def_prefix(def_node, kind)
|
|
775
925
|
return "def self." if kind == :singleton
|
|
776
|
-
return "def self?." if @module_function_methods.include?(
|
|
926
|
+
return "def self?." if @module_function_methods.include?(def_node)
|
|
777
927
|
|
|
778
928
|
"def "
|
|
779
929
|
end
|
|
@@ -783,9 +933,13 @@ module Rigor
|
|
|
783
933
|
# private methods are implementation details, not part of the type contract downstream consumers (Steep,
|
|
784
934
|
# IDE, gem users) read. The default now skips private and protected methods; the `:include_private` flag
|
|
785
935
|
# restores the slice-4 behaviour for callers that want every method.
|
|
936
|
+
#
|
|
937
|
+
# A module function renders as `def self?.name`, which RBS reads as a public singleton method and a private
|
|
938
|
+
# instance method, so a `private` written before the `module_function` toggle does not exclude it (#1968).
|
|
786
939
|
def visibility_excludes?(def_node, class_name, kind, scope_index)
|
|
787
940
|
return false if kind == :singleton
|
|
788
941
|
return false if @include_private
|
|
942
|
+
return false if @module_function_methods.include?(def_node)
|
|
789
943
|
|
|
790
944
|
scope = scope_index[def_node] || scope_index.each_value.first
|
|
791
945
|
return false if scope.nil?
|
|
@@ -960,6 +1114,9 @@ module Rigor
|
|
|
960
1114
|
# #1676 — a top-level `def` is collected with no class; it is reported, never typed.
|
|
961
1115
|
def classify_collected_def(path, def_node, class_name, kind, scope_index)
|
|
962
1116
|
return skipped(path, def_node, class_name, kind, :top_level_def) if class_name.nil?
|
|
1117
|
+
if @uncertain_module_function_defs.include?(def_node)
|
|
1118
|
+
return skipped(path, def_node, class_name, kind, :uncertain_module_function)
|
|
1119
|
+
end
|
|
963
1120
|
|
|
964
1121
|
classify_def(path, def_node, class_name, kind, scope_index)
|
|
965
1122
|
end
|
|
@@ -1020,7 +1177,7 @@ module Rigor
|
|
|
1020
1177
|
|
|
1021
1178
|
scope = scope_index[def_node]
|
|
1022
1179
|
method_def = lookup_existing_method(class_name, def_node.name, kind, scope&.environment, scope)
|
|
1023
|
-
head = "#{method_def_prefix(
|
|
1180
|
+
head = "#{method_def_prefix(def_node, kind)}#{def_node.name}: "
|
|
1024
1181
|
if return_defaulted
|
|
1025
1182
|
return mixed_provenance_candidate(path, def_node, class_name, kind, scope_index, inline, method_def, head)
|
|
1026
1183
|
end
|
|
@@ -1240,7 +1397,37 @@ module Rigor
|
|
|
1240
1397
|
# different method (issue #1382). Delegates to {Rigor::Inference::DefReturnTyper} — the same body-typing +
|
|
1241
1398
|
# explicit-return-union the `rigor annotate` def-line annotator uses.
|
|
1242
1399
|
def infer_return_type(def_node, scope_index)
|
|
1243
|
-
Inference::DefReturnTyper.call(def_node, scope_index)
|
|
1400
|
+
absorb_empty_literals(Inference::DefReturnTyper.call(def_node, scope_index, keep_lookup_nil: true))
|
|
1401
|
+
end
|
|
1402
|
+
|
|
1403
|
+
# Issue #1153 — an empty-collection early return (`return [] if table.empty?`) types its arm `[]`, which the
|
|
1404
|
+
# engine's union keeps beside a sibling `Array[E]` arm on purpose (the empty tuple is a distinct shape). A
|
|
1405
|
+
# signature has no use for the distinction: `[]` is an `Array[E]` for every `E`, so `Array[E] | []` is just
|
|
1406
|
+
# `Array[E]`, and the leftover `[]` arm made the proposal read as if the empty literal pinned the element.
|
|
1407
|
+
# Issue #1149 — the same holds for the empty closed hash shape `{}` (a frozen empty fallback,
|
|
1408
|
+
# `table[key] || EMPTY`) beside a `Hash[K, V]` nominal. Each empty literal is absorbed only beside its OWN
|
|
1409
|
+
# nominal: a `[]` beside a `Hash` arm, or a `{}` beside an `Array` arm, is untouched. A lone empty literal,
|
|
1410
|
+
# one beside another tuple / shape (`[1, 2] | []`), an open `{ ... }` shape, and a non-empty shape are
|
|
1411
|
+
# untouched too, since each of those is information the declaration should keep.
|
|
1412
|
+
def absorb_empty_literals(type)
|
|
1413
|
+
return type unless type.is_a?(Type::Union)
|
|
1414
|
+
|
|
1415
|
+
kept = type.members.reject { |member| absorbed_empty_literal?(member, type.members) }
|
|
1416
|
+
kept.size == type.members.size ? type : Type::Combinator.union(*kept)
|
|
1417
|
+
end
|
|
1418
|
+
|
|
1419
|
+
def absorbed_empty_literal?(member, members)
|
|
1420
|
+
if member.is_a?(Type::Tuple) && member.elements.empty?
|
|
1421
|
+
nominal_arm?(members, "Array")
|
|
1422
|
+
elsif member.is_a?(Type::HashShape) && member.pairs.empty? && member.closed?
|
|
1423
|
+
nominal_arm?(members, "Hash")
|
|
1424
|
+
else
|
|
1425
|
+
false
|
|
1426
|
+
end
|
|
1427
|
+
end
|
|
1428
|
+
|
|
1429
|
+
def nominal_arm?(members, class_name)
|
|
1430
|
+
members.any? { |member| member.is_a?(Type::Nominal) && member.class_name == class_name }
|
|
1244
1431
|
end
|
|
1245
1432
|
|
|
1246
1433
|
def dynamic_top?(type)
|
|
@@ -1287,7 +1474,7 @@ module Rigor
|
|
|
1287
1474
|
return equivalent(path, def_node, class_name, kind, inferred, VOID_RETURN_RBS) if declares_void?(method_def)
|
|
1288
1475
|
|
|
1289
1476
|
declared = build_declared_return(method_def)
|
|
1290
|
-
if declared_untyped?(declared)
|
|
1477
|
+
if declared_untyped?(declared, method_def)
|
|
1291
1478
|
return declared_untyped_candidate(path, def_node, class_name, kind, inferred, method_def, rendered: rendered)
|
|
1292
1479
|
end
|
|
1293
1480
|
|
|
@@ -1354,7 +1541,7 @@ module Rigor
|
|
|
1354
1541
|
returned = paren_wrap_union(elaborated_rbs(inferred, owner: class_name))
|
|
1355
1542
|
annotations = overload.annotations.map { |a| "#{a.location&.source || "%a{#{a.string}}"} " }.join
|
|
1356
1543
|
visibility = member.respond_to?(:visibility) && member.visibility ? "#{member.visibility} " : ""
|
|
1357
|
-
"#{visibility}#{method_def_prefix(
|
|
1544
|
+
"#{visibility}#{method_def_prefix(def_node, kind)}#{def_node.name}: " \
|
|
1358
1545
|
"#{annotations}#{spell_with_return(overload.method_type, returned)}"
|
|
1359
1546
|
end
|
|
1360
1547
|
|
|
@@ -1413,20 +1600,30 @@ module Rigor
|
|
|
1413
1600
|
# skips. ADR-93's inline `# @rbs param: Type` annotation (no return clause) synthesizes exactly this
|
|
1414
1601
|
# spelling for every method it documents, so the silent drop hit the parameter-annotated, return-bare
|
|
1415
1602
|
# shape ADR-14 exists to help.
|
|
1416
|
-
|
|
1417
|
-
|
|
1603
|
+
#
|
|
1604
|
+
# The translation alone cannot say the author wrote `untyped`: an interface return (`-> _Taker[1]`,
|
|
1605
|
+
# `-> _ToS`) also translates to `Dynamic`, and reading it as an absent declaration replaced it with an
|
|
1606
|
+
# unrelated nominal under `--overwrite` (#2178). So every overload's return must be `untyped` as written.
|
|
1607
|
+
def declared_untyped?(declared, method_def)
|
|
1608
|
+
declared.is_a?(Type::Dynamic) &&
|
|
1609
|
+
method_def.method_types.all? { |mt| mt.type.return_type.is_a?(RBS::Types::Bases::Any) }
|
|
1418
1610
|
end
|
|
1419
1611
|
|
|
1420
1612
|
# Proposed without {#tighter?}: a declared `untyped` return carries no information to weigh it against, the
|
|
1421
1613
|
# same position a method with no declaration at all is in. Over a declaration the author wrote, though, a
|
|
1422
1614
|
# VALUE literal the body proves (`-> 1`, `-> "x"`, `-> :k`) is an implementation detail the `untyped` was
|
|
1423
1615
|
# abstracting over, so it is widened to its class (`1 | 2` to `Integer`, `"S" | nil` to `(String | nil)`; #1489).
|
|
1424
|
-
# `
|
|
1425
|
-
#
|
|
1426
|
-
#
|
|
1427
|
-
#
|
|
1616
|
+
# `true` and `false` widen to `bool` (#1891): a predicate's base `-> true` made every subclass override
|
|
1617
|
+
# returning `false` a `def.override-return-widened`. A return that is only `nil` has no such widening — it is
|
|
1618
|
+
# the shape of a template method a subclass fills in — so it is declined and the `untyped` stays. `nil` in a
|
|
1619
|
+
# union (`T?`) and a computed `Float` are honest types and stay. An `untyped` that is only the inline reader's
|
|
1620
|
+
# synthesis ({#synthesized_declaration?}) is no word of the author's and keeps the strictest carrier.
|
|
1621
|
+
# `declared_return_rbs` carries `"untyped"` so the `[tighter, was: untyped]` print tag says a declaration
|
|
1622
|
+
# existed.
|
|
1428
1623
|
def declared_untyped_candidate(path, def_node, class_name, kind, inferred, method_def, rendered: nil)
|
|
1429
1624
|
unless synthesized_declaration?(method_def)
|
|
1625
|
+
return equivalent(path, def_node, class_name, kind, inferred, "untyped") if bare_nil?(inferred)
|
|
1626
|
+
|
|
1430
1627
|
widened = widen_value_literals(inferred)
|
|
1431
1628
|
unless widened.equal?(inferred)
|
|
1432
1629
|
# A `rendered` line was spelled from the unwidened type; there is nothing to rebuild it from.
|
|
@@ -1442,21 +1639,60 @@ module Rigor
|
|
|
1442
1639
|
VALUE_LITERAL_CLASSES = [Integer, String, Symbol].freeze
|
|
1443
1640
|
private_constant :VALUE_LITERAL_CLASSES
|
|
1444
1641
|
|
|
1445
|
-
#
|
|
1642
|
+
# Core generics whose RBS parameters are all `out` or `unchecked out`, so a widened argument is a supertype of
|
|
1643
|
+
# the body's type. An invariant or `in` parameter (a user `class Sink[in T]`) is not widened: `Sink[Integer]`
|
|
1644
|
+
# is no supertype of `Sink[1]` there.
|
|
1645
|
+
COVARIANT_CARRIERS = %w[Array Hash Set Range Enumerator].freeze
|
|
1646
|
+
private_constant :COVARIANT_CARRIERS
|
|
1647
|
+
|
|
1648
|
+
# The same object back when nothing needed widening. A literal nested in a tuple element, a record or hash
|
|
1649
|
+
# value, or a covariant generic argument (`[1, "a"]`, `{ a: 1 }`, `Hash[String, 1]`) widens the same way, while
|
|
1650
|
+
# the carrier's shape and a record's keys stay: a subclass override returning `[2, "b"]` is no widening of
|
|
1651
|
+
# `[Integer, String]`. A nested `nil` is an honest member there, as in a top-level union.
|
|
1446
1652
|
def widen_value_literals(type)
|
|
1447
1653
|
case type
|
|
1448
1654
|
when Type::Constant
|
|
1655
|
+
return widen_boolean_literal if [true, false].include?(type.value)
|
|
1449
1656
|
return type unless VALUE_LITERAL_CLASSES.any? { |klass| type.value.instance_of?(klass) }
|
|
1450
1657
|
|
|
1451
1658
|
Type::Combinator.nominal_of(type.value.class.name)
|
|
1452
|
-
when Type::Union
|
|
1453
|
-
|
|
1454
|
-
|
|
1659
|
+
when Type::Union then widen_members(type.members, type) { |members| Type::Combinator.union(*members) }
|
|
1660
|
+
when Type::Tuple then widen_members(type.elements, type) { |elements| Type::Combinator.tuple_of(*elements) }
|
|
1661
|
+
when Type::HashShape then widen_hash_shape_values(type)
|
|
1662
|
+
when Type::Nominal
|
|
1663
|
+
return type unless COVARIANT_CARRIERS.include?(type.class_name)
|
|
1664
|
+
|
|
1665
|
+
widen_members(type.type_args, type) do |args|
|
|
1666
|
+
Type::Combinator.nominal_of(type.class_name, type_args: args)
|
|
1667
|
+
end
|
|
1455
1668
|
else
|
|
1456
1669
|
type
|
|
1457
1670
|
end
|
|
1458
1671
|
end
|
|
1459
1672
|
|
|
1673
|
+
def widen_members(members, type)
|
|
1674
|
+
widened = members.map { |member| widen_value_literals(member) }
|
|
1675
|
+
widened.each_with_index.all? { |m, i| m.equal?(members[i]) } ? type : yield(widened)
|
|
1676
|
+
end
|
|
1677
|
+
|
|
1678
|
+
def widen_hash_shape_values(shape)
|
|
1679
|
+
widen_members(shape.pairs.values, shape) do |values|
|
|
1680
|
+
Type::HashShape.new(
|
|
1681
|
+
shape.pairs.keys.zip(values).to_h,
|
|
1682
|
+
required_keys: shape.required_keys, optional_keys: shape.optional_keys,
|
|
1683
|
+
read_only_keys: shape.read_only_keys, extra_keys: shape.extra_keys
|
|
1684
|
+
)
|
|
1685
|
+
end
|
|
1686
|
+
end
|
|
1687
|
+
|
|
1688
|
+
def widen_boolean_literal
|
|
1689
|
+
Type::Combinator.union(Type::Combinator.constant_of(true), Type::Combinator.constant_of(false))
|
|
1690
|
+
end
|
|
1691
|
+
|
|
1692
|
+
def bare_nil?(type)
|
|
1693
|
+
type.is_a?(Type::Constant) && type.value.nil?
|
|
1694
|
+
end
|
|
1695
|
+
|
|
1460
1696
|
# A declaration only the inline reader's synthesis (a `virtual:` buffer) supplies — ADR-93 gives every `def` an
|
|
1461
1697
|
# `untyped` member, annotated or not — is no word of the author's about the return, so it keeps the proposal
|
|
1462
1698
|
# a method with no declaration at all gets: the strictest carrier the body proves.
|
|
@@ -1590,11 +1826,14 @@ module Rigor
|
|
|
1590
1826
|
end
|
|
1591
1827
|
|
|
1592
1828
|
# `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
|
-
#
|
|
1829
|
+
# `nil`, an Integer, a Symbol, a String) as that literal and every other carrier as a nominal. Asking
|
|
1830
|
+
# which of the two branches it took keeps the list of literal-spellable classes from forking a second
|
|
1831
|
+
# copy into this file. A Range's and a Set's nominal carries a type argument (`Range[Integer]`, #1891;
|
|
1832
|
+
# `Set[untyped]`, #2177), so they are excluded by class rather than by comparing the erasure with the class
|
|
1833
|
+
# name: like a Float each is a nominal, not a pinned literal.
|
|
1596
1834
|
def rbs_literal?(type)
|
|
1597
|
-
type.is_a?(Type::Constant) && type.
|
|
1835
|
+
type.is_a?(Type::Constant) && !type.value.is_a?(Range) && !type.value.is_a?(::Set) &&
|
|
1836
|
+
type.erase_to_rbs != type.value.class.name
|
|
1598
1837
|
end
|
|
1599
1838
|
|
|
1600
1839
|
# Heuristic added after the third-round self-dogfood: `FallbackTracer#size` body is `@events.size`, where
|
|
@@ -1662,7 +1901,7 @@ module Rigor
|
|
|
1662
1901
|
def render_rbs_line(def_node, inferred, class_name, kind)
|
|
1663
1902
|
params = def_node.parameters
|
|
1664
1903
|
head = "(#{render_param_list(params, class_name, def_node.name)})#{block_signature_suffix(params)}"
|
|
1665
|
-
prefix = method_def_prefix(
|
|
1904
|
+
prefix = method_def_prefix(def_node, kind)
|
|
1666
1905
|
"#{prefix}#{def_node.name}: #{head} -> #{paren_wrap_union(elaborated_rbs(inferred, owner: class_name))}"
|
|
1667
1906
|
end
|
|
1668
1907
|
|
|
@@ -1766,12 +2005,17 @@ module Rigor
|
|
|
1766
2005
|
# Per-file context the attr_* walker threads through its recursive descent. Keeps parameter lists in
|
|
1767
2006
|
# check. `obs_ivar_map` carries the observation-derived fallback types built by
|
|
1768
2007
|
# {#build_observed_ivar_map}; it is empty when sig-gen is invoked without `--params=observed`.
|
|
1769
|
-
|
|
2008
|
+
# `definer_table` is {#definer_table}'s map, which drops an attribute another definer replaces. A context
|
|
2009
|
+
# with `definers` only records each attribute's slot and call node for that map, and builds nothing.
|
|
2010
|
+
AttrWalkContext = Struct.new(:path, :scope_index, :obs_ivar_map, :out, :definer_table, :definers,
|
|
2011
|
+
keyword_init: true)
|
|
1770
2012
|
private_constant :AttrWalkContext
|
|
1771
2013
|
|
|
1772
|
-
def collect_attr_candidates(root, path, scope_index, obs_ivar_map = {})
|
|
2014
|
+
def collect_attr_candidates(root, path, scope_index, obs_ivar_map = {}, definer_table = {})
|
|
2015
|
+
@lookup_ivars = Set.new
|
|
2016
|
+
collect_lookup_ivar_writes(root, scope_index)
|
|
1773
2017
|
ctx = AttrWalkContext.new(path: path, scope_index: scope_index,
|
|
1774
|
-
obs_ivar_map: obs_ivar_map, out: [])
|
|
2018
|
+
obs_ivar_map: obs_ivar_map, out: [], definer_table: definer_table)
|
|
1775
2019
|
walk_attr_calls(root, [], false, ctx)
|
|
1776
2020
|
ctx.out
|
|
1777
2021
|
end
|
|
@@ -1816,11 +2060,67 @@ module Rigor
|
|
|
1816
2060
|
class_name = prefix.join("::")
|
|
1817
2061
|
symbol_names = extract_symbol_arguments(call_node)
|
|
1818
2062
|
return if symbol_names.empty?
|
|
2063
|
+
return record_attr_definers(call_node, class_name, symbol_names, ctx) if ctx.definers
|
|
1819
2064
|
|
|
1820
2065
|
ivar_lookup = ivar_type_lookup(ctx.scope_index, class_name, ctx.obs_ivar_map)
|
|
2066
|
+
offset = call_node.location.start_offset
|
|
2067
|
+
symbol_names.each do |attr_name|
|
|
2068
|
+
ivar_type = with_lookup_nil(ivar_lookup.call(attr_name), class_name, attr_name)
|
|
2069
|
+
candidates = build_attr_candidates(call_node.name, class_name, attr_name, ivar_type, ctx)
|
|
2070
|
+
ctx.out.concat(candidates.select do |c|
|
|
2071
|
+
live_definer?(ctx.definer_table, [class_name, :instance, c.method_name], offset)
|
|
2072
|
+
end)
|
|
2073
|
+
end
|
|
2074
|
+
end
|
|
2075
|
+
|
|
2076
|
+
# #2197 — an attribute whose ivar an instance method assigns from an optimistic lookup (`@x = h[k]`) reads
|
|
2077
|
+
# `nil` on a miss, as such a return does ({Inference::DefReturnTyper.optimistic_value?}), so the written
|
|
2078
|
+
# reader keeps the `nil` the engine's nil-free reading of the lookup drops.
|
|
2079
|
+
def with_lookup_nil(type, class_name, attr_name)
|
|
2080
|
+
return type if type.nil? || !@lookup_ivars.include?([class_name, :"@#{attr_name}"])
|
|
2081
|
+
|
|
2082
|
+
Type::Combinator.union(type, Type::Combinator.constant_of(nil))
|
|
2083
|
+
end
|
|
2084
|
+
|
|
2085
|
+
LOOKUP_IVAR_WRITES = [
|
|
2086
|
+
Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode
|
|
2087
|
+
].freeze
|
|
2088
|
+
private_constant :LOOKUP_IVAR_WRITES
|
|
2089
|
+
|
|
2090
|
+
# Records `[class_name, ivar]` for each ivar an instance `def` assigns an optimistic lookup to. The class
|
|
2091
|
+
# prefixes come from {#collect_other_ivar_writes}' walk, whose own result is not needed here.
|
|
2092
|
+
def collect_lookup_ivar_writes(root, scope_index)
|
|
2093
|
+
collect_other_ivar_writes(root, [], false, Hash.new { |h, k| h[k] = Set.new }) do |def_node, prefix|
|
|
2094
|
+
next if def_node.receiver || prefix.empty?
|
|
2095
|
+
|
|
2096
|
+
scan_lookup_ivar_writes(def_node.body, prefix, scope_index)
|
|
2097
|
+
end
|
|
2098
|
+
end
|
|
2099
|
+
|
|
2100
|
+
def scan_lookup_ivar_writes(node, prefix, scope_index)
|
|
2101
|
+
return unless node.is_a?(Prism::Node)
|
|
2102
|
+
|
|
2103
|
+
record_lookup_ivar_write(node, prefix, scope_index)
|
|
2104
|
+
node.rigor_each_child { |child| scan_lookup_ivar_writes(child, prefix, scope_index) }
|
|
2105
|
+
end
|
|
2106
|
+
|
|
2107
|
+
def record_lookup_ivar_write(node, prefix, scope_index)
|
|
2108
|
+
return unless LOOKUP_IVAR_WRITES.any? { |klass| node.is_a?(klass) }
|
|
2109
|
+
|
|
2110
|
+
scope = scope_index[node.value] || scope_index[node]
|
|
2111
|
+
return if scope.nil? || !Inference::DefReturnTyper.optimistic_value?(
|
|
2112
|
+
node.value, scope, Inference::DefReturnTyper::LookupContext.new(scope_index, {}.compare_by_identity)
|
|
2113
|
+
)
|
|
2114
|
+
|
|
2115
|
+
@lookup_ivars << [prefix.join("::"), node.name]
|
|
2116
|
+
end
|
|
2117
|
+
|
|
2118
|
+
def record_attr_definers(call_node, class_name, symbol_names, ctx)
|
|
1821
2119
|
symbol_names.each do |attr_name|
|
|
1822
|
-
|
|
1823
|
-
|
|
2120
|
+
ATTR_KINDS.fetch(call_node.name).each do |variant|
|
|
2121
|
+
method_name = variant == :writer ? :"#{attr_name}=" : attr_name
|
|
2122
|
+
ctx.definers << [[class_name, :instance, method_name], call_node]
|
|
2123
|
+
end
|
|
1824
2124
|
end
|
|
1825
2125
|
end
|
|
1826
2126
|
|
|
@@ -1837,7 +2137,8 @@ module Rigor
|
|
|
1837
2137
|
# pre-pass resolved the type to `nil` or `Dynamic[top]` — typically because `@ivar = param` inside
|
|
1838
2138
|
# `initialize` typed the param as `untyped` — the observation-derived type is substituted. This lets
|
|
1839
2139
|
# `attr_reader :name` emit a concrete type when `ClassName.new("alice")` call sites are visible to the
|
|
1840
|
-
# observation scan.
|
|
2140
|
+
# observation scan. {#build_observed_ivar_map} leaves out an ivar anything else writes, so a writer-backed
|
|
2141
|
+
# field is skipped (issue #1410).
|
|
1841
2142
|
def ivar_type_lookup(scope_index, class_name, obs_ivar_map = {})
|
|
1842
2143
|
any_scope = scope_index.each_value.first
|
|
1843
2144
|
return ->(_) {} if any_scope.nil?
|
|
@@ -1854,12 +2155,112 @@ module Rigor
|
|
|
1854
2155
|
# ivars assigned directly from `def initialize` parameters. Only populated when `@observations` is
|
|
1855
2156
|
# non-empty (i.e. `--params=observed` was supplied). Matches the pattern `@ivar_name = param_name` where
|
|
1856
2157
|
# `param_name` is a required / optional positional or keyword parameter of `initialize`.
|
|
2158
|
+
#
|
|
2159
|
+
# Issue #1410: the observed arguments describe the field only while those `@ivar = param_name` writes are
|
|
2160
|
+
# all that writes it. An `attr_writer` / `attr_accessor`, a write in another instance method or block, or
|
|
2161
|
+
# any other write in `initialize` lets the field hold values the observation never saw, so such an ivar is
|
|
2162
|
+
# left out and its accessor is skipped as `untyped_return`, as a hand-written setter is.
|
|
1857
2163
|
def build_observed_ivar_map(root)
|
|
1858
2164
|
return {} if @observations.empty?
|
|
1859
2165
|
|
|
1860
2166
|
result = {}
|
|
1861
2167
|
collect_init_ivar_obs(root, [], result)
|
|
1862
|
-
|
|
2168
|
+
other_writes = Hash.new { |h, k| h[k] = Set.new }
|
|
2169
|
+
collect_other_ivar_writes(root, [], false, other_writes)
|
|
2170
|
+
result.to_h do |class_name, map|
|
|
2171
|
+
others = other_writes[class_name]
|
|
2172
|
+
[class_name, others.include?(ANY_IVAR) ? {} : map.reject { |attr_name, _| others.include?(:"@#{attr_name}") }]
|
|
2173
|
+
end
|
|
2174
|
+
end
|
|
2175
|
+
|
|
2176
|
+
IVAR_WRITE_NODES = [
|
|
2177
|
+
Prism::InstanceVariableWriteNode, Prism::InstanceVariableOperatorWriteNode,
|
|
2178
|
+
Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode, Prism::InstanceVariableTargetNode
|
|
2179
|
+
].freeze
|
|
2180
|
+
ANY_IVAR = :*
|
|
2181
|
+
private_constant :IVAR_WRITE_NODES, :ANY_IVAR
|
|
2182
|
+
|
|
2183
|
+
# Records into `other_writes[class_name]` every instance ivar a class writes other than through an
|
|
2184
|
+
# `initialize`'s `@ivar = param_name`: the ivars an implicit-self `attr_writer` / `attr_accessor` names,
|
|
2185
|
+
# and every write inside an instance `def` or a block, an `instance_variable_set` on `self` included
|
|
2186
|
+
# ({#written_ivar}). `def self.x`, a `class << self` body and a bare class-body write set the class
|
|
2187
|
+
# object's ivars and do not count.
|
|
2188
|
+
#
|
|
2189
|
+
# `on_def`, when given, is called with each instance `def` reached and its class prefix, so another
|
|
2190
|
+
# per-class reading of instance-method ivar writes shares this walk ({#collect_lookup_ivar_writes}).
|
|
2191
|
+
def collect_other_ivar_writes(node, prefix, in_block, other_writes, &on_def)
|
|
2192
|
+
return unless node.is_a?(Prism::Node)
|
|
2193
|
+
|
|
2194
|
+
case node
|
|
2195
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
2196
|
+
child_prefix = Source::ConstantPath.declaration_prefix(prefix, node.constant_path)
|
|
2197
|
+
return collect_other_ivar_writes(node.body, child_prefix, false, other_writes, &on_def) if child_prefix
|
|
2198
|
+
when Prism::SingletonClassNode then return
|
|
2199
|
+
when Prism::DefNode
|
|
2200
|
+
on_def&.call(node, prefix)
|
|
2201
|
+
return record_def_ivar_writes(node, prefix, other_writes)
|
|
2202
|
+
when Prism::BlockNode, Prism::LambdaNode then in_block = true
|
|
2203
|
+
end
|
|
2204
|
+
record_class_level_write(node, prefix, in_block, other_writes) unless prefix.empty?
|
|
2205
|
+
|
|
2206
|
+
node.rigor_each_child { |child| collect_other_ivar_writes(child, prefix, in_block, other_writes, &on_def) }
|
|
2207
|
+
end
|
|
2208
|
+
|
|
2209
|
+
# The ivars an `attr_writer` / `attr_accessor` on `self` names, or the ivar a write in a block sets.
|
|
2210
|
+
def record_class_level_write(node, prefix, in_block, other_writes)
|
|
2211
|
+
writes = other_writes[prefix.join("::")]
|
|
2212
|
+
attr_writer_names(node).each { |name| writes << :"@#{name}" }
|
|
2213
|
+
name = written_ivar(node) if in_block
|
|
2214
|
+
writes << name if name
|
|
2215
|
+
end
|
|
2216
|
+
|
|
2217
|
+
ATTR_WRITER_DECLARATIONS = %i[attr_writer attr_accessor].freeze
|
|
2218
|
+
SEND_NAMES = %i[send public_send __send__].freeze
|
|
2219
|
+
private_constant :ATTR_WRITER_DECLARATIONS, :SEND_NAMES
|
|
2220
|
+
|
|
2221
|
+
# The literal names `attr_writer :x` / `attr_accessor :x` declares when called on `self`, implicitly or as
|
|
2222
|
+
# `self.`, or through `send` / `public_send` / `__send__` with the declaration's name as a literal first
|
|
2223
|
+
# argument (`send(:attr_writer, :x)`). Any other call declares none this check sees.
|
|
2224
|
+
def attr_writer_names(node)
|
|
2225
|
+
return [] unless node.is_a?(Prism::CallNode) && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
|
|
2226
|
+
|
|
2227
|
+
names = Source::Literals.symbol_arguments(node)
|
|
2228
|
+
return names if ATTR_WRITER_DECLARATIONS.include?(node.name)
|
|
2229
|
+
return [] unless SEND_NAMES.include?(node.name)
|
|
2230
|
+
|
|
2231
|
+
ATTR_WRITER_DECLARATIONS.include?(Source::Literals.symbol_arg(node, 0)) ? names.drop(1) : []
|
|
2232
|
+
end
|
|
2233
|
+
|
|
2234
|
+
# Every ivar write in an instance `def` counts, except, in `initialize`, the `@ivar = param_name` write
|
|
2235
|
+
# {#scan_ivar_param_assignments} keys the observation on.
|
|
2236
|
+
def record_def_ivar_writes(def_node, prefix, other_writes)
|
|
2237
|
+
return if def_node.body.nil? || def_node.receiver || prefix.empty?
|
|
2238
|
+
|
|
2239
|
+
credited = Set.new.compare_by_identity
|
|
2240
|
+
if def_node.name == :initialize && def_node.parameters
|
|
2241
|
+
params = build_init_param_index(def_node.parameters).keys.to_set
|
|
2242
|
+
scan_ivar_param_assignments(def_node.body, params, {}, credited)
|
|
2243
|
+
end
|
|
2244
|
+
each_node(def_node.body) do |node|
|
|
2245
|
+
name = written_ivar(node)
|
|
2246
|
+
other_writes[prefix.join("::")] << name if name && !credited.include?(node)
|
|
2247
|
+
end
|
|
2248
|
+
end
|
|
2249
|
+
|
|
2250
|
+
# The ivar `node` writes: a write node's name, an `instance_variable_set` on `self`'s literal name, or
|
|
2251
|
+
# `ANY_IVAR` for one whose name the analysis cannot read ({Inference::ReflectiveIvarWrite}).
|
|
2252
|
+
def written_ivar(node)
|
|
2253
|
+
return node.name if IVAR_WRITE_NODES.any? { |klass| node.is_a?(klass) }
|
|
2254
|
+
return nil unless Inference::ReflectiveIvarWrite.self_write?(node)
|
|
2255
|
+
|
|
2256
|
+
Inference::ReflectiveIvarWrite.literal_name(node) || ANY_IVAR
|
|
2257
|
+
end
|
|
2258
|
+
|
|
2259
|
+
def each_node(node, &)
|
|
2260
|
+
return unless node.is_a?(Prism::Node)
|
|
2261
|
+
|
|
2262
|
+
yield node
|
|
2263
|
+
node.rigor_each_child { |child| each_node(child, &) }
|
|
1863
2264
|
end
|
|
1864
2265
|
|
|
1865
2266
|
def collect_init_ivar_obs(node, prefix, result)
|
|
@@ -1945,21 +2346,36 @@ module Rigor
|
|
|
1945
2346
|
|
|
1946
2347
|
# Walk a def body for direct `@ivar = local_var` assignments where `local_var` is one of the listed
|
|
1947
2348
|
# parameter names. Records ivar_name (Symbol with `@` prefix) → param_name. Does not recurse into nested
|
|
1948
|
-
# defs / classes / modules.
|
|
1949
|
-
|
|
2349
|
+
# defs / classes / modules. A read counts only when it reaches the method's own parameter: its
|
|
2350
|
+
# `LocalVariableReadNode#depth` equals the number of blocks / lambdas between it and the method, so a
|
|
2351
|
+
# block parameter of the same name (`each { |count| @count = count }`) is not the constructor argument
|
|
2352
|
+
# (#1410). `credited`, when given, collects every write node the result is keyed on.
|
|
2353
|
+
def scan_ivar_param_assignments(node, param_names, result, credited = nil, scopes = 0)
|
|
1950
2354
|
return unless node.is_a?(Prism::Node)
|
|
1951
2355
|
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
result[
|
|
2356
|
+
ivar, param = param_assignment(node, param_names, scopes)
|
|
2357
|
+
if ivar
|
|
2358
|
+
result[ivar] ||= param
|
|
2359
|
+
credited << node if credited && result[ivar] == param
|
|
1956
2360
|
end
|
|
1957
2361
|
|
|
1958
2362
|
return if node.is_a?(Prism::DefNode) ||
|
|
1959
2363
|
node.is_a?(Prism::ClassNode) ||
|
|
1960
2364
|
node.is_a?(Prism::ModuleNode)
|
|
1961
2365
|
|
|
1962
|
-
|
|
2366
|
+
scopes += 1 if node.is_a?(Prism::BlockNode) || node.is_a?(Prism::LambdaNode)
|
|
2367
|
+
node.rigor_each_child { |c| scan_ivar_param_assignments(c, param_names, result, credited, scopes) }
|
|
2368
|
+
end
|
|
2369
|
+
|
|
2370
|
+
# `[ivar, param]` for `@ivar = param` whose read reaches the method's own parameter `param`, else nil.
|
|
2371
|
+
def param_assignment(node, param_names, scopes)
|
|
2372
|
+
return nil unless node.is_a?(Prism::InstanceVariableWriteNode)
|
|
2373
|
+
|
|
2374
|
+
read = node.value
|
|
2375
|
+
return nil unless read.is_a?(Prism::LocalVariableReadNode) && read.depth == scopes
|
|
2376
|
+
return nil unless param_names.include?(read.name)
|
|
2377
|
+
|
|
2378
|
+
[node.name, read.name]
|
|
1963
2379
|
end
|
|
1964
2380
|
|
|
1965
2381
|
def build_attr_candidates(call_name, class_name, attr_name, ivar_type, ctx)
|