rigortype 0.4.6 → 0.4.8
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 +4 -2
- data/data/core_overlay/bigdecimal.rbs +33 -0
- data/data/core_overlay/resolv.rbs +30 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +45 -13
- data/data/gem_overlay/addressable/addressable.rbs +40 -0
- 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 +6 -3
- data/docs/handbook/11-sig-gen.md +85 -3
- data/docs/handbook/appendix-phpstan.md +4 -4
- data/docs/install.md +20 -0
- data/docs/manual/01-installation.md +67 -26
- data/docs/manual/02-cli-reference.md +455 -113
- data/docs/manual/03-configuration.md +76 -5
- data/docs/manual/04-diagnostics.md +5 -4
- data/docs/manual/06-baseline.md +6 -4
- data/docs/manual/08-skills.md +11 -0
- data/docs/manual/09-editor-integration.md +21 -2
- data/docs/manual/10-mcp-server.md +38 -2
- data/docs/manual/12-caching.md +4 -3
- data/docs/manual/14-rails-quickstart.md +1 -1
- data/docs/manual/19-effect-labels.md +3 -4
- data/docs/manual/plugins/rigor-actionpack.md +32 -15
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +23 -1
- data/docs/manual/plugins/rigor-sorbet.md +17 -3
- data/lib/rigor/analysis/baseline.rb +24 -27
- data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
- data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +12 -4
- data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +16 -4
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -1
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
- data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +41 -12
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
- data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
- data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
- data/lib/rigor/analysis/check_rules/rule_ids.rb +9 -2
- 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 +2 -2
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules.rb +720 -141
- data/lib/rigor/analysis/crash_signature.rb +45 -3
- data/lib/rigor/analysis/dependency_recorder.rb +237 -80
- 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 +93 -6
- data/lib/rigor/analysis/fact_store.rb +3 -3
- data/lib/rigor/analysis/incremental.rb +11 -1
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +4 -3
- data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
- data/lib/rigor/analysis/incremental_session.rb +306 -53
- data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
- data/lib/rigor/analysis/rule_catalog.rb +39 -3
- data/lib/rigor/analysis/run_cache_key.rb +27 -3
- data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
- data/lib/rigor/analysis/runner/declaration_position.rb +8 -8
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +264 -112
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +4 -9
- data/lib/rigor/analysis/runner/pool_coordinator.rb +173 -328
- data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
- 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/worker_payload.rb +36 -0
- data/lib/rigor/analysis/runner.rb +298 -109
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +62 -23
- 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 +65 -71
- data/lib/rigor/bleeding_edge.rb +22 -10
- 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/file_digest.rb +39 -31
- data/lib/rigor/cache/incremental_snapshot.rb +95 -17
- 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 +90 -4
- data/lib/rigor/cache/rbs_environment.rb +8 -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 +160 -57
- data/lib/rigor/cli/catalogue.rb +256 -0
- data/lib/rigor/cli/check_command.rb +112 -104
- 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 +25 -20
- 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 +26 -18
- 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 +29 -10
- data/lib/rigor/cli/lsp_command.rb +250 -23
- data/lib/rigor/cli/mcp_command.rb +12 -7
- 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 +40 -1
- data/lib/rigor/cli/plugin_command.rb +93 -46
- data/lib/rigor/cli/plugins_command.rb +17 -18
- data/lib/rigor/cli/plugins_renderer.rb +10 -6
- 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 +21 -15
- data/lib/rigor/cli/sig_gen_command.rb +92 -27
- 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 +2 -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 +16 -11
- data/lib/rigor/cli/type_of_project_scope.rb +1 -4
- 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 +214 -56
- data/lib/rigor/config_audit.rb +33 -19
- data/lib/rigor/configuration/severity_profile.rb +14 -5
- data/lib/rigor/configuration.rb +104 -35
- data/lib/rigor/effects/collector.rb +53 -21
- 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 +64 -33
- data/lib/rigor/environment/class_registry.rb +3 -3
- data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
- data/lib/rigor/environment/core_root.rb +77 -0
- data/lib/rigor/environment/default_libraries.rb +18 -1
- data/lib/rigor/environment/deferred_signature_plan.rb +381 -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/hkt_registry_holder.rb +6 -0
- 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_collection_discovery.rb +123 -8
- data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
- data/lib/rigor/environment/rbs_hierarchy.rb +12 -15
- data/lib/rigor/environment/rbs_loader.rb +772 -110
- data/lib/rigor/environment/reflection.rb +7 -7
- data/lib/rigor/environment/required_features.rb +0 -3
- data/lib/rigor/environment/ruby_library_roots.rb +1 -1
- data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
- data/lib/rigor/environment.rb +169 -42
- data/lib/rigor/flow_contribution/merger.rb +4 -4
- data/lib/rigor/inference/acceptance.rb +136 -40
- data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
- data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
- data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
- data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
- data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
- data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
- data/lib/rigor/inference/block_auto_splat.rb +1 -1
- data/lib/rigor/inference/block_parameter_binder.rb +2 -3
- data/lib/rigor/inference/block_run_count.rb +185 -0
- data/lib/rigor/inference/budget_trace.rb +8 -2
- data/lib/rigor/inference/builtins/array_catalog.rb +2 -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 +88 -77
- data/lib/rigor/inference/callee_write_census.rb +815 -0
- data/lib/rigor/inference/captured_locals.rb +2 -0
- data/lib/rigor/inference/census_foreign_block.rb +354 -0
- data/lib/rigor/inference/clause_widening.rb +1 -1
- data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
- data/lib/rigor/inference/construction_escape.rb +258 -0
- data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
- data/lib/rigor/inference/declaration_walk.rb +4 -0
- data/lib/rigor/inference/deep_erasure.rb +39 -7
- data/lib/rigor/inference/def_handle.rb +1 -1
- data/lib/rigor/inference/def_node_resolver.rb +33 -15
- data/lib/rigor/inference/def_return_typer.rb +47 -5
- data/lib/rigor/inference/definer_resolution.rb +19 -9
- data/lib/rigor/inference/definers_index.rb +162 -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 +16 -0
- data/lib/rigor/inference/expression_typer.rb +1559 -320
- data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
- data/lib/rigor/inference/flow_tracer.rb +31 -14
- data/lib/rigor/inference/fork_map.rb +3 -0
- data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
- data/lib/rigor/inference/global_write_census.rb +81 -5
- 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/in_effect_refinements.rb +7 -0
- data/lib/rigor/inference/indexed_narrowing.rb +61 -15
- data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
- data/lib/rigor/inference/ivar_writer_census.rb +154 -8
- data/lib/rigor/inference/key_presence_guard.rb +51 -24
- data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
- data/lib/rigor/inference/last_line.rb +5 -2
- data/lib/rigor/inference/loop_value_record.rb +66 -14
- data/lib/rigor/inference/match_rebinding/frame.rb +57 -0
- data/lib/rigor/inference/match_rebinding/yielding_match.rb +101 -0
- data/lib/rigor/inference/match_rebinding.rb +28 -5
- 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 +44 -30
- data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
- data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
- data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
- data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +51 -27
- data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +606 -78
- 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 +3 -3
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +467 -84
- 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/singleton_type_params.rb +104 -0
- 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 +123 -25
- data/lib/rigor/inference/method_parameter_binder.rb +136 -56
- data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
- data/lib/rigor/inference/module_function_state.rb +24 -3
- data/lib/rigor/inference/multi_target_binder.rb +2 -2
- data/lib/rigor/inference/mutation_rejoin.rb +240 -4
- data/lib/rigor/inference/mutation_widening.rb +199 -29
- data/lib/rigor/inference/narrowing.rb +238 -50
- data/lib/rigor/inference/operand_effects.rb +43 -2
- data/lib/rigor/inference/optimistic_origin.rb +134 -9
- data/lib/rigor/inference/origin_lookup.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/precision_scanner.rb +1 -1
- data/lib/rigor/inference/project_method_ownership.rb +30 -9
- data/lib/rigor/inference/project_patched_methods.rb +65 -13
- data/lib/rigor/inference/project_patched_scanner.rb +120 -25
- data/lib/rigor/inference/project_patched_visibility.rb +39 -3
- data/lib/rigor/inference/project_yielder.rb +11 -17
- data/lib/rigor/inference/rbs_type_translator.rb +68 -15
- data/lib/rigor/inference/receiver_alias.rb +20 -0
- data/lib/rigor/inference/receiver_writer_call.rb +67 -0
- data/lib/rigor/inference/recursion_rebinding.rb +157 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +57 -17
- data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -16
- data/lib/rigor/inference/refined_dispatch.rb +13 -0
- data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
- data/lib/rigor/inference/remover_holder_scan.rb +287 -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 +0 -1
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
- data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +70 -26
- 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 +295 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
- data/lib/rigor/inference/scope_indexer.rb +2213 -226
- data/lib/rigor/inference/self_call_ivar_writes.rb +103 -12
- data/lib/rigor/inference/self_preserving_block.rb +87 -0
- data/lib/rigor/inference/set_mutation.rb +189 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +42 -8
- data/lib/rigor/inference/singleton_object_constant.rb +32 -2
- data/lib/rigor/inference/statement_evaluator.rb +3268 -358
- data/lib/rigor/inference/string_mutation.rb +14 -0
- data/lib/rigor/inference/struct_fold_safety.rb +4 -6
- data/lib/rigor/inference/synthetic_method_scanner.rb +40 -17
- data/lib/rigor/inference/toplevel_def_census.rb +67 -0
- data/lib/rigor/inference/unknown_store_widening.rb +103 -4
- data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
- data/lib/rigor/inference/untyped_rejoin.rb +1 -1
- data/lib/rigor/inference/void_tail_summary.rb +29 -9
- data/lib/rigor/language_server/buffer_table.rb +3 -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 +8 -3
- 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 +39 -1
- 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 +7 -3
- data/lib/rigor/language_server/signature_help_provider.rb +4 -4
- data/lib/rigor/lens/claims/comparison.rb +78 -0
- data/lib/rigor/lens/claims/request.rb +131 -0
- data/lib/rigor/lens/claims/type_reader.rb +216 -0
- data/lib/rigor/lens/claims.rb +208 -0
- data/lib/rigor/lens/contract_reader.rb +5 -0
- data/lib/rigor/lens/document.rb +1 -1
- data/lib/rigor/lens/file_reader.rb +21 -8
- data/lib/rigor/lens/plugins.rb +1 -1
- data/lib/rigor/lens/run.rb +8 -0
- data/lib/rigor/lens.rb +1 -1
- data/lib/rigor/library_probe.rb +68 -0
- data/lib/rigor/mcp/server.rb +73 -2
- data/lib/rigor/mcp.rb +1 -1
- data/lib/rigor/memo_audit.rb +282 -0
- data/lib/rigor/plugin/access_denied_error.rb +1 -2
- data/lib/rigor/plugin/base.rb +15 -2
- data/lib/rigor/plugin/bundled_catalog.rb +1 -12
- data/lib/rigor/plugin/io_boundary.rb +26 -4
- 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 +16 -16
- data/lib/rigor/plugin/registry.rb +16 -5
- 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 +48 -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 +44 -5
- data/lib/rigor/protection/mutation_cache.rb +4 -1
- 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 +81 -22
- data/lib/rigor/reflection/declaration_collectors.rb +55 -0
- data/lib/rigor/reflection/declarations.rb +58 -0
- data/lib/rigor/reflection/project_method.rb +177 -0
- data/lib/rigor/reflection.rb +2 -0
- data/lib/rigor/rooted_glob.rb +32 -1
- data/lib/rigor/run_scoped_slots.rb +38 -0
- data/lib/rigor/runtime/jit.rb +1 -1
- data/lib/rigor/scope/discovery_index.rb +168 -8
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
- data/lib/rigor/scope/resolution_chain.rb +901 -54
- data/lib/rigor/scope.rb +644 -37
- data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
- data/lib/rigor/sig_gen/classification.rb +18 -1
- data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
- data/lib/rigor/sig_gen/generator.rb +588 -66
- 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 +37 -4
- data/lib/rigor/sig_gen/observation_collector.rb +14 -7
- 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/platform_guarded_files.rb +184 -0
- data/lib/rigor/sig_gen/renderer.rb +15 -9
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +62 -2
- 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 +238 -37
- data/lib/rigor/sig_gen.rb +1 -0
- data/lib/rigor/source/forwardable_delegators.rb +122 -0
- data/lib/rigor/source/node_children.rb +13 -0
- 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 +173 -21
- data/lib/rigor/type/constant.rb +4 -5
- data/lib/rigor/type/hash_shape.rb +21 -40
- data/lib/rigor/type/interface.rb +60 -0
- data/lib/rigor/type/union.rb +1 -1
- data/lib/rigor/type.rb +1 -0
- 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/type_of_memo/serving.rb +99 -0
- data/lib/rigor/type_of_memo/stats.rb +43 -0
- data/lib/rigor/type_of_memo.rb +337 -0
- data/lib/rigor/typing_state.rb +1094 -0
- 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 +38 -36
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -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 +3 -3
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +115 -36
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
- 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 +132 -33
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -6
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +54 -17
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
- 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 +11 -14
- 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 +30 -9
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +55 -13
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -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 +1 -1
- 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 +8 -7
- 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-graphql/lib/rigor/plugin/graphql.rb +12 -2
- 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 +8 -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 +200 -4
- 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 +4 -3
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +5 -5
- 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 +3 -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 +210 -3
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +97 -24
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +4 -8
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
- data/sig/rigor/analysis/baseline.rbs +0 -3
- data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
- data/sig/rigor/analysis/fact_store.rbs +2 -2
- data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
- data/sig/rigor/cache/descriptor.rbs +14 -0
- data/sig/rigor/cli/diff_command.rbs +1 -1
- data/sig/rigor/cli/explain_command.rbs +1 -1
- data/sig/rigor/effects/registry.rbs +2 -2
- data/sig/rigor/environment.rbs +4 -1
- data/sig/rigor/inference.rbs +4 -1
- data/sig/rigor/plugin/additional_initializer.rbs +0 -5
- data/sig/rigor/plugin/base.rbs +53 -18
- data/sig/rigor/plugin/effect_attribution.rbs +3 -3
- data/sig/rigor/plugin/effect_edge.rbs +1 -1
- data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
- data/sig/rigor/plugin/interfaces.rbs +51 -0
- data/sig/rigor/plugin/io_boundary.rbs +3 -3
- data/sig/rigor/plugin/manifest.rbs +9 -9
- data/sig/rigor/plugin/node_context.rbs +21 -0
- data/sig/rigor/plugin/protocol_contract.rbs +3 -7
- data/sig/rigor/reflection.rbs +37 -0
- data/sig/rigor/scope.rbs +43 -4
- data/sig/rigor/trinary.rbs +1 -0
- data/sig/rigor/type.rbs +9 -3
- data/sig/rigor.rbs +3 -2
- data/skills/rigor-ask/SKILL.md +1 -0
- 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/01-detect.md +1 -0
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +17 -2
- data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
- data/skills/rigor-rbs-contract/SKILL.md +147 -0
- data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
- data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
- data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
- data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
- data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
- data/skills/rigor-rbs-setup/SKILL.md +3 -1
- data/skills/rigor-type-oracle/SKILL.md +26 -1
- data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
- metadata +82 -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"
|
|
@@ -16,6 +17,7 @@ require_relative "../inference/def_return_typer"
|
|
|
16
17
|
require_relative "../inference/scope_indexer"
|
|
17
18
|
require_relative "../inference/project_seed_tables"
|
|
18
19
|
require_relative "../inference/module_function_state"
|
|
20
|
+
require_relative "bot_proposal_guard"
|
|
19
21
|
require_relative "../inference/rbs_type_translator"
|
|
20
22
|
require_relative "alias_index"
|
|
21
23
|
require_relative "meta_class_shape"
|
|
@@ -26,6 +28,8 @@ require_relative "declaration_equivalence"
|
|
|
26
28
|
require_relative "inline_declarations"
|
|
27
29
|
require_relative "layout_index"
|
|
28
30
|
require_relative "method_candidate"
|
|
31
|
+
require_relative "platform_guarded_files"
|
|
32
|
+
require_relative "type_name_qualification"
|
|
29
33
|
require_relative "writer"
|
|
30
34
|
require_relative "../rooted_glob"
|
|
31
35
|
|
|
@@ -79,16 +83,20 @@ module Rigor
|
|
|
79
83
|
attr_reader :unrenderable, :unresolvable_superclasses
|
|
80
84
|
|
|
81
85
|
# @param paths — files / directories to scan.
|
|
82
|
-
# @param observations —
|
|
86
|
+
# @param observations —
|
|
83
87
|
# ADR-14 slice 3 — per-target-method arg-tuple observations
|
|
84
88
|
# produced by {ObservationCollector}. An empty Hash (the default)
|
|
85
89
|
# means "no observations available; emit `untyped` for every
|
|
86
90
|
# parameter position" per ADR-5 clause 2.
|
|
91
|
+
# @param target_writer — the {Writer} a `--write` or `--check` run hands the candidates to. When given, a
|
|
92
|
+
# class counts as written for a subclass only if that writer accepts its target file (#2461); nil (print,
|
|
93
|
+
# diff, JSON) counts every class with a candidate the writer is sure to write.
|
|
87
94
|
# @param overwrite — `--overwrite`: a member declared inline whose `sig/` copy disagrees is proposed as an
|
|
88
95
|
# `inline-overwrite` of the whole member instead of refused (ADR-112 WD4).
|
|
89
96
|
def initialize(configuration:, paths:, observations: {}, include_private: false, effect_annotator: nil,
|
|
90
|
-
overwrite: false)
|
|
97
|
+
overwrite: false, target_writer: nil)
|
|
91
98
|
@configuration = configuration
|
|
99
|
+
@target_writer = target_writer
|
|
92
100
|
@overwrite = overwrite
|
|
93
101
|
@paths = paths
|
|
94
102
|
@observations = normalize_observations(observations)
|
|
@@ -114,6 +122,36 @@ module Rigor
|
|
|
114
122
|
@unresolvable_superclasses = {}
|
|
115
123
|
# Issue #722 — whole-run: `{ class name => the lexical nesting its header is written in }`.
|
|
116
124
|
@superclass_nestings = {}
|
|
125
|
+
# Issue #2437 — whole-run: every class a walked file declares with a `class` keyword or a `Data.define` /
|
|
126
|
+
# `Struct.new` layout, so the report can tell a project superclass this run did not write from one nothing
|
|
127
|
+
# declares.
|
|
128
|
+
@source_classes = Set.new
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# How the report names an unresolvable superclass token ({#unresolvable_superclasses}). It is a project
|
|
132
|
+
# superclass when it names a class the walked source declares and this run wrote nothing for, whose own chain
|
|
133
|
+
# would close if the project's classes were written:
|
|
134
|
+
# each step a source class, ending at no superclass or at one the RBS environment knows (#2437). What is
|
|
135
|
+
# missing is then the project's own declaration, not a gem's RBS. `class ApplicationRecord <
|
|
136
|
+
# ActiveRecord::Base` with no RBS for the gem is not such a class: writing it would not help.
|
|
137
|
+
#
|
|
138
|
+
# Returns `[:project, token]` for such a class. Otherwise `[:external, root]`, where `root` is the first token
|
|
139
|
+
# on the chain that is not a source class: `User < ApplicationRecord` reports `ActiveRecord::Base`, the class
|
|
140
|
+
# whose RBS is missing, rather than the project class between them.
|
|
141
|
+
def unresolvable_superclass_report(token)
|
|
142
|
+
name = bare_superclass_name(token)
|
|
143
|
+
root = token
|
|
144
|
+
seen = {}
|
|
145
|
+
while @source_classes.include?(name) && !seen[name]
|
|
146
|
+
seen[name] = true
|
|
147
|
+
nesting, superclass = @superclass_nestings[name]
|
|
148
|
+
return [:project, token] if superclass.nil?
|
|
149
|
+
|
|
150
|
+
root = resolve_superclass_token(superclass, nesting, @source_classes, name)
|
|
151
|
+
name = bare_superclass_name(root)
|
|
152
|
+
return [:project, token] if Reflection.rbs_class_known?(name, environment: @environment)
|
|
153
|
+
end
|
|
154
|
+
[:external, root]
|
|
117
155
|
end
|
|
118
156
|
|
|
119
157
|
# Lifts legacy plain-`Array[Type]` observation entries into {ObservedCall} carriers. Specs from the
|
|
@@ -128,13 +166,45 @@ module Rigor
|
|
|
128
166
|
def run
|
|
129
167
|
resolved = resolve_paths(@paths)
|
|
130
168
|
@resolved_paths = resolved
|
|
169
|
+
# Issue #3035 — the scanned files Ruby loads only through a platform-conditional require; their
|
|
170
|
+
# candidates are demoted in {#demote_platform_guarded}.
|
|
171
|
+
@platform_guarded_files = PlatformGuardedFiles.call(
|
|
172
|
+
resolved, ruby_version: @configuration.target_ruby
|
|
173
|
+
)
|
|
131
174
|
@environment = build_environment(resolved)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
175
|
+
# ADR-84 WD2, as `Runner#run_analysis` mints it for `check` (#2300): one return-memo bucket for the whole
|
|
176
|
+
# run. Without it the memo keys on each file's merged `discovered_def_nodes`, a new table per file, so a
|
|
177
|
+
# callee reached from two files was re-inferred in each. A fresh token per run keeps a second run in one
|
|
178
|
+
# process from reading the first's entries, and the bucket is released with the other run-scoped slots
|
|
179
|
+
# when the run ends ({RunScopedSlots}), so a finished run holds none of it.
|
|
180
|
+
@run_generation = Object.new.freeze
|
|
181
|
+
# Both read the files as they are now, so a second `run` of one generator re-reads them.
|
|
182
|
+
@project_tables = nil
|
|
183
|
+
@file_base_scope = nil
|
|
184
|
+
# #2461 — which `sig/` targets the writer will write to, read from the files as they are now.
|
|
185
|
+
@writable_targets = {}
|
|
186
|
+
# ADR-130 WD5 — with interface dispatch off, so a return read through a `Rigor::Plugin` interface member
|
|
187
|
+
# (`_FactStore#publish: ... -> nil`) is not written into RBS a plugin author never wrote against the
|
|
188
|
+
# interface: the output is the output of erasure.
|
|
189
|
+
candidates = Inference::MethodDispatcher::RbsDispatch.without_interface_dispatch do
|
|
190
|
+
resolved.flat_map { |path| analyse_file(path, @environment) }
|
|
191
|
+
end
|
|
192
|
+
demoted = shelter_withheld_classes(
|
|
193
|
+
demote_platform_guarded(
|
|
194
|
+
demote_overridden_base_methods(
|
|
195
|
+
demote_bot_proposals(
|
|
196
|
+
demote_unresolvable_superclasses(
|
|
197
|
+
demote_generic_reopenings(demote_inline_generic_classes(resolve_superclass_spellings(candidates)))
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
)
|
|
136
201
|
)
|
|
137
202
|
)
|
|
203
|
+
# Again after the override demotion: a demoted method can leave its file with nothing to write, and a class
|
|
204
|
+
# shell riding on that file then declares nothing a subclass elsewhere could rest on.
|
|
205
|
+
annotate_effects(stamp_generic_reopenings(demote_unresolvable_superclasses(demoted)))
|
|
206
|
+
ensure
|
|
207
|
+
RunScopedSlots.release
|
|
138
208
|
end
|
|
139
209
|
|
|
140
210
|
private
|
|
@@ -209,10 +279,10 @@ module Rigor
|
|
|
209
279
|
decls.each do |decl|
|
|
210
280
|
next unless decl.is_a?(::RBS::AST::Declarations::Class) || decl.is_a?(::RBS::AST::Declarations::Module)
|
|
211
281
|
|
|
212
|
-
|
|
213
|
-
return decl if
|
|
282
|
+
path = ::Rigor::Environment::RbsLoader.declared_class_path(decl, prefix)
|
|
283
|
+
return decl if path.join("::") == class_name
|
|
214
284
|
|
|
215
|
-
found = find_declaration(decl.members,
|
|
285
|
+
found = find_declaration(decl.members, path, class_name)
|
|
216
286
|
return found if found
|
|
217
287
|
end
|
|
218
288
|
nil
|
|
@@ -223,6 +293,88 @@ module Rigor
|
|
|
223
293
|
(1..segments.size).any? { |n| generic.key?(segments.first(n).join("::")) }
|
|
224
294
|
end
|
|
225
295
|
|
|
296
|
+
# Issue #2436 — a class the loaded RBS declares generic (`Array`, `Hash`, a gem's or the project's own) is
|
|
297
|
+
# reopened with that declaration's type parameters copied onto the header ({#stamp_generic_reopenings}),
|
|
298
|
+
# which is exact only while every parameter is plain. A bound or a default is a type spelled relative to the
|
|
299
|
+
# declaring file's namespace, and copying it under another nesting can resolve it to a different class, so
|
|
300
|
+
# rbs would reject the header as it rejects a parameterless one (`GenericParameterMismatchError`): the class,
|
|
301
|
+
# and every class whose signature mentions it, fails its definition build. A method of such a class, or of
|
|
302
|
+
# one nested in it, is skipped instead — unless `sig/` already declares the class, whose own header the
|
|
303
|
+
# writer then writes into ({#demote_inline_generic_classes}' rule).
|
|
304
|
+
def demote_generic_reopenings(candidates)
|
|
305
|
+
loader = @environment&.rbs_loader
|
|
306
|
+
return candidates if loader.nil?
|
|
307
|
+
|
|
308
|
+
layout = nil
|
|
309
|
+
candidates.map do |candidate|
|
|
310
|
+
next candidate unless Classification::EMITTABLE.include?(candidate.classification)
|
|
311
|
+
next candidate unless namespace_prefixes(candidate.class_name).any? { |name| bounded_generic?(loader, name) }
|
|
312
|
+
|
|
313
|
+
layout ||= LayoutIndex.new(signature_paths: @configuration.signature_paths)
|
|
314
|
+
next candidate if layout.file_for(candidate.class_name)
|
|
315
|
+
|
|
316
|
+
demoted_candidate(candidate, :generic_reopening)
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# The other half of {#demote_generic_reopenings}: every emittable candidate carries the header spelling of
|
|
321
|
+
# each generic class the run may open a header for — its own class, the classes it is nested in, and the
|
|
322
|
+
# file's class shells and theirs — so the writer and `--print` spell the same header. Method bodies keep
|
|
323
|
+
# `untyped` for element types; nothing here names `E` in a member.
|
|
324
|
+
#
|
|
325
|
+
# Issue #2580 — a skipped candidate is stamped too, and its class counts towards the map: the writer
|
|
326
|
+
# repairs a parameterless header an earlier run wrote into the file a candidate routes to, and a file whose
|
|
327
|
+
# every proposal is already declared (or untyped) would otherwise keep a header rbs rejects.
|
|
328
|
+
def stamp_generic_reopenings(candidates)
|
|
329
|
+
loader = @environment&.rbs_loader
|
|
330
|
+
return candidates if loader.nil?
|
|
331
|
+
|
|
332
|
+
spelled = header_type_params(candidates.flat_map { |c| [c.class_name, *c.class_shells] }.compact)
|
|
333
|
+
return candidates if spelled.empty?
|
|
334
|
+
|
|
335
|
+
candidates.map { |candidate| candidate.with_class_type_params(spelled) }
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# #2603 — the header spelling {#header_type_params} gives `class_name` alone, or nil when the loaded RBS does
|
|
339
|
+
# not declare it generic with plain parameters only. The writer asks it for a bare header in a `sig/` file no
|
|
340
|
+
# candidate of the run routes to. Nil before {#run} has built the environment.
|
|
341
|
+
def generic_header_spelling(class_name)
|
|
342
|
+
loader = @environment&.rbs_loader
|
|
343
|
+
return nil if loader.nil?
|
|
344
|
+
|
|
345
|
+
params = loader.class_declaration_type_params(class_name)
|
|
346
|
+
"[#{params.join(', ')}]" if !params.empty? && params.all? { |p| plain_type_param?(p) }
|
|
347
|
+
end
|
|
348
|
+
public :generic_header_spelling
|
|
349
|
+
|
|
350
|
+
# `{ class name => "[unchecked out E]" }` for every name in `names`, or a namespace it is nested in, whose
|
|
351
|
+
# loaded declaration is generic with plain parameters only.
|
|
352
|
+
def header_type_params(names)
|
|
353
|
+
names.uniq.flat_map { |name| namespace_prefixes(name) }.uniq.each_with_object({}) do |name, acc|
|
|
354
|
+
spelling = generic_header_spelling(name)
|
|
355
|
+
acc[name] = spelling if spelling
|
|
356
|
+
end.freeze
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def namespace_prefixes(class_name)
|
|
360
|
+
return [] if class_name.nil?
|
|
361
|
+
|
|
362
|
+
segments = class_name.split("::")
|
|
363
|
+
(1..segments.size).map { |n| segments.first(n).join("::") }
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def bounded_generic?(loader, name)
|
|
367
|
+
params = loader.class_declaration_type_params(name)
|
|
368
|
+
!params.empty? && !params.all? { |param| plain_type_param?(param) }
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# A name, a variance and `unchecked`, and nothing that spells a type: no upper or lower bound, no default.
|
|
372
|
+
def plain_type_param?(param)
|
|
373
|
+
%i[upper_bound lower_bound default_type].none? do |reader|
|
|
374
|
+
param.respond_to?(reader) && param.public_send(reader)
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
226
378
|
# Issue #744 — a base class's method is NOT emitted when a project subclass overrides it and the
|
|
227
379
|
# override is not emitted itself.
|
|
228
380
|
#
|
|
@@ -241,7 +393,7 @@ module Rigor
|
|
|
241
393
|
# (#744 half 2) and is not decided here: this pass only stops sig-gen manufacturing the conflict.
|
|
242
394
|
def demote_overridden_base_methods(candidates)
|
|
243
395
|
superclasses = candidates.each_with_object({}) do |candidate, acc|
|
|
244
|
-
(candidate.class_superclasses || {}).each { |name, sup| acc[name] = sup
|
|
396
|
+
(candidate.class_superclasses || {}).each { |name, sup| acc[name] = bare_superclass_name(sup) }
|
|
245
397
|
end
|
|
246
398
|
return candidates if superclasses.empty?
|
|
247
399
|
|
|
@@ -256,6 +408,82 @@ module Rigor
|
|
|
256
408
|
end
|
|
257
409
|
end
|
|
258
410
|
|
|
411
|
+
# Issue #3029 — an emittable `-> bot` proposal for an overridden, abstract or template method is skipped
|
|
412
|
+
# ({BotProposalGuard}). It runs before {#demote_overridden_base_methods}, so a subclass's `bot` override this
|
|
413
|
+
# skips counts there as an override that is not emitted, and its parent's declaration is held back too.
|
|
414
|
+
def demote_bot_proposals(candidates)
|
|
415
|
+
guard = nil
|
|
416
|
+
candidates.map do |candidate|
|
|
417
|
+
next candidate unless Classification::EMITTABLE.include?(candidate.classification)
|
|
418
|
+
next candidate if candidate.classification == Classification::INLINE_OVERWRITE
|
|
419
|
+
next candidate unless candidate.inferred_return.is_a?(Type::Bot)
|
|
420
|
+
|
|
421
|
+
guard ||= BotProposalGuard.new(project_tables)
|
|
422
|
+
reason = guard.reason_for(candidate)
|
|
423
|
+
reason ? demoted_candidate(candidate, reason) : candidate
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
# The reasons that withhold a member because of what its class's subclasses or callees do, not because the class
|
|
428
|
+
# itself cannot be declared. `:platform_guarded` counts too (#3035): a class whose every proposal the guard
|
|
429
|
+
# withheld — the raising-stub shape — still needs its shell on a subclass's file, or the subclass's own
|
|
430
|
+
# declaration reads `unresolvable-superclass` against a class this platform does load.
|
|
431
|
+
WITHHELD_FOR_OVERRIDES = %i[overridden_bot abstract_method template_method overridden_by_unsigned_subclass
|
|
432
|
+
platform_guarded].freeze
|
|
433
|
+
private_constant :WITHHELD_FOR_OVERRIDES
|
|
434
|
+
|
|
435
|
+
# Issue #3029 — a class whose members {#demote_bot_proposals} or {#demote_overridden_base_methods} withheld,
|
|
436
|
+
# leaving it nothing to emit, is still declared, as a class shell, so its subclasses keep their proposals.
|
|
437
|
+
# Without it an all-abstract base has nothing to write, {#demote_unresolvable_superclasses} finds no
|
|
438
|
+
# declaration for it, and every subclass proposal is skipped as `unresolvable-superclass`. The shell rides on
|
|
439
|
+
# each file that declares one of its subclasses, so it reaches disk wherever a subclass's members do. The writer
|
|
440
|
+
# spells its header with the run's recorded superclass and namespace kinds. A class the file already declares
|
|
441
|
+
# makes the shell a no-op, and one the loaded RBS declares gets none ({#wholly_withheld_classes}). A class whose
|
|
442
|
+
# own superclass chain is unresolvable is already skipped as `unresolvable-superclass` when this pass runs, so
|
|
443
|
+
# it is never withheld here and gets no shell.
|
|
444
|
+
def shelter_withheld_classes(candidates)
|
|
445
|
+
withheld = wholly_withheld_classes(candidates)
|
|
446
|
+
return candidates if withheld.empty?
|
|
447
|
+
|
|
448
|
+
shells_by_path = withheld_shell_paths(candidates, withheld)
|
|
449
|
+
candidates.map do |candidate|
|
|
450
|
+
extra = shells_by_path.fetch(candidate.path, nil)
|
|
451
|
+
next candidate if extra.nil?
|
|
452
|
+
|
|
453
|
+
shells = (candidate.class_shells + extra.to_a).uniq
|
|
454
|
+
shells.size == candidate.class_shells.size ? candidate : candidate.with_class_shells(shells)
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
# The classes to shelter: a withheld member and nothing to emit, and no declaration in the loaded RBS (`sig/`,
|
|
459
|
+
# the collection, gems). A declared class needs no shell for its subclasses' headers to resolve, and a shell
|
|
460
|
+
# spelling the source's superclass could contradict the declaration's (`RBS::SuperclassMismatchError`).
|
|
461
|
+
def wholly_withheld_classes(candidates)
|
|
462
|
+
by_class = candidates.select(&:class_name).group_by(&:class_name)
|
|
463
|
+
by_class.each_with_object(Set.new) do |(name, rows), acc|
|
|
464
|
+
next unless rows.any? { |c| WITHHELD_FOR_OVERRIDES.include?(c.skip_reason) }
|
|
465
|
+
next if rows.any? { |c| Classification::EMITTABLE.include?(c.classification) }
|
|
466
|
+
next if class_known?(name)
|
|
467
|
+
|
|
468
|
+
acc << name
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
# `{ path => Set[class name] }`: the files each withheld class's shell rides on, which are the files of every
|
|
473
|
+
# candidate whose class descends from it, however far down.
|
|
474
|
+
def withheld_shell_paths(candidates, withheld)
|
|
475
|
+
superclasses = candidates.each_with_object({}) do |candidate, acc|
|
|
476
|
+
(candidate.class_superclasses || {}).each { |name, sup| acc[name] = bare_superclass_name(sup) }
|
|
477
|
+
end
|
|
478
|
+
candidates.each_with_object(Hash.new { |h, k| h[k] = Set.new }) do |candidate, acc|
|
|
479
|
+
next if candidate.class_name.nil?
|
|
480
|
+
|
|
481
|
+
each_ancestor(candidate.class_name, superclasses) do |ancestor|
|
|
482
|
+
acc[candidate.path] << ancestor if withheld.include?(ancestor)
|
|
483
|
+
end
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
|
|
259
487
|
# `[class name, method name]` pairs an UNSIGNED override shadows: for every candidate that will not
|
|
260
488
|
# be emitted, every ancestor of its class paired with its method name.
|
|
261
489
|
def unsigned_override_ancestors(candidates, superclasses)
|
|
@@ -287,6 +515,54 @@ module Rigor
|
|
|
287
515
|
)
|
|
288
516
|
end
|
|
289
517
|
|
|
518
|
+
# Issue #3035 — a file Ruby loads only under a `RUBY_ENGINE` / `RUBY_PLATFORM` / `defined?(…)`
|
|
519
|
+
# require guard is not an ordinary definition for the platform sig-gen cannot pick:
|
|
520
|
+
# `stackprof/truffleruby.rb`'s raising `StackProf.run` stub proposed `-> bot` against the C
|
|
521
|
+
# extension's real signature. {PlatformGuardedFiles} names those files; a candidate from one is
|
|
522
|
+
# demoted when another file also provides the method — the guarded proposal would compete with
|
|
523
|
+
# or duplicate the definition this platform actually loads — or when its inferred return is
|
|
524
|
+
# `bot`, the shape a raising stub takes and never a contract worth writing from a file the
|
|
525
|
+
# current engine may never read. It runs after every demotion — LAST of them — so an earlier
|
|
526
|
+
# pass still sees the guarded candidate as emittable: a platform-conditional override is no
|
|
527
|
+
# "unsigned subclass" ({#demote_overridden_base_methods}), {BotProposalGuard} still reads its
|
|
528
|
+
# `bot` on the discovery tables, and demoting the base for a file this platform never loads
|
|
529
|
+
# would withhold a declaration the guarded file cannot shadow here. It runs BEFORE
|
|
530
|
+
# {#shelter_withheld_classes}, which must see the final emittable set: a class left with
|
|
531
|
+
# nothing to emit only because this pass withheld its last member still needs its shell.
|
|
532
|
+
def demote_platform_guarded(candidates)
|
|
533
|
+
guarded = @platform_guarded_files
|
|
534
|
+
return candidates if guarded.nil? || guarded.empty?
|
|
535
|
+
|
|
536
|
+
providers = method_providers(candidates)
|
|
537
|
+
candidates.map do |candidate|
|
|
538
|
+
next candidate unless platform_guarded_demote?(candidate, guarded, providers)
|
|
539
|
+
|
|
540
|
+
demoted_candidate(candidate, :platform_guarded)
|
|
541
|
+
end
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
# `[class_name, kind, method_name]` → the expanded paths of every candidate providing it, the
|
|
545
|
+
# guarded file's own included — "another definition also provides" is the `!= own` test.
|
|
546
|
+
def method_providers(candidates)
|
|
547
|
+
candidates.each_with_object(Hash.new { |hash, key| hash[key] = [] }) do |candidate, acc|
|
|
548
|
+
next if candidate.class_name.nil?
|
|
549
|
+
|
|
550
|
+
acc[[candidate.class_name, candidate.kind, candidate.method_name]] <<
|
|
551
|
+
File.expand_path(candidate.path)
|
|
552
|
+
end
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
def platform_guarded_demote?(candidate, guarded, providers)
|
|
556
|
+
return false unless Classification::EMITTABLE.include?(candidate.classification)
|
|
557
|
+
return false if candidate.class_name.nil?
|
|
558
|
+
|
|
559
|
+
own = File.expand_path(candidate.path)
|
|
560
|
+
return false unless guarded.include?(own)
|
|
561
|
+
|
|
562
|
+
slot = [candidate.class_name, candidate.kind, candidate.method_name]
|
|
563
|
+
candidate.inferred_return&.erase_to_rbs == "bot" || providers[slot].any? { |path| path != own }
|
|
564
|
+
end
|
|
565
|
+
|
|
290
566
|
# Issue #722 — resolves each recorded superclass token to the class Ruby means by it, and rewrites the
|
|
291
567
|
# emitted spelling when the two differ.
|
|
292
568
|
#
|
|
@@ -305,6 +581,10 @@ module Rigor
|
|
|
305
581
|
def resolve_superclass_spellings(candidates)
|
|
306
582
|
return candidates if @superclass_nestings.empty?
|
|
307
583
|
|
|
584
|
+
# Every shell counts here, written or not: the question is which class Ruby means by the token, and the
|
|
585
|
+
# source declares `Lib::Error` whether or not its file is written. Whether the resolved superclass reaches
|
|
586
|
+
# disk is {#demote_unresolvable_superclasses}'s question. Counting only written shells would leave `Error`
|
|
587
|
+
# unresolved, and a top-level `Error` some signature declares would then take it (#2454).
|
|
308
588
|
known = candidates.each_with_object(Set.new) do |candidate, acc|
|
|
309
589
|
acc << candidate.class_name if candidate.class_name
|
|
310
590
|
acc.merge(candidate.class_shells || [])
|
|
@@ -324,20 +604,32 @@ module Rigor
|
|
|
324
604
|
@superclass_nestings.each_with_object({}) do |(full, (nesting, token)), acc|
|
|
325
605
|
next if token.nil? || nesting.empty?
|
|
326
606
|
|
|
327
|
-
resolved = resolve_superclass_token(token, nesting, known)
|
|
607
|
+
resolved = resolve_superclass_token(token, nesting, known, full)
|
|
328
608
|
acc[full] = resolved if resolved && resolved != token
|
|
329
609
|
end
|
|
330
610
|
end
|
|
331
611
|
|
|
332
612
|
# The first candidate spelling that names a class this run declares or the RBS environment knows, or
|
|
333
|
-
#
|
|
334
|
-
|
|
613
|
+
# the token itself when none does — an unresolvable superclass is #735's business, not this pass's.
|
|
614
|
+
#
|
|
615
|
+
# `full` is the class whose header carries the token. Ruby evaluates the superclass expression before the
|
|
616
|
+
# new constant exists, so the innermost candidate `full` itself (`module A; module C; class B < B`, where
|
|
617
|
+
# `A::C::B` is the class being opened) is not yet a candidate: the lookup falls through to `A::B` (#2579).
|
|
618
|
+
def resolve_superclass_token(token, nesting, known, full)
|
|
619
|
+
# A rooted token (`::Hash`) already names one class; the nesting must not capture it (#2459).
|
|
620
|
+
return token if token.start_with?("::")
|
|
621
|
+
|
|
335
622
|
base = token.sub(/\[.*\]\z/, "")
|
|
336
|
-
args = token[base.length..] || ""
|
|
337
623
|
nesting.length.downto(1) do |i|
|
|
338
624
|
candidate = "#{nesting[0, i].join('::')}::#{base}"
|
|
339
|
-
|
|
340
|
-
|
|
625
|
+
next if candidate == full
|
|
626
|
+
|
|
627
|
+
# The arguments {#record_superclass} computed belong to the top-level class of the bare name, so they are
|
|
628
|
+
# computed again for the class the token resolves to: `Hash` inside a `module Foo` declaring a plain
|
|
629
|
+
# `Foo::Hash` takes none (#2459).
|
|
630
|
+
return generic_superclass_spelling(candidate) if known.include?(candidate) ||
|
|
631
|
+
Reflection.rbs_class_known?(candidate,
|
|
632
|
+
environment: @environment)
|
|
341
633
|
end
|
|
342
634
|
token
|
|
343
635
|
end
|
|
@@ -370,28 +662,96 @@ module Rigor
|
|
|
370
662
|
# ran — source discovery and the plugins still type it. Emitting the class with the superclass merely
|
|
371
663
|
# OMITTED would be the harmful middle: the class becomes RBS-known with only the emitted methods, so
|
|
372
664
|
# every inherited call fires `call.undefined-method`.
|
|
665
|
+
#
|
|
666
|
+
# Demoting a class can leave a file with nothing to write, which takes the class shells riding on it off the
|
|
667
|
+
# emitted set, so the pass repeats until no further class becomes unresolvable. The emitted set only shrinks,
|
|
668
|
+
# so the unresolvable set only grows and the loop ends.
|
|
373
669
|
def demote_unresolvable_superclasses(candidates)
|
|
374
670
|
superclasses = candidates.each_with_object({}) do |candidate, acc|
|
|
375
671
|
(candidate.class_superclasses || {}).each { |name, sup| acc[name] = sup }
|
|
376
672
|
end
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
673
|
+
current = candidates
|
|
674
|
+
unresolvable = {}
|
|
675
|
+
loop do
|
|
676
|
+
found = unresolvable_superclass_roots(superclasses, declared_by_run(current))
|
|
677
|
+
return current if found.size == unresolvable.size
|
|
678
|
+
|
|
679
|
+
unresolvable = found
|
|
680
|
+
current = candidates.map do |candidate|
|
|
681
|
+
owner = unresolvable_for(candidate.class_name, unresolvable)
|
|
682
|
+
candidate = record_unresolvable_superclass(candidate, owner) if owner
|
|
683
|
+
drop_unresolvable_shells(candidate, unresolvable)
|
|
684
|
+
end
|
|
685
|
+
end
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
# Issue #2715 — a nested declaration's class shell rides on a surviving sibling's candidate (`class QueryColumn`
|
|
689
|
+
# beside `class Query < BaseRecord` wrapping `class QueryError`), so demoting the container's own candidates
|
|
690
|
+
# leaves the shell, and the writer renders the container around it with the unresolvable superclass: the next
|
|
691
|
+
# `check` fails `RBS::NoSuperclassFoundError`. A shell inside a demoted class goes with it, like a demoted
|
|
692
|
+
# class's own members, and a demoted candidate sheds them too: a shell left on one would still count as emitted
|
|
693
|
+
# ({#declared_by_run}), so a written subclass of the dropped nested class would fail its build.
|
|
694
|
+
# Writing the container bare instead would make it RBS-known without its ancestry, which
|
|
695
|
+
# a later run (or a path-subset run) would take for a resolvable superclass of its subclasses.
|
|
696
|
+
def drop_unresolvable_shells(candidate, unresolvable)
|
|
697
|
+
shells = (candidate.class_shells || []).to_a
|
|
698
|
+
kept = shells.reject do |name|
|
|
699
|
+
owner = unresolvable_for(name, unresolvable)
|
|
700
|
+
@unresolvable_superclasses[owner.first] ||= owner.last if owner
|
|
701
|
+
owner
|
|
382
702
|
end
|
|
383
|
-
|
|
703
|
+
kept.size == shells.size ? candidate : candidate.with_class_shells(kept)
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
def unresolvable_superclass_roots(superclasses, emitted)
|
|
707
|
+
superclasses.each_with_object({}) do |(name, _sup), acc|
|
|
384
708
|
root = unresolved_superclass_root(name, superclasses, emitted)
|
|
385
709
|
acc[name] = root if root
|
|
386
710
|
end
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
candidates.map do |candidate|
|
|
390
|
-
owner = unresolvable_for(candidate.class_name, unresolvable)
|
|
391
|
-
next candidate if owner.nil?
|
|
711
|
+
end
|
|
392
712
|
|
|
393
|
-
|
|
713
|
+
# Everything this run puts on disk, superclass or not: a chain that reaches `class Record` — emitted here, with
|
|
714
|
+
# no superclass of its own — terminates in a class the next run WILL know. Only what the writer is sure to
|
|
715
|
+
# write counts, because a subclass written against a class nothing declares fails its build
|
|
716
|
+
# (`RBS::NoSuperclassFoundError`) on the next run:
|
|
717
|
+
#
|
|
718
|
+
# - a class with a candidate the writer is sure to write ({#written?}). A class whose every method is skipped
|
|
719
|
+
# is not written, so it does not count (#2437); a class shell on it would close it instead (#2438);
|
|
720
|
+
# - a class shell on a file with such a candidate: a shell on a file whose every method is skipped, already
|
|
721
|
+
# declared by `sig/`, or a tighter return the writer will not apply never reaches disk (#2454);
|
|
722
|
+
# - the wrapper namespaces of either, which the writer renders around them with their recorded superclasses.
|
|
723
|
+
#
|
|
724
|
+
# Counting too few only demotes a subclass, the output before #2454 (ADR-5).
|
|
725
|
+
def declared_by_run(candidates)
|
|
726
|
+
written = candidates.select { |candidate| written?(candidate) }
|
|
727
|
+
written_paths = written.to_set(&:path)
|
|
728
|
+
declared = Set.new
|
|
729
|
+
written.each { |candidate| declared << candidate.class_name if candidate.class_name }
|
|
730
|
+
candidates.each do |candidate|
|
|
731
|
+
declared.merge(candidate.class_shells || []) if written_paths.include?(candidate.path)
|
|
394
732
|
end
|
|
733
|
+
declared.flat_map { |name| wrapper_names(name) }.to_set | declared
|
|
734
|
+
end
|
|
735
|
+
|
|
736
|
+
# `A::B::C` is written inside `A` and `A::B`.
|
|
737
|
+
def wrapper_names(name)
|
|
738
|
+
segments = name.split("::")
|
|
739
|
+
(1...segments.size).map { |n| segments.first(n).join("::") }
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
# Whether the writer will put `candidate` on disk: a candidate it is sure to write ({Writer.writes?}), and,
|
|
743
|
+
# when the run writes or compares files (`--write`, `--check`), aimed at a target that writer will write to
|
|
744
|
+
# (#2461). Print, diff and JSON runs have no writer: where a file would go depends on the working directory
|
|
745
|
+
# the writer takes its sig root from, and their output must not.
|
|
746
|
+
def written?(candidate)
|
|
747
|
+
return false unless Writer.writes?(candidate)
|
|
748
|
+
return true if @target_writer.nil?
|
|
749
|
+
|
|
750
|
+
target = @target_writer.path_mapper.target_for(candidate.path, class_name: candidate.class_name,
|
|
751
|
+
method_name: candidate.method_name,
|
|
752
|
+
kind: candidate.rbs_kind)
|
|
753
|
+
@writable_targets[target] = @target_writer.accepts_target?(target) unless @writable_targets.key?(target)
|
|
754
|
+
@writable_targets[target]
|
|
395
755
|
end
|
|
396
756
|
|
|
397
757
|
# A nested class or module goes with its wrapper. `WikiPage::Webhookable`'s declaration is emitted
|
|
@@ -417,9 +777,9 @@ module Rigor
|
|
|
417
777
|
return superclass if seen[name]
|
|
418
778
|
|
|
419
779
|
seen[name] = true
|
|
420
|
-
# The recorded spelling may carry the type arguments {#generic_superclass_spelling} added
|
|
421
|
-
# resolvability question is about the class, so it is asked of the bare name.
|
|
422
|
-
base = superclass
|
|
780
|
+
# The recorded spelling may carry the type arguments {#generic_superclass_spelling} added, and a rooted
|
|
781
|
+
# token its `::` (#2459); the resolvability question is about the class, so it is asked of the bare name.
|
|
782
|
+
base = bare_superclass_name(superclass)
|
|
423
783
|
return nil if Reflection.rbs_class_known?(base, environment: @environment)
|
|
424
784
|
return superclass unless emitted.include?(base)
|
|
425
785
|
|
|
@@ -460,30 +820,56 @@ module Rigor
|
|
|
460
820
|
end.uniq
|
|
461
821
|
end
|
|
462
822
|
|
|
463
|
-
# #1154 — the project's discovery tables,
|
|
464
|
-
#
|
|
465
|
-
#
|
|
466
|
-
#
|
|
467
|
-
|
|
468
|
-
|
|
823
|
+
# #1154 / #2300 — the project's discovery tables, built once per run over the configured project paths as well
|
|
824
|
+
# as the files sig-gen reads, so a run over one file reads what a `check` of it reads. Every file is indexed
|
|
825
|
+
# over them ({#file_base_scope}): a helper, an ancestor or a constant another file defines resolves as it does
|
|
826
|
+
# under `check` instead of reading ABSENT. The declared-parameter ivar credit reads them whole
|
|
827
|
+
# ({#ivar_writer_scope}).
|
|
828
|
+
def project_tables
|
|
829
|
+
@project_tables ||= Inference::ProjectSeedTables.for_paths(
|
|
469
830
|
((@resolved_paths || []) + resolve_paths(Array(@configuration.paths))).uniq { |path| File.expand_path(path) }
|
|
470
831
|
)
|
|
471
|
-
|
|
832
|
+
end
|
|
833
|
+
|
|
834
|
+
# The member layouts stay per-file. sig-gen writes a value class for every `Data.define` / `Struct.new` layout
|
|
835
|
+
# the indexed scope carries ({#collect_meta_layouts}), so a seeded layout would re-emit every value class of the
|
|
836
|
+
# project in every file.
|
|
837
|
+
PER_FILE_TABLES = %i[data_member_layouts struct_member_layouts].freeze
|
|
838
|
+
private_constant :PER_FILE_TABLES
|
|
839
|
+
|
|
840
|
+
# #1154 — the declared-parameter ivar credit must see the ivar writers in other files (an ancestor, another
|
|
841
|
+
# object's `instance_variable_set`, a reopening) as `rigor check` does.
|
|
842
|
+
def ivar_writer_scope(base_scope)
|
|
843
|
+
base_scope.with_discovery(base_scope.discovery.with(**project_tables))
|
|
472
844
|
end
|
|
473
845
|
|
|
474
846
|
# #1406 — sig-gen keeps the census's own answer: a declared ivar type read back would echo the declaration.
|
|
847
|
+
# #2489 — `base_scope` names the file, so the class-ivar census joins the writes another file's reopening and
|
|
848
|
+
# the class's ancestors make ({Inference::ScopeIndexer::IvarContributions}), as `rigor check`'s does.
|
|
475
849
|
def index_file(root, base_scope)
|
|
476
850
|
Inference::ScopeIndexer.index(
|
|
477
851
|
root, default_scope: base_scope, ivar_writer_scope: ivar_writer_scope(base_scope), declared_ivar_types: false
|
|
478
852
|
)
|
|
479
853
|
end
|
|
480
854
|
|
|
855
|
+
# The scope each file is indexed under: the project's tables less the per-file ones ({#project_tables}), and the
|
|
856
|
+
# run's return-memo token (see {#run}), which {#ivar_writer_scope} inherits, so every reading of a callee shares
|
|
857
|
+
# one memo bucket. Built once per run.
|
|
858
|
+
def file_base_scope(environment)
|
|
859
|
+
@file_base_scope ||= begin
|
|
860
|
+
base = Scope.empty(environment: environment)
|
|
861
|
+
tables = project_tables.except(*PER_FILE_TABLES)
|
|
862
|
+
tables = tables.merge(run_generation: @run_generation) if @run_generation
|
|
863
|
+
base.with_discovery(base.discovery.with(**tables))
|
|
864
|
+
end
|
|
865
|
+
end
|
|
866
|
+
|
|
481
867
|
def analyse_file(path, environment)
|
|
482
868
|
source = File.read(path)
|
|
483
869
|
parse_result = Prism.parse(source, filepath: path, version: @configuration.target_ruby)
|
|
484
870
|
return [] if parse_result.errors.any?
|
|
485
871
|
|
|
486
|
-
scope_index = index_file(parse_result.value,
|
|
872
|
+
scope_index = index_file(parse_result.value, file_base_scope(environment).with_source_path(path))
|
|
487
873
|
|
|
488
874
|
reset_file_maps
|
|
489
875
|
@meta_layouts = collect_meta_layouts(scope_index)
|
|
@@ -518,11 +904,12 @@ module Rigor
|
|
|
518
904
|
#
|
|
519
905
|
# Only a definer whose statement certainly runs can be last: a direct statement of a `class`, `module` or
|
|
520
906
|
# `class << self` body, of a `Data.define` / `Struct.new` block or of the file ({#walk_statements} records
|
|
521
|
-
# them)
|
|
522
|
-
#
|
|
907
|
+
# them), or a `def` passed to `private`, `public` or `protected` in such a statement ({#walk_statements}, #2254).
|
|
908
|
+
# One under a condition, in a block or a lambda, or after an uncertain `module_function` reset may not run, as
|
|
909
|
+
# far as this reading tells. A slot such a definer could
|
|
523
910
|
# still take over from the last certain one is contested, and nothing is written for it.
|
|
524
911
|
#
|
|
525
|
-
# @return
|
|
912
|
+
# @return the definers of each slot as `[offset, certain]` pairs, keyed by `[class_name, kind, method_name]`.
|
|
526
913
|
MEMBER_OFFSET = -1
|
|
527
914
|
private_constant :MEMBER_OFFSET
|
|
528
915
|
|
|
@@ -635,6 +1022,8 @@ module Rigor
|
|
|
635
1022
|
@in_effect_refinements = nil
|
|
636
1023
|
out = []
|
|
637
1024
|
@block_depth = 0
|
|
1025
|
+
@hook_blocks = {}.compare_by_identity
|
|
1026
|
+
@hook_block_prefixes = []
|
|
638
1027
|
@certain_bodies = Set.new.compare_by_identity
|
|
639
1028
|
@certain_definers = Set.new.compare_by_identity
|
|
640
1029
|
mark_certain_body(root.statements)
|
|
@@ -649,7 +1038,9 @@ module Rigor
|
|
|
649
1038
|
when Prism::ClassNode, Prism::ModuleNode
|
|
650
1039
|
return if descend_into_namespace?(node, prefix, out)
|
|
651
1040
|
when Prism::SingletonClassNode
|
|
652
|
-
|
|
1041
|
+
# `class << M` inside `module M` opens the same singleton as `class << self` (#3155).
|
|
1042
|
+
if node.body && (node.expression.is_a?(Prism::SelfNode) ||
|
|
1043
|
+
Inference::ScopeIndexer.def_receiver_targets_lexical_self?(node.expression, prefix))
|
|
653
1044
|
mark_certain_body(node.body)
|
|
654
1045
|
walk_defs(node.body, prefix, true, false, out)
|
|
655
1046
|
return
|
|
@@ -677,16 +1068,37 @@ module Rigor
|
|
|
677
1068
|
# the `self` one returns above): it defines a singleton method on `obj`, not a private `Object` method.
|
|
678
1069
|
def walk_def_children(node, prefix, in_singleton_class, module_function_active, out)
|
|
679
1070
|
return if refine_block_call?(node)
|
|
1071
|
+
# #2259 — a `def` in `class_methods do … end` (ActiveSupport::Concern) defines a method of the including class's
|
|
1072
|
+
# singleton, not of this module. Omitted, not written class-side: no precedent places it on the singleton.
|
|
1073
|
+
return if class_methods_block?(node)
|
|
680
1074
|
|
|
681
1075
|
in_block = node.is_a?(Prism::BlockNode) || node.is_a?(Prism::LambdaNode) ||
|
|
682
1076
|
node.is_a?(Prism::SingletonClassNode)
|
|
683
1077
|
@block_depth += 1 if in_block
|
|
1078
|
+
hook_block = @hook_blocks.key?(node)
|
|
1079
|
+
@hook_block_prefixes.push(prefix) if hook_block
|
|
684
1080
|
node.rigor_each_child do |child|
|
|
685
1081
|
walk_defs(child, prefix, in_singleton_class, module_function_active, out)
|
|
686
1082
|
end
|
|
1083
|
+
@hook_block_prefixes.pop if hook_block
|
|
687
1084
|
@block_depth -= 1 if in_block
|
|
688
1085
|
end
|
|
689
1086
|
|
|
1087
|
+
# Issue #3015 — the `included do … end` / `prepended do … end` statements of a body that runs `extend
|
|
1088
|
+
# ActiveSupport::Concern`: Ruby runs the block in the includer's class body, so a `def self.x` or `class << self`
|
|
1089
|
+
# def there is the includer's singleton method, not the module's, and {#collect_def_node} leaves it out.
|
|
1090
|
+
def mark_hook_blocks(namespace_node, prefix)
|
|
1091
|
+
body = namespace_node.body
|
|
1092
|
+
return unless namespace_node.is_a?(Prism::ModuleNode) && Inference::ScopeIndexer.concern_body?(body, prefix)
|
|
1093
|
+
|
|
1094
|
+
body.body.each do |statement|
|
|
1095
|
+
next unless statement.is_a?(Prism::CallNode) && statement.receiver.nil? &&
|
|
1096
|
+
%i[included prepended].include?(statement.name) && statement.block.is_a?(Prism::BlockNode)
|
|
1097
|
+
|
|
1098
|
+
@hook_blocks[statement] = true
|
|
1099
|
+
end
|
|
1100
|
+
end
|
|
1101
|
+
|
|
690
1102
|
# [#722](https://github.com/rigortype/rigor/issues/722) residue 3 — the qualified name is built by
|
|
691
1103
|
# `Source::ConstantPath.declaration_prefix`, the same reader the engine's declaration walks use, so a
|
|
692
1104
|
# ROOTED header re-anchors here too. Appending the rendered name to the enclosing prefix wrote
|
|
@@ -706,6 +1118,7 @@ module Rigor
|
|
|
706
1118
|
|
|
707
1119
|
full = child_prefix.join("::")
|
|
708
1120
|
@namespace_kinds[full] = node.is_a?(Prism::ClassNode) ? :class : :module
|
|
1121
|
+
@source_classes << full if node.is_a?(Prism::ClassNode)
|
|
709
1122
|
record_superclass(node, full, prefix)
|
|
710
1123
|
walk_namespace_body(node, child_prefix, out)
|
|
711
1124
|
true
|
|
@@ -725,6 +1138,17 @@ module Rigor
|
|
|
725
1138
|
!@in_effect_refinements.class_body_refine?(node)
|
|
726
1139
|
end
|
|
727
1140
|
|
|
1141
|
+
# A block call whose members are not this class body's: a `refine` block (#1763), and a `class_methods` block
|
|
1142
|
+
# (#2259), whose members belong to the including class's singleton.
|
|
1143
|
+
def skipped_block_call?(node)
|
|
1144
|
+
refine_block_call?(node) || class_methods_block?(node)
|
|
1145
|
+
end
|
|
1146
|
+
|
|
1147
|
+
def class_methods_block?(node)
|
|
1148
|
+
node.is_a?(Prism::CallNode) && node.receiver.nil? && node.name == :class_methods &&
|
|
1149
|
+
node.block.is_a?(Prism::BlockNode)
|
|
1150
|
+
end
|
|
1151
|
+
|
|
728
1152
|
def declined_self_header?(node)
|
|
729
1153
|
path = node.constant_path
|
|
730
1154
|
path.is_a?(Prism::ConstantPathNode) && path.parent.is_a?(Prism::SelfNode) &&
|
|
@@ -750,10 +1174,43 @@ module Rigor
|
|
|
750
1174
|
# the name it resolves to may be a class another file declares. Resolved once every file has been
|
|
751
1175
|
# walked, in {#resolve_superclass_spellings}.
|
|
752
1176
|
spelling = generic_superclass_spelling(superclass)
|
|
1177
|
+
# Issue #2579 — a token that names the class being opened (`class A::B < A::B`, or a top-level `class B < B`
|
|
1178
|
+
# with nothing else of that name) would write a self-superclass, which RBS rejects as a recursive ancestor.
|
|
1179
|
+
# No superclass is safer than that, so the declaration is written without one.
|
|
1180
|
+
return if bare_superclass_name(spelling) == full
|
|
1181
|
+
|
|
753
1182
|
# The token rides along because `@class_superclasses` is per-FILE scratch, reset before the next
|
|
754
1183
|
# file, while this table is whole-run.
|
|
755
1184
|
@superclass_nestings[full] = [prefix.dup, spelling]
|
|
756
1185
|
@class_superclasses[full] = spelling
|
|
1186
|
+
register_core_subclass_shell(full, prefix, superclass)
|
|
1187
|
+
end
|
|
1188
|
+
|
|
1189
|
+
# Issue #2224 — a subclass of a core or stdlib class got a declaration only when it had a method of its own to
|
|
1190
|
+
# emit, so after `class MyHash < Hash; end` a method returning one was written `-> MyHash` against a class no
|
|
1191
|
+
# signature declared, and `rbs validate` failed with `NoTypeFoundError`. As a class shell it is declared
|
|
1192
|
+
# `class MyHash < ::Hash[untyped, untyped]` beside the file's other declarations. A class the RBS environment
|
|
1193
|
+
# already declares (the project's `sig/`, an earlier run) is left to that declaration, and a superclass the
|
|
1194
|
+
# header's nesting shadows is not core's: `Hash` inside a `module Foo` that declares `Foo::Hash` is the project's.
|
|
1195
|
+
# A rooted `::Hash` names core's class whatever the nesting declares, so the shadow cannot capture it (#2459).
|
|
1196
|
+
def register_core_subclass_shell(full, prefix, superclass)
|
|
1197
|
+
return if class_known?(full)
|
|
1198
|
+
|
|
1199
|
+
bare = bare_superclass_name(superclass)
|
|
1200
|
+
return unless @environment&.rbs_loader&.core_or_stdlib_class?(bare)
|
|
1201
|
+
|
|
1202
|
+
unless superclass.start_with?("::")
|
|
1203
|
+
namespaces = TypeNameQualification.enclosing_namespaces(prefix.join("::"))
|
|
1204
|
+
return if TypeNameQualification.shadowed?(bare.split("::").first, namespaces, method(:declared_constant?))
|
|
1205
|
+
end
|
|
1206
|
+
|
|
1207
|
+
@class_shells << full
|
|
1208
|
+
end
|
|
1209
|
+
|
|
1210
|
+
# The class a recorded superclass token names, as the fully qualified key every class-name set and the RBS
|
|
1211
|
+
# loader use: the type arguments {#generic_superclass_spelling} added and a rooted token's `::` both dropped.
|
|
1212
|
+
def bare_superclass_name(token)
|
|
1213
|
+
token.sub(/\[.*\]\z/, "").delete_prefix("::")
|
|
757
1214
|
end
|
|
758
1215
|
|
|
759
1216
|
# Issue #735 — a GENERIC superclass has to be written with its arguments. `class Entries < Array`
|
|
@@ -766,7 +1223,7 @@ module Rigor
|
|
|
766
1223
|
loader = @environment&.rbs_loader
|
|
767
1224
|
return superclass if loader.nil?
|
|
768
1225
|
|
|
769
|
-
arity = loader.class_type_param_names(superclass).size
|
|
1226
|
+
arity = loader.class_type_param_names(bare_superclass_name(superclass)).size
|
|
770
1227
|
return superclass if arity.zero?
|
|
771
1228
|
|
|
772
1229
|
"#{superclass}[#{(['untyped'] * arity).join(', ')}]"
|
|
@@ -814,6 +1271,7 @@ module Rigor
|
|
|
814
1271
|
@meta_layouts.each do |class_name, layout|
|
|
815
1272
|
@namespace_kinds[class_name] = :class
|
|
816
1273
|
@class_shells << class_name
|
|
1274
|
+
@source_classes << class_name
|
|
817
1275
|
@class_superclasses[class_name] = MetaClassShape::SUPERCLASSES.fetch(layout.kind)
|
|
818
1276
|
end
|
|
819
1277
|
end
|
|
@@ -840,6 +1298,7 @@ module Rigor
|
|
|
840
1298
|
return if namespace_node.body.nil?
|
|
841
1299
|
|
|
842
1300
|
mark_certain_body(namespace_node.body)
|
|
1301
|
+
mark_hook_blocks(namespace_node, prefix)
|
|
843
1302
|
walk_defs(namespace_node.body, prefix, false, false, out)
|
|
844
1303
|
end
|
|
845
1304
|
|
|
@@ -862,6 +1321,8 @@ module Rigor
|
|
|
862
1321
|
@certain_definers << stmt
|
|
863
1322
|
# A `begin … end` statement's body (and `ensure`) runs whenever the statement does.
|
|
864
1323
|
mark_certain_body(stmt) if stmt.is_a?(Prism::BeginNode)
|
|
1324
|
+
# A `def` passed to a visibility call (`private def x`, #2254) runs with its statement.
|
|
1325
|
+
@certain_definers.merge(Inference::ModuleFunctionState.visibility_call_defs(stmt))
|
|
865
1326
|
end
|
|
866
1327
|
walk_defs(stmt, prefix, in_singleton_class, active, out)
|
|
867
1328
|
end
|
|
@@ -876,7 +1337,12 @@ module Rigor
|
|
|
876
1337
|
return
|
|
877
1338
|
end
|
|
878
1339
|
|
|
879
|
-
|
|
1340
|
+
# `def Foo.x` inside `module Foo` is `def self.x` (#2950), and a def on another object's singleton is no method
|
|
1341
|
+
# of this class (#3155); the rule is the one check indexes defs by.
|
|
1342
|
+
kind = Inference::ScopeIndexer.def_side(node, prefix, in_singleton_class)
|
|
1343
|
+
return if kind.nil?
|
|
1344
|
+
return if kind == :singleton && @hook_block_prefixes.last == prefix
|
|
1345
|
+
|
|
880
1346
|
class_name = prefix.join("::")
|
|
881
1347
|
if Inference::ModuleFunctionState.sig_gen_module_function?(module_function_active, kind)
|
|
882
1348
|
@module_function_methods << node
|
|
@@ -959,8 +1425,8 @@ module Rigor
|
|
|
959
1425
|
# `Object#initialize: () -> void`. The mismatch surfaces a `Ruby::MethodParameterMismatch` warning even
|
|
960
1426
|
# when `rigor check` itself is clean.
|
|
961
1427
|
#
|
|
962
|
-
#
|
|
963
|
-
#
|
|
1428
|
+
# A default constructor is skipped (`classify_def` returns nil for it); a non-trivial one is not, and
|
|
1429
|
+
# {#initialize_stub_candidate} emits a permissive `(<param shape>) -> void` stub matching the runtime
|
|
964
1430
|
# parameter list.
|
|
965
1431
|
def initialize_excludes?(def_node, kind)
|
|
966
1432
|
return false unless kind == :instance
|
|
@@ -1088,25 +1554,36 @@ module Rigor
|
|
|
1088
1554
|
# return position it is not parenthesised.
|
|
1089
1555
|
def positional_type(observations, index, prefix, owner = nil)
|
|
1090
1556
|
types = observations.filter_map { |obs| obs.positional[index] }
|
|
1091
|
-
"#{prefix}#{union_erase(types, owner: owner)}"
|
|
1557
|
+
"#{prefix}#{union_erase(widen_observed_params(types), owner: owner)}"
|
|
1092
1558
|
end
|
|
1093
1559
|
|
|
1094
1560
|
def render_keyword_param(keyword, observations, owner = nil)
|
|
1095
1561
|
optional_marker = keyword.is_a?(Prism::OptionalKeywordParameterNode) ? "?" : ""
|
|
1096
1562
|
types = observations.filter_map { |obs| obs.keyword[keyword.name] }
|
|
1097
|
-
rendered = types.empty? ? "untyped" : paren_wrap_union(union_erase(types, owner: owner))
|
|
1563
|
+
rendered = types.empty? ? "untyped" : paren_wrap_union(union_erase(widen_observed_params(types), owner: owner))
|
|
1098
1564
|
"#{optional_marker}#{keyword.name}: #{rendered}"
|
|
1099
1565
|
end
|
|
1100
1566
|
|
|
1567
|
+
# #2254 — an observed argument is one call's value, so a literal (`"hello"`) proposes its class (`String`), as
|
|
1568
|
+
# {#widen_value_literals} does for a return. Widening always: a parameter the body only compares against a
|
|
1569
|
+
# literal (a discriminant) is widened too, which loses precision and never adds a false positive.
|
|
1570
|
+
def widen_observed_params(types)
|
|
1571
|
+
types.map { |type| widen_value_literals(type) }
|
|
1572
|
+
end
|
|
1573
|
+
|
|
1574
|
+
# Issue #2459 — a ROOTED path (`::Hash`, `::A::B`) keeps its leading `::`: it names exactly one class, and the
|
|
1575
|
+
# passes after {#record_superclass} must not re-resolve it against the header's nesting, where a project
|
|
1576
|
+
# `Foo::Hash` would capture it. Every pass that compares the token against class names strips the anchor.
|
|
1101
1577
|
def qualified_constant_path(constant_path)
|
|
1102
1578
|
case constant_path
|
|
1103
1579
|
when Prism::ConstantReadNode
|
|
1104
1580
|
constant_path.name.to_s
|
|
1105
1581
|
when Prism::ConstantPathNode
|
|
1106
|
-
parent = qualified_constant_path(constant_path.parent) if constant_path.parent
|
|
1107
1582
|
name = constant_path.name&.to_s
|
|
1108
1583
|
return nil if name.nil?
|
|
1584
|
+
return "::#{name}" if constant_path.parent.nil?
|
|
1109
1585
|
|
|
1586
|
+
parent = qualified_constant_path(constant_path.parent)
|
|
1110
1587
|
parent ? "#{parent}::#{name}" : name
|
|
1111
1588
|
end
|
|
1112
1589
|
end
|
|
@@ -1485,7 +1962,7 @@ module Rigor
|
|
|
1485
1962
|
return equivalent(path, def_node, class_name, kind, inferred, declared_rbs)
|
|
1486
1963
|
end
|
|
1487
1964
|
|
|
1488
|
-
unless tighter?(declared, inferred) && !literal_decline?(inferred, def_node)
|
|
1965
|
+
unless tighter?(declared, inferred) && !literal_decline?(inferred, def_node, declared)
|
|
1489
1966
|
return equivalent(path, def_node, class_name, kind, inferred, declared_rbs)
|
|
1490
1967
|
end
|
|
1491
1968
|
|
|
@@ -1520,16 +1997,16 @@ module Rigor
|
|
|
1520
1997
|
# about the return. The class's own declaration is kept whole instead: its visibility, the overload's
|
|
1521
1998
|
# annotations, type parameters, parameters and block, with only the return replaced.
|
|
1522
1999
|
#
|
|
1523
|
-
#
|
|
2000
|
+
# Two shapes are declined (`nil`):
|
|
1524
2001
|
# - several overloads. The body is typed once, so the inferred return covers every overload together and
|
|
1525
2002
|
# says nothing about which overload returns what: collapsing them into one line would drop the
|
|
1526
2003
|
# overloads, and giving each the joint return would widen the overloads the author wrote narrower.
|
|
1527
2004
|
# - a name the class's own `sig/` spells as an `alias`. Its parameters are the aliased method's, and a
|
|
1528
2005
|
# proposal would read as a rewrite of that method's line rather than of the alias. An alias only an
|
|
1529
2006
|
# ancestor declares takes the override path below, like any other ancestor declaration.
|
|
1530
|
-
#
|
|
1531
|
-
#
|
|
1532
|
-
#
|
|
2007
|
+
# A method this class does not declare is not declined: when only an ancestor declares it, the proposal is a
|
|
2008
|
+
# new override on this class, rendered from the `def`'s own shape like any new method. The ancestor's
|
|
2009
|
+
# parameters describe the ancestor's `def`, not this one, which may take other arguments (ADR-14 clause 2).
|
|
1533
2010
|
def tighter_return_line(def_node, method_def, class_name, kind, inferred)
|
|
1534
2011
|
member = signature_member(method_def, class_name)
|
|
1535
2012
|
return render_rbs_line(def_node, inferred, class_name, kind) if member.nil?
|
|
@@ -1774,11 +2251,24 @@ module Rigor
|
|
|
1774
2251
|
|
|
1775
2252
|
def structurally_covers?(inferred_member, declared_member)
|
|
1776
2253
|
return true if inferred_member == declared_member
|
|
2254
|
+
return true if nominal_subclass_of?(inferred_member, declared_member)
|
|
1777
2255
|
|
|
1778
2256
|
result = inferred_member.accepts(declared_member)
|
|
1779
2257
|
result.respond_to?(:yes?) && result.yes?
|
|
1780
2258
|
end
|
|
1781
2259
|
|
|
2260
|
+
# Issue #2914 — a proposed member that is a subclass of the declared one keeps that member: declared
|
|
2261
|
+
# `Prism::Node?` against an inferred `Prism::DefNode?` narrows what a caller receives without dropping a
|
|
2262
|
+
# branch the caller could observe, unlike a lost `nil` or a lost alternate. Only a bare nominal pair whose
|
|
2263
|
+
# ordering the analyzer's class data decides (`Environment#class_ordering`, RBS hierarchy included) is
|
|
2264
|
+
# read this way; a generic pair or an `:unknown` ordering stays uncovered, so the guard still declines.
|
|
2265
|
+
def nominal_subclass_of?(inferred_member, declared_member)
|
|
2266
|
+
return false unless inferred_member.is_a?(Type::Nominal) && declared_member.is_a?(Type::Nominal)
|
|
2267
|
+
return false unless inferred_member.type_args.empty? && declared_member.type_args.empty?
|
|
2268
|
+
|
|
2269
|
+
@environment&.class_ordering(inferred_member.class_name, declared_member.class_name) == :subclass
|
|
2270
|
+
end
|
|
2271
|
+
|
|
1782
2272
|
GENERIC_COLLECTION_CLASSES = %w[
|
|
1783
2273
|
Array Hash Set Range Enumerable Enumerator Enumerator::Lazy
|
|
1784
2274
|
].freeze
|
|
@@ -1794,8 +2284,8 @@ module Rigor
|
|
|
1794
2284
|
# The two reasons a `Type::Constant` the body proves is not the method's contract. Both say the same
|
|
1795
2285
|
# thing from opposite ends: the precision is real about this body and says nothing about the promise
|
|
1796
2286
|
# the method makes to its callers.
|
|
1797
|
-
def literal_decline?(inferred, def_node)
|
|
1798
|
-
computed_literal_tightening?(inferred, def_node) || pins_literal_over_declaration?(inferred)
|
|
2287
|
+
def literal_decline?(inferred, def_node, declared)
|
|
2288
|
+
computed_literal_tightening?(inferred, def_node) || pins_literal_over_declaration?(inferred, declared)
|
|
1799
2289
|
end
|
|
1800
2290
|
|
|
1801
2291
|
# Issue #837 — a proposal that erases to an RBS LITERAL type never tightens an existing declaration.
|
|
@@ -1820,9 +2310,14 @@ module Rigor
|
|
|
1820
2310
|
# is refused when ANY member is a literal: a mixed `("a" | Float)` pins `"a"` just as a bare one does.
|
|
1821
2311
|
# A literal nested inside a shape carrier (`[1, 2]`, a `HashShape` value) is a different question,
|
|
1822
2312
|
# already answered for the collection declarations that matter by {#narrows_collection_to_shape?}.
|
|
1823
|
-
|
|
2313
|
+
#
|
|
2314
|
+
# A literal the declaration itself spells is not pinned over it (#2914): declared `Prism::Node?` against
|
|
2315
|
+
# a proposed `Prism::DefNode?` carries the same `nil` member the author wrote, so only the narrowed
|
|
2316
|
+
# nominal arm is new. A literal the declaration does not spell is refused as before.
|
|
2317
|
+
def pins_literal_over_declaration?(inferred, declared)
|
|
1824
2318
|
members = inferred.is_a?(Type::Union) ? inferred.members : [inferred]
|
|
1825
|
-
|
|
2319
|
+
declared_members = declared.is_a?(Type::Union) ? declared.members : [declared]
|
|
2320
|
+
members.any? { |member| rbs_literal?(member) && !declared_members.include?(member) }
|
|
1826
2321
|
end
|
|
1827
2322
|
|
|
1828
2323
|
# `Type::Constant#erase_to_rbs` spells the values RBS writes as literal types (`true` / `false` /
|
|
@@ -1912,8 +2407,35 @@ module Rigor
|
|
|
1912
2407
|
# `owner:` is the class or module the rendered member belongs to. {AliasIndex} needs it to judge whether
|
|
1913
2408
|
# a matching alias is one this reader would expect to see; a call site that cannot supply it renders the
|
|
1914
2409
|
# type unfolded, which is the same output as before #1002.
|
|
2410
|
+
#
|
|
2411
|
+
# Issue #2230 — qualified last, after the alias fold has matched the unqualified spelling it keys on, so a core
|
|
2412
|
+
# name a namespace of the owner shadows (`Set` inside a `module Foo` that declares `Foo::Set`) is written `::Set`.
|
|
1915
2413
|
def elaborated_rbs(type, owner: nil)
|
|
1916
|
-
alias_index.fold(TypeElaborator.elaborate(type, environment: @environment).erase_to_rbs, owner)
|
|
2414
|
+
folded = alias_index.fold(TypeElaborator.elaborate(type, environment: @environment).erase_to_rbs, owner)
|
|
2415
|
+
captured = owner_type_param_names(owner)
|
|
2416
|
+
TypeNameQualification.qualify(folded, owner) { |name| captured.include?(name) || declared_constant?(name) }
|
|
2417
|
+
end
|
|
2418
|
+
|
|
2419
|
+
# Issue #2436 — inside `class Array[unchecked out E]` a bare `E` names the type variable, not a class `::E`,
|
|
2420
|
+
# so a class whose spelling equals one of the owner's declared type-parameter names is anchored. The names
|
|
2421
|
+
# come as `"Array::E"`, the form {TypeNameQualification.qualify} asks about for the owner's own namespace;
|
|
2422
|
+
# a namespace the owner is nested in does not bind its parameters inside the owner's declaration.
|
|
2423
|
+
def owner_type_param_names(owner)
|
|
2424
|
+
loader = owner && @environment&.rbs_loader
|
|
2425
|
+
return Set.new if loader.nil?
|
|
2426
|
+
|
|
2427
|
+
@owner_type_param_names ||= {}
|
|
2428
|
+
@owner_type_param_names.fetch(owner) do
|
|
2429
|
+
@owner_type_param_names[owner] = loader.class_declaration_type_params(owner).to_set do |p|
|
|
2430
|
+
"#{owner}::#{p.name}"
|
|
2431
|
+
end
|
|
2432
|
+
end
|
|
2433
|
+
end
|
|
2434
|
+
|
|
2435
|
+
# Whether a type name written as `name` resolves to a constant the run can see: one the RBS environment
|
|
2436
|
+
# declares, or a class or module the project's source declares, whether or not `sig/` has caught up with it.
|
|
2437
|
+
def declared_constant?(name)
|
|
2438
|
+
class_known?(name) || Reflection.discovered_class?(name, scope: file_base_scope(@environment))
|
|
1917
2439
|
end
|
|
1918
2440
|
|
|
1919
2441
|
# Issue #1002 — the reverse of {#translate_method_type_return}'s `alias_expander:`. Folding happens here,
|
|
@@ -2044,7 +2566,7 @@ module Rigor
|
|
|
2044
2566
|
return
|
|
2045
2567
|
end
|
|
2046
2568
|
when Prism::CallNode
|
|
2047
|
-
return if
|
|
2569
|
+
return if skipped_block_call?(node)
|
|
2048
2570
|
|
|
2049
2571
|
collect_attr_call(node, prefix, in_singleton_class, ctx)
|
|
2050
2572
|
end
|
|
@@ -2151,8 +2673,8 @@ module Rigor
|
|
|
2151
2673
|
end
|
|
2152
2674
|
end
|
|
2153
2675
|
|
|
2154
|
-
#
|
|
2155
|
-
#
|
|
2676
|
+
# Records the observation-derived types of ivars assigned directly from `def initialize` parameters, keyed
|
|
2677
|
+
# by class name and then attribute name. Only populated when `@observations` is
|
|
2156
2678
|
# non-empty (i.e. `--params=observed` was supplied). Matches the pattern `@ivar_name = param_name` where
|
|
2157
2679
|
# `param_name` is a required / optional positional or keyword parameter of `initialize`.
|
|
2158
2680
|
#
|
|
@@ -2285,7 +2807,7 @@ module Rigor
|
|
|
2285
2807
|
node.rigor_each_child { |c| collect_init_ivar_obs(c, prefix, result) }
|
|
2286
2808
|
end
|
|
2287
2809
|
|
|
2288
|
-
#
|
|
2810
|
+
# Derives the observation-derived attribute types of a single `def initialize`, matching `@ivar = param_name`
|
|
2289
2811
|
# assignments against the available `[class_name, :initialize]` observations.
|
|
2290
2812
|
def ivar_obs_from_initialize(class_name, def_node)
|
|
2291
2813
|
obs_list = @observations[[class_name, :initialize]]
|
|
@@ -2300,8 +2822,8 @@ module Rigor
|
|
|
2300
2822
|
build_ivar_obs_type_map(ivar_to_param, param_index, obs_list)
|
|
2301
2823
|
end
|
|
2302
2824
|
|
|
2303
|
-
#
|
|
2304
|
-
#
|
|
2825
|
+
# Maps `{ ivar_name => param_name }` to attribute types by looking up each param's observation types and
|
|
2826
|
+
# unioning them.
|
|
2305
2827
|
def build_ivar_obs_type_map(ivar_to_param, param_index, obs_list)
|
|
2306
2828
|
ivar_to_param.filter_map do |ivar_name, param_name|
|
|
2307
2829
|
types = collect_param_obs_types(obs_list, param_name, param_index[param_name])
|