rigortype 0.4.5 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/data/gem_overlay/activesupport/core_ext.rbs +16 -13
- data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
- data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
- data/docs/handbook/01-getting-started.md +5 -2
- data/docs/handbook/11-sig-gen.md +61 -5
- data/docs/handbook/appendix-phpstan.md +4 -4
- data/docs/manual/02-cli-reference.md +192 -27
- data/docs/manual/03-configuration.md +38 -4
- data/docs/manual/04-diagnostics.md +1 -1
- data/docs/manual/06-baseline.md +6 -4
- data/docs/manual/09-editor-integration.md +13 -2
- data/docs/manual/10-mcp-server.md +47 -3
- data/docs/manual/12-caching.md +9 -1
- data/docs/manual/plugins/rigor-actionmailer.md +30 -0
- data/docs/manual/plugins/rigor-actionpack.md +49 -11
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
- data/docs/manual/plugins/rigor-devise.md +15 -0
- data/docs/manual/plugins/rigor-rspec.md +2 -1
- data/lib/rigor/analysis/baseline.rb +24 -27
- data/lib/rigor/analysis/buffer_binding.rb +14 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
- data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +105 -12
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +143 -15
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +4 -1
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
- data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
- data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
- data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +53 -2
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules.rb +217 -52
- data/lib/rigor/analysis/crash_signature.rb +2 -2
- data/lib/rigor/analysis/dependency_recorder.rb +15 -4
- data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
- data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
- data/lib/rigor/analysis/edit_replay.rb +154 -0
- data/lib/rigor/analysis/fact_store.rb +3 -3
- data/lib/rigor/analysis/incremental.rb +37 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
- data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
- data/lib/rigor/analysis/incremental_session.rb +202 -43
- data/lib/rigor/analysis/path_expansion.rb +41 -0
- data/lib/rigor/analysis/require_relative_closure.rb +100 -0
- data/lib/rigor/analysis/rule_catalog.rb +7 -3
- data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
- data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +8 -4
- data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +204 -96
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
- data/lib/rigor/analysis/runner/pool_coordinator.rb +157 -320
- data/lib/rigor/analysis/runner/project_pre_passes.rb +48 -6
- data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
- data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
- data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
- data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
- data/lib/rigor/analysis/runner.rb +233 -120
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
- data/lib/rigor/analysis/template_unit_positions.rb +2 -3
- data/lib/rigor/analysis/template_units.rb +9 -1
- data/lib/rigor/analysis/worker_session.rb +38 -33
- data/lib/rigor/bleeding_edge.rb +3 -6
- data/lib/rigor/builtins/imported_refinements.rb +6 -8
- data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
- data/lib/rigor/builtins/regex_refinement.rb +4 -6
- data/lib/rigor/cache/descriptor.rb +6 -5
- data/lib/rigor/cache/incremental_snapshot.rb +60 -18
- data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
- data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
- data/lib/rigor/cache/rbs_constant_table.rb +1 -1
- data/lib/rigor/cache/rbs_descriptor.rb +54 -3
- data/lib/rigor/cache/rbs_environment.rb +4 -1
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
- data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
- data/lib/rigor/cache/store.rb +11 -10
- data/lib/rigor/ci_detector.rb +1 -1
- data/lib/rigor/cli/annotate_command.rb +24 -19
- data/lib/rigor/cli/baseline_command.rb +145 -55
- data/lib/rigor/cli/catalogue.rb +256 -0
- data/lib/rigor/cli/check_command.rb +188 -93
- data/lib/rigor/cli/check_runner_factory.rb +2 -2
- data/lib/rigor/cli/command.rb +15 -1
- data/lib/rigor/cli/coverage_command.rb +21 -8
- data/lib/rigor/cli/coverage_mutation.rb +5 -7
- data/lib/rigor/cli/coverage_renderer.rb +2 -1
- data/lib/rigor/cli/coverage_scan.rb +3 -5
- data/lib/rigor/cli/diff_command.rb +27 -21
- data/lib/rigor/cli/docs_command.rb +1 -2
- data/lib/rigor/cli/doctor_command.rb +12 -12
- data/lib/rigor/cli/effects_command.rb +50 -40
- data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
- data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
- data/lib/rigor/cli/effects_renderer.rb +2 -1
- data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
- data/lib/rigor/cli/explain_command.rb +18 -8
- data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
- data/lib/rigor/cli/json_document.rb +41 -0
- data/lib/rigor/cli/lens_command.rb +17 -13
- data/lib/rigor/cli/lsp_command.rb +250 -23
- data/lib/rigor/cli/mcp_command.rb +14 -8
- data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
- data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
- data/lib/rigor/cli/options.rb +60 -4
- data/lib/rigor/cli/plugin_command.rb +93 -46
- data/lib/rigor/cli/plugins_command.rb +22 -18
- data/lib/rigor/cli/plugins_renderer.rb +34 -8
- data/lib/rigor/cli/protection_fork_scan.rb +2 -2
- data/lib/rigor/cli/protection_renderer.rb +2 -1
- data/lib/rigor/cli/show_bleedingedge_command.rb +23 -16
- data/lib/rigor/cli/sig_gen_command.rb +130 -34
- data/lib/rigor/cli/skill_command.rb +32 -3
- data/lib/rigor/cli/skill_deep_probe.rb +1 -1
- data/lib/rigor/cli/skill_describe.rb +1 -1
- data/lib/rigor/cli/trace_command.rb +14 -9
- data/lib/rigor/cli/triage_command.rb +10 -11
- data/lib/rigor/cli/triage_renderer.rb +2 -1
- data/lib/rigor/cli/type_of_command.rb +18 -12
- data/lib/rigor/cli/type_of_project_scope.rb +24 -0
- data/lib/rigor/cli/type_of_renderer.rb +4 -1
- data/lib/rigor/cli/type_scan_command.rb +14 -9
- data/lib/rigor/cli/type_scan_renderer.rb +2 -1
- data/lib/rigor/cli/unused_command.rb +27 -17
- data/lib/rigor/cli/upgrade_command.rb +5 -0
- data/lib/rigor/cli.rb +209 -54
- data/lib/rigor/config_audit.rb +17 -6
- data/lib/rigor/configuration/severity_profile.rb +5 -5
- data/lib/rigor/configuration.rb +86 -34
- data/lib/rigor/effects/definition_context.rb +0 -2
- data/lib/rigor/effects/envelope.rb +1 -1
- data/lib/rigor/effects/framework_units.rb +1 -1
- data/lib/rigor/effects/mutation_classifier.rb +4 -7
- data/lib/rigor/effects/propagator.rb +1 -1
- data/lib/rigor/effects/registry.rb +2 -2
- data/lib/rigor/effects/snapshot.rb +1 -1
- data/lib/rigor/effects/taint_cause.rb +0 -2
- data/lib/rigor/effects/unit_scan.rb +9 -13
- data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
- data/lib/rigor/environment/class_registry.rb +3 -3
- data/lib/rigor/environment/core_root.rb +77 -0
- data/lib/rigor/environment/default_libraries.rb +1 -1
- data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
- data/lib/rigor/environment/gated_signature_guard.rb +1 -1
- data/lib/rigor/environment/gated_signature_plan.rb +5 -2
- data/lib/rigor/environment/lockfile_resolver.rb +28 -12
- data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
- data/lib/rigor/environment/member_consistency.rb +56 -7
- data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
- data/lib/rigor/environment/project_path_gems.rb +124 -0
- data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
- data/lib/rigor/environment/rbs_loader.rb +592 -70
- data/lib/rigor/environment/reflection.rb +7 -7
- data/lib/rigor/environment/required_features.rb +136 -32
- data/lib/rigor/environment/ruby_library_roots.rb +122 -0
- data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
- data/lib/rigor/environment.rb +44 -11
- data/lib/rigor/flow_contribution/merger.rb +4 -4
- data/lib/rigor/inference/acceptance.rb +93 -32
- data/lib/rigor/inference/block_parameter_binder.rb +2 -3
- data/lib/rigor/inference/block_run_count.rb +178 -0
- data/lib/rigor/inference/budget_trace.rb +8 -2
- data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
- data/lib/rigor/inference/callee_mutation_chain.rb +126 -0
- data/lib/rigor/inference/callee_write_census.rb +815 -0
- data/lib/rigor/inference/construction_escape.rb +217 -0
- data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
- data/lib/rigor/inference/deep_erasure.rb +170 -0
- data/lib/rigor/inference/def_handle.rb +1 -1
- data/lib/rigor/inference/def_node_resolver.rb +1 -1
- data/lib/rigor/inference/def_return_typer.rb +204 -12
- data/lib/rigor/inference/definer_resolution.rb +21 -5
- data/lib/rigor/inference/definers_index.rb +106 -0
- data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
- data/lib/rigor/inference/element_read_widening.rb +49 -3
- data/lib/rigor/inference/error_info.rb +17 -3
- data/lib/rigor/inference/expression_typer.rb +173 -57
- data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
- data/lib/rigor/inference/global_write_census.rb +110 -1
- data/lib/rigor/inference/global_writer_reach.rb +260 -0
- data/lib/rigor/inference/hkt_body.rb +1 -2
- data/lib/rigor/inference/hkt_body_parser.rb +3 -5
- data/lib/rigor/inference/hkt_reducer.rb +2 -2
- data/lib/rigor/inference/hkt_registry.rb +3 -5
- data/lib/rigor/inference/indexed_narrowing.rb +4 -7
- data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
- data/lib/rigor/inference/ivar_writer_census.rb +246 -0
- data/lib/rigor/inference/key_presence_guard.rb +2 -1
- data/lib/rigor/inference/loop_value_record.rb +57 -0
- data/lib/rigor/inference/match_rebinding/frame.rb +65 -0
- data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
- data/lib/rigor/inference/match_rebinding.rb +11 -1
- data/lib/rigor/inference/memo_audit.rb +55 -0
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +113 -12
- data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -3
- data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
- data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +59 -24
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
- data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
- data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher.rb +109 -29
- data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
- data/lib/rigor/inference/module_function_state.rb +23 -1
- data/lib/rigor/inference/multi_target_binder.rb +1 -1
- data/lib/rigor/inference/mutation_rejoin.rb +240 -4
- data/lib/rigor/inference/mutation_widening.rb +162 -29
- data/lib/rigor/inference/narrowing.rb +113 -38
- data/lib/rigor/inference/object_mixins.rb +13 -1
- data/lib/rigor/inference/optimistic_origin.rb +1 -1
- data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
- data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
- data/lib/rigor/inference/pre_eval_constants.rb +1 -1
- data/lib/rigor/inference/project_method_ownership.rb +6 -3
- data/lib/rigor/inference/project_seed_tables.rb +2 -1
- data/lib/rigor/inference/project_yielder.rb +122 -0
- data/lib/rigor/inference/receiver_alias.rb +20 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
- data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
- data/lib/rigor/inference/remover_holder_scan.rb +233 -0
- data/lib/rigor/inference/remover_narrowing.rb +224 -0
- data/lib/rigor/inference/rewrite_mutation.rb +53 -2
- data/lib/rigor/inference/scope_indexer/certainty.rb +9 -1
- data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
- data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +205 -0
- data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
- data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
- data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
- data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
- data/lib/rigor/inference/scope_indexer.rb +614 -146
- data/lib/rigor/inference/self_call_ivar_writes.rb +101 -17
- data/lib/rigor/inference/set_mutation.rb +189 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
- data/lib/rigor/inference/statement_evaluator.rb +3036 -278
- data/lib/rigor/inference/struct_fold_safety.rb +4 -6
- data/lib/rigor/inference/synthetic_method_index.rb +23 -0
- data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +73 -14
- data/lib/rigor/inference/toplevel_def_census.rb +59 -0
- data/lib/rigor/inference/unknown_store_widening.rb +103 -4
- data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
- data/lib/rigor/inference/void_tail_summary.rb +2 -1
- data/lib/rigor/language_server/buffer_table.rb +9 -3
- data/lib/rigor/language_server/completion_provider.rb +26 -20
- data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +28 -9
- data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
- data/lib/rigor/language_server/folding_range_provider.rb +1 -2
- data/lib/rigor/language_server/hover_provider.rb +1 -7
- data/lib/rigor/language_server/hover_renderer.rb +6 -8
- data/lib/rigor/language_server/project_context.rb +98 -4
- data/lib/rigor/language_server/publish_batcher.rb +3 -4
- data/lib/rigor/language_server/selection_range_provider.rb +2 -4
- data/lib/rigor/language_server/server.rb +12 -6
- data/lib/rigor/language_server/signature_help_provider.rb +4 -4
- data/lib/rigor/lens/plugins.rb +1 -1
- data/lib/rigor/mcp/server.rb +82 -3
- data/lib/rigor/mcp.rb +1 -1
- data/lib/rigor/plugin/access_denied_error.rb +1 -2
- data/lib/rigor/plugin/base.rb +26 -11
- data/lib/rigor/plugin/bundled_catalog.rb +1 -12
- data/lib/rigor/plugin/io_boundary.rb +1 -1
- data/lib/rigor/plugin/loader.rb +25 -8
- data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
- data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
- data/lib/rigor/plugin/manifest.rb +38 -16
- data/lib/rigor/plugin/registry.rb +41 -3
- data/lib/rigor/plugin/services.rb +2 -3
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
- data/lib/rigor/plugin/trust_policy.rb +2 -2
- data/lib/rigor/plugin/type_node_resolver.rb +2 -4
- data/lib/rigor/plugin.rb +6 -8
- data/lib/rigor/plugin_gap_advisory.rb +5 -1
- data/lib/rigor/project_environment.rb +16 -2
- data/lib/rigor/protection/analysis_guard.rb +1 -1
- data/lib/rigor/protection/discovery_seed.rb +96 -2
- data/lib/rigor/protection/mutator.rb +1 -1
- data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
- data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
- data/lib/rigor/rbs_extended/reporter.rb +27 -3
- data/lib/rigor/rbs_extended.rb +2 -3
- data/lib/rigor/reflection/constant_ancestors.rb +5 -3
- data/lib/rigor/rooted_glob.rb +25 -2
- data/lib/rigor/run_scoped_slots.rb +38 -0
- data/lib/rigor/runtime/jit.rb +1 -1
- data/lib/rigor/scope/discovery_index.rb +19 -6
- data/lib/rigor/scope/resolution_chain.rb +3 -1
- data/lib/rigor/scope.rb +379 -36
- data/lib/rigor/sig_gen/classification.rb +4 -0
- data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
- data/lib/rigor/sig_gen/generator.rb +668 -84
- data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
- data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
- data/lib/rigor/sig_gen/layout_index.rb +3 -3
- data/lib/rigor/sig_gen/method_candidate.rb +23 -4
- data/lib/rigor/sig_gen/observation_collector.rb +7 -6
- data/lib/rigor/sig_gen/observed_call.rb +1 -1
- data/lib/rigor/sig_gen/path_mapper.rb +6 -0
- data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +21 -12
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
- data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
- data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
- data/lib/rigor/sig_gen/write_reporter.rb +63 -14
- data/lib/rigor/sig_gen/write_result.rb +23 -12
- data/lib/rigor/sig_gen/writer.rb +217 -37
- data/lib/rigor/type/acceptance_router.rb +2 -2
- data/lib/rigor/type/accepts_result.rb +1 -1
- data/lib/rigor/type/app.rb +4 -6
- data/lib/rigor/type/combinator.rb +13 -16
- data/lib/rigor/type/constant.rb +48 -7
- data/lib/rigor/type/hash_shape.rb +26 -40
- data/lib/rigor/type/struct_instance.rb +3 -2
- data/lib/rigor/type/union.rb +1 -1
- data/lib/rigor/type_node/identifier.rb +1 -2
- data/lib/rigor/type_node/resolver_chain.rb +1 -1
- data/lib/rigor/type_node/string_literal.rb +1 -2
- data/lib/rigor/type_node/symbol_literal.rb +1 -2
- data/lib/rigor/type_node/union.rb +3 -6
- data/lib/rigor/value_semantics.rb +1 -10
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +89 -70
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +25 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +79 -19
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +6 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
- data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
- data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +5 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
- data/sig/rigor/analysis/baseline.rbs +0 -2
- data/sig/rigor/analysis/fact_store.rbs +1 -0
- data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
- data/sig/rigor/cli/diff_command.rbs +1 -1
- data/sig/rigor/cli/explain_command.rbs +1 -1
- data/sig/rigor/effects/file_collection.rbs +5 -6
- data/sig/rigor/effects/registry.rbs +8 -6
- data/sig/rigor/effects/summary.rbs +6 -5
- data/sig/rigor/environment.rbs +5 -3
- data/sig/rigor/inference/optimistic_origin.rbs +11 -0
- data/sig/rigor/inference.rbs +10 -1
- data/sig/rigor/plugin/additional_initializer.rbs +0 -5
- data/sig/rigor/plugin/base.rbs +1 -0
- data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
- data/sig/rigor/plugin/effect_attribution.rbs +25 -10
- data/sig/rigor/plugin/effect_edge.rbs +12 -5
- data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
- data/sig/rigor/plugin/io_boundary.rbs +6 -5
- data/sig/rigor/plugin/manifest.rbs +16 -18
- data/sig/rigor/plugin/protocol_contract.rbs +6 -9
- data/sig/rigor/plugin/registry.rbs +1 -1
- data/sig/rigor/scope.rbs +51 -50
- data/sig/rigor/type.rbs +0 -6
- data/skills/rigor-plugin-author/SKILL.md +4 -4
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
- data/skills/rigor-plugin-review/SKILL.md +3 -3
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
- metadata +55 -3
|
@@ -24,10 +24,10 @@ module Rigor
|
|
|
24
24
|
# currently declaration-sourced ivar*, so a second hop (`c = @count; d = c`) and an `||=` rewrite land on
|
|
25
25
|
# the plain `with_local` path and carry no mark.
|
|
26
26
|
#
|
|
27
|
-
# A branch join
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
27
|
+
# A branch join keeps the mark when both arms stamp it: `if c then r = @count else r = @count end` stamps
|
|
28
|
+
# `(:local, :r)` on each arm, and `join_declaration_sourced` INTERSECTS, so the mark survives. Only an
|
|
29
|
+
# *asymmetric* join drops it, by that intersection rather than by reaching `with_local`. The normative
|
|
30
|
+
# statement — establishment, drop, join and the full list of
|
|
31
31
|
# unmarked shapes, each verified against the implementation — is
|
|
32
32
|
# `docs/internal-spec/inference-engine.md` § "Declaration-sourced provenance mark (ADR-58)"; keep this
|
|
33
33
|
# comment subordinate to it.
|
|
@@ -36,8 +36,8 @@ module Rigor
|
|
|
36
36
|
# a Hash literal, loops and blocks included.
|
|
37
37
|
NODE_CLASSES = [Prism::HashNode, Prism::KeywordHashNode].freeze
|
|
38
38
|
|
|
39
|
-
#
|
|
40
|
-
#
|
|
39
|
+
# One entry per LATER occurrence of a repeated literal key, carrying `hash_node`, `key_node`,
|
|
40
|
+
# `first_key_node` and `key_label` (`key_node` is the repeat the diagnostic points at;
|
|
41
41
|
# `first_key_node` is the earliest occurrence its message references). Empty when the tree has no
|
|
42
42
|
# qualifying duplicates.
|
|
43
43
|
def initialize(scope_index)
|
|
@@ -21,6 +21,21 @@ module Rigor
|
|
|
21
21
|
# - the direct receiver / subject: `param.foo`, `case param`
|
|
22
22
|
# - an indexed element: `param[i] - x` (the `-` receiver is the `[]` call whose root is `param`)
|
|
23
23
|
# - any method chain: `param.foo.bar`, `param.foo(arg).baz`, `param.key?(:x)`
|
|
24
|
+
# - an array or hash literal holding the parameter (issue #2189): `[param].first.upcase`,
|
|
25
|
+
# `{a: param}[:a].upcase`. The engine reads such an element back precisely, so the read is the
|
|
26
|
+
# parameter's lower bound again; any other call on the literal declines too, the FP-safe direction.
|
|
27
|
+
# - an instance variable the same method wrote from such a value (issue #2189): `@x = param; @x.upcase`.
|
|
28
|
+
# This one IS a scope mark — {Inference::StatementEvaluator#eval_ivar_write} stamps `:inferred_param`
|
|
29
|
+
# under the ivar's `@`-prefixed name, which no local can spell — because the write and the read are
|
|
30
|
+
# separate statements. The mark is method-local: the class-ivar census that seeds a sibling method's
|
|
31
|
+
# ivars types the write's RHS without the inference seed, so a cross-method read is not a lower bound.
|
|
32
|
+
# - `Array(param)` (issue #2280): the conversion passes the parameter's elements through, so
|
|
33
|
+
# `Array(param).first.upcase` reads the lower bound back.
|
|
34
|
+
# - an `if` / `unless` / ternary whose value is the parameter (issue #2280): `(c ? param : param).upcase`.
|
|
35
|
+
# Unlike `&&` / `||`, a branch that does not hold the parameter keeps its diagnostic: the form is rooted
|
|
36
|
+
# when every branch that can run is. A predicate the engine folds to one arm (the same certainty
|
|
37
|
+
# `StatementEvaluator#eval_if` elides the dead arm on) leaves only that arm to decide, so
|
|
38
|
+
# `(c ? param : 1).upcase` declines when `c` folds truthy and reports `Integer` when it folds falsey.
|
|
24
39
|
#
|
|
25
40
|
# Every value derived from an inferred parameter by dispatch is itself a lower bound, so firing against it
|
|
26
41
|
# is an FP by the same construction. No-op on a normal `check` run — `Scope#inferred_param?` is false
|
|
@@ -32,27 +47,105 @@ module Rigor
|
|
|
32
47
|
# length, which is tiny in practice).
|
|
33
48
|
MAX_DEPTH = 64
|
|
34
49
|
|
|
35
|
-
|
|
50
|
+
# `fold` is whether `scope` is still the scope the walked node is evaluated in. The callers pass the scope
|
|
51
|
+
# at the start of the expression; once the walk passes something evaluated earlier inside it (a leading
|
|
52
|
+
# statement, the left operand of `&&`, an earlier literal element, a conditional's predicate), that
|
|
53
|
+
# earlier code may have rebound what a nested predicate reads (`(c = false; c ? param : 1)`), so a
|
|
54
|
+
# conditional below no longer folds its predicate in `scope`.
|
|
55
|
+
def rooted?(node, scope, depth = 0, fold: true)
|
|
36
56
|
return false if node.nil? || depth > MAX_DEPTH
|
|
37
57
|
|
|
38
58
|
case node
|
|
39
|
-
when Prism::LocalVariableReadNode then scope.inferred_param?(node.name)
|
|
40
|
-
when Prism::CallNode then
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
59
|
+
when Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode then scope.inferred_param?(node.name)
|
|
60
|
+
when Prism::CallNode then call_rooted?(node, scope, depth + 1, fold)
|
|
61
|
+
when Prism::ArrayNode, Prism::HashNode, Prism::KeywordHashNode, Prism::AssocNode
|
|
62
|
+
literal_rooted?(node, scope, depth + 1, fold)
|
|
63
|
+
else combining_rooted?(node, scope, depth + 1, fold)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def call_rooted?(node, scope, depth, fold)
|
|
68
|
+
return rooted?(node.receiver, scope, depth, fold: fold) unless array_conversion?(node, scope)
|
|
69
|
+
|
|
70
|
+
rooted?(node.arguments.arguments.first, scope, depth, fold: fold)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# A receiverless `Array(x)` reaching Kernel#Array: exactly one plain argument, no block, and no project
|
|
74
|
+
# `Array` on `self`'s class or its project ancestors to bind ahead of Kernel's.
|
|
75
|
+
def array_conversion?(node, scope)
|
|
76
|
+
node.receiver.nil? && node.name == :Array && node.block.nil? &&
|
|
77
|
+
node.arguments&.arguments&.size == 1 && !project_array_method?(scope)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def project_array_method?(scope)
|
|
81
|
+
self_type = scope.self_type
|
|
82
|
+
case self_type
|
|
83
|
+
when Type::Nominal then scope.discovered_method_through_ancestors?(self_type.class_name, :Array, :instance)
|
|
84
|
+
when Type::Singleton
|
|
85
|
+
scope.discovered_method_through_ancestors?(self_type.class_name, :Array, :singleton)
|
|
86
|
+
else false
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# An array or hash literal is rooted when any element is: the engine reads the element back precisely.
|
|
91
|
+
def literal_rooted?(node, scope, depth, fold)
|
|
92
|
+
parts = node.is_a?(Prism::AssocNode) ? [node.key, node.value] : node.elements
|
|
93
|
+
parts.each_with_index.any? { |part, index| rooted?(part, scope, depth, fold: fold && index.zero?) }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Value-combining forms: the produced value is a union of the operands, so it is a lower bound when ANY
|
|
97
|
+
# operand is inferred-parameter-rooted (`cps[i] - x rescue -1`, `a && param.foo`, `param || b`). Declining
|
|
98
|
+
# on the whole expression is FP-safe by the same lower-bound argument.
|
|
99
|
+
def combining_rooted?(node, scope, depth, fold)
|
|
100
|
+
case node
|
|
44
101
|
when Prism::RescueModifierNode
|
|
45
|
-
rooted?(node.expression, scope, depth
|
|
102
|
+
rooted?(node.expression, scope, depth, fold: fold) ||
|
|
103
|
+
rooted?(node.rescue_expression, scope, depth, fold: false)
|
|
46
104
|
when Prism::AndNode, Prism::OrNode
|
|
47
|
-
rooted?(node.left, scope, depth
|
|
48
|
-
when Prism::ParenthesesNode then
|
|
49
|
-
when Prism::StatementsNode then
|
|
105
|
+
rooted?(node.left, scope, depth, fold: fold) || rooted?(node.right, scope, depth, fold: false)
|
|
106
|
+
when Prism::ParenthesesNode then body_rooted?(node.body, scope, depth, fold)
|
|
107
|
+
when Prism::StatementsNode then body_rooted?(node, scope, depth, fold)
|
|
108
|
+
when Prism::IfNode, Prism::UnlessNode then conditional_rooted?(node, scope, depth, fold)
|
|
109
|
+
when Prism::ElseNode then rooted?(node.statements, scope, depth, fold: fold)
|
|
50
110
|
else false
|
|
51
111
|
end
|
|
52
112
|
end
|
|
53
113
|
|
|
54
|
-
|
|
55
|
-
|
|
114
|
+
# A body's value is its last statement; any statement before it runs first.
|
|
115
|
+
def body_rooted?(body, scope, depth, fold)
|
|
116
|
+
return rooted?(body, scope, depth, fold: fold) unless body.is_a?(Prism::StatementsNode)
|
|
117
|
+
|
|
118
|
+
rooted?(body.body.last, scope, depth, fold: fold && body.body.size == 1)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Every branch that can run must be rooted; an absent branch is `nil`, never rooted. The predicate is
|
|
122
|
+
# typed only when the branches disagree, so a conditional that never mentions a parameter costs nothing.
|
|
123
|
+
# An arm runs after the predicate, so a conditional nested in one never folds.
|
|
124
|
+
def conditional_rooted?(node, scope, depth, fold)
|
|
125
|
+
truthy_rooted, falsey_rooted = arms(node).map { |arm| rooted?(arm, scope, depth, fold: false) }
|
|
126
|
+
return truthy_rooted if truthy_rooted == falsey_rooted
|
|
127
|
+
return false unless fold
|
|
128
|
+
|
|
129
|
+
case predicate_certainty(node.predicate, scope)
|
|
130
|
+
when :truthy then truthy_rooted
|
|
131
|
+
when :falsey then falsey_rooted
|
|
132
|
+
else false
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# `[runs when the predicate is truthy, runs when it is falsey]`.
|
|
137
|
+
def arms(node)
|
|
138
|
+
node.is_a?(Prism::IfNode) ? [node.statements, node.subsequent] : [node.else_clause, node.statements]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# The arm certainty `StatementEvaluator#branch_certainty` elides on: a decidable version guard, else the
|
|
142
|
+
# predicate's folded truthiness unless an optimistic carrier props it up.
|
|
143
|
+
def predicate_certainty(predicate, scope)
|
|
144
|
+
guard = Inference::VersionGuard.verdict(predicate)
|
|
145
|
+
return guard if guard
|
|
146
|
+
return nil unless Inference::OptimisticOrigin.resolve(predicate, scope).nil?
|
|
147
|
+
|
|
148
|
+
Inference::Narrowing.predicate_certainty(scope.type_of(predicate))
|
|
56
149
|
end
|
|
57
150
|
end
|
|
58
151
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
5
|
require_relative "rule_walk"
|
|
6
|
+
require_relative "../../scope/resolution_chain"
|
|
6
7
|
require_relative "../../source/constant_path"
|
|
7
8
|
require_relative "../../source/node_children"
|
|
8
9
|
|
|
@@ -37,36 +38,55 @@ module Rigor
|
|
|
37
38
|
# `def` that sits directly in a class / module body, never one nested in another `def`. On the
|
|
38
39
|
# shared full DFS that prune becomes the `:inside_def` gate; the enclosing class / module name stack
|
|
39
40
|
# the legacy walk threaded as `qualified_prefix` is now `context.qualified_prefix`.
|
|
40
|
-
|
|
41
|
+
#
|
|
42
|
+
# ADR-125 — the walk also visits class-body `CallNode`s (a `def` body is gated off), to record the
|
|
43
|
+
# macro calls whose literal arguments name a method (`before_action :load_roles`).
|
|
44
|
+
NODE_CLASSES = [Prism::DefNode, Prism::CallNode].freeze
|
|
41
45
|
RULE_WALK_GATES = %i[inside_def detached_ivar_facet].freeze
|
|
42
46
|
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
+
# Groups the writes by class qualified name, then by ivar name. Each write carries `node`, `type`,
|
|
48
|
+
# `declared`, `method_name`, `visibility`, `per_invocation` and `action`. Empty when the tree has no
|
|
49
|
+
# qualifying writes.
|
|
50
|
+
# `declared` is the translated type of the class's RBS declaration of the ivar — an `@x: T` member or
|
|
51
|
+
# the slot an `attr_*` member implies, own or inherited — and nil when the RBS declares none.
|
|
52
|
+
#
|
|
53
|
+
# ADR-125 — `method_name` is the enclosing `def`'s name and `visibility` its instance visibility
|
|
54
|
+
# (`:public` / `:protected` / `:private`, or nil when the discovery table has no settled answer).
|
|
55
|
+
# `per_invocation` is whether the class is, or descends from, a class a loaded plugin declares in
|
|
56
|
+
# `per_invocation_classes:`. `action` is whether the enclosing method is one of that class's entry
|
|
57
|
+
# points: public, and named by none of the declaring plugin's `per_invocation_non_action_macros:` in a
|
|
58
|
+
# class body of this file (`before_action :load_roles`, `helper_method :current_roles`).
|
|
47
59
|
def initialize(scope_index)
|
|
48
60
|
@scope_index = scope_index
|
|
49
61
|
@accumulator = {}
|
|
62
|
+
@per_invocation = {}
|
|
63
|
+
@macro_arguments = {}
|
|
50
64
|
end
|
|
51
65
|
|
|
52
66
|
# Legacy single-collector walk — kept as the oracle the ADR-53 Track B equivalence harness compares
|
|
53
67
|
# {RuleWalk} against; deleted when Track B completes.
|
|
54
68
|
def collect(root)
|
|
55
69
|
walk(root, [])
|
|
56
|
-
|
|
70
|
+
results
|
|
57
71
|
end
|
|
58
72
|
|
|
59
73
|
# {RuleWalk} entry point: the legacy walk's per-`def` logic, invoked at each class-/module-body `def`
|
|
60
74
|
# under the shared traversal contract. `collect_def_writes`' verbatim body reads the enclosing class
|
|
61
75
|
# prefix from `context.qualified_prefix`.
|
|
62
|
-
def visit(
|
|
63
|
-
|
|
76
|
+
def visit(node, context)
|
|
77
|
+
if node.is_a?(Prism::CallNode)
|
|
78
|
+
record_macro_call(node, context.qualified_prefix)
|
|
79
|
+
else
|
|
80
|
+
collect_def_writes(node, context.qualified_prefix)
|
|
81
|
+
end
|
|
64
82
|
end
|
|
65
83
|
|
|
66
84
|
# The accumulated result, frozen the same way `#collect` returns it — used by {RuleWalk}-driven
|
|
67
85
|
# callers after the walk completes.
|
|
68
86
|
def results
|
|
69
|
-
@accumulator.
|
|
87
|
+
@accumulator.to_h do |class_name, writes_by_ivar|
|
|
88
|
+
[class_name, writes_by_ivar.transform_values { |writes| with_actions(class_name, writes) }.freeze]
|
|
89
|
+
end.freeze
|
|
70
90
|
end
|
|
71
91
|
|
|
72
92
|
private
|
|
@@ -87,29 +107,71 @@ module Rigor
|
|
|
87
107
|
when Prism::DefNode
|
|
88
108
|
collect_def_writes(node, qualified_prefix)
|
|
89
109
|
return
|
|
110
|
+
when Prism::CallNode
|
|
111
|
+
record_macro_call(node, qualified_prefix)
|
|
90
112
|
end
|
|
91
113
|
|
|
92
114
|
node.rigor_each_child { |child| walk(child, qualified_prefix) }
|
|
93
115
|
end
|
|
94
116
|
|
|
117
|
+
# A receiverless (or `self.`) call in a class body, with the method names its literal Symbol / String
|
|
118
|
+
# arguments spell. Kept for every such call: which macro names count is the registry's answer, read
|
|
119
|
+
# once per class when the writes are finalised.
|
|
120
|
+
def record_macro_call(call_node, qualified_prefix)
|
|
121
|
+
return if qualified_prefix.empty?
|
|
122
|
+
return unless call_node.receiver.nil? || call_node.receiver.is_a?(Prism::SelfNode)
|
|
123
|
+
|
|
124
|
+
names = (call_node.arguments&.arguments || []).filter_map { |argument| literal_method_name(argument) }
|
|
125
|
+
return if names.empty?
|
|
126
|
+
|
|
127
|
+
((@macro_arguments[qualified_prefix.join("::")] ||= {})[call_node.name] ||= []).concat(names)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# A Symbol / String literal's name, or the name of a `def` passed inline (`helper_method def
|
|
131
|
+
# current_user … end`). A literal with bytes no Symbol can hold (`before_action "\xff"`, #1821) names no
|
|
132
|
+
# method, so it answers nil.
|
|
133
|
+
def literal_method_name(argument)
|
|
134
|
+
return argument.name if argument.is_a?(Prism::DefNode) && argument.receiver.nil?
|
|
135
|
+
return nil unless argument.is_a?(Prism::SymbolNode) || argument.is_a?(Prism::StringNode)
|
|
136
|
+
|
|
137
|
+
text = argument.unescaped
|
|
138
|
+
text.valid_encoding? ? text.to_sym : nil
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Each write with `action` set (see {#initialize}). A frozen copy, so `#results` stays repeatable.
|
|
142
|
+
def with_actions(class_name, writes)
|
|
143
|
+
macros = @per_invocation[class_name]
|
|
144
|
+
excluded = macros ? non_action_names(class_name, macros) : nil
|
|
145
|
+
writes.map do |write|
|
|
146
|
+
action = !excluded.nil? && write[:visibility] == :public && !excluded.include?(write[:method_name])
|
|
147
|
+
write.merge(action: action).freeze
|
|
148
|
+
end.freeze
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def non_action_names(class_name, macros)
|
|
152
|
+
(@macro_arguments[class_name] || {}).each_with_object(Set.new) do |(macro, names), set|
|
|
153
|
+
set.merge(names) if macros.include?(macro)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
95
157
|
def collect_def_writes(def_node, qualified_prefix)
|
|
96
158
|
return if def_node.body.nil? || qualified_prefix.empty?
|
|
97
159
|
return if def_node.receiver.is_a?(Prism::SelfNode)
|
|
98
160
|
|
|
99
161
|
class_name = qualified_prefix.join("::")
|
|
100
|
-
gather_writes(def_node.body, class_name)
|
|
162
|
+
gather_writes(def_node.body, class_name, def_node.name)
|
|
101
163
|
end
|
|
102
164
|
|
|
103
|
-
def gather_writes(node, class_name)
|
|
165
|
+
def gather_writes(node, class_name, method_name)
|
|
104
166
|
return unless node.is_a?(Prism::Node)
|
|
105
167
|
|
|
106
|
-
record_write(node, class_name) if node.is_a?(Prism::InstanceVariableWriteNode)
|
|
168
|
+
record_write(node, class_name, method_name) if node.is_a?(Prism::InstanceVariableWriteNode)
|
|
107
169
|
return if BARRIER_NODES.any? { |klass| node.is_a?(klass) }
|
|
108
170
|
|
|
109
|
-
node.rigor_each_child { |child| gather_writes(child, class_name) }
|
|
171
|
+
node.rigor_each_child { |child| gather_writes(child, class_name, method_name) }
|
|
110
172
|
end
|
|
111
173
|
|
|
112
|
-
def record_write(node, class_name)
|
|
174
|
+
def record_write(node, class_name, method_name)
|
|
113
175
|
scope = @scope_index[node]
|
|
114
176
|
return if scope.nil?
|
|
115
177
|
|
|
@@ -117,7 +179,73 @@ module Rigor
|
|
|
117
179
|
declared = declared_ivar(scope, class_name, node.name)
|
|
118
180
|
@accumulator[class_name] ||= {}
|
|
119
181
|
@accumulator[class_name][node.name] ||= []
|
|
120
|
-
@accumulator[class_name][node.name] << {
|
|
182
|
+
@accumulator[class_name][node.name] << {
|
|
183
|
+
node: node, type: rvalue_type, declared: declared, method_name: method_name,
|
|
184
|
+
visibility: method_visibility(scope, class_name, method_name),
|
|
185
|
+
per_invocation: !per_invocation_macros(scope, class_name).nil?
|
|
186
|
+
}
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# The enclosing `def`'s instance visibility, or nil when it is not a settled fact: a slot the
|
|
190
|
+
# discovery table does not hold, or one a later `define_method` / alias / unplaceable write contests
|
|
191
|
+
# (#2013). `initialize` and the other lifecycle hooks are private whatever the table says.
|
|
192
|
+
def method_visibility(scope, class_name, method_name)
|
|
193
|
+
return :private if CheckRules::IMPLICITLY_PRIVATE_METHODS.include?(method_name)
|
|
194
|
+
return nil if scope.discovery.contested?(:discovered_method_visibilities, [class_name, method_name])
|
|
195
|
+
|
|
196
|
+
scope.discovered_method_visibility(class_name, method_name)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# ADR-125 — when `class_name` is, or descends from, a class a loaded plugin declares per-invocation, the
|
|
200
|
+
# non-action macros its declaring manifests list (a Set, possibly empty); nil otherwise. Read off the
|
|
201
|
+
# class's instance {Scope::ResolutionChain}, so a project superclass chain
|
|
202
|
+
# (`UsersController < ApplicationController < ActionController::Base`) is followed as Ruby resolves it.
|
|
203
|
+
# A project entry matches by name. An external superclass matches through the first of its candidate
|
|
204
|
+
# spellings the environment knows, by name or by an RBS-known `:equal` / `:subclass` ordering. When the
|
|
205
|
+
# environment knows none of them (rigor-actionpack leaves `ActionController::Base` undeclared on
|
|
206
|
+
# purpose), a candidate that is literally a declared name matches, unless some candidate names a class
|
|
207
|
+
# the project itself declares: then the spelling may resolve there instead. An ambiguous spelling (no
|
|
208
|
+
# candidates) and a truncated chain answer nil: the carve-out only ever withholds a finding, so an
|
|
209
|
+
# unproved descent keeps the rule firing. Memoised per class.
|
|
210
|
+
def per_invocation_macros(scope, class_name)
|
|
211
|
+
return @per_invocation[class_name] if @per_invocation.key?(class_name)
|
|
212
|
+
|
|
213
|
+
@per_invocation[class_name] = matched_per_invocation_macros(scope, class_name)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def matched_per_invocation_macros(scope, class_name)
|
|
217
|
+
environment = scope.environment
|
|
218
|
+
registry = environment&.plugin_registry
|
|
219
|
+
return nil if registry.nil? || registry.per_invocation_classes.empty?
|
|
220
|
+
|
|
221
|
+
chain = Scope::ResolutionChain.for(scope, class_name, :instance, :methods)
|
|
222
|
+
return nil if chain.truncated?
|
|
223
|
+
|
|
224
|
+
declared = chain.search(scope) { |entry| matched_declarations(entry, registry, scope) }
|
|
225
|
+
declared&.each_with_object(Set.new) { |name, set| set.merge(registry.per_invocation_non_action_macros(name)) }
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# The declared per-invocation names `entry` matches, or nil (so {ResolutionChain#search} moves on).
|
|
229
|
+
def matched_declarations(entry, registry, scope)
|
|
230
|
+
return (registry.per_invocation_class?(entry.name.to_s) ? [entry.name.to_s] : nil) unless entry.external?
|
|
231
|
+
return nil unless entry.superclass_edge
|
|
232
|
+
|
|
233
|
+
environment = scope.environment
|
|
234
|
+
names = entry.candidates.map { |name| name.to_s.delete_prefix("::") }
|
|
235
|
+
known = names.find { |name| environment.class_known?(name) }
|
|
236
|
+
return literal_declarations(names, registry, scope) if known.nil?
|
|
237
|
+
|
|
238
|
+
matched = registry.per_invocation_classes.select do |declared|
|
|
239
|
+
%i[equal subclass].include?(environment.class_ordering(known, declared))
|
|
240
|
+
end
|
|
241
|
+
matched.empty? ? nil : matched
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def literal_declarations(names, registry, scope)
|
|
245
|
+
return nil if names.any? { |name| scope.discovered_classes.key?(name) }
|
|
246
|
+
|
|
247
|
+
matched = names.select { |name| registry.per_invocation_class?(name) }
|
|
248
|
+
matched.empty? ? nil : matched
|
|
121
249
|
end
|
|
122
250
|
|
|
123
251
|
def declared_ivar(scope, class_name, ivar_name)
|
|
@@ -4,6 +4,7 @@ require "prism"
|
|
|
4
4
|
|
|
5
5
|
require_relative "../../inference/scope_indexer"
|
|
6
6
|
require_relative "../../source/node_children"
|
|
7
|
+
require_relative "rebinding_block_callees"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
9
10
|
module Analysis
|
|
@@ -41,6 +42,9 @@ module Rigor
|
|
|
41
42
|
|
|
42
43
|
def refinements = (@refinements ||= Inference::InEffectRefinements.new(@root))
|
|
43
44
|
|
|
45
|
+
# Issue #1817 — the file's block-passing calls, for `call.unresolved-toplevel`'s rebinding-callee decline.
|
|
46
|
+
def block_calls = (@block_calls ||= RebindingBlockCallees::Sites.new(@root))
|
|
47
|
+
|
|
44
48
|
# Does a `def <local>.<name>` for this call's local receiver and method name — or a call that gives the
|
|
45
49
|
# local's object methods Rigor does not enumerate (issue #1722) — sit in the scope the call is in?
|
|
46
50
|
def singleton_local_def?(call_node)
|
|
@@ -148,7 +152,6 @@ module Rigor
|
|
|
148
152
|
end
|
|
149
153
|
end
|
|
150
154
|
|
|
151
|
-
# `o.singleton_class`.
|
|
152
155
|
def singleton_class_of_local?(node)
|
|
153
156
|
node.is_a?(Prism::CallNode) && node.name == :singleton_class && node.arguments.nil? &&
|
|
154
157
|
node.receiver.is_a?(Prism::LocalVariableReadNode)
|
|
@@ -12,16 +12,13 @@ module Rigor
|
|
|
12
12
|
# decides, with NO loop / block suppression and NO threaded context — so the collector declares no
|
|
13
13
|
# `RULE_WALK_GATES` and ignores the walk's `context`.
|
|
14
14
|
#
|
|
15
|
-
# The per-node dispatch
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# accumulated {Diagnostic}s, in the same emission order the inline `NodeWalker.each` produced them (the
|
|
20
|
-
# shared walk is the same visit-before-descend DFS over `compact_child_nodes`).
|
|
15
|
+
# The per-node dispatch stays in `CheckRules` (ADR-53 WD4) and is handed in as a callable built in the
|
|
16
|
+
# `CheckRules` module context, so its calls to the private diagnostic builders remain implicit-self. The
|
|
17
|
+
# collector only owns the traversal hook and accumulation; `#results` are the accumulated {Diagnostic}s in
|
|
18
|
+
# the walk's emission order.
|
|
21
19
|
class MainPassCollector
|
|
22
20
|
# The node classes the main pass branches on. A plain `Prism::IfNode` covers ternaries and postfix `if`
|
|
23
|
-
# too
|
|
24
|
-
# classes carry issue #1367's `global.*` rules.
|
|
21
|
+
# too. The global-write classes and `MultiWriteNode` carry issue #1367's `global.*` rules.
|
|
25
22
|
NODE_CLASSES = [
|
|
26
23
|
Prism::CallNode, Prism::DefNode, Prism::IfNode, Prism::UnlessNode,
|
|
27
24
|
Prism::GlobalVariableWriteNode, Prism::GlobalVariableOperatorWriteNode, Prism::MultiWriteNode
|
|
@@ -19,7 +19,7 @@ module Rigor
|
|
|
19
19
|
# branched on, whose declaration the reader's author never opened. A configuration constant read in ten
|
|
20
20
|
# files would put a warning in all ten.
|
|
21
21
|
#
|
|
22
|
-
# This is [ADR-58](../../../../docs/adr/58-
|
|
22
|
+
# This is [ADR-58](../../../../docs/adr/58-ivar-field-typing.md)'s discipline applied to a
|
|
23
23
|
# different carrier: "a consumer may only ever *withhold* a firing it would otherwise make, so the mark
|
|
24
24
|
# can lose precision but can never manufacture a false positive"
|
|
25
25
|
# (`docs/internal-spec/inference-engine.md` § "Carrier and discipline"). The carrier is deliberately NOT
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "../../reflection"
|
|
4
4
|
require_relative "../dependency_recorder"
|
|
5
5
|
require_relative "../../inference/singleton_hook_decline"
|
|
6
|
+
require_relative "../../inference/object_mixins"
|
|
6
7
|
|
|
7
8
|
module Rigor
|
|
8
9
|
module Analysis
|
|
@@ -68,6 +69,43 @@ module Rigor
|
|
|
68
69
|
end
|
|
69
70
|
end
|
|
70
71
|
|
|
72
|
+
# Issue #2094 — the singleton side's twin of {.ancestor_may_answer?}: a module the class or an RBS superclass
|
|
73
|
+
# `extend`s (in its body, as `Float.extend(Ext)`, or as `Float.singleton_class.include(Ext)`) may answer
|
|
74
|
+
# `method_name` on the class object, read as {.mixin_defines?} / {.external_may_answer?} read an instance-side
|
|
75
|
+
# mixin. Only each level's own singleton segment is read: the class's further singleton ancestors are the
|
|
76
|
+
# RBS superclasses this walk visits in turn, and `Class` / `Module` are {.singleton_hook?}'s.
|
|
77
|
+
def singleton_mixin_may_answer?(scope, class_name, method_name)
|
|
78
|
+
singleton_class_names(scope, class_name).any? do |name|
|
|
79
|
+
DependencyRecorder.read_last_segment(:class, name) if DependencyRecorder.active?
|
|
80
|
+
|
|
81
|
+
extends_answer?(scope, name, method_name, open_mixins: !OBJECT_ROOTS.include?(name))
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def singleton_class_names(scope, class_name)
|
|
86
|
+
loader = scope.environment&.rbs_loader
|
|
87
|
+
pairs = loader ? loader.singleton_ancestors_for(class_name.to_s) : []
|
|
88
|
+
names = pairs.filter_map { |kind, name| name if kind == :singleton }
|
|
89
|
+
names.empty? ? [class_name.to_s] : names
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def extends_answer?(scope, name, method_name, open_mixins:)
|
|
93
|
+
chain = Scope::ResolutionChain.for(scope, name, :singleton, :methods)
|
|
94
|
+
return true if chain.truncated?
|
|
95
|
+
# A mixin call the indexer could not record (`Integer.extend(m)`, `Symbol.singleton_class.send(:include,
|
|
96
|
+
# M)`) leaves the side tainted: some module the tables do not name may answer any name.
|
|
97
|
+
return true if open_mixins && chain.wildcard_mixin?
|
|
98
|
+
|
|
99
|
+
entries = chain.level_count.zero? ? chain.entries : chain.level_entries(0)
|
|
100
|
+
entries.any? do |entry|
|
|
101
|
+
if entry.external?
|
|
102
|
+
external_may_answer?(scope, entry, method_name, open_mixins)
|
|
103
|
+
else
|
|
104
|
+
entry.name != name && mixin_defines?(scope, entry, method_name, open_mixins)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
71
109
|
# Whether a project subclass of `class_name`, at any depth, defines `method_name` (through its own
|
|
72
110
|
# ancestry, which reaches its project mixins) or a `method_missing`. Its parent is read as written and
|
|
73
111
|
# resolved the way the class header resolves it: the first project class the spelling can denote, else the
|
|
@@ -132,19 +170,44 @@ module Rigor
|
|
|
132
170
|
if Rigor::Reflection.rbs_class_known?(owner, scope: scope)
|
|
133
171
|
scope.discovered_method?(owner, method_name, :instance)
|
|
134
172
|
else
|
|
135
|
-
project_defines?(scope, owner, method_name) ||
|
|
136
|
-
(open_mixins && Inference::SingletonHookDecline.entry_hook_capable?(scope, entry)
|
|
173
|
+
project_defines?(scope, owner, method_name) || macro_names?(scope, owner, method_name) ||
|
|
174
|
+
(open_mixins && (Inference::SingletonHookDecline.entry_hook_capable?(scope, entry) ||
|
|
175
|
+
open_extend?(scope, owner)))
|
|
137
176
|
end
|
|
138
177
|
end
|
|
139
178
|
|
|
179
|
+
# Issue #2094 — a class-body macro of the module names the method ({Inference::ObjectMixins.macro_named?}).
|
|
180
|
+
# Answers the named method only, so it holds on `Object`, `Kernel` and `BasicObject` too, where a module
|
|
181
|
+
# that defines the name answers.
|
|
182
|
+
def macro_names?(scope, owner, method_name)
|
|
183
|
+
Inference::ObjectMixins.macro_named?(scope, owner, method_name)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Issue #2094 — the module `extend`s a module whose surface is open: one neither RBS nor the project declares
|
|
187
|
+
# (a gem's DSL with no signatures), or a project module {Inference::SingletonHookDecline} reads as
|
|
188
|
+
# hook-capable. Any macro the module's body calls may come from it and define any name, and its `extended`
|
|
189
|
+
# hook may too, so the module answers any name, the reading an undeclared `include` already gets. A chain cut
|
|
190
|
+
# at its budget may hold such a module past the cut.
|
|
191
|
+
def open_extend?(scope, owner)
|
|
192
|
+
chain = Scope::ResolutionChain.for(scope, owner, :singleton, :methods)
|
|
193
|
+
return true if chain.truncated?
|
|
194
|
+
|
|
195
|
+
entries = chain.level_count.zero? ? chain.entries : chain.level_entries(0)
|
|
196
|
+
entries.any? { |extended| Inference::SingletonHookDecline.entry_hook_capable?(scope, extended) }
|
|
197
|
+
end
|
|
198
|
+
|
|
140
199
|
# An external the project declares (a module with no `def`, which the chain does not hold as a project entry)
|
|
141
|
-
# is known, and answers
|
|
200
|
+
# is known, and answers through RBS, or through what its own body's macros and `extend`s may define (issue
|
|
201
|
+
# #2094: {.macro_names?} and {.open_extend?}, asked of each candidate the project declares).
|
|
142
202
|
def external_may_answer?(scope, entry, method_name, open_mixins)
|
|
143
203
|
candidates = entry.candidates
|
|
144
204
|
return open_mixins if candidates.empty?
|
|
145
205
|
return true if candidates.any? do |candidate|
|
|
146
206
|
Rigor::Reflection.instance_method_definition(candidate, method_name, scope: scope)
|
|
147
207
|
end
|
|
208
|
+
return true if candidates.any? do |candidate|
|
|
209
|
+
macro_names?(scope, candidate, method_name) || (open_mixins && open_extend?(scope, candidate))
|
|
210
|
+
end
|
|
148
211
|
|
|
149
212
|
open_mixins && candidates.none? { |candidate| declared?(scope, candidate) }
|
|
150
213
|
end
|