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
|
@@ -28,14 +28,14 @@ module Rigor
|
|
|
28
28
|
ALLOWED_KEY_CLASSES = [Symbol, String, Integer, Float, TrueClass, FalseClass, NilClass].freeze
|
|
29
29
|
EXTRA_KEY_POLICIES = %i[open closed].freeze
|
|
30
30
|
POLICY_KEYWORDS = %i[required_keys optional_keys read_only_keys extra_keys].freeze
|
|
31
|
-
# A Symbol whose text matches renders as a bare RBS record key (`lang:`) in {#erase_to_rbs};
|
|
32
|
-
# anything else is a quoted Symbol with a fat arrow (`:"data-contrast" =>`). See {#
|
|
31
|
+
# A Symbol whose text matches renders as a bare RBS record key (`lang:`) in {#describe} and {#erase_to_rbs};
|
|
32
|
+
# anything else is a quoted Symbol with a fat arrow (`:"data-contrast" =>`). See {#key_prefix}.
|
|
33
33
|
BARE_RECORD_KEY = /\A[A-Za-z_][A-Za-z0-9_]*[?!]?\z/
|
|
34
34
|
|
|
35
35
|
attr_reader :pairs, :required_keys, :optional_keys, :read_only_keys, :extra_keys
|
|
36
36
|
|
|
37
37
|
# @param pairs — ordered map of
|
|
38
|
-
# keys to declared types. Keys MUST be
|
|
38
|
+
# keys to declared types. Keys MUST be one of ALLOWED_KEY_CLASSES;
|
|
39
39
|
# values MUST be Rigor::Type instances. The hash is duped and
|
|
40
40
|
# frozen at construction; callers MUST NOT mutate the input
|
|
41
41
|
# afterwards (mutation does not affect the carrier, but the
|
|
@@ -74,7 +74,7 @@ module Rigor
|
|
|
74
74
|
return hash_erasure unless closed?
|
|
75
75
|
return hash_erasure if pairs.each_key.any? { |k| !k.is_a?(Symbol) || !record_key_spellable?(k) }
|
|
76
76
|
|
|
77
|
-
rendered = pairs.map { |k, v| "#{
|
|
77
|
+
rendered = pairs.map { |k, v| "#{key_prefix(k)} #{v.erase_to_rbs}" }
|
|
78
78
|
"{ #{rendered.join(', ')} }"
|
|
79
79
|
end
|
|
80
80
|
|
|
@@ -193,46 +193,27 @@ module Rigor
|
|
|
193
193
|
def render_entry(key, value, verbosity)
|
|
194
194
|
prefix = []
|
|
195
195
|
prefix << "readonly" if read_only_key?(key)
|
|
196
|
-
|
|
197
|
-
prefix << "#{rendered_key}#{key_separator(key)}"
|
|
196
|
+
prefix << key_prefix(key)
|
|
198
197
|
"#{prefix.join(' ')} #{value.describe(verbosity)}"
|
|
199
198
|
end
|
|
200
199
|
|
|
201
|
-
#
|
|
202
|
-
#
|
|
203
|
-
#
|
|
204
|
-
#
|
|
205
|
-
#
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
# Symbol / String keys keep the compact colon form (`a: 1`, `"k": 2`); every other scalar key uses
|
|
214
|
-
# the hashrocket (`1 => 2`) — the form the user would have to write in source.
|
|
215
|
-
def key_separator(key)
|
|
216
|
-
case key
|
|
217
|
-
when Symbol, String then ":"
|
|
218
|
-
else " =>"
|
|
219
|
-
end
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
# An RBS record entry's key + separator, for the {#erase_to_rbs} path (which only ever sees
|
|
223
|
-
# Symbol keys — string-keyed shapes degrade to `Hash[...]` before reaching here). A Symbol whose
|
|
224
|
-
# text is a plain identifier is a bare key with a colon (`lang:`, Ruby keywords like `class`
|
|
225
|
-
# included). Any other Symbol is a quoted Symbol literal with a fat arrow (`:"data-contrast" =>`):
|
|
226
|
-
# RBS rejects a bare non-identifier (`data-contrast:`) and a quoted key with a colon
|
|
227
|
-
# (`"data-contrast":`), and reads `"data-contrast" =>` as the String key, so the written record
|
|
228
|
-
# declared a key the body never has (#2176). Optional keys carry a leading `?`.
|
|
229
|
-
def erase_key_prefix(key)
|
|
200
|
+
# A key and its separator as RBS spells them, with a leading `?` for an optional key. `describe`
|
|
201
|
+
# (the diagnostic display) and {#erase_to_rbs} (which only ever sees Symbol keys — string-keyed
|
|
202
|
+
# shapes degrade to `Hash[...]` before reaching here) share it, so a key reads the same in a
|
|
203
|
+
# message as in the signature it erases to. A Symbol whose text is a plain identifier is a bare
|
|
204
|
+
# key with a colon (`lang:`, Ruby keywords like `class` included). Any other Symbol is a quoted
|
|
205
|
+
# Symbol literal with a fat arrow (`:"data-contrast" =>`): RBS rejects a bare non-identifier
|
|
206
|
+
# (`data-contrast:`) and a quoted key with a colon (`"data-contrast":`), and reads
|
|
207
|
+
# `"data-contrast" =>` as the String key, so the written record declared a key the body never has
|
|
208
|
+
# (#2176). Every other key, a String included, is its `inspect` literal with a fat arrow
|
|
209
|
+
# (`"a-b" =>`, `1 =>`, `nil =>`): the compact `"a-b":` would read as a Symbol key in Ruby, so a
|
|
210
|
+
# String-vs-Symbol key mismatch in a diagnostic was unreadable (#2235).
|
|
211
|
+
def key_prefix(key)
|
|
230
212
|
optional = optional_key?(key) ? "?" : ""
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
end
|
|
213
|
+
return "#{optional}#{key.inspect} =>" unless key.is_a?(Symbol)
|
|
214
|
+
return "#{optional}#{key}:" if BARE_RECORD_KEY.match?(key.to_s)
|
|
215
|
+
|
|
216
|
+
"#{optional}:#{key.to_s.inspect} =>"
|
|
236
217
|
end
|
|
237
218
|
|
|
238
219
|
# `String#inspect` escapes `#{`, `#$` and `#@` as `\#`, which RBS's parser does not read back as
|
data/lib/rigor/type/union.rb
CHANGED
|
@@ -49,7 +49,7 @@ module Rigor
|
|
|
49
49
|
end
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
-
# ADR-1 § "
|
|
52
|
+
# ADR-1 § "Erasure Must Be Conservative" + the value-lattice rule `untyped | T = untyped` (every `T`
|
|
53
53
|
# is gradually consistent with `untyped`). When any union member erases to `"untyped"`, the whole
|
|
54
54
|
# union erases to `"untyped"` — the RBS surface has no carrier for "Dynamic-origin alongside a
|
|
55
55
|
# static facet", and the gradual-consistency contract guarantees the substitution is sound at every
|
|
@@ -22,8 +22,7 @@ module Rigor
|
|
|
22
22
|
# Freeze the String field so the resulting Data object is `Ractor.shareable?` regardless of
|
|
23
23
|
# whether the caller passed a `# frozen_string_literal: true` constant or a dynamically built
|
|
24
24
|
# String. The same discipline applies to every other TypeNode value object — they live in the
|
|
25
|
-
# parser's hot path and are the natural carriers to flow through future Ractor boundaries
|
|
26
|
-
# (see CURRENT_WORK Open Items #8).
|
|
25
|
+
# parser's hot path and are the natural carriers to flow through future Ractor boundaries.
|
|
27
26
|
super(name: name.frozen? ? name : name.dup.freeze)
|
|
28
27
|
end
|
|
29
28
|
end
|
|
@@ -16,7 +16,7 @@ module Rigor
|
|
|
16
16
|
# resolver in the chain plus the built-in registry through the
|
|
17
17
|
# same entry point.
|
|
18
18
|
#
|
|
19
|
-
#
|
|
19
|
+
# Built by `Environment#build_name_scope` from
|
|
20
20
|
# `Plugin::Registry#type_node_resolvers`. The chain is
|
|
21
21
|
# immutable and re-entrant; the parser may consult it many
|
|
22
22
|
# times for the same node.
|
|
@@ -7,8 +7,7 @@ module Rigor
|
|
|
7
7
|
# `Pick[T, "name"]`, `pick_of[Shape, "a" | "b"]`, and downstream
|
|
8
8
|
# plugin resolvers that accept literal key selectors.
|
|
9
9
|
#
|
|
10
|
-
# ADR-13 follow-up
|
|
11
|
-
# #2): the RBS::Extended grammar previously could not tokenise
|
|
10
|
+
# ADR-13 follow-up: the RBS::Extended grammar previously could not tokenise
|
|
12
11
|
# `"name"` inside a type-arg position. The resolver translates
|
|
13
12
|
# this node to a `Type::Constant` carrying the string value.
|
|
14
13
|
# Slice 1 supports double-quoted strings without escape
|
|
@@ -7,8 +7,7 @@ module Rigor
|
|
|
7
7
|
# `Pick[T, :name]`, `pick_of[Shape, :a | :b]`, and downstream
|
|
8
8
|
# plugin resolvers that accept literal key selectors.
|
|
9
9
|
#
|
|
10
|
-
# ADR-13 follow-up
|
|
11
|
-
# #2): the RBS::Extended grammar previously could not tokenise
|
|
10
|
+
# ADR-13 follow-up: the RBS::Extended grammar previously could not tokenise
|
|
12
11
|
# `:name` inside a type-arg position; this addition closes the
|
|
13
12
|
# gap so `ImportedRefinements.parse` produces a uniform AST.
|
|
14
13
|
# The resolver translates this node to a `Type::Constant`
|
|
@@ -5,8 +5,7 @@ module Rigor
|
|
|
5
5
|
# Union AST node. Carries the nodes of a `T1 | T2 | …` union
|
|
6
6
|
# expression authored at type-arg position.
|
|
7
7
|
#
|
|
8
|
-
# ADR-13 follow-up
|
|
9
|
-
# #2): together with {SymbolLiteral} / {StringLiteral} this
|
|
8
|
+
# ADR-13 follow-up: together with {SymbolLiteral} / {StringLiteral} this
|
|
10
9
|
# closes the parser-side gap that prevented `Pick[T, :a | :b]`
|
|
11
10
|
# / `Pick[T, "a" | "b"]` from tokenising. The resolver pass
|
|
12
11
|
# recurses through each node and folds them via
|
|
@@ -16,10 +15,8 @@ module Rigor
|
|
|
16
15
|
# shadowing `Data#members`, which is the introspection method
|
|
17
16
|
# every `Data.define` mix-in inherits.
|
|
18
17
|
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
# `Union` nodes are permitted but the parser does not
|
|
22
|
-
# synthesise them by itself.
|
|
18
|
+
# The parser builds a flat list (left-associative); the constructor
|
|
19
|
+
# rejects a nested `Union` node.
|
|
23
20
|
class Union < Data.define(:nodes)
|
|
24
21
|
def initialize(nodes:)
|
|
25
22
|
unless nodes.is_a?(Array) && nodes.size >= 2 && nodes.all? { |m| valid_member?(m) }
|
|
@@ -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,7 +18,7 @@ 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
|