rigortype 0.4.6 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/data/gem_overlay/activesupport/core_ext.rbs +16 -13
- data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
- data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
- data/docs/handbook/01-getting-started.md +5 -2
- data/docs/handbook/11-sig-gen.md +50 -3
- data/docs/handbook/appendix-phpstan.md +4 -4
- data/docs/manual/02-cli-reference.md +152 -26
- data/docs/manual/03-configuration.md +36 -3
- data/docs/manual/06-baseline.md +6 -4
- data/docs/manual/09-editor-integration.md +13 -2
- data/docs/manual/10-mcp-server.md +38 -2
- data/docs/manual/12-caching.md +4 -3
- data/docs/manual/plugins/rigor-actionpack.md +19 -11
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
- data/lib/rigor/analysis/baseline.rb +24 -27
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
- data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +0 -1
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
- data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
- data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
- data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
- data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules.rb +124 -44
- data/lib/rigor/analysis/crash_signature.rb +2 -2
- data/lib/rigor/analysis/dependency_recorder.rb +4 -4
- data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
- data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
- data/lib/rigor/analysis/edit_replay.rb +93 -6
- data/lib/rigor/analysis/fact_store.rb +3 -3
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
- data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
- data/lib/rigor/analysis/incremental_session.rb +27 -16
- data/lib/rigor/analysis/rule_catalog.rb +2 -2
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +2 -2
- data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +193 -93
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
- data/lib/rigor/analysis/runner/pool_coordinator.rb +147 -323
- data/lib/rigor/analysis/runner/project_pre_passes.rb +3 -3
- 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 +77 -71
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
- data/lib/rigor/analysis/template_unit_positions.rb +2 -3
- data/lib/rigor/analysis/template_units.rb +9 -1
- data/lib/rigor/analysis/worker_session.rb +24 -31
- data/lib/rigor/bleeding_edge.rb +3 -6
- data/lib/rigor/builtins/imported_refinements.rb +6 -8
- data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
- data/lib/rigor/builtins/regex_refinement.rb +4 -6
- data/lib/rigor/cache/descriptor.rb +6 -5
- data/lib/rigor/cache/incremental_snapshot.rb +31 -12
- data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
- data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
- data/lib/rigor/cache/rbs_constant_table.rb +1 -1
- data/lib/rigor/cache/rbs_descriptor.rb +54 -3
- data/lib/rigor/cache/rbs_environment.rb +4 -1
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
- data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
- data/lib/rigor/cache/store.rb +11 -10
- data/lib/rigor/ci_detector.rb +1 -1
- data/lib/rigor/cli/annotate_command.rb +24 -19
- data/lib/rigor/cli/baseline_command.rb +145 -55
- data/lib/rigor/cli/catalogue.rb +256 -0
- data/lib/rigor/cli/check_command.rb +108 -102
- 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 +18 -17
- data/lib/rigor/cli/explain_command.rb +18 -8
- data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
- data/lib/rigor/cli/json_document.rb +41 -0
- data/lib/rigor/cli/lens_command.rb +12 -8
- 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 +84 -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 +1 -1
- data/lib/rigor/cli/trace_command.rb +14 -9
- data/lib/rigor/cli/triage_command.rb +10 -11
- data/lib/rigor/cli/triage_renderer.rb +2 -1
- data/lib/rigor/cli/type_of_command.rb +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 +209 -54
- data/lib/rigor/config_audit.rb +1 -1
- data/lib/rigor/configuration/severity_profile.rb +5 -5
- data/lib/rigor/configuration.rb +72 -30
- data/lib/rigor/effects/definition_context.rb +0 -2
- data/lib/rigor/effects/envelope.rb +1 -1
- data/lib/rigor/effects/framework_units.rb +1 -1
- data/lib/rigor/effects/mutation_classifier.rb +4 -7
- data/lib/rigor/effects/propagator.rb +1 -1
- data/lib/rigor/effects/registry.rb +2 -2
- data/lib/rigor/effects/snapshot.rb +1 -1
- data/lib/rigor/effects/taint_cause.rb +0 -2
- data/lib/rigor/effects/unit_scan.rb +9 -13
- data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
- data/lib/rigor/environment/class_registry.rb +3 -3
- data/lib/rigor/environment/core_root.rb +77 -0
- data/lib/rigor/environment/default_libraries.rb +1 -1
- data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
- data/lib/rigor/environment/gated_signature_guard.rb +1 -1
- data/lib/rigor/environment/gated_signature_plan.rb +5 -2
- data/lib/rigor/environment/lockfile_resolver.rb +28 -12
- data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
- data/lib/rigor/environment/member_consistency.rb +56 -7
- data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
- data/lib/rigor/environment/project_path_gems.rb +124 -0
- data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
- data/lib/rigor/environment/rbs_loader.rb +480 -67
- 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 +41 -10
- data/lib/rigor/flow_contribution/merger.rb +4 -4
- data/lib/rigor/inference/acceptance.rb +93 -32
- data/lib/rigor/inference/block_parameter_binder.rb +2 -3
- data/lib/rigor/inference/block_run_count.rb +178 -0
- data/lib/rigor/inference/budget_trace.rb +8 -2
- data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
- data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
- data/lib/rigor/inference/callee_write_census.rb +815 -0
- data/lib/rigor/inference/construction_escape.rb +217 -0
- data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
- data/lib/rigor/inference/deep_erasure.rb +39 -7
- data/lib/rigor/inference/def_handle.rb +1 -1
- data/lib/rigor/inference/def_node_resolver.rb +1 -1
- data/lib/rigor/inference/def_return_typer.rb +46 -4
- data/lib/rigor/inference/definer_resolution.rb +4 -2
- data/lib/rigor/inference/definers_index.rb +106 -0
- data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
- data/lib/rigor/inference/element_read_widening.rb +49 -3
- data/lib/rigor/inference/expression_typer.rb +116 -49
- data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
- data/lib/rigor/inference/global_write_census.rb +76 -2
- data/lib/rigor/inference/global_writer_reach.rb +260 -0
- data/lib/rigor/inference/hkt_body.rb +1 -2
- data/lib/rigor/inference/hkt_body_parser.rb +3 -5
- data/lib/rigor/inference/hkt_reducer.rb +2 -2
- data/lib/rigor/inference/hkt_registry.rb +3 -5
- data/lib/rigor/inference/indexed_narrowing.rb +4 -7
- data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
- data/lib/rigor/inference/ivar_writer_census.rb +51 -0
- data/lib/rigor/inference/key_presence_guard.rb +2 -1
- data/lib/rigor/inference/loop_value_record.rb +8 -4
- data/lib/rigor/inference/match_rebinding/frame.rb +46 -0
- data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
- data/lib/rigor/inference/match_rebinding.rb +11 -1
- data/lib/rigor/inference/memo_audit.rb +55 -0
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
- data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
- data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
- data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher.rb +64 -14
- data/lib/rigor/inference/module_function_state.rb +11 -1
- data/lib/rigor/inference/multi_target_binder.rb +1 -1
- data/lib/rigor/inference/mutation_rejoin.rb +240 -4
- data/lib/rigor/inference/mutation_widening.rb +157 -27
- data/lib/rigor/inference/narrowing.rb +47 -33
- data/lib/rigor/inference/optimistic_origin.rb +1 -1
- data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
- data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
- data/lib/rigor/inference/pre_eval_constants.rb +1 -1
- data/lib/rigor/inference/project_method_ownership.rb +6 -3
- data/lib/rigor/inference/project_yielder.rb +11 -17
- data/lib/rigor/inference/receiver_alias.rb +20 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
- data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
- data/lib/rigor/inference/remover_holder_scan.rb +233 -0
- data/lib/rigor/inference/remover_narrowing.rb +224 -0
- data/lib/rigor/inference/rewrite_mutation.rb +53 -2
- data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
- data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +58 -15
- data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
- data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
- data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
- data/lib/rigor/inference/scope_indexer.rb +137 -46
- data/lib/rigor/inference/self_call_ivar_writes.rb +84 -12
- data/lib/rigor/inference/set_mutation.rb +189 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
- data/lib/rigor/inference/statement_evaluator.rb +2328 -138
- data/lib/rigor/inference/struct_fold_safety.rb +4 -6
- data/lib/rigor/inference/synthetic_method_scanner.rb +5 -6
- data/lib/rigor/inference/toplevel_def_census.rb +59 -0
- data/lib/rigor/inference/unknown_store_widening.rb +103 -4
- data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
- data/lib/rigor/inference/void_tail_summary.rb +2 -1
- data/lib/rigor/language_server/buffer_table.rb +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/plugins.rb +1 -1
- data/lib/rigor/mcp/server.rb +73 -2
- data/lib/rigor/mcp.rb +1 -1
- data/lib/rigor/plugin/access_denied_error.rb +1 -2
- data/lib/rigor/plugin/bundled_catalog.rb +1 -12
- data/lib/rigor/plugin/io_boundary.rb +1 -1
- data/lib/rigor/plugin/loader.rb +25 -8
- data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
- data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
- data/lib/rigor/plugin/manifest.rb +9 -13
- data/lib/rigor/plugin/registry.rb +3 -3
- data/lib/rigor/plugin/services.rb +2 -3
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
- data/lib/rigor/plugin/trust_policy.rb +2 -2
- data/lib/rigor/plugin/type_node_resolver.rb +2 -4
- data/lib/rigor/plugin.rb +6 -8
- data/lib/rigor/plugin_gap_advisory.rb +5 -1
- data/lib/rigor/project_environment.rb +16 -2
- data/lib/rigor/protection/analysis_guard.rb +1 -1
- data/lib/rigor/protection/discovery_seed.rb +44 -5
- data/lib/rigor/protection/mutator.rb +1 -1
- data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
- data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
- data/lib/rigor/rbs_extended/reporter.rb +27 -3
- data/lib/rigor/rbs_extended.rb +2 -3
- data/lib/rigor/reflection/constant_ancestors.rb +5 -3
- data/lib/rigor/rooted_glob.rb +15 -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 +19 -6
- data/lib/rigor/scope/resolution_chain.rb +3 -1
- data/lib/rigor/scope.rb +365 -31
- data/lib/rigor/sig_gen/classification.rb +4 -0
- data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
- data/lib/rigor/sig_gen/generator.rb +371 -58
- data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
- data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
- data/lib/rigor/sig_gen/layout_index.rb +3 -3
- data/lib/rigor/sig_gen/method_candidate.rb +23 -4
- data/lib/rigor/sig_gen/observation_collector.rb +3 -5
- 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/renderer.rb +15 -9
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -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 +217 -37
- data/lib/rigor/type/acceptance_router.rb +2 -2
- data/lib/rigor/type/accepts_result.rb +1 -1
- data/lib/rigor/type/app.rb +4 -6
- data/lib/rigor/type/combinator.rb +9 -13
- data/lib/rigor/type/constant.rb +4 -5
- data/lib/rigor/type/hash_shape.rb +21 -40
- data/lib/rigor/type/union.rb +1 -1
- data/lib/rigor/type_node/identifier.rb +1 -2
- data/lib/rigor/type_node/resolver_chain.rb +1 -1
- data/lib/rigor/type_node/string_literal.rb +1 -2
- data/lib/rigor/type_node/symbol_literal.rb +1 -2
- data/lib/rigor/type_node/union.rb +3 -6
- data/lib/rigor/value_semantics.rb +1 -10
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +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 +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +66 -19
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
- data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
- data/plugins/rigor-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 +4 -5
- data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
- 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 +1 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
- data/sig/rigor/analysis/baseline.rbs +0 -3
- data/sig/rigor/analysis/fact_store.rbs +1 -0
- data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
- data/sig/rigor/cli/diff_command.rbs +1 -1
- data/sig/rigor/cli/explain_command.rbs +1 -1
- data/sig/rigor/effects/registry.rbs +2 -2
- data/sig/rigor/inference.rbs +4 -1
- data/sig/rigor/plugin/additional_initializer.rbs +0 -5
- data/sig/rigor/plugin/base.rbs +1 -0
- 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/io_boundary.rbs +2 -1
- data/sig/rigor/plugin/manifest.rbs +9 -9
- data/sig/rigor/plugin/protocol_contract.rbs +3 -7
- data/sig/rigor/scope.rbs +23 -3
- data/skills/rigor-plugin-author/SKILL.md +4 -4
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
- data/skills/rigor-plugin-review/SKILL.md +3 -3
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
- metadata +36 -3
data/docs/manual/06-baseline.md
CHANGED
|
@@ -77,7 +77,9 @@ a diagnostic another layer has already suppressed.
|
|
|
77
77
|
| `rigor baseline drift` | Show how buckets have moved — `--only=over` for buckets that grew, `reducible` for ones you have already improved past, `cleared` for empty ones. |
|
|
78
78
|
| `rigor baseline prune` | Drop buckets that match nothing any more. `--dry-run` previews. |
|
|
79
79
|
|
|
80
|
-
`generate` and `regenerate` analyse with your loaded configuration unchanged except that `baseline:` is off, so options such as `bleeding_edge`, `target_ruby` or `severity_overrides` shape the recorded set exactly as they shape `rigor check`.
|
|
80
|
+
`generate` and `regenerate` analyse with your loaded configuration unchanged except that `baseline:` is off, so options such as `bleeding_edge`, `target_ruby` or `severity_overrides` shape the recorded set exactly as they shape `rigor check`. They analyse the configured `paths:` only: a positional path argument is rejected with a usage error (exit `64`), since a baseline over a subset of files would silently drop the entries of every other file. The other three subcommands reject a positional path the same way.
|
|
81
|
+
|
|
82
|
+
The summary counts only the diagnostics the written buckets cover. A diagnostic with no rule — a **parse error** in a file Rigor could not parse, a missing `paths:` entry, or an internal error — or no path cannot be keyed, so it never enters the baseline. The command prints a second line saying how many, why, and which files. The exit status stays 0, but the next `rigor check` with `baseline:` wired still reports them and exits non-zero until you fix those files, exclude them, or remove a bad `paths:` entry.
|
|
81
83
|
|
|
82
84
|
`generate` and `regenerate` take `--match-mode=rule` (the
|
|
83
85
|
default — one bucket per file × rule) or `--match-mode=message`
|
|
@@ -103,11 +105,11 @@ snapshot in the repository and compares against it explicitly:
|
|
|
103
105
|
|
|
104
106
|
```sh
|
|
105
107
|
# Once: capture the current diagnostic surface.
|
|
106
|
-
rigor check --format=json > rigor.
|
|
107
|
-
git add rigor.
|
|
108
|
+
rigor check --format=json > rigor-check.json
|
|
109
|
+
git add rigor-check.json
|
|
108
110
|
|
|
109
111
|
# Per PR: compare against the committed snapshot.
|
|
110
|
-
rigor diff rigor.
|
|
112
|
+
rigor diff rigor-check.json
|
|
111
113
|
```
|
|
112
114
|
|
|
113
115
|
[`rigor diff`](02-cli-reference.md#rigor-diff) prints a `+ NEW`
|
|
@@ -25,6 +25,7 @@ design + capability matrix lives in
|
|
|
25
25
|
| `textDocument/documentSymbol` | Outline tree from Prism AST: `class` / `module` / `def` with nesting. |
|
|
26
26
|
| `workspace/didChangeWatchedFiles` | Invalidates the per-session `Environment` + `Cache::Store` cache so saved files repropagate. |
|
|
27
27
|
| `workspace/didChangeConfiguration` | Same — re-reads `.rigor.yml` / `Gemfile.lock` etc. |
|
|
28
|
+
| `window/showMessage` | One warning per [configuration warning](03-configuration.md) — the lines `rigor check` prints on STDERR, such as a key that has no effect or a `disable:` token that names no rule. Sent on `initialized`, and again after a save only when re-reading `.rigor.yml` changed the set of warnings. |
|
|
28
29
|
|
|
29
30
|
## Prerequisites
|
|
30
31
|
|
|
@@ -51,8 +52,10 @@ rigor lsp [--transport=stdio] [--log=PATH] [--config=PATH]
|
|
|
51
52
|
|
|
52
53
|
- `--transport=stdio` — default; only value accepted in v1. TCP /
|
|
53
54
|
Unix-socket transports are queued.
|
|
54
|
-
- `--log=PATH` — wire log
|
|
55
|
-
server
|
|
55
|
+
- `--log=PATH` — append the wire log (each message in and out, one
|
|
56
|
+
line each) and the server's debug output from the start of the loop to
|
|
57
|
+
a file. Output before the loop starts still goes to stderr. Without
|
|
58
|
+
it, no wire log is written and server-side logs go to stderr.
|
|
56
59
|
- `--config=PATH` — mirrors `rigor check --config=PATH`. Without
|
|
57
60
|
it, `Configuration.discover` walks `.rigor.yml` / `.rigor.dist.yml`
|
|
58
61
|
from the project root.
|
|
@@ -194,6 +197,14 @@ For a legacy bundler-based install, swap the connection list to
|
|
|
194
197
|
there, the LSP should too. If `rigor check` fails, fix that
|
|
195
198
|
first.
|
|
196
199
|
|
|
200
|
+
**A rule still fires although `.rigor.yml` turns it off.**
|
|
201
|
+
|
|
202
|
+
- Look for a `rigor:` warning the server showed when it started (or
|
|
203
|
+
after the save that changed `.rigor.yml`): a misspelt key such as
|
|
204
|
+
`undefined-method: "off"` at the top level, or a `disable:` token
|
|
205
|
+
that names no rule, has no effect, and the server says so once.
|
|
206
|
+
The same warnings appear on STDERR from `rigor check`.
|
|
207
|
+
|
|
197
208
|
**Completion popup is empty.**
|
|
198
209
|
|
|
199
210
|
- Completion only fires on a node with a known type. Receivers
|
|
@@ -193,6 +193,28 @@ All messages except `notifications/*` require a response.
|
|
|
193
193
|
|
|
194
194
|
## Tool reference
|
|
195
195
|
|
|
196
|
+
Every tool that takes a `config` argument reports the configuration's
|
|
197
|
+
warnings — the ones `rigor check` prints on STDERR, such as a key that
|
|
198
|
+
has no effect or a `disable:` token that names no rule. `rigor_check`
|
|
199
|
+
carries them in its own document; every other such tool appends a
|
|
200
|
+
second `text` content block holding one JSON object, after the tool's
|
|
201
|
+
own output, which is left unchanged:
|
|
202
|
+
|
|
203
|
+
```json
|
|
204
|
+
{"config_warnings": [{"kind": "unknown_key", "message": "`undefined-method` is not a recognized configuration key; it has no effect.", "key": "undefined-method", "suggestion": null}]}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Each entry has the shape of `rigor check --format json`'s
|
|
208
|
+
`config_warnings` entries. There is no such block when the
|
|
209
|
+
configuration has no warnings, or when it fails to load (the tool
|
|
210
|
+
reports that error itself).
|
|
211
|
+
|
|
212
|
+
`rigor_type_of` and `rigor_explain` print JSON whose top-level shape changes
|
|
213
|
+
in the next minor release (`{ "schema_version": 1, "results" | "rules" |
|
|
214
|
+
"skip_reasons": [...] }`). Until then, a successful call carries a final
|
|
215
|
+
`text` content block, after any `config_warnings` block, with the one-line
|
|
216
|
+
notice the CLI prints on STDERR. The tool's own document is left unchanged.
|
|
217
|
+
|
|
196
218
|
### rigor_check
|
|
197
219
|
|
|
198
220
|
Analyze one or more Ruby files or directories for type errors,
|
|
@@ -222,9 +244,16 @@ undefined methods, arity mismatches, and nil-receiver risks.
|
|
|
222
244
|
}
|
|
223
245
|
```
|
|
224
246
|
|
|
247
|
+
When `.rigor.yml` has [configuration warnings](03-configuration.md)
|
|
248
|
+
(a key or a `disable:` token that has no effect), the document carries
|
|
249
|
+
them under `config_warnings`, as `rigor check --format json` does.
|
|
250
|
+
|
|
225
251
|
`isError` is `false` when the run completes normally, even if
|
|
226
|
-
diagnostics were found. `isError: true` signals a
|
|
227
|
-
|
|
252
|
+
diagnostics were found. `isError: true` signals a usage or runtime
|
|
253
|
+
failure: an unreadable `config`, or a `rigor_coverage` call whose
|
|
254
|
+
`paths` leave nothing to measure. A missing path given to `rigor_check`
|
|
255
|
+
is not one of these: it is reported in the diagnostics, and the call
|
|
256
|
+
returns `isError: false`.
|
|
228
257
|
|
|
229
258
|
**Typical agent use:** call `rigor_check` on the files an agent is
|
|
230
259
|
about to edit, record the baseline, apply the edit, call again, diff
|
|
@@ -484,6 +513,13 @@ as `Constant` / `Nominal` / shaped / refined (precise) versus
|
|
|
484
513
|
|
|
485
514
|
**Returns:** JSON — the same as `rigor coverage --format json`.
|
|
486
515
|
|
|
516
|
+
A `paths` entry that is missing, or is not a Ruby file, is reported as
|
|
517
|
+
`rigor coverage` reports it. Beside files that are measured it is skipped. The result
|
|
518
|
+
then carries a second text block with the CLI's stderr, unfiltered: path
|
|
519
|
+
warnings such as `missing.rb:1:1: warning: no such file or directory (skipped)`,
|
|
520
|
+
and any other warnings. When no entry holds a Ruby file, the call is
|
|
521
|
+
`isError: true` and the block says so.
|
|
522
|
+
|
|
487
523
|
```json
|
|
488
524
|
{
|
|
489
525
|
"summary": {
|
data/docs/manual/12-caching.md
CHANGED
|
@@ -190,9 +190,10 @@ unchanged tree, so it cannot see an edit that fails to re-check a
|
|
|
190
190
|
file it affects. `--verify-incremental-edits=N` also edits `N`
|
|
191
191
|
files one at a time, in memory and not on disk, and compares each
|
|
192
192
|
warm recheck with a full analysis of the edited tree. Each file is
|
|
193
|
-
edited
|
|
194
|
-
`module` declarations with every body dropped
|
|
195
|
-
|
|
193
|
+
edited up to four times: once emptied, once reduced to its
|
|
194
|
+
`class` / `module` declarations with every body dropped, and up
|
|
195
|
+
to twice with one statement of a `class` / `module` body removed
|
|
196
|
+
and the rest kept. It replays only removals, never an addition.
|
|
196
197
|
|
|
197
198
|
Rigor is also precise about *what* an edit changed. Re-checking a
|
|
198
199
|
file that many others depend on — a base class, a widely-used
|
|
@@ -397,21 +397,29 @@ diff — not a policy scoped to `app/views/**/*`. See
|
|
|
397
397
|
knows what `rigor-rails-routes` published, and `permit`
|
|
398
398
|
validation only what `rigor-activerecord` published — enabling
|
|
399
399
|
those producers widens what this plugin can check.
|
|
400
|
-
- **`params[:key]`
|
|
401
|
-
types as `ActionController::Parameters`, and so does the
|
|
402
|
-
of every builder method that always returns one — `require`,
|
|
400
|
+
- **`params[:key]` is a gradual carrier.** Inside a controller,
|
|
401
|
+
`params` types as `ActionController::Parameters`, and so does the
|
|
402
|
+
result of every builder method that always returns one — `require`,
|
|
403
403
|
`permit`, `permit!`, `expect`, `slice`, `slice!`, `except`,
|
|
404
404
|
`without`, `extract!`, `merge`, `merge!`, `reverse_merge`,
|
|
405
405
|
`reverse_merge!`, `with_defaults`, `with_defaults!`, `compact`,
|
|
406
406
|
`compact_blank`, `deep_dup` — so a chain built from them keeps a
|
|
407
|
-
concrete receiver throughout. A subscript read
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
407
|
+
concrete receiver throughout. A subscript read answers
|
|
408
|
+
`Dynamic[ActionController::Parameters | nil]` (#2430): the static
|
|
409
|
+
facet is the nilable `Parameters`, so `params[:user][:name]` and
|
|
410
|
+
`params[:user].permit(:name)` keep dispatching on that facet, and
|
|
411
|
+
the `Dynamic` wrapper lets the value cross a declared boundary such
|
|
412
|
+
as a `String` parameter, as a scalar param does at runtime. The
|
|
413
|
+
facet is nilable rather than the non-nil nominal on purpose:
|
|
414
|
+
`params[:missing]` is `nil` at runtime, and a non-nil type would
|
|
415
|
+
let the flow rules fold live conditions (`if params[:q]`,
|
|
416
|
+
`url.nil?`) to a constant and report working code. Methods on the
|
|
417
|
+
carrier outside the chain (`params[:q].to_s`) answer `Dynamic[top]`,
|
|
418
|
+
because the engine dispatches a `Dynamic` receiver without projecting
|
|
419
|
+
its facet: a loss of precision, not a false positive. Methods whose
|
|
420
|
+
result depends on the call — `dig`, `fetch`, `compact!`, and the
|
|
421
|
+
block-less `select` / `reject` / `transform_keys` /
|
|
422
|
+
`transform_values` — are untyped for the same reason.
|
|
415
423
|
- **`flash[:key]` and `session[:key]` stay untyped too**, for that
|
|
416
424
|
same reason and measured the same way. Both are leaf reads that
|
|
417
425
|
return whatever was stored — or `nil` for a key that is not set.
|
|
@@ -32,6 +32,17 @@ path, no vendoring, no `signature_paths:` wiring.
|
|
|
32
32
|
> surface. When it is loaded, the auto overlay stands down so the two
|
|
33
33
|
> never double-declare.
|
|
34
34
|
|
|
35
|
+
If the project also runs `rbs collection install` for `activesupport`,
|
|
36
|
+
some methods are declared twice — `Object#acts_like?`,
|
|
37
|
+
`Object#html_safe?`, `Time#to_fs` among them. Rigor merges the two:
|
|
38
|
+
the plugin's signatures are tried first and the collection's still
|
|
39
|
+
apply, so a call either one accepts type-checks. When your own `sig/`
|
|
40
|
+
or inline RBS declares one of the plugin's methods, yours is used
|
|
41
|
+
instead and the plugin's stands down; the run reports one
|
|
42
|
+
`rbs.coverage.plugin-signature-stood-down` info row for the plugin's
|
|
43
|
+
file, naming those methods and the file whose declaration is used for
|
|
44
|
+
each.
|
|
45
|
+
|
|
35
46
|
## What it covers
|
|
36
47
|
|
|
37
48
|
Roughly the top ~40 selectors plus their close neighbours, across:
|
|
@@ -55,7 +66,15 @@ Roughly the top ~40 selectors plus their close neighbours, across:
|
|
|
55
66
|
- **Hash** — `#symbolize_keys` / `#stringify_keys` (+ deep / bang) and
|
|
56
67
|
their `#to_options` alias, `#deep_merge`, `#reverse_merge` /
|
|
57
68
|
`#with_defaults` (+ bang), `#with_indifferent_access`, `#except!`,
|
|
58
|
-
`#extract!`.
|
|
69
|
+
`#extract!`. `#symbolize_keys` / `#to_options` map the key type per
|
|
70
|
+
member, as ActiveSupport does (`key.to_sym rescue key`): a `String` or
|
|
71
|
+
`Symbol` key reads as `Symbol`, and every other key (an `Integer`, a
|
|
72
|
+
`Float`, a project class) reads as `untyped`, because a mixin or a
|
|
73
|
+
project file can give any class a `to_sym`. So
|
|
74
|
+
`Hash[String, Integer]` gives `Hash[Symbol, Integer]` and
|
|
75
|
+
`Hash[Integer, Integer]` gives `Hash[untyped, Integer]`. Without the
|
|
76
|
+
plugin, on the activesupport overlay alone, the key reads `untyped`.
|
|
77
|
+
The bang forms answer the receiver's own type.
|
|
59
78
|
- **Range** — `#overlaps?`, `#to_fs` / `#to_formatted_s`.
|
|
60
79
|
- **Enumerable** — `#index_by`, `#index_with`, `#pluck`, `#exclude?`.
|
|
61
80
|
|
|
@@ -97,6 +97,13 @@ module Rigor
|
|
|
97
97
|
new(buckets, project_root: project_root)
|
|
98
98
|
end
|
|
99
99
|
|
|
100
|
+
# The diagnostics a baseline cannot address: no rule (a Prism parse error, a path or internal error) or no path.
|
|
101
|
+
# `#filter` passes these through and `from_diagnostics` leaves them out of every bucket; this is the one
|
|
102
|
+
# definition of both.
|
|
103
|
+
def unkeyable(diagnostics)
|
|
104
|
+
diagnostics.reject { |d| d.qualified_rule && d.path }
|
|
105
|
+
end
|
|
106
|
+
|
|
100
107
|
private
|
|
101
108
|
|
|
102
109
|
def parse_loaded(raw, path:, project_root:)
|
|
@@ -138,17 +145,12 @@ module Rigor
|
|
|
138
145
|
raise LoadError, "#{path}: ignored[#{index}] `message:` is not a valid Regexp: #{e.message}"
|
|
139
146
|
end
|
|
140
147
|
|
|
141
|
-
#
|
|
142
|
-
#
|
|
143
|
-
#
|
|
144
|
-
# contributes to a single bucket regardless of message.
|
|
145
|
-
# File paths are normalised to relative before keying.
|
|
148
|
+
# Groups diagnostics by [file, rule, regex_or_nil]. In message mode, each unique message gets its own
|
|
149
|
+
# bucket; in rule mode, every diagnostic for a (file, rule) pair contributes to a single bucket regardless
|
|
150
|
+
# of message. File paths are normalised to relative before keying.
|
|
146
151
|
def group_for_baseline(diagnostics, match_mode, project_root)
|
|
147
152
|
root = Pathname.new(project_root)
|
|
148
|
-
diagnostics.each_with_object({}) do |diag, into|
|
|
149
|
-
next if diag.qualified_rule.nil?
|
|
150
|
-
next if diag.path.nil?
|
|
151
|
-
|
|
153
|
+
(diagnostics - unkeyable(diagnostics)).each_with_object({}) do |diag, into|
|
|
152
154
|
rel = relative_path(diag.path, root)
|
|
153
155
|
key = case match_mode
|
|
154
156
|
when :rule
|
|
@@ -160,18 +162,18 @@ module Rigor
|
|
|
160
162
|
end
|
|
161
163
|
end
|
|
162
164
|
|
|
165
|
+
def relative_path(path, root_pathname)
|
|
166
|
+
Pathname.new(path).relative_path_from(root_pathname).to_s
|
|
167
|
+
rescue ArgumentError
|
|
168
|
+
path # different drive letter on Windows or non-child path
|
|
169
|
+
end
|
|
170
|
+
|
|
163
171
|
# Generates a Regexp source string for the baseline row. The string is `Regexp.escape`d so the YAML
|
|
164
172
|
# round-trip produces a regex that matches the literal message. Users hand-editing the row can replace
|
|
165
173
|
# the escaped form with a pattern.
|
|
166
174
|
def message_pattern_for(message)
|
|
167
175
|
Regexp.new(Regexp.escape(message.to_s))
|
|
168
176
|
end
|
|
169
|
-
|
|
170
|
-
def relative_path(path, root_pathname)
|
|
171
|
-
Pathname.new(path).relative_path_from(root_pathname).to_s
|
|
172
|
-
rescue ArgumentError
|
|
173
|
-
path # different drive letter on Windows or non-child path
|
|
174
|
-
end
|
|
175
177
|
end
|
|
176
178
|
|
|
177
179
|
class LoadError < StandardError; end
|
|
@@ -181,11 +183,8 @@ module Rigor
|
|
|
181
183
|
def initialize(buckets, project_root: Dir.pwd)
|
|
182
184
|
@project_root = Pathname.new(project_root)
|
|
183
185
|
@buckets = buckets.freeze
|
|
184
|
-
#
|
|
185
|
-
#
|
|
186
|
-
# - message-pattern rows (message_regex != nil)
|
|
187
|
-
# The matcher walks message-pattern rows first (tighter match takes precedence); diagnostics that
|
|
188
|
-
# don't match any message row fall through to the rule-ID row if one exists.
|
|
186
|
+
# Buckets grouped by (file, rule). `claim_bucket_for` splits each group into message-pattern rows, tried
|
|
187
|
+
# first as the tighter match, and the rule-ID row (message_regex == nil) as the fallback.
|
|
189
188
|
@by_pair = buckets.group_by { |b| [b.file, b.rule] }.freeze
|
|
190
189
|
freeze
|
|
191
190
|
end
|
|
@@ -216,7 +215,7 @@ module Rigor
|
|
|
216
215
|
|
|
217
216
|
# Diagnostics that lacked a rule or a path bypass the baseline entirely (the baseline can't address
|
|
218
217
|
# them).
|
|
219
|
-
unkeyable =
|
|
218
|
+
unkeyable = self.class.unkeyable(diagnostics)
|
|
220
219
|
[surfaced + unkeyable, silenced_count]
|
|
221
220
|
end
|
|
222
221
|
|
|
@@ -225,7 +224,7 @@ module Rigor
|
|
|
225
224
|
# - `:within` — `actual <= count` (silenced by the filter).
|
|
226
225
|
# - `:over` — `actual > count` (over threshold; surfaced in the regular `rigor check` output).
|
|
227
226
|
# - `:cleared` — `actual == 0` (the bucket can be pruned).
|
|
228
|
-
# - `:reducible` — `0 < actual < count` (the bucket's count can be tightened;
|
|
227
|
+
# - `:reducible` — `0 < actual < count` (the bucket's count can be tightened; `rigor baseline regenerate`
|
|
229
228
|
# handles this).
|
|
230
229
|
DriftRow = Struct.new(:bucket, :actual_count, :status, keyword_init: true) do
|
|
231
230
|
def delta
|
|
@@ -254,8 +253,8 @@ module Rigor
|
|
|
254
253
|
end
|
|
255
254
|
end
|
|
256
255
|
|
|
257
|
-
# Returns a new Baseline with the given buckets dropped. Used by `rigor baseline prune`
|
|
258
|
-
#
|
|
256
|
+
# Returns a new Baseline with the given buckets dropped. Used by `rigor baseline prune` to remove cleared
|
|
257
|
+
# buckets (`actual == 0`) from the on-disk file.
|
|
259
258
|
def without(buckets_to_drop)
|
|
260
259
|
dropset = buckets_to_drop.to_set
|
|
261
260
|
self.class.new(buckets.reject { |b| dropset.include?(b) }, project_root: @project_root)
|
|
@@ -311,9 +310,7 @@ module Rigor
|
|
|
311
310
|
# over rule-ID rows because they're more specific. A diagnostic that matches no row goes into a
|
|
312
311
|
# synthetic "no-bucket" bin keyed by (file, rule).
|
|
313
312
|
bins = {}
|
|
314
|
-
diagnostics.each do |diag|
|
|
315
|
-
next if diag.qualified_rule.nil? || diag.path.nil?
|
|
316
|
-
|
|
313
|
+
(diagnostics - self.class.unkeyable(diagnostics)).each do |diag|
|
|
317
314
|
bucket = claim_bucket_for(diag)
|
|
318
315
|
key = if bucket
|
|
319
316
|
[bucket.file, bucket.rule,
|
|
@@ -23,10 +23,11 @@ module Rigor
|
|
|
23
23
|
# `BlockNode` ancestors. Mutation tracking through loop bodies is incomplete (`shift = 0; loop {
|
|
24
24
|
# shift += 7 }` keeps `shift` at `Constant<7>` per the analyzer), so a `Constant<bool>` predicate
|
|
25
25
|
# inside the loop body is suspect.
|
|
26
|
-
# - **Defensive predicate calls**: `.nil?` / `.empty?` / `.zero?` / `.any?` / `.none?` / `.all
|
|
27
|
-
# read like the user explicitly checking for a runtime condition that the type
|
|
28
|
-
# can't happen — Rigor's strict-on-returns RBS often disagrees with the user's
|
|
29
|
-
# `Module#name` returns `String` per RBS but anonymous classes really do return
|
|
26
|
+
# - **Defensive predicate calls**: `.nil?` / `.empty?` / `.zero?` / `.any?` / `.none?` / `.all?` /
|
|
27
|
+
# `.respond_to?`. These read like the user explicitly checking for a runtime condition that the type
|
|
28
|
+
# system already proves can't happen — Rigor's strict-on-returns RBS often disagrees with the user's
|
|
29
|
+
# defensive check (e.g. `Module#name` returns `String` per RBS but anonymous classes really do return
|
|
30
|
+
# `nil`). Skipping
|
|
30
31
|
# these forms keeps the rule useful for genuine logic errors without faulting defensive code.
|
|
31
32
|
#
|
|
32
33
|
# Also skipped (so the literal-only `unreachable-branch` rule doesn't double-fire alongside this one):
|
|
@@ -53,8 +54,6 @@ module Rigor
|
|
|
53
54
|
NODE_CLASSES = [Prism::IfNode, Prism::UnlessNode].freeze
|
|
54
55
|
RULE_WALK_GATES = [:loop_or_block].freeze
|
|
55
56
|
|
|
56
|
-
# @return one entry per qualifying predicate. Empty when the tree carries no firing
|
|
57
|
-
# predicates.
|
|
58
57
|
def initialize(scope_index)
|
|
59
58
|
@scope_index = scope_index
|
|
60
59
|
@results = []
|
|
@@ -35,8 +35,8 @@ module Rigor
|
|
|
35
35
|
# exactly like the legacy walk.
|
|
36
36
|
NODE_CLASSES = [Prism::DefNode].freeze
|
|
37
37
|
|
|
38
|
-
#
|
|
39
|
-
#
|
|
38
|
+
# One entry per dead-assignment write, as `{def_node:, write_node:}`. Empty when the tree has no
|
|
39
|
+
# qualifying writes.
|
|
40
40
|
def initialize(scope_index)
|
|
41
41
|
@scope_index = scope_index
|
|
42
42
|
@results = []
|
|
@@ -24,10 +24,10 @@ module Rigor
|
|
|
24
24
|
# currently declaration-sourced ivar*, so a second hop (`c = @count; d = c`) and an `||=` rewrite land on
|
|
25
25
|
# the plain `with_local` path and carry no mark.
|
|
26
26
|
#
|
|
27
|
-
# A branch join
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
27
|
+
# A branch join keeps the mark when both arms stamp it: `if c then r = @count else r = @count end` stamps
|
|
28
|
+
# `(:local, :r)` on each arm, and `join_declaration_sourced` INTERSECTS, so the mark survives. Only an
|
|
29
|
+
# *asymmetric* join drops it, by that intersection rather than by reaching `with_local`. The normative
|
|
30
|
+
# statement — establishment, drop, join and the full list of
|
|
31
31
|
# unmarked shapes, each verified against the implementation — is
|
|
32
32
|
# `docs/internal-spec/inference-engine.md` § "Declaration-sourced provenance mark (ADR-58)"; keep this
|
|
33
33
|
# comment subordinate to it.
|
|
@@ -36,8 +36,8 @@ module Rigor
|
|
|
36
36
|
# a Hash literal, loops and blocks included.
|
|
37
37
|
NODE_CLASSES = [Prism::HashNode, Prism::KeywordHashNode].freeze
|
|
38
38
|
|
|
39
|
-
#
|
|
40
|
-
#
|
|
39
|
+
# One entry per LATER occurrence of a repeated literal key, carrying `hash_node`, `key_node`,
|
|
40
|
+
# `first_key_node` and `key_label` (`key_node` is the repeat the diagnostic points at;
|
|
41
41
|
# `first_key_node` is the earliest occurrence its message references). Empty when the tree has no
|
|
42
42
|
# qualifying duplicates.
|
|
43
43
|
def initialize(scope_index)
|
|
@@ -29,6 +29,13 @@ module Rigor
|
|
|
29
29
|
# under the ivar's `@`-prefixed name, which no local can spell — because the write and the read are
|
|
30
30
|
# separate statements. The mark is method-local: the class-ivar census that seeds a sibling method's
|
|
31
31
|
# ivars types the write's RHS without the inference seed, so a cross-method read is not a lower bound.
|
|
32
|
+
# - `Array(param)` (issue #2280): the conversion passes the parameter's elements through, so
|
|
33
|
+
# `Array(param).first.upcase` reads the lower bound back.
|
|
34
|
+
# - an `if` / `unless` / ternary whose value is the parameter (issue #2280): `(c ? param : param).upcase`.
|
|
35
|
+
# Unlike `&&` / `||`, a branch that does not hold the parameter keeps its diagnostic: the form is rooted
|
|
36
|
+
# when every branch that can run is. A predicate the engine folds to one arm (the same certainty
|
|
37
|
+
# `StatementEvaluator#eval_if` elides the dead arm on) leaves only that arm to decide, so
|
|
38
|
+
# `(c ? param : 1).upcase` declines when `c` folds truthy and reports `Integer` when it folds falsey.
|
|
32
39
|
#
|
|
33
40
|
# Every value derived from an inferred parameter by dispatch is itself a lower bound, so firing against it
|
|
34
41
|
# is an FP by the same construction. No-op on a normal `check` run — `Scope#inferred_param?` is false
|
|
@@ -40,40 +47,105 @@ module Rigor
|
|
|
40
47
|
# length, which is tiny in practice).
|
|
41
48
|
MAX_DEPTH = 64
|
|
42
49
|
|
|
43
|
-
|
|
50
|
+
# `fold` is whether `scope` is still the scope the walked node is evaluated in. The callers pass the scope
|
|
51
|
+
# at the start of the expression; once the walk passes something evaluated earlier inside it (a leading
|
|
52
|
+
# statement, the left operand of `&&`, an earlier literal element, a conditional's predicate), that
|
|
53
|
+
# earlier code may have rebound what a nested predicate reads (`(c = false; c ? param : 1)`), so a
|
|
54
|
+
# conditional below no longer folds its predicate in `scope`.
|
|
55
|
+
def rooted?(node, scope, depth = 0, fold: true)
|
|
44
56
|
return false if node.nil? || depth > MAX_DEPTH
|
|
45
57
|
|
|
46
58
|
case node
|
|
47
59
|
when Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode then scope.inferred_param?(node.name)
|
|
48
|
-
when Prism::CallNode then
|
|
60
|
+
when Prism::CallNode then call_rooted?(node, scope, depth + 1, fold)
|
|
49
61
|
when Prism::ArrayNode, Prism::HashNode, Prism::KeywordHashNode, Prism::AssocNode
|
|
50
|
-
literal_rooted?(node, scope, depth + 1)
|
|
51
|
-
else combining_rooted?(node, scope, depth + 1)
|
|
62
|
+
literal_rooted?(node, scope, depth + 1, fold)
|
|
63
|
+
else combining_rooted?(node, scope, depth + 1, fold)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def call_rooted?(node, scope, depth, fold)
|
|
68
|
+
return rooted?(node.receiver, scope, depth, fold: fold) unless array_conversion?(node, scope)
|
|
69
|
+
|
|
70
|
+
rooted?(node.arguments.arguments.first, scope, depth, fold: fold)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# A receiverless `Array(x)` reaching Kernel#Array: exactly one plain argument, no block, and no project
|
|
74
|
+
# `Array` on `self`'s class or its project ancestors to bind ahead of Kernel's.
|
|
75
|
+
def array_conversion?(node, scope)
|
|
76
|
+
node.receiver.nil? && node.name == :Array && node.block.nil? &&
|
|
77
|
+
node.arguments&.arguments&.size == 1 && !project_array_method?(scope)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def project_array_method?(scope)
|
|
81
|
+
self_type = scope.self_type
|
|
82
|
+
case self_type
|
|
83
|
+
when Type::Nominal then scope.discovered_method_through_ancestors?(self_type.class_name, :Array, :instance)
|
|
84
|
+
when Type::Singleton
|
|
85
|
+
scope.discovered_method_through_ancestors?(self_type.class_name, :Array, :singleton)
|
|
86
|
+
else false
|
|
52
87
|
end
|
|
53
88
|
end
|
|
54
89
|
|
|
55
90
|
# An array or hash literal is rooted when any element is: the engine reads the element back precisely.
|
|
56
|
-
def literal_rooted?(node, scope, depth)
|
|
91
|
+
def literal_rooted?(node, scope, depth, fold)
|
|
57
92
|
parts = node.is_a?(Prism::AssocNode) ? [node.key, node.value] : node.elements
|
|
58
|
-
parts.any? { |part| rooted?(part, scope, depth) }
|
|
93
|
+
parts.each_with_index.any? { |part, index| rooted?(part, scope, depth, fold: fold && index.zero?) }
|
|
59
94
|
end
|
|
60
95
|
|
|
61
96
|
# Value-combining forms: the produced value is a union of the operands, so it is a lower bound when ANY
|
|
62
97
|
# operand is inferred-parameter-rooted (`cps[i] - x rescue -1`, `a && param.foo`, `param || b`). Declining
|
|
63
98
|
# on the whole expression is FP-safe by the same lower-bound argument.
|
|
64
|
-
def combining_rooted?(node, scope, depth)
|
|
99
|
+
def combining_rooted?(node, scope, depth, fold)
|
|
65
100
|
case node
|
|
66
101
|
when Prism::RescueModifierNode
|
|
67
|
-
rooted?(node.expression, scope, depth
|
|
68
|
-
|
|
69
|
-
when Prism::
|
|
70
|
-
|
|
102
|
+
rooted?(node.expression, scope, depth, fold: fold) ||
|
|
103
|
+
rooted?(node.rescue_expression, scope, depth, fold: false)
|
|
104
|
+
when Prism::AndNode, Prism::OrNode
|
|
105
|
+
rooted?(node.left, scope, depth, fold: fold) || rooted?(node.right, scope, depth, fold: false)
|
|
106
|
+
when Prism::ParenthesesNode then body_rooted?(node.body, scope, depth, fold)
|
|
107
|
+
when Prism::StatementsNode then body_rooted?(node, scope, depth, fold)
|
|
108
|
+
when Prism::IfNode, Prism::UnlessNode then conditional_rooted?(node, scope, depth, fold)
|
|
109
|
+
when Prism::ElseNode then rooted?(node.statements, scope, depth, fold: fold)
|
|
110
|
+
else false
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# A body's value is its last statement; any statement before it runs first.
|
|
115
|
+
def body_rooted?(body, scope, depth, fold)
|
|
116
|
+
return rooted?(body, scope, depth, fold: fold) unless body.is_a?(Prism::StatementsNode)
|
|
117
|
+
|
|
118
|
+
rooted?(body.body.last, scope, depth, fold: fold && body.body.size == 1)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Every branch that can run must be rooted; an absent branch is `nil`, never rooted. The predicate is
|
|
122
|
+
# typed only when the branches disagree, so a conditional that never mentions a parameter costs nothing.
|
|
123
|
+
# An arm runs after the predicate, so a conditional nested in one never folds.
|
|
124
|
+
def conditional_rooted?(node, scope, depth, fold)
|
|
125
|
+
truthy_rooted, falsey_rooted = arms(node).map { |arm| rooted?(arm, scope, depth, fold: false) }
|
|
126
|
+
return truthy_rooted if truthy_rooted == falsey_rooted
|
|
127
|
+
return false unless fold
|
|
128
|
+
|
|
129
|
+
case predicate_certainty(node.predicate, scope)
|
|
130
|
+
when :truthy then truthy_rooted
|
|
131
|
+
when :falsey then falsey_rooted
|
|
71
132
|
else false
|
|
72
133
|
end
|
|
73
134
|
end
|
|
74
135
|
|
|
75
|
-
|
|
76
|
-
|
|
136
|
+
# `[runs when the predicate is truthy, runs when it is falsey]`.
|
|
137
|
+
def arms(node)
|
|
138
|
+
node.is_a?(Prism::IfNode) ? [node.statements, node.subsequent] : [node.else_clause, node.statements]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# The arm certainty `StatementEvaluator#branch_certainty` elides on: a decidable version guard, else the
|
|
142
|
+
# predicate's folded truthiness unless an optimistic carrier props it up.
|
|
143
|
+
def predicate_certainty(predicate, scope)
|
|
144
|
+
guard = Inference::VersionGuard.verdict(predicate)
|
|
145
|
+
return guard if guard
|
|
146
|
+
return nil unless Inference::OptimisticOrigin.resolve(predicate, scope).nil?
|
|
147
|
+
|
|
148
|
+
Inference::Narrowing.predicate_certainty(scope.type_of(predicate))
|
|
77
149
|
end
|
|
78
150
|
end
|
|
79
151
|
end
|
|
@@ -44,8 +44,9 @@ module Rigor
|
|
|
44
44
|
NODE_CLASSES = [Prism::DefNode, Prism::CallNode].freeze
|
|
45
45
|
RULE_WALK_GATES = %i[inside_def detached_ivar_facet].freeze
|
|
46
46
|
|
|
47
|
-
#
|
|
48
|
-
# method_name
|
|
47
|
+
# Groups the writes by class qualified name, then by ivar name. Each write carries `node`, `type`,
|
|
48
|
+
# `declared`, `method_name`, `visibility`, `per_invocation` and `action`. Empty when the tree has no
|
|
49
|
+
# qualifying writes.
|
|
49
50
|
# `declared` is the translated type of the class's RBS declaration of the ivar — an `@x: T` member or
|
|
50
51
|
# the slot an `attr_*` member implies, own or inherited — and nil when the RBS declares none.
|
|
51
52
|
#
|
|
@@ -12,16 +12,13 @@ module Rigor
|
|
|
12
12
|
# decides, with NO loop / block suppression and NO threaded context — so the collector declares no
|
|
13
13
|
# `RULE_WALK_GATES` and ignores the walk's `context`.
|
|
14
14
|
#
|
|
15
|
-
# The per-node dispatch
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# accumulated {Diagnostic}s, in the same emission order the inline `NodeWalker.each` produced them (the
|
|
20
|
-
# shared walk is the same visit-before-descend DFS over `compact_child_nodes`).
|
|
15
|
+
# The per-node dispatch stays in `CheckRules` (ADR-53 WD4) and is handed in as a callable built in the
|
|
16
|
+
# `CheckRules` module context, so its calls to the private diagnostic builders remain implicit-self. The
|
|
17
|
+
# collector only owns the traversal hook and accumulation; `#results` are the accumulated {Diagnostic}s in
|
|
18
|
+
# the walk's emission order.
|
|
21
19
|
class MainPassCollector
|
|
22
20
|
# The node classes the main pass branches on. A plain `Prism::IfNode` covers ternaries and postfix `if`
|
|
23
|
-
# too
|
|
24
|
-
# classes carry issue #1367's `global.*` rules.
|
|
21
|
+
# too. The global-write classes and `MultiWriteNode` carry issue #1367's `global.*` rules.
|
|
25
22
|
NODE_CLASSES = [
|
|
26
23
|
Prism::CallNode, Prism::DefNode, Prism::IfNode, Prism::UnlessNode,
|
|
27
24
|
Prism::GlobalVariableWriteNode, Prism::GlobalVariableOperatorWriteNode, Prism::MultiWriteNode
|
|
@@ -19,7 +19,7 @@ module Rigor
|
|
|
19
19
|
# branched on, whose declaration the reader's author never opened. A configuration constant read in ten
|
|
20
20
|
# files would put a warning in all ten.
|
|
21
21
|
#
|
|
22
|
-
# This is [ADR-58](../../../../docs/adr/58-
|
|
22
|
+
# This is [ADR-58](../../../../docs/adr/58-ivar-field-typing.md)'s discipline applied to a
|
|
23
23
|
# different carrier: "a consumer may only ever *withhold* a firing it would otherwise make, so the mark
|
|
24
24
|
# can lose precision but can never manufacture a false positive"
|
|
25
25
|
# (`docs/internal-spec/inference-engine.md` § "Carrier and discipline"). The carrier is deliberately NOT
|
|
@@ -39,8 +39,8 @@ module Rigor
|
|
|
39
39
|
SEND_CALLS = %i[send __send__ public_send].freeze
|
|
40
40
|
# Reading the block through these runs it with its own `self`, or does not run it.
|
|
41
41
|
PLAIN_PROC_CALLS = %i[call yield [] === ! arity lambda? parameters nil?].freeze
|
|
42
|
-
# Calls, on any receiver, that can reach a local by name:
|
|
43
|
-
# the `*_eval` family (with an argument)
|
|
42
|
+
# Calls, on any receiver, that can reach a local by name: `binding`, `eval`, `local_variable_get`. The string
|
|
43
|
+
# forms of the `*_eval` family (`STRING_EVAL_CALLS`, with an argument) run in the caller's scope too.
|
|
44
44
|
BINDING_CALLS = %i[binding eval local_variable_get].freeze
|
|
45
45
|
STRING_EVAL_CALLS = %i[instance_eval class_eval module_eval].freeze
|
|
46
46
|
private_constant :STRING_EVAL_CALLS
|
|
@@ -96,7 +96,7 @@ module Rigor
|
|
|
96
96
|
found
|
|
97
97
|
end
|
|
98
98
|
|
|
99
|
-
# `
|
|
99
|
+
# The project's named `def` row counts, keyed by method name and then by owner and side; built once per file.
|
|
100
100
|
def def_rows(scope)
|
|
101
101
|
@def_rows ||= RebindingBlockCallees.def_row_counts(scope)
|
|
102
102
|
end
|
|
@@ -55,8 +55,7 @@ module Rigor
|
|
|
55
55
|
collect_ensure_returns(begin_node)
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
-
#
|
|
59
|
-
# `#collect` returns it.
|
|
58
|
+
# One `{return_node:}` entry per offending `return`, frozen the same way `#collect` returns them.
|
|
60
59
|
def results
|
|
61
60
|
@results.freeze
|
|
62
61
|
end
|