rigortype 0.4.5 → 0.4.7
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 +2 -2
- data/data/gem_overlay/activesupport/core_ext.rbs +16 -13
- data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
- data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
- data/docs/handbook/01-getting-started.md +5 -2
- data/docs/handbook/11-sig-gen.md +61 -5
- data/docs/handbook/appendix-phpstan.md +4 -4
- data/docs/manual/02-cli-reference.md +192 -27
- data/docs/manual/03-configuration.md +38 -4
- data/docs/manual/04-diagnostics.md +1 -1
- data/docs/manual/06-baseline.md +6 -4
- data/docs/manual/09-editor-integration.md +13 -2
- data/docs/manual/10-mcp-server.md +47 -3
- data/docs/manual/12-caching.md +9 -1
- data/docs/manual/plugins/rigor-actionmailer.md +30 -0
- data/docs/manual/plugins/rigor-actionpack.md +49 -11
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
- data/docs/manual/plugins/rigor-devise.md +15 -0
- data/docs/manual/plugins/rigor-rspec.md +2 -1
- data/lib/rigor/analysis/baseline.rb +24 -27
- data/lib/rigor/analysis/buffer_binding.rb +14 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
- data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +105 -12
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +143 -15
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +4 -1
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
- 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/return_in_ensure_collector.rb +1 -2
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
- data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
- data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +53 -2
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules.rb +217 -52
- data/lib/rigor/analysis/crash_signature.rb +2 -2
- data/lib/rigor/analysis/dependency_recorder.rb +15 -4
- data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
- data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
- data/lib/rigor/analysis/edit_replay.rb +154 -0
- data/lib/rigor/analysis/fact_store.rb +3 -3
- data/lib/rigor/analysis/incremental.rb +37 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
- data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
- data/lib/rigor/analysis/incremental_session.rb +202 -43
- data/lib/rigor/analysis/path_expansion.rb +41 -0
- data/lib/rigor/analysis/require_relative_closure.rb +100 -0
- data/lib/rigor/analysis/rule_catalog.rb +7 -3
- data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
- data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +8 -4
- data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +204 -96
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
- data/lib/rigor/analysis/runner/pool_coordinator.rb +157 -320
- data/lib/rigor/analysis/runner/project_pre_passes.rb +48 -6
- data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
- data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
- data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
- data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
- data/lib/rigor/analysis/runner.rb +233 -120
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
- data/lib/rigor/analysis/template_unit_positions.rb +2 -3
- data/lib/rigor/analysis/template_units.rb +9 -1
- data/lib/rigor/analysis/worker_session.rb +38 -33
- data/lib/rigor/bleeding_edge.rb +3 -6
- data/lib/rigor/builtins/imported_refinements.rb +6 -8
- data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
- data/lib/rigor/builtins/regex_refinement.rb +4 -6
- data/lib/rigor/cache/descriptor.rb +6 -5
- data/lib/rigor/cache/incremental_snapshot.rb +60 -18
- data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
- data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
- data/lib/rigor/cache/rbs_constant_table.rb +1 -1
- data/lib/rigor/cache/rbs_descriptor.rb +54 -3
- data/lib/rigor/cache/rbs_environment.rb +4 -1
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
- data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
- data/lib/rigor/cache/store.rb +11 -10
- data/lib/rigor/ci_detector.rb +1 -1
- data/lib/rigor/cli/annotate_command.rb +24 -19
- data/lib/rigor/cli/baseline_command.rb +145 -55
- data/lib/rigor/cli/catalogue.rb +256 -0
- data/lib/rigor/cli/check_command.rb +188 -93
- data/lib/rigor/cli/check_runner_factory.rb +2 -2
- data/lib/rigor/cli/command.rb +15 -1
- data/lib/rigor/cli/coverage_command.rb +21 -8
- data/lib/rigor/cli/coverage_mutation.rb +5 -7
- data/lib/rigor/cli/coverage_renderer.rb +2 -1
- data/lib/rigor/cli/coverage_scan.rb +3 -5
- data/lib/rigor/cli/diff_command.rb +27 -21
- data/lib/rigor/cli/docs_command.rb +1 -2
- data/lib/rigor/cli/doctor_command.rb +12 -12
- data/lib/rigor/cli/effects_command.rb +50 -40
- data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
- data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
- data/lib/rigor/cli/effects_renderer.rb +2 -1
- data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
- data/lib/rigor/cli/explain_command.rb +18 -8
- data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
- data/lib/rigor/cli/json_document.rb +41 -0
- data/lib/rigor/cli/lens_command.rb +17 -13
- data/lib/rigor/cli/lsp_command.rb +250 -23
- data/lib/rigor/cli/mcp_command.rb +14 -8
- data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
- data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
- data/lib/rigor/cli/options.rb +60 -4
- data/lib/rigor/cli/plugin_command.rb +93 -46
- data/lib/rigor/cli/plugins_command.rb +22 -18
- data/lib/rigor/cli/plugins_renderer.rb +34 -8
- data/lib/rigor/cli/protection_fork_scan.rb +2 -2
- data/lib/rigor/cli/protection_renderer.rb +2 -1
- data/lib/rigor/cli/show_bleedingedge_command.rb +23 -16
- data/lib/rigor/cli/sig_gen_command.rb +130 -34
- data/lib/rigor/cli/skill_command.rb +32 -3
- data/lib/rigor/cli/skill_deep_probe.rb +1 -1
- data/lib/rigor/cli/skill_describe.rb +1 -1
- data/lib/rigor/cli/trace_command.rb +14 -9
- data/lib/rigor/cli/triage_command.rb +10 -11
- data/lib/rigor/cli/triage_renderer.rb +2 -1
- data/lib/rigor/cli/type_of_command.rb +18 -12
- data/lib/rigor/cli/type_of_project_scope.rb +24 -0
- data/lib/rigor/cli/type_of_renderer.rb +4 -1
- data/lib/rigor/cli/type_scan_command.rb +14 -9
- data/lib/rigor/cli/type_scan_renderer.rb +2 -1
- data/lib/rigor/cli/unused_command.rb +27 -17
- data/lib/rigor/cli/upgrade_command.rb +5 -0
- data/lib/rigor/cli.rb +209 -54
- data/lib/rigor/config_audit.rb +17 -6
- data/lib/rigor/configuration/severity_profile.rb +5 -5
- data/lib/rigor/configuration.rb +86 -34
- data/lib/rigor/effects/definition_context.rb +0 -2
- data/lib/rigor/effects/envelope.rb +1 -1
- data/lib/rigor/effects/framework_units.rb +1 -1
- data/lib/rigor/effects/mutation_classifier.rb +4 -7
- data/lib/rigor/effects/propagator.rb +1 -1
- data/lib/rigor/effects/registry.rb +2 -2
- data/lib/rigor/effects/snapshot.rb +1 -1
- data/lib/rigor/effects/taint_cause.rb +0 -2
- data/lib/rigor/effects/unit_scan.rb +9 -13
- data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
- data/lib/rigor/environment/class_registry.rb +3 -3
- data/lib/rigor/environment/core_root.rb +77 -0
- data/lib/rigor/environment/default_libraries.rb +1 -1
- data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
- data/lib/rigor/environment/gated_signature_guard.rb +1 -1
- data/lib/rigor/environment/gated_signature_plan.rb +5 -2
- data/lib/rigor/environment/lockfile_resolver.rb +28 -12
- data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
- data/lib/rigor/environment/member_consistency.rb +56 -7
- data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
- data/lib/rigor/environment/project_path_gems.rb +124 -0
- data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
- data/lib/rigor/environment/rbs_loader.rb +592 -70
- data/lib/rigor/environment/reflection.rb +7 -7
- data/lib/rigor/environment/required_features.rb +136 -32
- data/lib/rigor/environment/ruby_library_roots.rb +122 -0
- data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
- data/lib/rigor/environment.rb +44 -11
- data/lib/rigor/flow_contribution/merger.rb +4 -4
- data/lib/rigor/inference/acceptance.rb +93 -32
- data/lib/rigor/inference/block_parameter_binder.rb +2 -3
- data/lib/rigor/inference/block_run_count.rb +178 -0
- data/lib/rigor/inference/budget_trace.rb +8 -2
- data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
- data/lib/rigor/inference/callee_mutation_chain.rb +126 -0
- data/lib/rigor/inference/callee_write_census.rb +815 -0
- data/lib/rigor/inference/construction_escape.rb +217 -0
- data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
- data/lib/rigor/inference/deep_erasure.rb +170 -0
- data/lib/rigor/inference/def_handle.rb +1 -1
- data/lib/rigor/inference/def_node_resolver.rb +1 -1
- data/lib/rigor/inference/def_return_typer.rb +204 -12
- data/lib/rigor/inference/definer_resolution.rb +21 -5
- data/lib/rigor/inference/definers_index.rb +106 -0
- data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
- data/lib/rigor/inference/element_read_widening.rb +49 -3
- data/lib/rigor/inference/error_info.rb +17 -3
- data/lib/rigor/inference/expression_typer.rb +173 -57
- data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
- data/lib/rigor/inference/global_write_census.rb +110 -1
- data/lib/rigor/inference/global_writer_reach.rb +260 -0
- data/lib/rigor/inference/hkt_body.rb +1 -2
- data/lib/rigor/inference/hkt_body_parser.rb +3 -5
- data/lib/rigor/inference/hkt_reducer.rb +2 -2
- data/lib/rigor/inference/hkt_registry.rb +3 -5
- data/lib/rigor/inference/indexed_narrowing.rb +4 -7
- data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
- data/lib/rigor/inference/ivar_writer_census.rb +246 -0
- data/lib/rigor/inference/key_presence_guard.rb +2 -1
- data/lib/rigor/inference/loop_value_record.rb +57 -0
- data/lib/rigor/inference/match_rebinding/frame.rb +65 -0
- data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
- data/lib/rigor/inference/match_rebinding.rb +11 -1
- data/lib/rigor/inference/memo_audit.rb +55 -0
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +113 -12
- data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -3
- data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
- data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +59 -24
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
- data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
- data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher.rb +109 -29
- data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
- data/lib/rigor/inference/module_function_state.rb +23 -1
- data/lib/rigor/inference/multi_target_binder.rb +1 -1
- data/lib/rigor/inference/mutation_rejoin.rb +240 -4
- data/lib/rigor/inference/mutation_widening.rb +162 -29
- data/lib/rigor/inference/narrowing.rb +113 -38
- data/lib/rigor/inference/object_mixins.rb +13 -1
- data/lib/rigor/inference/optimistic_origin.rb +1 -1
- data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
- data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
- data/lib/rigor/inference/pre_eval_constants.rb +1 -1
- data/lib/rigor/inference/project_method_ownership.rb +6 -3
- data/lib/rigor/inference/project_seed_tables.rb +2 -1
- data/lib/rigor/inference/project_yielder.rb +122 -0
- data/lib/rigor/inference/receiver_alias.rb +20 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
- data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
- data/lib/rigor/inference/remover_holder_scan.rb +233 -0
- data/lib/rigor/inference/remover_narrowing.rb +224 -0
- data/lib/rigor/inference/rewrite_mutation.rb +53 -2
- data/lib/rigor/inference/scope_indexer/certainty.rb +9 -1
- data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
- data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +205 -0
- data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
- data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
- data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
- data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
- data/lib/rigor/inference/scope_indexer.rb +614 -146
- data/lib/rigor/inference/self_call_ivar_writes.rb +101 -17
- data/lib/rigor/inference/set_mutation.rb +189 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
- data/lib/rigor/inference/statement_evaluator.rb +3036 -278
- data/lib/rigor/inference/struct_fold_safety.rb +4 -6
- 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 +73 -14
- data/lib/rigor/inference/toplevel_def_census.rb +59 -0
- data/lib/rigor/inference/unknown_store_widening.rb +103 -4
- data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
- data/lib/rigor/inference/void_tail_summary.rb +2 -1
- data/lib/rigor/language_server/buffer_table.rb +9 -3
- data/lib/rigor/language_server/completion_provider.rb +26 -20
- data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +28 -9
- data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
- data/lib/rigor/language_server/folding_range_provider.rb +1 -2
- data/lib/rigor/language_server/hover_provider.rb +1 -7
- data/lib/rigor/language_server/hover_renderer.rb +6 -8
- data/lib/rigor/language_server/project_context.rb +98 -4
- data/lib/rigor/language_server/publish_batcher.rb +3 -4
- data/lib/rigor/language_server/selection_range_provider.rb +2 -4
- data/lib/rigor/language_server/server.rb +12 -6
- data/lib/rigor/language_server/signature_help_provider.rb +4 -4
- data/lib/rigor/lens/plugins.rb +1 -1
- data/lib/rigor/mcp/server.rb +82 -3
- data/lib/rigor/mcp.rb +1 -1
- data/lib/rigor/plugin/access_denied_error.rb +1 -2
- data/lib/rigor/plugin/base.rb +26 -11
- data/lib/rigor/plugin/bundled_catalog.rb +1 -12
- data/lib/rigor/plugin/io_boundary.rb +1 -1
- data/lib/rigor/plugin/loader.rb +25 -8
- data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
- data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
- data/lib/rigor/plugin/manifest.rb +38 -16
- data/lib/rigor/plugin/registry.rb +41 -3
- data/lib/rigor/plugin/services.rb +2 -3
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
- data/lib/rigor/plugin/trust_policy.rb +2 -2
- data/lib/rigor/plugin/type_node_resolver.rb +2 -4
- data/lib/rigor/plugin.rb +6 -8
- data/lib/rigor/plugin_gap_advisory.rb +5 -1
- data/lib/rigor/project_environment.rb +16 -2
- data/lib/rigor/protection/analysis_guard.rb +1 -1
- data/lib/rigor/protection/discovery_seed.rb +96 -2
- data/lib/rigor/protection/mutator.rb +1 -1
- data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
- data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
- data/lib/rigor/rbs_extended/reporter.rb +27 -3
- data/lib/rigor/rbs_extended.rb +2 -3
- data/lib/rigor/reflection/constant_ancestors.rb +5 -3
- data/lib/rigor/rooted_glob.rb +25 -2
- data/lib/rigor/run_scoped_slots.rb +38 -0
- data/lib/rigor/runtime/jit.rb +1 -1
- data/lib/rigor/scope/discovery_index.rb +19 -6
- data/lib/rigor/scope/resolution_chain.rb +3 -1
- data/lib/rigor/scope.rb +379 -36
- data/lib/rigor/sig_gen/classification.rb +4 -0
- data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
- data/lib/rigor/sig_gen/generator.rb +668 -84
- data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
- data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
- data/lib/rigor/sig_gen/layout_index.rb +3 -3
- data/lib/rigor/sig_gen/method_candidate.rb +23 -4
- data/lib/rigor/sig_gen/observation_collector.rb +7 -6
- data/lib/rigor/sig_gen/observed_call.rb +1 -1
- data/lib/rigor/sig_gen/path_mapper.rb +6 -0
- data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +21 -12
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
- data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
- data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
- data/lib/rigor/sig_gen/write_reporter.rb +63 -14
- data/lib/rigor/sig_gen/write_result.rb +23 -12
- data/lib/rigor/sig_gen/writer.rb +217 -37
- data/lib/rigor/type/acceptance_router.rb +2 -2
- data/lib/rigor/type/accepts_result.rb +1 -1
- data/lib/rigor/type/app.rb +4 -6
- data/lib/rigor/type/combinator.rb +13 -16
- data/lib/rigor/type/constant.rb +48 -7
- data/lib/rigor/type/hash_shape.rb +26 -40
- data/lib/rigor/type/struct_instance.rb +3 -2
- data/lib/rigor/type/union.rb +1 -1
- data/lib/rigor/type_node/identifier.rb +1 -2
- data/lib/rigor/type_node/resolver_chain.rb +1 -1
- data/lib/rigor/type_node/string_literal.rb +1 -2
- data/lib/rigor/type_node/symbol_literal.rb +1 -2
- data/lib/rigor/type_node/union.rb +3 -6
- data/lib/rigor/value_semantics.rb +1 -10
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +89 -70
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +25 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +79 -19
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +6 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
- data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
- data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +5 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
- data/sig/rigor/analysis/baseline.rbs +0 -2
- data/sig/rigor/analysis/fact_store.rbs +1 -0
- data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
- data/sig/rigor/cli/diff_command.rbs +1 -1
- data/sig/rigor/cli/explain_command.rbs +1 -1
- 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 +10 -1
- data/sig/rigor/plugin/additional_initializer.rbs +0 -5
- data/sig/rigor/plugin/base.rbs +1 -0
- 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 +6 -5
- data/sig/rigor/plugin/manifest.rbs +16 -18
- data/sig/rigor/plugin/protocol_contract.rbs +6 -9
- data/sig/rigor/plugin/registry.rbs +1 -1
- data/sig/rigor/scope.rbs +51 -50
- data/sig/rigor/type.rbs +0 -6
- data/skills/rigor-plugin-author/SKILL.md +4 -4
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
- data/skills/rigor-plugin-review/SKILL.md +3 -3
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
- metadata +55 -3
|
@@ -4,6 +4,7 @@ require "prism"
|
|
|
4
4
|
|
|
5
5
|
require_relative "../configuration"
|
|
6
6
|
require_relative "../environment"
|
|
7
|
+
require_relative "../run_scoped_slots"
|
|
7
8
|
require_relative "../project_environment"
|
|
8
9
|
require_relative "../scope"
|
|
9
10
|
require_relative "../reflection"
|
|
@@ -14,6 +15,7 @@ require_relative "../source/literals"
|
|
|
14
15
|
require_relative "../source/node_children"
|
|
15
16
|
require_relative "../inference/def_return_typer"
|
|
16
17
|
require_relative "../inference/scope_indexer"
|
|
18
|
+
require_relative "../inference/project_seed_tables"
|
|
17
19
|
require_relative "../inference/module_function_state"
|
|
18
20
|
require_relative "../inference/rbs_type_translator"
|
|
19
21
|
require_relative "alias_index"
|
|
@@ -25,6 +27,7 @@ require_relative "declaration_equivalence"
|
|
|
25
27
|
require_relative "inline_declarations"
|
|
26
28
|
require_relative "layout_index"
|
|
27
29
|
require_relative "method_candidate"
|
|
30
|
+
require_relative "type_name_qualification"
|
|
28
31
|
require_relative "writer"
|
|
29
32
|
require_relative "../rooted_glob"
|
|
30
33
|
|
|
@@ -78,16 +81,20 @@ module Rigor
|
|
|
78
81
|
attr_reader :unrenderable, :unresolvable_superclasses
|
|
79
82
|
|
|
80
83
|
# @param paths — files / directories to scan.
|
|
81
|
-
# @param observations —
|
|
84
|
+
# @param observations —
|
|
82
85
|
# ADR-14 slice 3 — per-target-method arg-tuple observations
|
|
83
86
|
# produced by {ObservationCollector}. An empty Hash (the default)
|
|
84
87
|
# means "no observations available; emit `untyped` for every
|
|
85
88
|
# parameter position" per ADR-5 clause 2.
|
|
89
|
+
# @param target_writer — the {Writer} a `--write` or `--check` run hands the candidates to. When given, a
|
|
90
|
+
# class counts as written for a subclass only if that writer accepts its target file (#2461); nil (print,
|
|
91
|
+
# diff, JSON) counts every class with a candidate the writer is sure to write.
|
|
86
92
|
# @param overwrite — `--overwrite`: a member declared inline whose `sig/` copy disagrees is proposed as an
|
|
87
93
|
# `inline-overwrite` of the whole member instead of refused (ADR-112 WD4).
|
|
88
94
|
def initialize(configuration:, paths:, observations: {}, include_private: false, effect_annotator: nil,
|
|
89
|
-
overwrite: false)
|
|
95
|
+
overwrite: false, target_writer: nil)
|
|
90
96
|
@configuration = configuration
|
|
97
|
+
@target_writer = target_writer
|
|
91
98
|
@overwrite = overwrite
|
|
92
99
|
@paths = paths
|
|
93
100
|
@observations = normalize_observations(observations)
|
|
@@ -99,7 +106,7 @@ module Rigor
|
|
|
99
106
|
# candidates from one file don't leak into another; initialising empty here gives downstream consumers
|
|
100
107
|
# (`build_candidate`, `method_def_prefix`) a never-nil invariant without per-call-site defensive guards.
|
|
101
108
|
@namespace_kinds = {}
|
|
102
|
-
@module_function_methods = Set.new
|
|
109
|
+
@module_function_methods = Set.new.compare_by_identity
|
|
103
110
|
@uncertain_module_function_defs = Set.new.compare_by_identity
|
|
104
111
|
@class_shells = Set.new
|
|
105
112
|
@class_superclasses = {}
|
|
@@ -113,6 +120,36 @@ module Rigor
|
|
|
113
120
|
@unresolvable_superclasses = {}
|
|
114
121
|
# Issue #722 — whole-run: `{ class name => the lexical nesting its header is written in }`.
|
|
115
122
|
@superclass_nestings = {}
|
|
123
|
+
# Issue #2437 — whole-run: every class a walked file declares with a `class` keyword or a `Data.define` /
|
|
124
|
+
# `Struct.new` layout, so the report can tell a project superclass this run did not write from one nothing
|
|
125
|
+
# declares.
|
|
126
|
+
@source_classes = Set.new
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# How the report names an unresolvable superclass token ({#unresolvable_superclasses}). It is a project
|
|
130
|
+
# superclass when it names a class the walked source declares and this run wrote nothing for, whose own chain
|
|
131
|
+
# would close if the project's classes were written:
|
|
132
|
+
# each step a source class, ending at no superclass or at one the RBS environment knows (#2437). What is
|
|
133
|
+
# missing is then the project's own declaration, not a gem's RBS. `class ApplicationRecord <
|
|
134
|
+
# ActiveRecord::Base` with no RBS for the gem is not such a class: writing it would not help.
|
|
135
|
+
#
|
|
136
|
+
# Returns `[:project, token]` for such a class. Otherwise `[:external, root]`, where `root` is the first token
|
|
137
|
+
# on the chain that is not a source class: `User < ApplicationRecord` reports `ActiveRecord::Base`, the class
|
|
138
|
+
# whose RBS is missing, rather than the project class between them.
|
|
139
|
+
def unresolvable_superclass_report(token)
|
|
140
|
+
name = bare_superclass_name(token)
|
|
141
|
+
root = token
|
|
142
|
+
seen = {}
|
|
143
|
+
while @source_classes.include?(name) && !seen[name]
|
|
144
|
+
seen[name] = true
|
|
145
|
+
nesting, superclass = @superclass_nestings[name]
|
|
146
|
+
return [:project, token] if superclass.nil?
|
|
147
|
+
|
|
148
|
+
root = resolve_superclass_token(superclass, nesting, @source_classes, name)
|
|
149
|
+
name = bare_superclass_name(root)
|
|
150
|
+
return [:project, token] if Reflection.rbs_class_known?(name, environment: @environment)
|
|
151
|
+
end
|
|
152
|
+
[:external, root]
|
|
116
153
|
end
|
|
117
154
|
|
|
118
155
|
# Lifts legacy plain-`Array[Type]` observation entries into {ObservedCall} carriers. Specs from the
|
|
@@ -126,13 +163,30 @@ module Rigor
|
|
|
126
163
|
|
|
127
164
|
def run
|
|
128
165
|
resolved = resolve_paths(@paths)
|
|
166
|
+
@resolved_paths = resolved
|
|
129
167
|
@environment = build_environment(resolved)
|
|
168
|
+
# ADR-84 WD2, as `Runner#run_analysis` mints it for `check` (#2300): one return-memo bucket for the whole
|
|
169
|
+
# run. Without it the memo keys on each file's merged `discovered_def_nodes`, a new table per file, so a
|
|
170
|
+
# callee reached from two files was re-inferred in each. A fresh token per run keeps a second run in one
|
|
171
|
+
# process from reading the first's entries, and the bucket is released with the other run-scoped slots
|
|
172
|
+
# when the run ends ({RunScopedSlots}), so a finished run holds none of it.
|
|
173
|
+
@run_generation = Object.new.freeze
|
|
174
|
+
# Both read the files as they are now, so a second `run` of one generator re-reads them.
|
|
175
|
+
@project_tables = nil
|
|
176
|
+
@file_base_scope = nil
|
|
177
|
+
# #2461 — which `sig/` targets the writer will write to, read from the files as they are now.
|
|
178
|
+
@writable_targets = {}
|
|
130
179
|
candidates = resolved.flat_map { |path| analyse_file(path, @environment) }
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
180
|
+
demoted = demote_overridden_base_methods(
|
|
181
|
+
demote_unresolvable_superclasses(
|
|
182
|
+
demote_generic_reopenings(demote_inline_generic_classes(resolve_superclass_spellings(candidates)))
|
|
134
183
|
)
|
|
135
184
|
)
|
|
185
|
+
# Again after the override demotion: a demoted method can leave its file with nothing to write, and a class
|
|
186
|
+
# shell riding on that file then declares nothing a subclass elsewhere could rest on.
|
|
187
|
+
annotate_effects(stamp_generic_reopenings(demote_unresolvable_superclasses(demoted)))
|
|
188
|
+
ensure
|
|
189
|
+
RunScopedSlots.release
|
|
136
190
|
end
|
|
137
191
|
|
|
138
192
|
private
|
|
@@ -207,10 +261,10 @@ module Rigor
|
|
|
207
261
|
decls.each do |decl|
|
|
208
262
|
next unless decl.is_a?(::RBS::AST::Declarations::Class) || decl.is_a?(::RBS::AST::Declarations::Module)
|
|
209
263
|
|
|
210
|
-
|
|
211
|
-
return decl if
|
|
264
|
+
path = ::Rigor::Environment::RbsLoader.declared_class_path(decl, prefix)
|
|
265
|
+
return decl if path.join("::") == class_name
|
|
212
266
|
|
|
213
|
-
found = find_declaration(decl.members,
|
|
267
|
+
found = find_declaration(decl.members, path, class_name)
|
|
214
268
|
return found if found
|
|
215
269
|
end
|
|
216
270
|
nil
|
|
@@ -221,6 +275,88 @@ module Rigor
|
|
|
221
275
|
(1..segments.size).any? { |n| generic.key?(segments.first(n).join("::")) }
|
|
222
276
|
end
|
|
223
277
|
|
|
278
|
+
# Issue #2436 — a class the loaded RBS declares generic (`Array`, `Hash`, a gem's or the project's own) is
|
|
279
|
+
# reopened with that declaration's type parameters copied onto the header ({#stamp_generic_reopenings}),
|
|
280
|
+
# which is exact only while every parameter is plain. A bound or a default is a type spelled relative to the
|
|
281
|
+
# declaring file's namespace, and copying it under another nesting can resolve it to a different class, so
|
|
282
|
+
# rbs would reject the header as it rejects a parameterless one (`GenericParameterMismatchError`): the class,
|
|
283
|
+
# and every class whose signature mentions it, fails its definition build. A method of such a class, or of
|
|
284
|
+
# one nested in it, is skipped instead — unless `sig/` already declares the class, whose own header the
|
|
285
|
+
# writer then writes into ({#demote_inline_generic_classes}' rule).
|
|
286
|
+
def demote_generic_reopenings(candidates)
|
|
287
|
+
loader = @environment&.rbs_loader
|
|
288
|
+
return candidates if loader.nil?
|
|
289
|
+
|
|
290
|
+
layout = nil
|
|
291
|
+
candidates.map do |candidate|
|
|
292
|
+
next candidate unless Classification::EMITTABLE.include?(candidate.classification)
|
|
293
|
+
next candidate unless namespace_prefixes(candidate.class_name).any? { |name| bounded_generic?(loader, name) }
|
|
294
|
+
|
|
295
|
+
layout ||= LayoutIndex.new(signature_paths: @configuration.signature_paths)
|
|
296
|
+
next candidate if layout.file_for(candidate.class_name)
|
|
297
|
+
|
|
298
|
+
demoted_candidate(candidate, :generic_reopening)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# The other half of {#demote_generic_reopenings}: every emittable candidate carries the header spelling of
|
|
303
|
+
# each generic class the run may open a header for — its own class, the classes it is nested in, and the
|
|
304
|
+
# file's class shells and theirs — so the writer and `--print` spell the same header. Method bodies keep
|
|
305
|
+
# `untyped` for element types; nothing here names `E` in a member.
|
|
306
|
+
#
|
|
307
|
+
# Issue #2580 — a skipped candidate is stamped too, and its class counts towards the map: the writer
|
|
308
|
+
# repairs a parameterless header an earlier run wrote into the file a candidate routes to, and a file whose
|
|
309
|
+
# every proposal is already declared (or untyped) would otherwise keep a header rbs rejects.
|
|
310
|
+
def stamp_generic_reopenings(candidates)
|
|
311
|
+
loader = @environment&.rbs_loader
|
|
312
|
+
return candidates if loader.nil?
|
|
313
|
+
|
|
314
|
+
spelled = header_type_params(candidates.flat_map { |c| [c.class_name, *c.class_shells] }.compact)
|
|
315
|
+
return candidates if spelled.empty?
|
|
316
|
+
|
|
317
|
+
candidates.map { |candidate| candidate.with_class_type_params(spelled) }
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# #2603 — the header spelling {#header_type_params} gives `class_name` alone, or nil when the loaded RBS does
|
|
321
|
+
# not declare it generic with plain parameters only. The writer asks it for a bare header in a `sig/` file no
|
|
322
|
+
# candidate of the run routes to. Nil before {#run} has built the environment.
|
|
323
|
+
def generic_header_spelling(class_name)
|
|
324
|
+
loader = @environment&.rbs_loader
|
|
325
|
+
return nil if loader.nil?
|
|
326
|
+
|
|
327
|
+
params = loader.class_declaration_type_params(class_name)
|
|
328
|
+
"[#{params.join(', ')}]" if !params.empty? && params.all? { |p| plain_type_param?(p) }
|
|
329
|
+
end
|
|
330
|
+
public :generic_header_spelling
|
|
331
|
+
|
|
332
|
+
# `{ class name => "[unchecked out E]" }` for every name in `names`, or a namespace it is nested in, whose
|
|
333
|
+
# loaded declaration is generic with plain parameters only.
|
|
334
|
+
def header_type_params(names)
|
|
335
|
+
names.uniq.flat_map { |name| namespace_prefixes(name) }.uniq.each_with_object({}) do |name, acc|
|
|
336
|
+
spelling = generic_header_spelling(name)
|
|
337
|
+
acc[name] = spelling if spelling
|
|
338
|
+
end.freeze
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def namespace_prefixes(class_name)
|
|
342
|
+
return [] if class_name.nil?
|
|
343
|
+
|
|
344
|
+
segments = class_name.split("::")
|
|
345
|
+
(1..segments.size).map { |n| segments.first(n).join("::") }
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def bounded_generic?(loader, name)
|
|
349
|
+
params = loader.class_declaration_type_params(name)
|
|
350
|
+
!params.empty? && !params.all? { |param| plain_type_param?(param) }
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# A name, a variance and `unchecked`, and nothing that spells a type: no upper or lower bound, no default.
|
|
354
|
+
def plain_type_param?(param)
|
|
355
|
+
%i[upper_bound lower_bound default_type].none? do |reader|
|
|
356
|
+
param.respond_to?(reader) && param.public_send(reader)
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
224
360
|
# Issue #744 — a base class's method is NOT emitted when a project subclass overrides it and the
|
|
225
361
|
# override is not emitted itself.
|
|
226
362
|
#
|
|
@@ -239,7 +375,7 @@ module Rigor
|
|
|
239
375
|
# (#744 half 2) and is not decided here: this pass only stops sig-gen manufacturing the conflict.
|
|
240
376
|
def demote_overridden_base_methods(candidates)
|
|
241
377
|
superclasses = candidates.each_with_object({}) do |candidate, acc|
|
|
242
|
-
(candidate.class_superclasses || {}).each { |name, sup| acc[name] = sup
|
|
378
|
+
(candidate.class_superclasses || {}).each { |name, sup| acc[name] = bare_superclass_name(sup) }
|
|
243
379
|
end
|
|
244
380
|
return candidates if superclasses.empty?
|
|
245
381
|
|
|
@@ -303,6 +439,10 @@ module Rigor
|
|
|
303
439
|
def resolve_superclass_spellings(candidates)
|
|
304
440
|
return candidates if @superclass_nestings.empty?
|
|
305
441
|
|
|
442
|
+
# Every shell counts here, written or not: the question is which class Ruby means by the token, and the
|
|
443
|
+
# source declares `Lib::Error` whether or not its file is written. Whether the resolved superclass reaches
|
|
444
|
+
# disk is {#demote_unresolvable_superclasses}'s question. Counting only written shells would leave `Error`
|
|
445
|
+
# unresolved, and a top-level `Error` some signature declares would then take it (#2454).
|
|
306
446
|
known = candidates.each_with_object(Set.new) do |candidate, acc|
|
|
307
447
|
acc << candidate.class_name if candidate.class_name
|
|
308
448
|
acc.merge(candidate.class_shells || [])
|
|
@@ -322,20 +462,32 @@ module Rigor
|
|
|
322
462
|
@superclass_nestings.each_with_object({}) do |(full, (nesting, token)), acc|
|
|
323
463
|
next if token.nil? || nesting.empty?
|
|
324
464
|
|
|
325
|
-
resolved = resolve_superclass_token(token, nesting, known)
|
|
465
|
+
resolved = resolve_superclass_token(token, nesting, known, full)
|
|
326
466
|
acc[full] = resolved if resolved && resolved != token
|
|
327
467
|
end
|
|
328
468
|
end
|
|
329
469
|
|
|
330
470
|
# The first candidate spelling that names a class this run declares or the RBS environment knows, or
|
|
331
|
-
#
|
|
332
|
-
|
|
471
|
+
# the token itself when none does — an unresolvable superclass is #735's business, not this pass's.
|
|
472
|
+
#
|
|
473
|
+
# `full` is the class whose header carries the token. Ruby evaluates the superclass expression before the
|
|
474
|
+
# new constant exists, so the innermost candidate `full` itself (`module A; module C; class B < B`, where
|
|
475
|
+
# `A::C::B` is the class being opened) is not yet a candidate: the lookup falls through to `A::B` (#2579).
|
|
476
|
+
def resolve_superclass_token(token, nesting, known, full)
|
|
477
|
+
# A rooted token (`::Hash`) already names one class; the nesting must not capture it (#2459).
|
|
478
|
+
return token if token.start_with?("::")
|
|
479
|
+
|
|
333
480
|
base = token.sub(/\[.*\]\z/, "")
|
|
334
|
-
args = token[base.length..] || ""
|
|
335
481
|
nesting.length.downto(1) do |i|
|
|
336
482
|
candidate = "#{nesting[0, i].join('::')}::#{base}"
|
|
337
|
-
|
|
338
|
-
|
|
483
|
+
next if candidate == full
|
|
484
|
+
|
|
485
|
+
# The arguments {#record_superclass} computed belong to the top-level class of the bare name, so they are
|
|
486
|
+
# computed again for the class the token resolves to: `Hash` inside a `module Foo` declaring a plain
|
|
487
|
+
# `Foo::Hash` takes none (#2459).
|
|
488
|
+
return generic_superclass_spelling(candidate) if known.include?(candidate) ||
|
|
489
|
+
Reflection.rbs_class_known?(candidate,
|
|
490
|
+
environment: @environment)
|
|
339
491
|
end
|
|
340
492
|
token
|
|
341
493
|
end
|
|
@@ -368,28 +520,77 @@ module Rigor
|
|
|
368
520
|
# ran — source discovery and the plugins still type it. Emitting the class with the superclass merely
|
|
369
521
|
# OMITTED would be the harmful middle: the class becomes RBS-known with only the emitted methods, so
|
|
370
522
|
# every inherited call fires `call.undefined-method`.
|
|
523
|
+
#
|
|
524
|
+
# Demoting a class can leave a file with nothing to write, which takes the class shells riding on it off the
|
|
525
|
+
# emitted set, so the pass repeats until no further class becomes unresolvable. The emitted set only shrinks,
|
|
526
|
+
# so the unresolvable set only grows and the loop ends.
|
|
371
527
|
def demote_unresolvable_superclasses(candidates)
|
|
372
528
|
superclasses = candidates.each_with_object({}) do |candidate, acc|
|
|
373
529
|
(candidate.class_superclasses || {}).each { |name, sup| acc[name] = sup }
|
|
374
530
|
end
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
531
|
+
current = candidates
|
|
532
|
+
unresolvable = {}
|
|
533
|
+
loop do
|
|
534
|
+
found = unresolvable_superclass_roots(superclasses, declared_by_run(current))
|
|
535
|
+
return current if found.size == unresolvable.size
|
|
536
|
+
|
|
537
|
+
unresolvable = found
|
|
538
|
+
current = candidates.map do |candidate|
|
|
539
|
+
owner = unresolvable_for(candidate.class_name, unresolvable)
|
|
540
|
+
owner ? record_unresolvable_superclass(candidate, owner) : candidate
|
|
541
|
+
end
|
|
380
542
|
end
|
|
381
|
-
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
def unresolvable_superclass_roots(superclasses, emitted)
|
|
546
|
+
superclasses.each_with_object({}) do |(name, _sup), acc|
|
|
382
547
|
root = unresolved_superclass_root(name, superclasses, emitted)
|
|
383
548
|
acc[name] = root if root
|
|
384
549
|
end
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
candidates.map do |candidate|
|
|
388
|
-
owner = unresolvable_for(candidate.class_name, unresolvable)
|
|
389
|
-
next candidate if owner.nil?
|
|
550
|
+
end
|
|
390
551
|
|
|
391
|
-
|
|
552
|
+
# Everything this run puts on disk, superclass or not: a chain that reaches `class Record` — emitted here, with
|
|
553
|
+
# no superclass of its own — terminates in a class the next run WILL know. Only what the writer is sure to
|
|
554
|
+
# write counts, because a subclass written against a class nothing declares fails its build
|
|
555
|
+
# (`RBS::NoSuperclassFoundError`) on the next run:
|
|
556
|
+
#
|
|
557
|
+
# - a class with a candidate the writer is sure to write ({#written?}). A class whose every method is skipped
|
|
558
|
+
# is not written, so it does not count (#2437); a class shell on it would close it instead (#2438);
|
|
559
|
+
# - a class shell on a file with such a candidate: a shell on a file whose every method is skipped, already
|
|
560
|
+
# declared by `sig/`, or a tighter return the writer will not apply never reaches disk (#2454);
|
|
561
|
+
# - the wrapper namespaces of either, which the writer renders around them with their recorded superclasses.
|
|
562
|
+
#
|
|
563
|
+
# Counting too few only demotes a subclass, the output before #2454 (ADR-5).
|
|
564
|
+
def declared_by_run(candidates)
|
|
565
|
+
written = candidates.select { |candidate| written?(candidate) }
|
|
566
|
+
written_paths = written.to_set(&:path)
|
|
567
|
+
declared = Set.new
|
|
568
|
+
written.each { |candidate| declared << candidate.class_name if candidate.class_name }
|
|
569
|
+
candidates.each do |candidate|
|
|
570
|
+
declared.merge(candidate.class_shells || []) if written_paths.include?(candidate.path)
|
|
392
571
|
end
|
|
572
|
+
declared.flat_map { |name| wrapper_names(name) }.to_set | declared
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
# `A::B::C` is written inside `A` and `A::B`.
|
|
576
|
+
def wrapper_names(name)
|
|
577
|
+
segments = name.split("::")
|
|
578
|
+
(1...segments.size).map { |n| segments.first(n).join("::") }
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
# Whether the writer will put `candidate` on disk: a candidate it is sure to write ({Writer.writes?}), and,
|
|
582
|
+
# when the run writes or compares files (`--write`, `--check`), aimed at a target that writer will write to
|
|
583
|
+
# (#2461). Print, diff and JSON runs have no writer: where a file would go depends on the working directory
|
|
584
|
+
# the writer takes its sig root from, and their output must not.
|
|
585
|
+
def written?(candidate)
|
|
586
|
+
return false unless Writer.writes?(candidate)
|
|
587
|
+
return true if @target_writer.nil?
|
|
588
|
+
|
|
589
|
+
target = @target_writer.path_mapper.target_for(candidate.path, class_name: candidate.class_name,
|
|
590
|
+
method_name: candidate.method_name,
|
|
591
|
+
kind: candidate.rbs_kind)
|
|
592
|
+
@writable_targets[target] = @target_writer.accepts_target?(target) unless @writable_targets.key?(target)
|
|
593
|
+
@writable_targets[target]
|
|
393
594
|
end
|
|
394
595
|
|
|
395
596
|
# A nested class or module goes with its wrapper. `WikiPage::Webhookable`'s declaration is emitted
|
|
@@ -415,9 +616,9 @@ module Rigor
|
|
|
415
616
|
return superclass if seen[name]
|
|
416
617
|
|
|
417
618
|
seen[name] = true
|
|
418
|
-
# The recorded spelling may carry the type arguments {#generic_superclass_spelling} added
|
|
419
|
-
# resolvability question is about the class, so it is asked of the bare name.
|
|
420
|
-
base = superclass
|
|
619
|
+
# The recorded spelling may carry the type arguments {#generic_superclass_spelling} added, and a rooted
|
|
620
|
+
# token its `::` (#2459); the resolvability question is about the class, so it is asked of the bare name.
|
|
621
|
+
base = bare_superclass_name(superclass)
|
|
421
622
|
return nil if Reflection.rbs_class_known?(base, environment: @environment)
|
|
422
623
|
return superclass unless emitted.include?(base)
|
|
423
624
|
|
|
@@ -458,22 +659,65 @@ module Rigor
|
|
|
458
659
|
end.uniq
|
|
459
660
|
end
|
|
460
661
|
|
|
662
|
+
# #1154 / #2300 — the project's discovery tables, built once per run over the configured project paths as well
|
|
663
|
+
# as the files sig-gen reads, so a run over one file reads what a `check` of it reads. Every file is indexed
|
|
664
|
+
# over them ({#file_base_scope}): a helper, an ancestor or a constant another file defines resolves as it does
|
|
665
|
+
# under `check` instead of reading ABSENT. The declared-parameter ivar credit reads them whole
|
|
666
|
+
# ({#ivar_writer_scope}).
|
|
667
|
+
def project_tables
|
|
668
|
+
@project_tables ||= Inference::ProjectSeedTables.for_paths(
|
|
669
|
+
((@resolved_paths || []) + resolve_paths(Array(@configuration.paths))).uniq { |path| File.expand_path(path) }
|
|
670
|
+
)
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
# The member layouts stay per-file. sig-gen writes a value class for every `Data.define` / `Struct.new` layout
|
|
674
|
+
# the indexed scope carries ({#collect_meta_layouts}), so a seeded layout would re-emit every value class of the
|
|
675
|
+
# project in every file.
|
|
676
|
+
PER_FILE_TABLES = %i[data_member_layouts struct_member_layouts].freeze
|
|
677
|
+
private_constant :PER_FILE_TABLES
|
|
678
|
+
|
|
679
|
+
# #1154 — the declared-parameter ivar credit must see the ivar writers in other files (an ancestor, another
|
|
680
|
+
# object's `instance_variable_set`, a reopening) as `rigor check` does.
|
|
681
|
+
def ivar_writer_scope(base_scope)
|
|
682
|
+
base_scope.with_discovery(base_scope.discovery.with(**project_tables))
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
# #1406 — sig-gen keeps the census's own answer: a declared ivar type read back would echo the declaration.
|
|
686
|
+
def index_file(root, base_scope)
|
|
687
|
+
Inference::ScopeIndexer.index(
|
|
688
|
+
root, default_scope: base_scope, ivar_writer_scope: ivar_writer_scope(base_scope), declared_ivar_types: false
|
|
689
|
+
)
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
# The scope each file is indexed under: the project's tables less the per-file ones ({#project_tables}), and the
|
|
693
|
+
# run's return-memo token (see {#run}), which {#ivar_writer_scope} inherits, so every reading of a callee shares
|
|
694
|
+
# one memo bucket. Built once per run.
|
|
695
|
+
def file_base_scope(environment)
|
|
696
|
+
@file_base_scope ||= begin
|
|
697
|
+
base = Scope.empty(environment: environment)
|
|
698
|
+
tables = project_tables.except(*PER_FILE_TABLES)
|
|
699
|
+
tables = tables.merge(run_generation: @run_generation) if @run_generation
|
|
700
|
+
base.with_discovery(base.discovery.with(**tables))
|
|
701
|
+
end
|
|
702
|
+
end
|
|
703
|
+
|
|
461
704
|
def analyse_file(path, environment)
|
|
462
705
|
source = File.read(path)
|
|
463
706
|
parse_result = Prism.parse(source, filepath: path, version: @configuration.target_ruby)
|
|
464
707
|
return [] if parse_result.errors.any?
|
|
465
708
|
|
|
466
|
-
|
|
467
|
-
scope_index = Inference::ScopeIndexer.index(parse_result.value, default_scope: base_scope)
|
|
709
|
+
scope_index = index_file(parse_result.value, file_base_scope(environment))
|
|
468
710
|
|
|
469
711
|
reset_file_maps
|
|
470
712
|
@meta_layouts = collect_meta_layouts(scope_index)
|
|
471
713
|
register_meta_classes
|
|
472
714
|
defs = collect_method_definitions(parse_result.value, scope_index)
|
|
715
|
+
definers = definer_table(parse_result.value, defs)
|
|
716
|
+
defs = live_definitions(defs, definers)
|
|
473
717
|
# Candidate construction freezes the per-file maps above (see {#build_candidate}), so every registration
|
|
474
718
|
# pass has to be finished before the first `build_candidate` call. Meta members lead the RETURNED order —
|
|
475
719
|
# a value class's members and constructors read first, ahead of the methods its block body defines.
|
|
476
|
-
meta_candidates =
|
|
720
|
+
meta_candidates = live_meta_candidates(path, scope_index, definers)
|
|
477
721
|
candidates_from_defs = defs.filter_map do |def_node, class_name, kind|
|
|
478
722
|
# An analyzer bug typing one def's body must cost only that def's candidate, never the whole
|
|
479
723
|
# `rigor sig-gen` run. The `check` path recovers each *file* this way (worker_session.rb); sig-gen
|
|
@@ -485,13 +729,109 @@ module Rigor
|
|
|
485
729
|
end
|
|
486
730
|
obs_ivar_map = build_observed_ivar_map(parse_result.value)
|
|
487
731
|
meta_candidates + candidates_from_defs +
|
|
488
|
-
collect_attr_candidates(parse_result.value, path, scope_index, obs_ivar_map)
|
|
732
|
+
collect_attr_candidates(parse_result.value, path, scope_index, obs_ivar_map, definers)
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
# Ruby keeps the LAST definition of a name: a `def x` followed by `attr_reader :x` (or the reverse) defines
|
|
736
|
+
# `x` once, and a second `def x` after a `module_function` reset replaces only the instance method. Writing
|
|
737
|
+
# one entry per definition gave RBS a duplicate method definition (#2008, #2023), so every definer of a
|
|
738
|
+
# `(class, kind, name)` slot is recorded with its source offset, and only the last one in a file is written.
|
|
739
|
+
# A module function defines two slots, its singleton copy and its instance method. A `Data.define` or
|
|
740
|
+
# `Struct.new` member is defined before any body that could redefine it, so it ranks first.
|
|
741
|
+
#
|
|
742
|
+
# Only a definer whose statement certainly runs can be last: a direct statement of a `class`, `module` or
|
|
743
|
+
# `class << self` body, of a `Data.define` / `Struct.new` block or of the file ({#walk_statements} records
|
|
744
|
+
# them), or a `def` passed to `private`, `public` or `protected` in such a statement ({#walk_statements}, #2254).
|
|
745
|
+
# One under a condition, in a block or a lambda, or after an uncertain `module_function` reset may not run, as
|
|
746
|
+
# far as this reading tells. A slot such a definer could
|
|
747
|
+
# still take over from the last certain one is contested, and nothing is written for it.
|
|
748
|
+
#
|
|
749
|
+
# @return the definers of each slot as `[offset, certain]` pairs, keyed by `[class_name, kind, method_name]`.
|
|
750
|
+
MEMBER_OFFSET = -1
|
|
751
|
+
private_constant :MEMBER_OFFSET
|
|
752
|
+
|
|
753
|
+
def definer_table(root, defs)
|
|
754
|
+
certain = @certain_definers
|
|
755
|
+
table = Hash.new { |h, k| h[k] = [] }
|
|
756
|
+
record_def_definers(defs, certain, table)
|
|
757
|
+
meta_member_slots.each { |slot| table[slot] << [MEMBER_OFFSET, true] }
|
|
758
|
+
ctx = AttrWalkContext.new(definers: [])
|
|
759
|
+
walk_attr_calls(root, [], false, ctx)
|
|
760
|
+
ctx.definers.each do |slot, call_node|
|
|
761
|
+
table[slot] << [call_node.location.start_offset, certain.include?(call_node)]
|
|
762
|
+
end
|
|
763
|
+
table
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
def record_def_definers(defs, certain, table)
|
|
767
|
+
defs.each do |def_node, class_name, kind|
|
|
768
|
+
next if class_name.nil?
|
|
769
|
+
|
|
770
|
+
definer_slots(def_node, class_name, kind).each do |slot, possible_only|
|
|
771
|
+
table[slot] << [def_node.location.start_offset, !possible_only && certain.include?(def_node)]
|
|
772
|
+
end
|
|
773
|
+
end
|
|
774
|
+
end
|
|
775
|
+
|
|
776
|
+
def live_meta_candidates(path, scope_index, table)
|
|
777
|
+
collect_meta_member_candidates(path, scope_index).select do |c|
|
|
778
|
+
live_definer?(table, [c.class_name, c.kind, c.method_name], MEMBER_OFFSET)
|
|
779
|
+
end
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
# `[slot, possible_only]` pairs. A `def` after an uncertain `module_function` reset may or may not install a
|
|
783
|
+
# singleton copy, so it is a possible definer of that slot only.
|
|
784
|
+
def definer_slots(def_node, class_name, kind)
|
|
785
|
+
slots = [[[class_name, kind, def_node.name], false]]
|
|
786
|
+
if @module_function_methods.include?(def_node)
|
|
787
|
+
slots << [[class_name, :singleton, def_node.name], false]
|
|
788
|
+
elsif @uncertain_module_function_defs.include?(def_node)
|
|
789
|
+
slots << [[class_name, :singleton, def_node.name], true]
|
|
790
|
+
end
|
|
791
|
+
slots
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
def meta_member_slots
|
|
795
|
+
@meta_layouts.flat_map do |class_name, layout|
|
|
796
|
+
MetaClassShape.of(kind: layout.kind, members: layout.member_names, keyword_init: layout.keyword_init)
|
|
797
|
+
.member_decls.map { |member| [class_name, member.kind, member.method_name] }
|
|
798
|
+
end
|
|
799
|
+
end
|
|
800
|
+
|
|
801
|
+
# Whether the definer at `offset` is the one Ruby keeps for `slot`. A slot with a single definer keeps it,
|
|
802
|
+
# certain or not, as before.
|
|
803
|
+
def live_definer?(table, slot, offset)
|
|
804
|
+
entries = table.fetch(slot, [])
|
|
805
|
+
return true if entries.size <= 1
|
|
806
|
+
|
|
807
|
+
last_certain = entries.select { |_, certain| certain }.map(&:first).max
|
|
808
|
+
return false if last_certain.nil?
|
|
809
|
+
return false if entries.any? { |entry_offset, certain| !certain && entry_offset > last_certain }
|
|
810
|
+
|
|
811
|
+
offset == last_certain
|
|
812
|
+
end
|
|
813
|
+
|
|
814
|
+
# The `defs` whose definition Ruby keeps. A module function whose instance method a later definition
|
|
815
|
+
# replaced keeps only its singleton copy (`M.label` still returns the first body), so it is written as
|
|
816
|
+
# `def self.label`. One whose singleton copy was replaced keeps only a private instance method, which
|
|
817
|
+
# sig-gen does not write, so it is dropped.
|
|
818
|
+
def live_definitions(defs, table)
|
|
819
|
+
defs.filter_map do |def_node, class_name, kind|
|
|
820
|
+
next [def_node, class_name, kind] if class_name.nil?
|
|
821
|
+
|
|
822
|
+
slots = definer_slots(def_node, class_name, kind).reject(&:last).map(&:first)
|
|
823
|
+
live = slots.select { |slot| live_definer?(table, slot, def_node.location.start_offset) }
|
|
824
|
+
next [def_node, class_name, kind] if live.size == slots.size
|
|
825
|
+
next nil unless live == [[class_name, :singleton, def_node.name]]
|
|
826
|
+
|
|
827
|
+
[def_node, class_name, :singleton]
|
|
828
|
+
end
|
|
489
829
|
end
|
|
490
830
|
|
|
491
831
|
# The per-file scratch state {#analyse_file} starts each file with.
|
|
492
832
|
def reset_file_maps
|
|
493
833
|
@namespace_kinds = {}
|
|
494
|
-
@module_function_methods = Set.new
|
|
834
|
+
@module_function_methods = Set.new.compare_by_identity
|
|
495
835
|
@uncertain_module_function_defs = Set.new.compare_by_identity
|
|
496
836
|
@class_shells = Set.new
|
|
497
837
|
@class_superclasses = {}
|
|
@@ -509,15 +849,19 @@ module Rigor
|
|
|
509
849
|
# - `@namespace_kinds[qualified_name]` records whether each segment came from `class Foo` (`:class`) or
|
|
510
850
|
# `module Foo` (`:module`). Used by the writer's `wrap_in_modules` step to emit the right keyword for
|
|
511
851
|
# each intermediate segment AND the leaf.
|
|
512
|
-
# - `@module_function_methods` records
|
|
513
|
-
#
|
|
514
|
-
#
|
|
852
|
+
# - `@module_function_methods` records the `def` nodes a `module_function` (no args) call preceded inside
|
|
853
|
+
# a module body. The renderer emits `def self?.name` for these, the RBS spelling that matches the dual
|
|
854
|
+
# instance + singleton dispatch the runtime produces. It is keyed by node, not by name: a later `def` of
|
|
855
|
+
# the same name after a reset is an ordinary method (#2023).
|
|
515
856
|
def collect_method_definitions(root, scope_index)
|
|
516
857
|
@declared_types = scope_index[root].declared_types
|
|
517
858
|
@walk_root = root
|
|
518
859
|
@in_effect_refinements = nil
|
|
519
860
|
out = []
|
|
520
861
|
@block_depth = 0
|
|
862
|
+
@certain_bodies = Set.new.compare_by_identity
|
|
863
|
+
@certain_definers = Set.new.compare_by_identity
|
|
864
|
+
mark_certain_body(root.statements)
|
|
521
865
|
walk_defs(root, [], false, false, out)
|
|
522
866
|
out
|
|
523
867
|
end
|
|
@@ -530,6 +874,7 @@ module Rigor
|
|
|
530
874
|
return if descend_into_namespace?(node, prefix, out)
|
|
531
875
|
when Prism::SingletonClassNode
|
|
532
876
|
if node.expression.is_a?(Prism::SelfNode) && node.body
|
|
877
|
+
mark_certain_body(node.body)
|
|
533
878
|
walk_defs(node.body, prefix, true, false, out)
|
|
534
879
|
return
|
|
535
880
|
end
|
|
@@ -539,6 +884,7 @@ module Rigor
|
|
|
539
884
|
when Prism::ConstantWriteNode
|
|
540
885
|
body = meta_block_body(node, prefix)
|
|
541
886
|
if body
|
|
887
|
+
mark_certain_body(body)
|
|
542
888
|
walk_defs(body, prefix + [node.name.to_s], false, false, out)
|
|
543
889
|
return
|
|
544
890
|
end
|
|
@@ -555,6 +901,9 @@ module Rigor
|
|
|
555
901
|
# the `self` one returns above): it defines a singleton method on `obj`, not a private `Object` method.
|
|
556
902
|
def walk_def_children(node, prefix, in_singleton_class, module_function_active, out)
|
|
557
903
|
return if refine_block_call?(node)
|
|
904
|
+
# #2259 — a `def` in `class_methods do … end` (ActiveSupport::Concern) defines a method of the including class's
|
|
905
|
+
# singleton, not of this module. Omitted, not written class-side: no precedent places it on the singleton.
|
|
906
|
+
return if class_methods_block?(node)
|
|
558
907
|
|
|
559
908
|
in_block = node.is_a?(Prism::BlockNode) || node.is_a?(Prism::LambdaNode) ||
|
|
560
909
|
node.is_a?(Prism::SingletonClassNode)
|
|
@@ -584,6 +933,7 @@ module Rigor
|
|
|
584
933
|
|
|
585
934
|
full = child_prefix.join("::")
|
|
586
935
|
@namespace_kinds[full] = node.is_a?(Prism::ClassNode) ? :class : :module
|
|
936
|
+
@source_classes << full if node.is_a?(Prism::ClassNode)
|
|
587
937
|
record_superclass(node, full, prefix)
|
|
588
938
|
walk_namespace_body(node, child_prefix, out)
|
|
589
939
|
true
|
|
@@ -603,6 +953,17 @@ module Rigor
|
|
|
603
953
|
!@in_effect_refinements.class_body_refine?(node)
|
|
604
954
|
end
|
|
605
955
|
|
|
956
|
+
# A block call whose members are not this class body's: a `refine` block (#1763), and a `class_methods` block
|
|
957
|
+
# (#2259), whose members belong to the including class's singleton.
|
|
958
|
+
def skipped_block_call?(node)
|
|
959
|
+
refine_block_call?(node) || class_methods_block?(node)
|
|
960
|
+
end
|
|
961
|
+
|
|
962
|
+
def class_methods_block?(node)
|
|
963
|
+
node.is_a?(Prism::CallNode) && node.receiver.nil? && node.name == :class_methods &&
|
|
964
|
+
node.block.is_a?(Prism::BlockNode)
|
|
965
|
+
end
|
|
966
|
+
|
|
606
967
|
def declined_self_header?(node)
|
|
607
968
|
path = node.constant_path
|
|
608
969
|
path.is_a?(Prism::ConstantPathNode) && path.parent.is_a?(Prism::SelfNode) &&
|
|
@@ -628,10 +989,43 @@ module Rigor
|
|
|
628
989
|
# the name it resolves to may be a class another file declares. Resolved once every file has been
|
|
629
990
|
# walked, in {#resolve_superclass_spellings}.
|
|
630
991
|
spelling = generic_superclass_spelling(superclass)
|
|
992
|
+
# Issue #2579 — a token that names the class being opened (`class A::B < A::B`, or a top-level `class B < B`
|
|
993
|
+
# with nothing else of that name) would write a self-superclass, which RBS rejects as a recursive ancestor.
|
|
994
|
+
# No superclass is safer than that, so the declaration is written without one.
|
|
995
|
+
return if bare_superclass_name(spelling) == full
|
|
996
|
+
|
|
631
997
|
# The token rides along because `@class_superclasses` is per-FILE scratch, reset before the next
|
|
632
998
|
# file, while this table is whole-run.
|
|
633
999
|
@superclass_nestings[full] = [prefix.dup, spelling]
|
|
634
1000
|
@class_superclasses[full] = spelling
|
|
1001
|
+
register_core_subclass_shell(full, prefix, superclass)
|
|
1002
|
+
end
|
|
1003
|
+
|
|
1004
|
+
# Issue #2224 — a subclass of a core or stdlib class got a declaration only when it had a method of its own to
|
|
1005
|
+
# emit, so after `class MyHash < Hash; end` a method returning one was written `-> MyHash` against a class no
|
|
1006
|
+
# signature declared, and `rbs validate` failed with `NoTypeFoundError`. As a class shell it is declared
|
|
1007
|
+
# `class MyHash < ::Hash[untyped, untyped]` beside the file's other declarations. A class the RBS environment
|
|
1008
|
+
# already declares (the project's `sig/`, an earlier run) is left to that declaration, and a superclass the
|
|
1009
|
+
# header's nesting shadows is not core's: `Hash` inside a `module Foo` that declares `Foo::Hash` is the project's.
|
|
1010
|
+
# A rooted `::Hash` names core's class whatever the nesting declares, so the shadow cannot capture it (#2459).
|
|
1011
|
+
def register_core_subclass_shell(full, prefix, superclass)
|
|
1012
|
+
return if class_known?(full)
|
|
1013
|
+
|
|
1014
|
+
bare = bare_superclass_name(superclass)
|
|
1015
|
+
return unless @environment&.rbs_loader&.core_or_stdlib_class?(bare)
|
|
1016
|
+
|
|
1017
|
+
unless superclass.start_with?("::")
|
|
1018
|
+
namespaces = TypeNameQualification.enclosing_namespaces(prefix.join("::"))
|
|
1019
|
+
return if TypeNameQualification.shadowed?(bare.split("::").first, namespaces, method(:declared_constant?))
|
|
1020
|
+
end
|
|
1021
|
+
|
|
1022
|
+
@class_shells << full
|
|
1023
|
+
end
|
|
1024
|
+
|
|
1025
|
+
# The class a recorded superclass token names, as the fully qualified key every class-name set and the RBS
|
|
1026
|
+
# loader use: the type arguments {#generic_superclass_spelling} added and a rooted token's `::` both dropped.
|
|
1027
|
+
def bare_superclass_name(token)
|
|
1028
|
+
token.sub(/\[.*\]\z/, "").delete_prefix("::")
|
|
635
1029
|
end
|
|
636
1030
|
|
|
637
1031
|
# Issue #735 — a GENERIC superclass has to be written with its arguments. `class Entries < Array`
|
|
@@ -644,7 +1038,7 @@ module Rigor
|
|
|
644
1038
|
loader = @environment&.rbs_loader
|
|
645
1039
|
return superclass if loader.nil?
|
|
646
1040
|
|
|
647
|
-
arity = loader.class_type_param_names(superclass).size
|
|
1041
|
+
arity = loader.class_type_param_names(bare_superclass_name(superclass)).size
|
|
648
1042
|
return superclass if arity.zero?
|
|
649
1043
|
|
|
650
1044
|
"#{superclass}[#{(['untyped'] * arity).join(', ')}]"
|
|
@@ -692,6 +1086,7 @@ module Rigor
|
|
|
692
1086
|
@meta_layouts.each do |class_name, layout|
|
|
693
1087
|
@namespace_kinds[class_name] = :class
|
|
694
1088
|
@class_shells << class_name
|
|
1089
|
+
@source_classes << class_name
|
|
695
1090
|
@class_superclasses[class_name] = MetaClassShape::SUPERCLASSES.fetch(layout.kind)
|
|
696
1091
|
end
|
|
697
1092
|
end
|
|
@@ -717,13 +1112,32 @@ module Rigor
|
|
|
717
1112
|
def walk_namespace_body(namespace_node, prefix, out)
|
|
718
1113
|
return if namespace_node.body.nil?
|
|
719
1114
|
|
|
1115
|
+
mark_certain_body(namespace_node.body)
|
|
720
1116
|
walk_defs(namespace_node.body, prefix, false, false, out)
|
|
721
1117
|
end
|
|
722
1118
|
|
|
1119
|
+
# A body whose direct statements certainly run when it does ({#definer_table}): its statement list unless the
|
|
1120
|
+
# body has a `rescue` clause, and its `ensure` clause. A `rescue` or `else` clause may not run.
|
|
1121
|
+
def mark_certain_body(body)
|
|
1122
|
+
return @certain_bodies << body unless body.is_a?(Prism::BeginNode)
|
|
1123
|
+
|
|
1124
|
+
# With a `rescue`, a statement before a definer can raise and skip it, so the main list is not certain.
|
|
1125
|
+
@certain_bodies << body.statements if body.statements && body.rescue_clause.nil?
|
|
1126
|
+
@certain_bodies << body.ensure_clause.statements if body.ensure_clause&.statements
|
|
1127
|
+
end
|
|
1128
|
+
|
|
723
1129
|
# The directive's reach is {Inference::ModuleFunctionState.each_sig_gen_statement}.
|
|
724
1130
|
def walk_statements(stmts_node, prefix, in_singleton_class, module_function_active, out)
|
|
1131
|
+
certain_body = @certain_bodies.include?(stmts_node)
|
|
725
1132
|
Inference::ModuleFunctionState.each_sig_gen_statement(stmts_node.body,
|
|
726
1133
|
module_function_active) do |stmt, active|
|
|
1134
|
+
if certain_body
|
|
1135
|
+
@certain_definers << stmt
|
|
1136
|
+
# A `begin … end` statement's body (and `ensure`) runs whenever the statement does.
|
|
1137
|
+
mark_certain_body(stmt) if stmt.is_a?(Prism::BeginNode)
|
|
1138
|
+
# A `def` passed to a visibility call (`private def x`, #2254) runs with its statement.
|
|
1139
|
+
@certain_definers.merge(Inference::ModuleFunctionState.visibility_call_defs(stmt))
|
|
1140
|
+
end
|
|
727
1141
|
walk_defs(stmt, prefix, in_singleton_class, active, out)
|
|
728
1142
|
end
|
|
729
1143
|
end
|
|
@@ -740,7 +1154,7 @@ module Rigor
|
|
|
740
1154
|
kind = node.receiver.is_a?(Prism::SelfNode) || in_singleton_class ? :singleton : :instance
|
|
741
1155
|
class_name = prefix.join("::")
|
|
742
1156
|
if Inference::ModuleFunctionState.sig_gen_module_function?(module_function_active, kind)
|
|
743
|
-
@module_function_methods <<
|
|
1157
|
+
@module_function_methods << node
|
|
744
1158
|
elsif Inference::ModuleFunctionState.sig_gen_uncertain?(module_function_active, kind)
|
|
745
1159
|
@uncertain_module_function_defs << node
|
|
746
1160
|
end
|
|
@@ -782,9 +1196,9 @@ module Rigor
|
|
|
782
1196
|
|
|
783
1197
|
# Returns "def self." (kind: :singleton), "def self?." (instance method declared inside a
|
|
784
1198
|
# `module_function` region — both instance + singleton dispatch at runtime), or "def " (plain instance).
|
|
785
|
-
def method_def_prefix(
|
|
1199
|
+
def method_def_prefix(def_node, kind)
|
|
786
1200
|
return "def self." if kind == :singleton
|
|
787
|
-
return "def self?." if @module_function_methods.include?(
|
|
1201
|
+
return "def self?." if @module_function_methods.include?(def_node)
|
|
788
1202
|
|
|
789
1203
|
"def "
|
|
790
1204
|
end
|
|
@@ -800,7 +1214,7 @@ module Rigor
|
|
|
800
1214
|
def visibility_excludes?(def_node, class_name, kind, scope_index)
|
|
801
1215
|
return false if kind == :singleton
|
|
802
1216
|
return false if @include_private
|
|
803
|
-
return false if @module_function_methods.include?(
|
|
1217
|
+
return false if @module_function_methods.include?(def_node)
|
|
804
1218
|
|
|
805
1219
|
scope = scope_index[def_node] || scope_index.each_value.first
|
|
806
1220
|
return false if scope.nil?
|
|
@@ -820,8 +1234,8 @@ module Rigor
|
|
|
820
1234
|
# `Object#initialize: () -> void`. The mismatch surfaces a `Ruby::MethodParameterMismatch` warning even
|
|
821
1235
|
# when `rigor check` itself is clean.
|
|
822
1236
|
#
|
|
823
|
-
#
|
|
824
|
-
#
|
|
1237
|
+
# A default constructor is skipped (`classify_def` returns nil for it); a non-trivial one is not, and
|
|
1238
|
+
# {#initialize_stub_candidate} emits a permissive `(<param shape>) -> void` stub matching the runtime
|
|
825
1239
|
# parameter list.
|
|
826
1240
|
def initialize_excludes?(def_node, kind)
|
|
827
1241
|
return false unless kind == :instance
|
|
@@ -949,25 +1363,36 @@ module Rigor
|
|
|
949
1363
|
# return position it is not parenthesised.
|
|
950
1364
|
def positional_type(observations, index, prefix, owner = nil)
|
|
951
1365
|
types = observations.filter_map { |obs| obs.positional[index] }
|
|
952
|
-
"#{prefix}#{union_erase(types, owner: owner)}"
|
|
1366
|
+
"#{prefix}#{union_erase(widen_observed_params(types), owner: owner)}"
|
|
953
1367
|
end
|
|
954
1368
|
|
|
955
1369
|
def render_keyword_param(keyword, observations, owner = nil)
|
|
956
1370
|
optional_marker = keyword.is_a?(Prism::OptionalKeywordParameterNode) ? "?" : ""
|
|
957
1371
|
types = observations.filter_map { |obs| obs.keyword[keyword.name] }
|
|
958
|
-
rendered = types.empty? ? "untyped" : paren_wrap_union(union_erase(types, owner: owner))
|
|
1372
|
+
rendered = types.empty? ? "untyped" : paren_wrap_union(union_erase(widen_observed_params(types), owner: owner))
|
|
959
1373
|
"#{optional_marker}#{keyword.name}: #{rendered}"
|
|
960
1374
|
end
|
|
961
1375
|
|
|
1376
|
+
# #2254 — an observed argument is one call's value, so a literal (`"hello"`) proposes its class (`String`), as
|
|
1377
|
+
# {#widen_value_literals} does for a return. Widening always: a parameter the body only compares against a
|
|
1378
|
+
# literal (a discriminant) is widened too, which loses precision and never adds a false positive.
|
|
1379
|
+
def widen_observed_params(types)
|
|
1380
|
+
types.map { |type| widen_value_literals(type) }
|
|
1381
|
+
end
|
|
1382
|
+
|
|
1383
|
+
# Issue #2459 — a ROOTED path (`::Hash`, `::A::B`) keeps its leading `::`: it names exactly one class, and the
|
|
1384
|
+
# passes after {#record_superclass} must not re-resolve it against the header's nesting, where a project
|
|
1385
|
+
# `Foo::Hash` would capture it. Every pass that compares the token against class names strips the anchor.
|
|
962
1386
|
def qualified_constant_path(constant_path)
|
|
963
1387
|
case constant_path
|
|
964
1388
|
when Prism::ConstantReadNode
|
|
965
1389
|
constant_path.name.to_s
|
|
966
1390
|
when Prism::ConstantPathNode
|
|
967
|
-
parent = qualified_constant_path(constant_path.parent) if constant_path.parent
|
|
968
1391
|
name = constant_path.name&.to_s
|
|
969
1392
|
return nil if name.nil?
|
|
1393
|
+
return "::#{name}" if constant_path.parent.nil?
|
|
970
1394
|
|
|
1395
|
+
parent = qualified_constant_path(constant_path.parent)
|
|
971
1396
|
parent ? "#{parent}::#{name}" : name
|
|
972
1397
|
end
|
|
973
1398
|
end
|
|
@@ -1038,7 +1463,7 @@ module Rigor
|
|
|
1038
1463
|
|
|
1039
1464
|
scope = scope_index[def_node]
|
|
1040
1465
|
method_def = lookup_existing_method(class_name, def_node.name, kind, scope&.environment, scope)
|
|
1041
|
-
head = "#{method_def_prefix(
|
|
1466
|
+
head = "#{method_def_prefix(def_node, kind)}#{def_node.name}: "
|
|
1042
1467
|
if return_defaulted
|
|
1043
1468
|
return mixed_provenance_candidate(path, def_node, class_name, kind, scope_index, inline, method_def, head)
|
|
1044
1469
|
end
|
|
@@ -1258,7 +1683,37 @@ module Rigor
|
|
|
1258
1683
|
# different method (issue #1382). Delegates to {Rigor::Inference::DefReturnTyper} — the same body-typing +
|
|
1259
1684
|
# explicit-return-union the `rigor annotate` def-line annotator uses.
|
|
1260
1685
|
def infer_return_type(def_node, scope_index)
|
|
1261
|
-
Inference::DefReturnTyper.call(def_node, scope_index)
|
|
1686
|
+
absorb_empty_literals(Inference::DefReturnTyper.call(def_node, scope_index, keep_lookup_nil: true))
|
|
1687
|
+
end
|
|
1688
|
+
|
|
1689
|
+
# Issue #1153 — an empty-collection early return (`return [] if table.empty?`) types its arm `[]`, which the
|
|
1690
|
+
# engine's union keeps beside a sibling `Array[E]` arm on purpose (the empty tuple is a distinct shape). A
|
|
1691
|
+
# signature has no use for the distinction: `[]` is an `Array[E]` for every `E`, so `Array[E] | []` is just
|
|
1692
|
+
# `Array[E]`, and the leftover `[]` arm made the proposal read as if the empty literal pinned the element.
|
|
1693
|
+
# Issue #1149 — the same holds for the empty closed hash shape `{}` (a frozen empty fallback,
|
|
1694
|
+
# `table[key] || EMPTY`) beside a `Hash[K, V]` nominal. Each empty literal is absorbed only beside its OWN
|
|
1695
|
+
# nominal: a `[]` beside a `Hash` arm, or a `{}` beside an `Array` arm, is untouched. A lone empty literal,
|
|
1696
|
+
# one beside another tuple / shape (`[1, 2] | []`), an open `{ ... }` shape, and a non-empty shape are
|
|
1697
|
+
# untouched too, since each of those is information the declaration should keep.
|
|
1698
|
+
def absorb_empty_literals(type)
|
|
1699
|
+
return type unless type.is_a?(Type::Union)
|
|
1700
|
+
|
|
1701
|
+
kept = type.members.reject { |member| absorbed_empty_literal?(member, type.members) }
|
|
1702
|
+
kept.size == type.members.size ? type : Type::Combinator.union(*kept)
|
|
1703
|
+
end
|
|
1704
|
+
|
|
1705
|
+
def absorbed_empty_literal?(member, members)
|
|
1706
|
+
if member.is_a?(Type::Tuple) && member.elements.empty?
|
|
1707
|
+
nominal_arm?(members, "Array")
|
|
1708
|
+
elsif member.is_a?(Type::HashShape) && member.pairs.empty? && member.closed?
|
|
1709
|
+
nominal_arm?(members, "Hash")
|
|
1710
|
+
else
|
|
1711
|
+
false
|
|
1712
|
+
end
|
|
1713
|
+
end
|
|
1714
|
+
|
|
1715
|
+
def nominal_arm?(members, class_name)
|
|
1716
|
+
members.any? { |member| member.is_a?(Type::Nominal) && member.class_name == class_name }
|
|
1262
1717
|
end
|
|
1263
1718
|
|
|
1264
1719
|
def dynamic_top?(type)
|
|
@@ -1305,7 +1760,7 @@ module Rigor
|
|
|
1305
1760
|
return equivalent(path, def_node, class_name, kind, inferred, VOID_RETURN_RBS) if declares_void?(method_def)
|
|
1306
1761
|
|
|
1307
1762
|
declared = build_declared_return(method_def)
|
|
1308
|
-
if declared_untyped?(declared)
|
|
1763
|
+
if declared_untyped?(declared, method_def)
|
|
1309
1764
|
return declared_untyped_candidate(path, def_node, class_name, kind, inferred, method_def, rendered: rendered)
|
|
1310
1765
|
end
|
|
1311
1766
|
|
|
@@ -1351,16 +1806,16 @@ module Rigor
|
|
|
1351
1806
|
# about the return. The class's own declaration is kept whole instead: its visibility, the overload's
|
|
1352
1807
|
# annotations, type parameters, parameters and block, with only the return replaced.
|
|
1353
1808
|
#
|
|
1354
|
-
#
|
|
1809
|
+
# Two shapes are declined (`nil`):
|
|
1355
1810
|
# - several overloads. The body is typed once, so the inferred return covers every overload together and
|
|
1356
1811
|
# says nothing about which overload returns what: collapsing them into one line would drop the
|
|
1357
1812
|
# overloads, and giving each the joint return would widen the overloads the author wrote narrower.
|
|
1358
1813
|
# - a name the class's own `sig/` spells as an `alias`. Its parameters are the aliased method's, and a
|
|
1359
1814
|
# proposal would read as a rewrite of that method's line rather than of the alias. An alias only an
|
|
1360
1815
|
# ancestor declares takes the override path below, like any other ancestor declaration.
|
|
1361
|
-
#
|
|
1362
|
-
#
|
|
1363
|
-
#
|
|
1816
|
+
# A method this class does not declare is not declined: when only an ancestor declares it, the proposal is a
|
|
1817
|
+
# new override on this class, rendered from the `def`'s own shape like any new method. The ancestor's
|
|
1818
|
+
# parameters describe the ancestor's `def`, not this one, which may take other arguments (ADR-14 clause 2).
|
|
1364
1819
|
def tighter_return_line(def_node, method_def, class_name, kind, inferred)
|
|
1365
1820
|
member = signature_member(method_def, class_name)
|
|
1366
1821
|
return render_rbs_line(def_node, inferred, class_name, kind) if member.nil?
|
|
@@ -1372,7 +1827,7 @@ module Rigor
|
|
|
1372
1827
|
returned = paren_wrap_union(elaborated_rbs(inferred, owner: class_name))
|
|
1373
1828
|
annotations = overload.annotations.map { |a| "#{a.location&.source || "%a{#{a.string}}"} " }.join
|
|
1374
1829
|
visibility = member.respond_to?(:visibility) && member.visibility ? "#{member.visibility} " : ""
|
|
1375
|
-
"#{visibility}#{method_def_prefix(
|
|
1830
|
+
"#{visibility}#{method_def_prefix(def_node, kind)}#{def_node.name}: " \
|
|
1376
1831
|
"#{annotations}#{spell_with_return(overload.method_type, returned)}"
|
|
1377
1832
|
end
|
|
1378
1833
|
|
|
@@ -1431,8 +1886,13 @@ module Rigor
|
|
|
1431
1886
|
# skips. ADR-93's inline `# @rbs param: Type` annotation (no return clause) synthesizes exactly this
|
|
1432
1887
|
# spelling for every method it documents, so the silent drop hit the parameter-annotated, return-bare
|
|
1433
1888
|
# shape ADR-14 exists to help.
|
|
1434
|
-
|
|
1435
|
-
|
|
1889
|
+
#
|
|
1890
|
+
# The translation alone cannot say the author wrote `untyped`: an interface return (`-> _Taker[1]`,
|
|
1891
|
+
# `-> _ToS`) also translates to `Dynamic`, and reading it as an absent declaration replaced it with an
|
|
1892
|
+
# unrelated nominal under `--overwrite` (#2178). So every overload's return must be `untyped` as written.
|
|
1893
|
+
def declared_untyped?(declared, method_def)
|
|
1894
|
+
declared.is_a?(Type::Dynamic) &&
|
|
1895
|
+
method_def.method_types.all? { |mt| mt.type.return_type.is_a?(RBS::Types::Bases::Any) }
|
|
1436
1896
|
end
|
|
1437
1897
|
|
|
1438
1898
|
# Proposed without {#tighter?}: a declared `untyped` return carries no information to weigh it against, the
|
|
@@ -1465,7 +1925,16 @@ module Rigor
|
|
|
1465
1925
|
VALUE_LITERAL_CLASSES = [Integer, String, Symbol].freeze
|
|
1466
1926
|
private_constant :VALUE_LITERAL_CLASSES
|
|
1467
1927
|
|
|
1468
|
-
#
|
|
1928
|
+
# Core generics whose RBS parameters are all `out` or `unchecked out`, so a widened argument is a supertype of
|
|
1929
|
+
# the body's type. An invariant or `in` parameter (a user `class Sink[in T]`) is not widened: `Sink[Integer]`
|
|
1930
|
+
# is no supertype of `Sink[1]` there.
|
|
1931
|
+
COVARIANT_CARRIERS = %w[Array Hash Set Range Enumerator].freeze
|
|
1932
|
+
private_constant :COVARIANT_CARRIERS
|
|
1933
|
+
|
|
1934
|
+
# The same object back when nothing needed widening. A literal nested in a tuple element, a record or hash
|
|
1935
|
+
# value, or a covariant generic argument (`[1, "a"]`, `{ a: 1 }`, `Hash[String, 1]`) widens the same way, while
|
|
1936
|
+
# the carrier's shape and a record's keys stay: a subclass override returning `[2, "b"]` is no widening of
|
|
1937
|
+
# `[Integer, String]`. A nested `nil` is an honest member there, as in a top-level union.
|
|
1469
1938
|
def widen_value_literals(type)
|
|
1470
1939
|
case type
|
|
1471
1940
|
when Type::Constant
|
|
@@ -1473,14 +1942,35 @@ module Rigor
|
|
|
1473
1942
|
return type unless VALUE_LITERAL_CLASSES.any? { |klass| type.value.instance_of?(klass) }
|
|
1474
1943
|
|
|
1475
1944
|
Type::Combinator.nominal_of(type.value.class.name)
|
|
1476
|
-
when Type::Union
|
|
1477
|
-
|
|
1478
|
-
|
|
1945
|
+
when Type::Union then widen_members(type.members, type) { |members| Type::Combinator.union(*members) }
|
|
1946
|
+
when Type::Tuple then widen_members(type.elements, type) { |elements| Type::Combinator.tuple_of(*elements) }
|
|
1947
|
+
when Type::HashShape then widen_hash_shape_values(type)
|
|
1948
|
+
when Type::Nominal
|
|
1949
|
+
return type unless COVARIANT_CARRIERS.include?(type.class_name)
|
|
1950
|
+
|
|
1951
|
+
widen_members(type.type_args, type) do |args|
|
|
1952
|
+
Type::Combinator.nominal_of(type.class_name, type_args: args)
|
|
1953
|
+
end
|
|
1479
1954
|
else
|
|
1480
1955
|
type
|
|
1481
1956
|
end
|
|
1482
1957
|
end
|
|
1483
1958
|
|
|
1959
|
+
def widen_members(members, type)
|
|
1960
|
+
widened = members.map { |member| widen_value_literals(member) }
|
|
1961
|
+
widened.each_with_index.all? { |m, i| m.equal?(members[i]) } ? type : yield(widened)
|
|
1962
|
+
end
|
|
1963
|
+
|
|
1964
|
+
def widen_hash_shape_values(shape)
|
|
1965
|
+
widen_members(shape.pairs.values, shape) do |values|
|
|
1966
|
+
Type::HashShape.new(
|
|
1967
|
+
shape.pairs.keys.zip(values).to_h,
|
|
1968
|
+
required_keys: shape.required_keys, optional_keys: shape.optional_keys,
|
|
1969
|
+
read_only_keys: shape.read_only_keys, extra_keys: shape.extra_keys
|
|
1970
|
+
)
|
|
1971
|
+
end
|
|
1972
|
+
end
|
|
1973
|
+
|
|
1484
1974
|
def widen_boolean_literal
|
|
1485
1975
|
Type::Combinator.union(Type::Combinator.constant_of(true), Type::Combinator.constant_of(false))
|
|
1486
1976
|
end
|
|
@@ -1624,11 +2114,12 @@ module Rigor
|
|
|
1624
2114
|
# `Type::Constant#erase_to_rbs` spells the values RBS writes as literal types (`true` / `false` /
|
|
1625
2115
|
# `nil`, an Integer, a Symbol, a String) as that literal and every other carrier as a nominal. Asking
|
|
1626
2116
|
# 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
|
|
1628
|
-
# excluded by class rather than by comparing the erasure with the class
|
|
1629
|
-
# nominal, not a pinned literal.
|
|
2117
|
+
# copy into this file. A Range's and a Set's nominal carries a type argument (`Range[Integer]`, #1891;
|
|
2118
|
+
# `Set[untyped]`, #2177), so they are excluded by class rather than by comparing the erasure with the class
|
|
2119
|
+
# name: like a Float each is a nominal, not a pinned literal.
|
|
1630
2120
|
def rbs_literal?(type)
|
|
1631
|
-
type.is_a?(Type::Constant) && !type.value.is_a?(Range) && type.
|
|
2121
|
+
type.is_a?(Type::Constant) && !type.value.is_a?(Range) && !type.value.is_a?(::Set) &&
|
|
2122
|
+
type.erase_to_rbs != type.value.class.name
|
|
1632
2123
|
end
|
|
1633
2124
|
|
|
1634
2125
|
# Heuristic added after the third-round self-dogfood: `FallbackTracer#size` body is `@events.size`, where
|
|
@@ -1696,7 +2187,7 @@ module Rigor
|
|
|
1696
2187
|
def render_rbs_line(def_node, inferred, class_name, kind)
|
|
1697
2188
|
params = def_node.parameters
|
|
1698
2189
|
head = "(#{render_param_list(params, class_name, def_node.name)})#{block_signature_suffix(params)}"
|
|
1699
|
-
prefix = method_def_prefix(
|
|
2190
|
+
prefix = method_def_prefix(def_node, kind)
|
|
1700
2191
|
"#{prefix}#{def_node.name}: #{head} -> #{paren_wrap_union(elaborated_rbs(inferred, owner: class_name))}"
|
|
1701
2192
|
end
|
|
1702
2193
|
|
|
@@ -1707,8 +2198,35 @@ module Rigor
|
|
|
1707
2198
|
# `owner:` is the class or module the rendered member belongs to. {AliasIndex} needs it to judge whether
|
|
1708
2199
|
# a matching alias is one this reader would expect to see; a call site that cannot supply it renders the
|
|
1709
2200
|
# type unfolded, which is the same output as before #1002.
|
|
2201
|
+
#
|
|
2202
|
+
# Issue #2230 — qualified last, after the alias fold has matched the unqualified spelling it keys on, so a core
|
|
2203
|
+
# name a namespace of the owner shadows (`Set` inside a `module Foo` that declares `Foo::Set`) is written `::Set`.
|
|
1710
2204
|
def elaborated_rbs(type, owner: nil)
|
|
1711
|
-
alias_index.fold(TypeElaborator.elaborate(type, environment: @environment).erase_to_rbs, owner)
|
|
2205
|
+
folded = alias_index.fold(TypeElaborator.elaborate(type, environment: @environment).erase_to_rbs, owner)
|
|
2206
|
+
captured = owner_type_param_names(owner)
|
|
2207
|
+
TypeNameQualification.qualify(folded, owner) { |name| captured.include?(name) || declared_constant?(name) }
|
|
2208
|
+
end
|
|
2209
|
+
|
|
2210
|
+
# Issue #2436 — inside `class Array[unchecked out E]` a bare `E` names the type variable, not a class `::E`,
|
|
2211
|
+
# so a class whose spelling equals one of the owner's declared type-parameter names is anchored. The names
|
|
2212
|
+
# come as `"Array::E"`, the form {TypeNameQualification.qualify} asks about for the owner's own namespace;
|
|
2213
|
+
# a namespace the owner is nested in does not bind its parameters inside the owner's declaration.
|
|
2214
|
+
def owner_type_param_names(owner)
|
|
2215
|
+
loader = owner && @environment&.rbs_loader
|
|
2216
|
+
return Set.new if loader.nil?
|
|
2217
|
+
|
|
2218
|
+
@owner_type_param_names ||= {}
|
|
2219
|
+
@owner_type_param_names.fetch(owner) do
|
|
2220
|
+
@owner_type_param_names[owner] = loader.class_declaration_type_params(owner).to_set do |p|
|
|
2221
|
+
"#{owner}::#{p.name}"
|
|
2222
|
+
end
|
|
2223
|
+
end
|
|
2224
|
+
end
|
|
2225
|
+
|
|
2226
|
+
# Whether a type name written as `name` resolves to a constant the run can see: one the RBS environment
|
|
2227
|
+
# declares, or a class or module the project's source declares, whether or not `sig/` has caught up with it.
|
|
2228
|
+
def declared_constant?(name)
|
|
2229
|
+
class_known?(name) || Reflection.discovered_class?(name, scope: file_base_scope(@environment))
|
|
1712
2230
|
end
|
|
1713
2231
|
|
|
1714
2232
|
# Issue #1002 — the reverse of {#translate_method_type_return}'s `alias_expander:`. Folding happens here,
|
|
@@ -1800,12 +2318,17 @@ module Rigor
|
|
|
1800
2318
|
# Per-file context the attr_* walker threads through its recursive descent. Keeps parameter lists in
|
|
1801
2319
|
# check. `obs_ivar_map` carries the observation-derived fallback types built by
|
|
1802
2320
|
# {#build_observed_ivar_map}; it is empty when sig-gen is invoked without `--params=observed`.
|
|
1803
|
-
|
|
2321
|
+
# `definer_table` is {#definer_table}'s map, which drops an attribute another definer replaces. A context
|
|
2322
|
+
# with `definers` only records each attribute's slot and call node for that map, and builds nothing.
|
|
2323
|
+
AttrWalkContext = Struct.new(:path, :scope_index, :obs_ivar_map, :out, :definer_table, :definers,
|
|
2324
|
+
keyword_init: true)
|
|
1804
2325
|
private_constant :AttrWalkContext
|
|
1805
2326
|
|
|
1806
|
-
def collect_attr_candidates(root, path, scope_index, obs_ivar_map = {})
|
|
2327
|
+
def collect_attr_candidates(root, path, scope_index, obs_ivar_map = {}, definer_table = {})
|
|
2328
|
+
@lookup_ivars = Set.new
|
|
2329
|
+
collect_lookup_ivar_writes(root, scope_index)
|
|
1807
2330
|
ctx = AttrWalkContext.new(path: path, scope_index: scope_index,
|
|
1808
|
-
obs_ivar_map: obs_ivar_map, out: [])
|
|
2331
|
+
obs_ivar_map: obs_ivar_map, out: [], definer_table: definer_table)
|
|
1809
2332
|
walk_attr_calls(root, [], false, ctx)
|
|
1810
2333
|
ctx.out
|
|
1811
2334
|
end
|
|
@@ -1834,7 +2357,7 @@ module Rigor
|
|
|
1834
2357
|
return
|
|
1835
2358
|
end
|
|
1836
2359
|
when Prism::CallNode
|
|
1837
|
-
return if
|
|
2360
|
+
return if skipped_block_call?(node)
|
|
1838
2361
|
|
|
1839
2362
|
collect_attr_call(node, prefix, in_singleton_class, ctx)
|
|
1840
2363
|
end
|
|
@@ -1850,11 +2373,67 @@ module Rigor
|
|
|
1850
2373
|
class_name = prefix.join("::")
|
|
1851
2374
|
symbol_names = extract_symbol_arguments(call_node)
|
|
1852
2375
|
return if symbol_names.empty?
|
|
2376
|
+
return record_attr_definers(call_node, class_name, symbol_names, ctx) if ctx.definers
|
|
1853
2377
|
|
|
1854
2378
|
ivar_lookup = ivar_type_lookup(ctx.scope_index, class_name, ctx.obs_ivar_map)
|
|
2379
|
+
offset = call_node.location.start_offset
|
|
1855
2380
|
symbol_names.each do |attr_name|
|
|
1856
|
-
ivar_type = ivar_lookup.call(attr_name)
|
|
1857
|
-
|
|
2381
|
+
ivar_type = with_lookup_nil(ivar_lookup.call(attr_name), class_name, attr_name)
|
|
2382
|
+
candidates = build_attr_candidates(call_node.name, class_name, attr_name, ivar_type, ctx)
|
|
2383
|
+
ctx.out.concat(candidates.select do |c|
|
|
2384
|
+
live_definer?(ctx.definer_table, [class_name, :instance, c.method_name], offset)
|
|
2385
|
+
end)
|
|
2386
|
+
end
|
|
2387
|
+
end
|
|
2388
|
+
|
|
2389
|
+
# #2197 — an attribute whose ivar an instance method assigns from an optimistic lookup (`@x = h[k]`) reads
|
|
2390
|
+
# `nil` on a miss, as such a return does ({Inference::DefReturnTyper.optimistic_value?}), so the written
|
|
2391
|
+
# reader keeps the `nil` the engine's nil-free reading of the lookup drops.
|
|
2392
|
+
def with_lookup_nil(type, class_name, attr_name)
|
|
2393
|
+
return type if type.nil? || !@lookup_ivars.include?([class_name, :"@#{attr_name}"])
|
|
2394
|
+
|
|
2395
|
+
Type::Combinator.union(type, Type::Combinator.constant_of(nil))
|
|
2396
|
+
end
|
|
2397
|
+
|
|
2398
|
+
LOOKUP_IVAR_WRITES = [
|
|
2399
|
+
Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode
|
|
2400
|
+
].freeze
|
|
2401
|
+
private_constant :LOOKUP_IVAR_WRITES
|
|
2402
|
+
|
|
2403
|
+
# Records `[class_name, ivar]` for each ivar an instance `def` assigns an optimistic lookup to. The class
|
|
2404
|
+
# prefixes come from {#collect_other_ivar_writes}' walk, whose own result is not needed here.
|
|
2405
|
+
def collect_lookup_ivar_writes(root, scope_index)
|
|
2406
|
+
collect_other_ivar_writes(root, [], false, Hash.new { |h, k| h[k] = Set.new }) do |def_node, prefix|
|
|
2407
|
+
next if def_node.receiver || prefix.empty?
|
|
2408
|
+
|
|
2409
|
+
scan_lookup_ivar_writes(def_node.body, prefix, scope_index)
|
|
2410
|
+
end
|
|
2411
|
+
end
|
|
2412
|
+
|
|
2413
|
+
def scan_lookup_ivar_writes(node, prefix, scope_index)
|
|
2414
|
+
return unless node.is_a?(Prism::Node)
|
|
2415
|
+
|
|
2416
|
+
record_lookup_ivar_write(node, prefix, scope_index)
|
|
2417
|
+
node.rigor_each_child { |child| scan_lookup_ivar_writes(child, prefix, scope_index) }
|
|
2418
|
+
end
|
|
2419
|
+
|
|
2420
|
+
def record_lookup_ivar_write(node, prefix, scope_index)
|
|
2421
|
+
return unless LOOKUP_IVAR_WRITES.any? { |klass| node.is_a?(klass) }
|
|
2422
|
+
|
|
2423
|
+
scope = scope_index[node.value] || scope_index[node]
|
|
2424
|
+
return if scope.nil? || !Inference::DefReturnTyper.optimistic_value?(
|
|
2425
|
+
node.value, scope, Inference::DefReturnTyper::LookupContext.new(scope_index, {}.compare_by_identity)
|
|
2426
|
+
)
|
|
2427
|
+
|
|
2428
|
+
@lookup_ivars << [prefix.join("::"), node.name]
|
|
2429
|
+
end
|
|
2430
|
+
|
|
2431
|
+
def record_attr_definers(call_node, class_name, symbol_names, ctx)
|
|
2432
|
+
symbol_names.each do |attr_name|
|
|
2433
|
+
ATTR_KINDS.fetch(call_node.name).each do |variant|
|
|
2434
|
+
method_name = variant == :writer ? :"#{attr_name}=" : attr_name
|
|
2435
|
+
ctx.definers << [[class_name, :instance, method_name], call_node]
|
|
2436
|
+
end
|
|
1858
2437
|
end
|
|
1859
2438
|
end
|
|
1860
2439
|
|
|
@@ -1885,8 +2464,8 @@ module Rigor
|
|
|
1885
2464
|
end
|
|
1886
2465
|
end
|
|
1887
2466
|
|
|
1888
|
-
#
|
|
1889
|
-
#
|
|
2467
|
+
# Records the observation-derived types of ivars assigned directly from `def initialize` parameters, keyed
|
|
2468
|
+
# by class name and then attribute name. Only populated when `@observations` is
|
|
1890
2469
|
# non-empty (i.e. `--params=observed` was supplied). Matches the pattern `@ivar_name = param_name` where
|
|
1891
2470
|
# `param_name` is a required / optional positional or keyword parameter of `initialize`.
|
|
1892
2471
|
#
|
|
@@ -1919,20 +2498,25 @@ module Rigor
|
|
|
1919
2498
|
# and every write inside an instance `def` or a block, an `instance_variable_set` on `self` included
|
|
1920
2499
|
# ({#written_ivar}). `def self.x`, a `class << self` body and a bare class-body write set the class
|
|
1921
2500
|
# object's ivars and do not count.
|
|
1922
|
-
|
|
2501
|
+
#
|
|
2502
|
+
# `on_def`, when given, is called with each instance `def` reached and its class prefix, so another
|
|
2503
|
+
# per-class reading of instance-method ivar writes shares this walk ({#collect_lookup_ivar_writes}).
|
|
2504
|
+
def collect_other_ivar_writes(node, prefix, in_block, other_writes, &on_def)
|
|
1923
2505
|
return unless node.is_a?(Prism::Node)
|
|
1924
2506
|
|
|
1925
2507
|
case node
|
|
1926
2508
|
when Prism::ClassNode, Prism::ModuleNode
|
|
1927
2509
|
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
|
|
2510
|
+
return collect_other_ivar_writes(node.body, child_prefix, false, other_writes, &on_def) if child_prefix
|
|
1929
2511
|
when Prism::SingletonClassNode then return
|
|
1930
|
-
when Prism::DefNode
|
|
2512
|
+
when Prism::DefNode
|
|
2513
|
+
on_def&.call(node, prefix)
|
|
2514
|
+
return record_def_ivar_writes(node, prefix, other_writes)
|
|
1931
2515
|
when Prism::BlockNode, Prism::LambdaNode then in_block = true
|
|
1932
2516
|
end
|
|
1933
2517
|
record_class_level_write(node, prefix, in_block, other_writes) unless prefix.empty?
|
|
1934
2518
|
|
|
1935
|
-
node.rigor_each_child { |child| collect_other_ivar_writes(child, prefix, in_block, other_writes) }
|
|
2519
|
+
node.rigor_each_child { |child| collect_other_ivar_writes(child, prefix, in_block, other_writes, &on_def) }
|
|
1936
2520
|
end
|
|
1937
2521
|
|
|
1938
2522
|
# The ivars an `attr_writer` / `attr_accessor` on `self` names, or the ivar a write in a block sets.
|
|
@@ -2014,7 +2598,7 @@ module Rigor
|
|
|
2014
2598
|
node.rigor_each_child { |c| collect_init_ivar_obs(c, prefix, result) }
|
|
2015
2599
|
end
|
|
2016
2600
|
|
|
2017
|
-
#
|
|
2601
|
+
# Derives the observation-derived attribute types of a single `def initialize`, matching `@ivar = param_name`
|
|
2018
2602
|
# assignments against the available `[class_name, :initialize]` observations.
|
|
2019
2603
|
def ivar_obs_from_initialize(class_name, def_node)
|
|
2020
2604
|
obs_list = @observations[[class_name, :initialize]]
|
|
@@ -2029,8 +2613,8 @@ module Rigor
|
|
|
2029
2613
|
build_ivar_obs_type_map(ivar_to_param, param_index, obs_list)
|
|
2030
2614
|
end
|
|
2031
2615
|
|
|
2032
|
-
#
|
|
2033
|
-
#
|
|
2616
|
+
# Maps `{ ivar_name => param_name }` to attribute types by looking up each param's observation types and
|
|
2617
|
+
# unioning them.
|
|
2034
2618
|
def build_ivar_obs_type_map(ivar_to_param, param_index, obs_list)
|
|
2035
2619
|
ivar_to_param.filter_map do |ivar_name, param_name|
|
|
2036
2620
|
types = collect_param_obs_types(obs_list, param_name, param_index[param_name])
|