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
|
@@ -46,16 +46,7 @@ module Rigor
|
|
|
46
46
|
key = (["self.class"] + fields.map(&:to_s)).join(", ")
|
|
47
47
|
|
|
48
48
|
# Codegen (not per-call reflection) so the generated `==` / `hash` read fields
|
|
49
|
-
# directly and run at hand-written speed on the inference hot path.
|
|
50
|
-
# `value_fields :class_name, :type_args` the emitted source is:
|
|
51
|
-
#
|
|
52
|
-
# def ==(other)
|
|
53
|
-
# other.is_a?(self.class) && class_name == other.class_name && type_args == other.type_args
|
|
54
|
-
# end
|
|
55
|
-
# alias_method :eql?, :==
|
|
56
|
-
# def hash
|
|
57
|
-
# [self.class, class_name, type_args].hash
|
|
58
|
-
# end
|
|
49
|
+
# directly and run at hand-written speed on the inference hot path.
|
|
59
50
|
class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
|
|
60
51
|
def ==(other)
|
|
61
52
|
other.is_a?(self.class)#{guard}
|
data/lib/rigor/version.rb
CHANGED
|
@@ -167,8 +167,8 @@ module Rigor
|
|
|
167
167
|
end
|
|
168
168
|
|
|
169
169
|
# Walks the channel body recursively (so `stream_from` / `stream_for` calls inside `subscribed`
|
|
170
|
-
# / helper methods are picked up). Returns
|
|
171
|
-
#
|
|
170
|
+
# / helper methods are picked up). Returns the literal stream names and whether any dynamic
|
|
171
|
+
# registration was seen, as a pair.
|
|
172
172
|
def collect_stream_registrations(node, names: [], dynamic: false)
|
|
173
173
|
return [names, dynamic] if node.nil?
|
|
174
174
|
|
|
@@ -11,8 +11,8 @@ module Rigor
|
|
|
11
11
|
# `subscription.perform("action_name", data)`.
|
|
12
12
|
# - `stream_names` — the set of literal-string stream names registered via `stream_from "name"`
|
|
13
13
|
# calls inside the channel body. Dynamic registrations (`stream_from interpolated_string`) are
|
|
14
|
-
# recorded separately as `dynamic_streams: true
|
|
15
|
-
# warnings
|
|
14
|
+
# recorded separately as `dynamic_streams: true`; when any channel has one, the analyzer suppresses
|
|
15
|
+
# unknown-stream warnings for the whole run.
|
|
16
16
|
class ChannelIndex
|
|
17
17
|
Entry = Data.define(:class_name, :file_path, :action_methods, :stream_names, :dynamic_streams) do
|
|
18
18
|
def includes_action?(name)
|
|
@@ -59,7 +59,6 @@ module Rigor
|
|
|
59
59
|
@by_name.keys
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
-
# All literal stream names registered across every discovered channel.
|
|
63
62
|
def all_stream_names
|
|
64
63
|
@entries.flat_map { |e| e.stream_names.to_a }.to_set
|
|
65
64
|
end
|
|
@@ -12,7 +12,7 @@ module Rigor
|
|
|
12
12
|
# rigor-actioncable — validates ActionCable `<Channel>.broadcast_to(...)` and
|
|
13
13
|
# `ActionCable.server.broadcast(stream_name, ...)` call sites against the discovered channel index.
|
|
14
14
|
#
|
|
15
|
-
# Tier 3F of the [Rails plugins roadmap](
|
|
15
|
+
# Tier 3F of the [Rails plugins roadmap](../../../../../docs/design/20260508-rails-plugins-roadmap.md).
|
|
16
16
|
# Statically discovers channel classes by walking `channel_search_paths` and parsing each file with
|
|
17
17
|
# Prism — no `actioncable` runtime dependency.
|
|
18
18
|
#
|
|
@@ -19,9 +19,9 @@ module Rigor
|
|
|
19
19
|
# - Reads the instance-side `def` nodes and records each one as an action method, capturing the
|
|
20
20
|
# arity envelope.
|
|
21
21
|
# - For each (class, action) pair, attempts to read every candidate view template under
|
|
22
|
-
# `app/views/<mailer_underscore>/<action>.{html,text}.erb`. Existing templates feed the
|
|
22
|
+
# `app/views/<mailer_underscore>/<action>.{html,text}.{erb,haml,slim}`. Existing templates feed the
|
|
23
23
|
# IoBoundary's cache descriptor (so the cache invalidates when the template changes); missing
|
|
24
|
-
# templates are recorded so the plugin can surface a diagnostic on the
|
|
24
|
+
# templates are recorded so the plugin can surface a diagnostic on the action's `def`.
|
|
25
25
|
#
|
|
26
26
|
# Limitations (intentional for v0.1.0):
|
|
27
27
|
#
|
|
@@ -30,9 +30,6 @@ module Rigor
|
|
|
30
30
|
# - Action methods are read from the syntactic instance-side `def` list. Methods built via
|
|
31
31
|
# `define_method`, `private`, or non-action helpers (e.g. methods starting with `_`) are out of
|
|
32
32
|
# scope. The discoverer filters obvious non-actions (`initialize`, names prefixed with `_`).
|
|
33
|
-
# - Adding a brand-new view file under `app/views/<mailer>/` will NOT invalidate the cached index
|
|
34
|
-
# until something the mailer file touches changes. This is the standard read-tracking trade-off
|
|
35
|
-
# — only files we successfully read get digested into the descriptor.
|
|
36
33
|
class MailerDiscoverer
|
|
37
34
|
DEFAULT_VIEWS_ROOT = "app/views"
|
|
38
35
|
VIEW_FORMATS = %w[html text].freeze
|
|
@@ -56,22 +53,22 @@ module Rigor
|
|
|
56
53
|
# Two-pass: first collect every module's defs (for the include-following step), then build
|
|
57
54
|
# per-class entries that pull in actions from include'd modules. GitLab's `Notify` mailer
|
|
58
55
|
# derives every action from `Emails::*` concerns under `app/mailers/emails/`.
|
|
59
|
-
module_actions = {} #
|
|
60
|
-
class_visits = [] # collected (class_name, path, def_nodes, includes)
|
|
56
|
+
module_actions = {} # fully-qualified module name => its action entries, keyed by method name
|
|
57
|
+
class_visits = [] # collected (class_name, path, def_nodes, includes, view_exempt)
|
|
61
58
|
|
|
62
59
|
ruby_files_under(@search_paths).each do |path|
|
|
63
60
|
contents = read_safely(path)
|
|
64
61
|
next if contents.nil?
|
|
65
62
|
|
|
66
63
|
tree = Prism.parse(contents).value
|
|
67
|
-
walk_for_mailers(tree, []) do |class_name, def_nodes, includes|
|
|
68
|
-
class_visits << [class_name, path, def_nodes, includes]
|
|
64
|
+
walk_for_mailers(tree, []) do |class_name, def_nodes, includes, view_exempt|
|
|
65
|
+
class_visits << [class_name, path, def_nodes, includes, view_exempt]
|
|
69
66
|
end
|
|
70
67
|
collect_module_actions(tree, [], module_actions)
|
|
71
68
|
end
|
|
72
69
|
|
|
73
|
-
entries = class_visits.map do |class_name, path, def_nodes, includes|
|
|
74
|
-
build_class_entry(class_name, path, def_nodes, includes, module_actions)
|
|
70
|
+
entries = class_visits.map do |class_name, path, def_nodes, includes, view_exempt|
|
|
71
|
+
build_class_entry(class_name, path, def_nodes, includes, module_actions, view_exempt)
|
|
75
72
|
end
|
|
76
73
|
MailerIndex.new(entries)
|
|
77
74
|
end
|
|
@@ -112,9 +109,9 @@ module Rigor
|
|
|
112
109
|
full_name = declared_constant_name(class_local_name, lexical_path)
|
|
113
110
|
superclass = strip_root(constant_path_name(node.superclass)) if node.superclass
|
|
114
111
|
if superclass && @base_classes.include?(superclass)
|
|
115
|
-
def_nodes = collect_action_defs(node.body)
|
|
112
|
+
def_nodes, view_exempt = collect_action_defs(node.body)
|
|
116
113
|
includes = collect_includes(node.body)
|
|
117
|
-
yield full_name, def_nodes, includes
|
|
114
|
+
yield full_name, def_nodes, includes, view_exempt
|
|
118
115
|
end
|
|
119
116
|
|
|
120
117
|
walk_for_mailers(node.body, [full_name], &) if node.body
|
|
@@ -139,8 +136,7 @@ module Rigor
|
|
|
139
136
|
end
|
|
140
137
|
|
|
141
138
|
# Walks the AST collecting every module's instance-side def nodes by fully-qualified module
|
|
142
|
-
# name. The same `collect_action_defs` filter applies (private / `_`-prefixed
|
|
143
|
-
# methods skipped).
|
|
139
|
+
# name. The same `collect_action_defs` filter applies (private / `_`-prefixed skipped).
|
|
144
140
|
def collect_module_actions(node, lexical_path, accumulator)
|
|
145
141
|
return if node.nil?
|
|
146
142
|
|
|
@@ -151,8 +147,7 @@ module Rigor
|
|
|
151
147
|
|
|
152
148
|
full_name = declared_constant_name(local_name, lexical_path)
|
|
153
149
|
if node.body
|
|
154
|
-
|
|
155
|
-
entries = def_nodes.to_h { |def_node| [def_node.name, build_action_entry(def_node)] }
|
|
150
|
+
entries = collect_action_defs(node.body).first.to_h { |def_node| [def_node.name, build_action_entry(def_node)] }
|
|
156
151
|
# UNIONed, never assigned: a concern module reopened in a second file adds actions rather
|
|
157
152
|
# than replacing the ones the first declaration spelled (same rule as {MailerIndex}'s
|
|
158
153
|
# per-class merge).
|
|
@@ -220,40 +215,44 @@ module Rigor
|
|
|
220
215
|
# `private` / `protected` (until a `public` switches back), named by `private :foo` /
|
|
221
216
|
# `protected :foo`, or defined inline (`private def foo`) — Rails builds `action_methods` from the
|
|
222
217
|
# public instance methods. `public :foo` after a `protected` section makes `foo` an action again.
|
|
223
|
-
#
|
|
218
|
+
# Methods named by a callback or `helper_method` macro (see {#collect_callback_names}) stay actions:
|
|
219
|
+
# Rails dispatches any public instance method, so `Mailer.target` still runs. They are returned as the
|
|
220
|
+
# view-exempt names, which exempt them from the missing-view check only.
|
|
224
221
|
#
|
|
225
222
|
# Pre-fix, Mastodon's `AdminMailer#process_params` / `set_instance` / `set_locale` /
|
|
226
223
|
# `set_important_headers!` all surfaced as missing-view because the bare `private` keyword
|
|
227
224
|
# wasn't honoured. ~19 false positives across Mastodon's mailers.
|
|
228
|
-
|
|
229
|
-
private_constant :CALLBACK_DECLARATIONS
|
|
230
|
-
|
|
231
|
-
VISIBILITY_KEYWORDS = %i[public private protected].freeze
|
|
225
|
+
VISIBILITY_KEYWORDS = %i[public private protected module_function].freeze
|
|
232
226
|
private_constant :VISIBILITY_KEYWORDS
|
|
233
227
|
|
|
228
|
+
# Returns `[action_def_nodes, view_exempt_names]`. The view-exempt names are the callback and
|
|
229
|
+
# `helper_method` targets of the body: they remain actions, and only the missing-view check skips them.
|
|
234
230
|
def collect_action_defs(body)
|
|
235
|
-
return [] if body.nil?
|
|
231
|
+
return [[], Set[]] if body.nil?
|
|
236
232
|
|
|
237
233
|
callback_names = collect_callback_names(body)
|
|
238
234
|
def_nodes, final_visibility = scan_visibility(body)
|
|
239
235
|
|
|
240
|
-
def_nodes.select do |node|
|
|
236
|
+
actions = def_nodes.select do |node|
|
|
241
237
|
next false if node.receiver.is_a?(Prism::SelfNode)
|
|
242
238
|
next false if node.name == :initialize
|
|
243
239
|
next false if node.name.to_s.start_with?("_")
|
|
244
|
-
next false if callback_names.include?(node.name)
|
|
245
240
|
|
|
246
241
|
# Rails builds `action_methods` from the PUBLIC instance methods, so only a method whose final
|
|
247
242
|
# visibility is public is an action.
|
|
248
243
|
final_visibility[node.name] == :public
|
|
249
244
|
end
|
|
245
|
+
[actions, callback_names]
|
|
250
246
|
end
|
|
251
247
|
|
|
252
|
-
# Symbol arguments to
|
|
248
|
+
# Symbol arguments to the macros the manifest's `per_invocation_non_action_macros` lists (`before_action
|
|
249
|
+
# :setup`, `after_deliver :record`, `helper_method :fmt`). The `skip_*` forms are left out, since
|
|
250
|
+
# `skip_before_action :x` names a callback a parent defined and does not exempt `x` from anything (#2319).
|
|
253
251
|
def collect_callback_names(body)
|
|
252
|
+
macros = Actionmailer.manifest.per_invocation_non_action_macros.reject { |macro| macro.start_with?("skip_") }
|
|
254
253
|
names = []
|
|
255
254
|
body.rigor_each_child do |node|
|
|
256
|
-
next unless node.is_a?(Prism::CallNode) && node.receiver.nil? &&
|
|
255
|
+
next unless node.is_a?(Prism::CallNode) && node.receiver.nil? && macros.include?(node.name)
|
|
257
256
|
|
|
258
257
|
(node.arguments&.arguments || []).each do |arg|
|
|
259
258
|
symbol = Rigor::Source::Literals.symbol(arg)
|
|
@@ -265,8 +264,8 @@ module Rigor
|
|
|
265
264
|
|
|
266
265
|
# Walks the class body in source order and returns `[def_nodes, final_visibility]`: every `def` the body
|
|
267
266
|
# spells (a direct statement or the inline argument of a visibility call, `private def x`), and the
|
|
268
|
-
# visibility each method name ends with. A bare `public` / `private` / `protected`
|
|
269
|
-
# the defs after it; `private :x` / `public :x` / `protected def x` set that one method, whichever side of
|
|
267
|
+
# visibility each method name ends with. A bare `public` / `private` / `protected` / `module_function` sets
|
|
268
|
+
# the default for the defs after it; `private :x` / `public :x` / `protected def x` set that one method, whichever side of
|
|
270
269
|
# its `def` they fall on in the source, and the last statement to name a method wins.
|
|
271
270
|
def scan_visibility(body)
|
|
272
271
|
def_nodes = []
|
|
@@ -286,7 +285,8 @@ module Rigor
|
|
|
286
285
|
[def_nodes, final_visibility]
|
|
287
286
|
end
|
|
288
287
|
|
|
289
|
-
# Applies one receiverless `public` / `private` / `protected` call and returns the
|
|
288
|
+
# Applies one receiverless `public` / `private` / `protected` / `module_function` call and returns the
|
|
289
|
+
# (possibly new) default.
|
|
290
290
|
def apply_visibility_call(node, default, def_nodes, final_visibility)
|
|
291
291
|
return default unless node.receiver.nil? && VISIBILITY_KEYWORDS.include?(node.name)
|
|
292
292
|
|
|
@@ -304,9 +304,9 @@ module Rigor
|
|
|
304
304
|
default
|
|
305
305
|
end
|
|
306
306
|
|
|
307
|
-
def build_class_entry(class_name, file_path, def_nodes, includes = [], module_actions = {})
|
|
307
|
+
def build_class_entry(class_name, file_path, def_nodes, includes = [], module_actions = {}, view_exempt = Set[])
|
|
308
308
|
actions = def_nodes.to_h do |def_node|
|
|
309
|
-
entry = build_action_entry(def_node)
|
|
309
|
+
entry = build_action_entry(def_node, view_exempt: view_exempt.include?(def_node.name))
|
|
310
310
|
[entry.method_name, entry]
|
|
311
311
|
end
|
|
312
312
|
|
|
@@ -326,7 +326,7 @@ module Rigor
|
|
|
326
326
|
end
|
|
327
327
|
end
|
|
328
328
|
|
|
329
|
-
missing_views = actions.keys.reject { |action| view_exists?(class_name, action) }
|
|
329
|
+
missing_views = actions.reject { |_, entry| entry.view_exempt }.keys.reject { |action| view_exists?(class_name, action) }
|
|
330
330
|
|
|
331
331
|
MailerIndex::ClassEntry.new(
|
|
332
332
|
class_name: class_name,
|
|
@@ -337,7 +337,7 @@ module Rigor
|
|
|
337
337
|
)
|
|
338
338
|
end
|
|
339
339
|
|
|
340
|
-
def build_action_entry(def_node)
|
|
340
|
+
def build_action_entry(def_node, view_exempt: false)
|
|
341
341
|
parameters = def_node.parameters
|
|
342
342
|
location = def_node.name_loc
|
|
343
343
|
|
|
@@ -346,7 +346,8 @@ module Rigor
|
|
|
346
346
|
method_name: def_node.name,
|
|
347
347
|
min_arity: 0, max_arity: 0,
|
|
348
348
|
def_line: location.start_line,
|
|
349
|
-
def_column: location.start_column + 1
|
|
349
|
+
def_column: location.start_column + 1,
|
|
350
|
+
view_exempt: view_exempt
|
|
350
351
|
)
|
|
351
352
|
end
|
|
352
353
|
|
|
@@ -359,7 +360,8 @@ module Rigor
|
|
|
359
360
|
min_arity: required_count,
|
|
360
361
|
max_arity: rest_present ? Float::INFINITY : required_count + optional_count,
|
|
361
362
|
def_line: location.start_line,
|
|
362
|
-
def_column: location.start_column + 1
|
|
363
|
+
def_column: location.start_column + 1,
|
|
364
|
+
view_exempt: view_exempt
|
|
363
365
|
)
|
|
364
366
|
end
|
|
365
367
|
|
|
@@ -11,7 +11,9 @@ module Rigor
|
|
|
11
11
|
# the mailer file)
|
|
12
12
|
# - the list of `(action, location)` pairs whose view templates are missing from `app/views/`
|
|
13
13
|
class MailerIndex
|
|
14
|
-
|
|
14
|
+
# `view_exempt` marks a callback or `helper_method` target: still an action (Rails dispatches it), but
|
|
15
|
+
# left out of the missing-view check.
|
|
16
|
+
ActionEntry = Data.define(:method_name, :min_arity, :max_arity, :def_line, :def_column, :view_exempt) do
|
|
15
17
|
def arity_label
|
|
16
18
|
return "#{min_arity}+" if max_arity == Float::INFINITY
|
|
17
19
|
return min_arity.to_s if min_arity == max_arity
|
|
@@ -67,7 +69,7 @@ module Rigor
|
|
|
67
69
|
end
|
|
68
70
|
|
|
69
71
|
# @param file_path — absolute path of a mailer file (canonicalised — see plugin entry's
|
|
70
|
-
# `
|
|
72
|
+
# `canonical_path`)
|
|
71
73
|
def find_by_file(file_path)
|
|
72
74
|
@entries.find { |entry| entry.file_path == file_path }
|
|
73
75
|
end
|
|
@@ -11,7 +11,7 @@ module Rigor
|
|
|
11
11
|
module Plugin
|
|
12
12
|
# rigor-actionmailer — validates `Mailer.action(args)` call sites and detects missing view templates.
|
|
13
13
|
#
|
|
14
|
-
# Tier 1C of the [Rails plugins roadmap](
|
|
14
|
+
# Tier 1C of the [Rails plugins roadmap](../../../../../docs/design/20260508-rails-plugins-roadmap.md).
|
|
15
15
|
# Statically discovers mailer classes by walking `mailer_search_paths` and parsing each file with
|
|
16
16
|
# Prism — no `action_mailer` runtime dependency.
|
|
17
17
|
#
|
|
@@ -38,8 +38,6 @@ module Rigor
|
|
|
38
38
|
# - Direct-superclass match only.
|
|
39
39
|
# - Action methods are read from the syntactic instance-side `def` list. `define_method` actions are
|
|
40
40
|
# out of scope.
|
|
41
|
-
# - Adding a brand-new view file does not invalidate the cache until something the mailer file
|
|
42
|
-
# touches changes.
|
|
43
41
|
class Actionmailer < Rigor::Plugin::Base
|
|
44
42
|
manifest(
|
|
45
43
|
id: "actionmailer",
|
|
@@ -55,7 +53,11 @@ module Rigor
|
|
|
55
53
|
#
|
|
56
54
|
# Bumped 2026-10-23 (#2308) — a method after a bare `protected` is no longer an action, so a cached 0.5.0
|
|
57
55
|
# index can list protected helpers as actions (and as missing-view candidates).
|
|
58
|
-
|
|
56
|
+
#
|
|
57
|
+
# Bumped 2026-10-26 (#2319) — a method named by a callback or `helper_method` macro (`prepend_before_action`,
|
|
58
|
+
# `after_deliver`, `helper_method`, ...) no longer draws a missing-view row, and a method defined after a bare
|
|
59
|
+
# `module_function` is no longer an action, so a cached 0.5.1 index can carry those rows and actions.
|
|
60
|
+
version: "0.5.2",
|
|
59
61
|
description: "Validates ActionMailer call shape and view template existence.",
|
|
60
62
|
config_schema: {
|
|
61
63
|
"mailer_search_paths" => { kind: :array, default: ["app/mailers"] },
|
|
@@ -102,7 +102,7 @@ module Rigor
|
|
|
102
102
|
|
|
103
103
|
# Phase 2 — filter-chain validation for ONE filter-DSL call (`before_action :name`, …). The
|
|
104
104
|
# enclosing controller is read from the node-rule `NodeContext` ancestors, looked up in the
|
|
105
|
-
# controller index to get the effective method set (including
|
|
105
|
+
# controller index to get the effective method set (including inherited methods), and
|
|
106
106
|
# each filter name reference is validated against it. Calls outside a known controller
|
|
107
107
|
# contribute nothing.
|
|
108
108
|
#
|
|
@@ -6,8 +6,7 @@ module Rigor
|
|
|
6
6
|
# Per-run frozen index of discovered controllers AND concerns (modules) in
|
|
7
7
|
# `controller_search_paths`. Phase 2 (filter-chain validation) consults the index at every call
|
|
8
8
|
# site to check that `before_action :name` references a method that actually exists on the
|
|
9
|
-
# controller,
|
|
10
|
-
# chain transitively `include`s.
|
|
9
|
+
# controller, any ancestor, or any module the controller or an ancestor transitively `include`s.
|
|
11
10
|
#
|
|
12
11
|
# Real-world example (Mastodon):
|
|
13
12
|
# AccountsController < ApplicationController
|
|
@@ -169,9 +168,6 @@ module Rigor
|
|
|
169
168
|
# and returning the first candidate that has an entry in the index. When nothing resolves,
|
|
170
169
|
# return the original name unchanged — the caller treats unresolved entries as "gem-shipped
|
|
171
170
|
# concerns we cannot see" (the `unresolved_include?` predicate).
|
|
172
|
-
#
|
|
173
|
-
# `name` may already be qualified (`Foo::Bar`); we only try lexical prefixing when the
|
|
174
|
-
# unqualified first segment doesn't match a top-level entry.
|
|
175
171
|
def resolve_constant_lexically(name, enclosing)
|
|
176
172
|
return nil if name.nil?
|
|
177
173
|
|
|
@@ -229,7 +229,8 @@ module Rigor
|
|
|
229
229
|
# been measured. Resolved ONCE per process: a failed resolution is as memoised as a successful
|
|
230
230
|
# one, so a project without Erubi does not pay a `$LOAD_PATH` walk per template.
|
|
231
231
|
#
|
|
232
|
-
# `Isolation.require_with_target_bundle` is
|
|
232
|
+
# `Isolation.require_with_target_bundle` is
|
|
233
|
+
# [ADR-90](../../../../../../docs/adr/90-target-library-resolution-from-project-bundle.md)'s
|
|
233
234
|
# path — Rigor's own gem env first, then the analysed project's bundle, `$LOAD_PATH` appended and
|
|
234
235
|
# only on a failed require. Erubi is not a Rigor dependency and is never added to one. Any load
|
|
235
236
|
# failure is equally "no Erubi here"; stdlib `ERB` answers the same question, so nothing is
|
|
@@ -18,11 +18,11 @@ module Rigor
|
|
|
18
18
|
# ## The inference is deliberately narrow
|
|
19
19
|
#
|
|
20
20
|
# Only `@user = Model.find(...)`-shaped assignments contribute, and only for a closed set of
|
|
21
|
-
# constructors and finders that **cannot return nil** — `find`, `find_by!`, `sole`, `first!`,
|
|
21
|
+
# constructors and finders that **cannot return nil** — `find`, `find_by!`, `find_sole_by`, `sole`, `first!`,
|
|
22
22
|
# `last!`, `new`, `create`, `create!`. A seeded type is a claim the engine acts on, and a wrong one
|
|
23
23
|
# is worse than none: seeding `@user` from `find_by` (which returns nil at runtime) would hand the
|
|
24
24
|
# flow rules a non-nil nominal and license folds that draw diagnostics on correct templates — the
|
|
25
|
-
# trap `Actionpack::
|
|
25
|
+
# trap `Actionpack::PARAMS_ONLY_CHAIN_METHODS` documents at length for `Parameters#[]`. Anything
|
|
26
26
|
# else assigns nothing, the ivar stays unseeded, and the template reads it as `Dynamic`, which
|
|
27
27
|
# taints honestly (ADR-5). A call on that list that returns, or may return, SEVERAL records —
|
|
28
28
|
# `find(a, b)`, `find([1, 2])`, `create([{…}, {…}])` — is excluded by the same rule; see
|
|
@@ -228,7 +228,7 @@ module Rigor
|
|
|
228
228
|
# `case`, a `rescue`, a loop, or a block that may not run — does not contribute, because a seed
|
|
229
229
|
# is a claim about what the template will FIND, and `@user = User.find(1) if params[:pick]`
|
|
230
230
|
# leaves `@user` nil on the other path. A non-nil nominal standing in for a runtime nil is the
|
|
231
|
-
# `Parameters#[]` trap (`Actionpack::
|
|
231
|
+
# `Parameters#[]` trap (`Actionpack::PARAMS_ONLY_CHAIN_METHODS`): the flow rules act on it and
|
|
232
232
|
# report live branches, which is exactly what a reader of a template would see with
|
|
233
233
|
# `view_type_checks:` on. The unseeded ivar reads `Dynamic` instead and taints honestly (ADR-5).
|
|
234
234
|
#
|
|
@@ -20,7 +20,7 @@ module Rigor
|
|
|
20
20
|
# fact published by `rigor-rails-routes` (ADR-9 cross-plugin API) and validates every implicit-self
|
|
21
21
|
# `*_path` / `*_url` call inside files under `controller_search_paths` (default `app/controllers`).
|
|
22
22
|
#
|
|
23
|
-
# Tier 2 of the [Rails plugins roadmap](
|
|
23
|
+
# Tier 2 of the [Rails plugins roadmap](../../../../../docs/design/20260508-rails-plugins-roadmap.md).
|
|
24
24
|
# Phase 1 (strong-parameters → AR column validation), Phase 2 (filter chains), and Phase 3 (render
|
|
25
25
|
# targets) ship as separate slices; each phase composes additively under the same plugin id.
|
|
26
26
|
#
|
|
@@ -37,14 +37,10 @@ module Rigor
|
|
|
37
37
|
#
|
|
38
38
|
# ## What it checks
|
|
39
39
|
#
|
|
40
|
-
# - **
|
|
41
|
-
# the
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
# recorded arity (a trailing `KeywordHashNode` like `users_path(format: :json)` is excluded; same
|
|
45
|
-
# convention `rigor-rails-routes` uses). Mismatches emit `wrong-helper-arity`.
|
|
46
|
-
# - **Trace** — recognised helpers also emit a `helper-call` info diagnostic naming the action and
|
|
47
|
-
# path, mirroring the trace shape of the upstream plugin.
|
|
40
|
+
# - **Trace** — recognised `*_path` / `*_url` helpers emit a `helper-call` info diagnostic naming
|
|
41
|
+
# the action and path. Unknown helpers (`unknown-helper`) and arity mismatches (`wrong-arity`)
|
|
42
|
+
# are reported by `rigor-rails-routes`, which owns the helper table, so this plugin does not
|
|
43
|
+
# repeat them.
|
|
48
44
|
#
|
|
49
45
|
# ## Limitations
|
|
50
46
|
#
|
|
@@ -88,7 +84,9 @@ module Rigor
|
|
|
88
84
|
# the engine reads as "a template exists here and has no unit". The bump is belt-and-braces: what
|
|
89
85
|
# actually invalidates is the claim itself, through the glob row in `TemplateUnits#digest` and the
|
|
90
86
|
# `claimed_globs` equality the carry checks. A manifest version is in no cache key of its own.
|
|
91
|
-
|
|
87
|
+
# Bumped 2026-10-27 (#2430) — `params[...]` answers the gradual `Dynamic[Parameters | nil]` rather than
|
|
88
|
+
# the bare union, so a cached 1.5.0 run carries the non-gradual answer at every `params[:k]` argument.
|
|
89
|
+
version: "1.6.0",
|
|
92
90
|
description: "Validates Action Pack route-helper calls and filter chains inside controllers, and types the request-context readers (`params` / `session` / `request` / `flash`) and their chains.",
|
|
93
91
|
config_schema: {
|
|
94
92
|
"controller_search_paths" => { kind: :array, default: ["app/controllers"] },
|
|
@@ -475,21 +473,21 @@ module Rigor
|
|
|
475
473
|
# `ActionController::Parameters` (above), but the chained `params.require(:user).permit(:name)` /
|
|
476
474
|
# `params.permit(...)` calls returned `Dynamic` at the first hop (Parameters ships no bundled RBS,
|
|
477
475
|
# so a call on it resolves lenient-to-Dynamic), leaking every downstream site (`.permit`, `.to_h`,
|
|
478
|
-
# `.each`) to unprotected.
|
|
479
|
-
# `Parameters` receiver and re-type the result as the same
|
|
480
|
-
# concrete
|
|
481
|
-
#
|
|
482
|
-
#
|
|
483
|
-
#
|
|
484
|
-
#
|
|
485
|
-
#
|
|
486
|
-
#
|
|
487
|
-
#
|
|
488
|
-
#
|
|
489
|
-
#
|
|
490
|
-
#
|
|
491
|
-
#
|
|
492
|
-
# ## The admission rule for
|
|
476
|
+
# `.each`) to unprotected. The methods that return another `Parameters` (`PARAMS_ONLY_CHAIN_METHODS`)
|
|
477
|
+
# gate on a `Parameters` receiver and re-type the result as the same
|
|
478
|
+
# lenient nominal — the sites after them stay concrete receivers and `coverage --protection` counts
|
|
479
|
+
# them as protected. `require` / `expect` are the exception below: the receiver of the call right
|
|
480
|
+
# after one is the carrier, which is not a concrete receiver.
|
|
481
|
+
#
|
|
482
|
+
# `require` and `expect` may return a scalar at runtime (`params.require(:id) → String`), so the
|
|
483
|
+
# earlier "RBS-less nominal is FP-safe" reading of them (2026-07, #534) is withdrawn by #2947: it held
|
|
484
|
+
# only while Parameters shipped no RBS. Once the collection's declaration is loaded, a nominal answer
|
|
485
|
+
# reports a scalar's method as `call.undefined-method`. They answer the gradual carrier
|
|
486
|
+
# `Dynamic[Parameters]` (`SCALAR_CAPABLE_CHAIN_METHODS`), which keeps the chain resolvable and is quiet
|
|
487
|
+
# with or without the RBS. The GitLab strong-params survey (108 leaked `.permit` sites) is the demand.
|
|
488
|
+
# Issue #534 — `expect` and `slice` joined 2026-09-01 (63 and 38 sites on mastodon).
|
|
489
|
+
#
|
|
490
|
+
# ## The admission rule for the nominal table (`PARAMS_ONLY_CHAIN_METHODS`)
|
|
493
491
|
#
|
|
494
492
|
# A method belongs here **iff its Rails implementation returns an `ActionController::Parameters`
|
|
495
493
|
# on every path** — either a fresh `new_instance_with_inherited_permitted_status(...)` or `self`.
|
|
@@ -497,9 +495,9 @@ module Rigor
|
|
|
497
495
|
# entry FP-safe, because the RBS-less nominal buys leniency on the *method surface* only:
|
|
498
496
|
#
|
|
499
497
|
# - Method-surface rules DO decline on it. `call.undefined-method` bails at
|
|
500
|
-
# `Rigor::Reflection.rbs_class_known?` (lib/rigor/analysis/check_rules.rb
|
|
501
|
-
# ships no RBS for Parameters, and the argument / arity rules gate on the same predicate
|
|
502
|
-
#
|
|
498
|
+
# `Rigor::Reflection.rbs_class_known?` (lib/rigor/analysis/check_rules.rb) because Rigor
|
|
499
|
+
# ships no RBS for Parameters, and the argument / arity rules gate on the same predicate. So
|
|
500
|
+
# `params.slice(:a).whatever_rails_adds` never fires, and a scalar-shaped
|
|
503
501
|
# use of a Parameters-typed value (`.to_i`, `.strip`, string interpolation) resolves lenient
|
|
504
502
|
# -to-Dynamic rather than to a diagnostic.
|
|
505
503
|
# - **Flow rules do NOT.** Truthiness folding, ternary arm selection and `.nil?` folding key on
|
|
@@ -542,7 +540,7 @@ module Rigor
|
|
|
542
540
|
# and still carries the chain — `params[:user][:name]` types `Parameters?` and
|
|
543
541
|
# `params[:user].permit(:name)` types `Parameters`, because the dispatcher strips the nil arm
|
|
544
542
|
# for the receiver gate. Idiomatic *chained* reads stay silent too: `call.possible-nil-receiver`
|
|
545
|
-
# restricts itself to `Prism::LocalVariableReadNode` receivers (`CheckRules#nil_receiver_diagnostic`
|
|
543
|
+
# restricts itself to `Prism::LocalVariableReadNode` receivers (`CheckRules#nil_receiver_diagnostic`), so
|
|
546
544
|
# `params[:q].strip` cannot fire. What it does fire on is the assigned form — `q = params[:q];
|
|
547
545
|
# q.strip` — at **error** severity, once per unguarded use. Guarded forms (`return if q.nil?`,
|
|
548
546
|
# `if q`, `q&.strip`, `|| ""`) all narrow correctly and stay silent, so this is not a broken
|
|
@@ -564,20 +562,60 @@ module Rigor
|
|
|
564
562
|
# The control specs stay, and they still bite: they pin the HAZARD (no truthiness fold, no `bot`
|
|
565
563
|
# branch, no possible-nil firing), not the Dynamic answer, so they go red if either typing's
|
|
566
564
|
# hazard ever returns — and they now carry a positive assertion that `params[:k]` types
|
|
567
|
-
# `Parameters
|
|
568
|
-
|
|
569
|
-
|
|
565
|
+
# `Dynamic[ActionController::Parameters?]` (#2430), so a silent regression to bare `Dynamic[top]`
|
|
566
|
+
# or to the non-nil nominal fails them too.
|
|
567
|
+
# The links that return an `ActionController::Parameters` on every path (checked against Rails 8.x
|
|
568
|
+
# `strong_parameters.rb`). Only these may be typed as the nominal.
|
|
569
|
+
PARAMS_ONLY_CHAIN_METHODS = %i[
|
|
570
|
+
permit permit! slice
|
|
570
571
|
except without extract! slice!
|
|
571
572
|
merge merge! reverse_merge reverse_merge! with_defaults with_defaults!
|
|
572
573
|
compact compact_blank deep_dup
|
|
573
574
|
].freeze
|
|
574
575
|
|
|
575
|
-
|
|
576
|
+
# Issue #2947 — `require` and `expect` are NOT in the nominal table above. Rails returns the value itself:
|
|
577
|
+
# `require` is `self[key]` (a scalar, or an Array for an Array of keys) and `expect` is `values.first`
|
|
578
|
+
# for one key, else an Array. The "RBS-less nominal is lenient" argument holds only while Parameters
|
|
579
|
+
# ships no RBS; with the collection's declaration loaded, a nominal answer reports `.to_sym` on a
|
|
580
|
+
# scalar as `call.undefined-method`. They answer the gradual carrier `Dynamic[Parameters]` instead:
|
|
581
|
+
# quiet at every boundary, and the chain keeps resolving through the carrier-gated rule.
|
|
582
|
+
SCALAR_CAPABLE_CHAIN_METHODS = %i[require expect].freeze
|
|
583
|
+
|
|
584
|
+
STRONG_PARAMS_CHAIN_METHODS = (PARAMS_ONLY_CHAIN_METHODS + SCALAR_CAPABLE_CHAIN_METHODS).freeze
|
|
585
|
+
|
|
586
|
+
dynamic_return receivers: [REQUEST_CONTEXT_READER_TYPES[:params]], methods: PARAMS_ONLY_CHAIN_METHODS do |call_node, _scope|
|
|
576
587
|
next nil unless call_node.is_a?(Prism::CallNode)
|
|
577
588
|
|
|
578
589
|
Rigor::Type::Combinator.nominal_of(REQUEST_CONTEXT_READER_TYPES[:params])
|
|
579
590
|
end
|
|
580
591
|
|
|
592
|
+
dynamic_return receivers: [REQUEST_CONTEXT_READER_TYPES[:params]], methods: SCALAR_CAPABLE_CHAIN_METHODS do |call_node, _scope|
|
|
593
|
+
next nil unless call_node.is_a?(Prism::CallNode)
|
|
594
|
+
|
|
595
|
+
Rigor::Type::Combinator.dynamic(Rigor::Type::Combinator.nominal_of(REQUEST_CONTEXT_READER_TYPES[:params]))
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
# Issue #2430 / #2947 — the chain on a carrier receiver (`params[:user].permit(:name)`,
|
|
599
|
+
# `params.require(:user).permit(:name)`). A carrier often holds an Array or a String at runtime, and
|
|
600
|
+
# those define many of the chain names with other returns (`compact`, `slice`, `without`, `deep_dup`,
|
|
601
|
+
# `extract!`, ...), so only `permit` / `permit!` — which no core or ActiveSupport class defines — can
|
|
602
|
+
# answer the plain nominal there: such a call raises NoMethodError or returns a real Parameters, and
|
|
603
|
+
# the plain nominal keeps the sites after it concrete receivers for `coverage --protection`. Every
|
|
604
|
+
# other chain method stays the gradual carrier.
|
|
605
|
+
CARRIER_NOMINAL_CHAIN_METHODS = %i[permit permit!].freeze
|
|
606
|
+
|
|
607
|
+
dynamic_return methods: STRONG_PARAMS_CHAIN_METHODS do |call_node, scope|
|
|
608
|
+
next nil unless call_node.is_a?(Prism::CallNode)
|
|
609
|
+
|
|
610
|
+
receiver = call_node.receiver
|
|
611
|
+
next nil unless receiver
|
|
612
|
+
next nil unless controller_scope?(scope)
|
|
613
|
+
next nil unless params_carrier_facet?(scope.type_of(receiver))
|
|
614
|
+
|
|
615
|
+
nominal = Rigor::Type::Combinator.nominal_of(REQUEST_CONTEXT_READER_TYPES[:params])
|
|
616
|
+
CARRIER_NOMINAL_CHAIN_METHODS.include?(call_node.name) ? nominal : Rigor::Type::Combinator.dynamic(nominal)
|
|
617
|
+
end
|
|
618
|
+
|
|
581
619
|
# Issue #534 item 1, reopened by #574. `#[]` is the single largest named-receiver pair on both survey
|
|
582
620
|
# apps (redmine 581, mastodon ~475). The honest contract is `Parameters | nil` — a missing key really
|
|
583
621
|
# returns nil — and the block comment above records why that typing was REJECTED in 2026-09-01: it put
|
|
@@ -593,15 +631,56 @@ module Rigor
|
|
|
593
631
|
#
|
|
594
632
|
# The non-nil `-> Parameters` typing stays rejected on its own merits, unchanged: it makes every
|
|
595
633
|
# `params[:k]` condition always-truthy and folds a ternary over one to its true arm.
|
|
634
|
+
#
|
|
635
|
+
# Issue #2430 — the answer is a GRADUAL carrier, `Dynamic[Parameters | nil]`, not the bare union.
|
|
636
|
+
# A declared boundary (a `String` parameter, a `-> String` return) rejects the bare union, because
|
|
637
|
+
# `Acceptance` passes only a `Dynamic` through every boundary, and a scalar param is a `String` at
|
|
638
|
+
# runtime. The static facet keeps the strong-params chain typed: the carrier-gated rules below
|
|
639
|
+
# dispatch on the facet, so `params[:user][:name]` and `params[:user].permit(:name)` keep resolving.
|
|
640
|
+
# The control hazards (no truthiness fold, no `bot`, no possible-nil on `q = params[:q]`) are
|
|
641
|
+
# unchanged, because the facet is the same `Parameters | nil` union.
|
|
596
642
|
dynamic_return receivers: [REQUEST_CONTEXT_READER_TYPES[:params]], methods: %i[[]] do |call_node, _scope|
|
|
597
643
|
next nil unless call_node.is_a?(Prism::CallNode)
|
|
598
644
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
645
|
+
params_read_type
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
# Issue #2430 — `Dynamic` is not a `Nominal`, so the receiver-gated rules never see a carrier
|
|
649
|
+
# receiver. These two rules gate on the carrier's facet instead, and only inside a controller: a
|
|
650
|
+
# carrier is minted by the `params` reader, which is controller-only, so no other scope can hold one.
|
|
651
|
+
dynamic_return methods: %i[[]] do |call_node, scope|
|
|
652
|
+
next nil unless call_node.is_a?(Prism::CallNode)
|
|
653
|
+
|
|
654
|
+
receiver = call_node.receiver
|
|
655
|
+
next nil unless receiver
|
|
656
|
+
next nil unless controller_scope?(scope)
|
|
657
|
+
next nil unless params_carrier_facet?(scope.type_of(receiver))
|
|
658
|
+
|
|
659
|
+
params_read_type
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
# The `params` carrier's static facet as a `Dynamic`: `Parameters | nil`, gradual at every boundary.
|
|
663
|
+
def params_read_type
|
|
664
|
+
Rigor::Type::Combinator.dynamic(
|
|
665
|
+
Rigor::Type::Combinator.union(
|
|
666
|
+
Rigor::Type::Combinator.nominal_of(REQUEST_CONTEXT_READER_TYPES[:params]),
|
|
667
|
+
Rigor::Type::Combinator.constant_of(nil)
|
|
668
|
+
)
|
|
602
669
|
)
|
|
603
670
|
end
|
|
604
671
|
|
|
672
|
+
# True when `type` is a `Dynamic` whose facet is `Parameters` or `Parameters | nil`: the carrier a
|
|
673
|
+
# `params[...]` read answers, and the value a chained `[]` / strong-params call receives.
|
|
674
|
+
def params_carrier_facet?(type)
|
|
675
|
+
return false unless type.is_a?(Rigor::Type::Dynamic)
|
|
676
|
+
|
|
677
|
+
facet = type.static_facet
|
|
678
|
+
members = facet.is_a?(Rigor::Type::Union) ? facet.members : [facet]
|
|
679
|
+
non_nil = members.reject { |member| member.is_a?(Rigor::Type::Constant) && member.value.nil? }
|
|
680
|
+
non_nil.size == 1 && non_nil.first.is_a?(Rigor::Type::Nominal) &&
|
|
681
|
+
non_nil.first.class_name == REQUEST_CONTEXT_READER_TYPES[:params]
|
|
682
|
+
end
|
|
683
|
+
|
|
605
684
|
# Phase 5c (2026-09-02, issue #534 "same lane") — the rest of the request-context surface the
|
|
606
685
|
# corpus sweep measured after the #578 / #585 batch: `request.post?` 28 and `request.xhr?` 27 on
|
|
607
686
|
# the two apps, `flash.now` inside the redmine 147 / mastodon 29 flash cluster. Each is typed from
|