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
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Analysis
|
|
7
|
+
module CheckRules
|
|
8
|
+
module RebindingBlockCallees
|
|
9
|
+
# Which method a block call reaches, and on what receiver: `callee_may_rebind?` reads the callee name, a
|
|
10
|
+
# callable receiver, and whether Ruby can reach `method_missing` from here.
|
|
11
|
+
module Dispatch
|
|
12
|
+
# Issue #2186 — a block given to `call` / `bind_call` (or `yield` / `===` / `[]`) on a literal `method(:x)`
|
|
13
|
+
# chain goes to the method it names: `Ext.method(:me).call { … }` is `Ext.me { … }`. On a receiver typed as
|
|
14
|
+
# one of CALLABLE_CLASSES it goes to a callable Rigor cannot follow; on any other receiver it is a call by
|
|
15
|
+
# that name.
|
|
16
|
+
METHOD_OBJECT_CALLS = %i[call yield === [] bind_call].freeze
|
|
17
|
+
METHOD_READERS = %i[method public_method singleton_method instance_method public_instance_method].freeze
|
|
18
|
+
# The readers whose receiver is the object the named method runs on.
|
|
19
|
+
BOUND_METHOD_READERS = %i[method public_method singleton_method].freeze
|
|
20
|
+
METHOD_WRAPPERS = %i[bind unbind to_proc curry].freeze
|
|
21
|
+
CALLABLE_CLASSES = %w[Method UnboundMethod Proc].freeze
|
|
22
|
+
private_constant :METHOD_READERS, :BOUND_METHOD_READERS, :METHOD_WRAPPERS, :CALLABLE_CLASSES
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# `[name, receiver]` of the method the block reaches: the call's own name, the literal name a `send` family
|
|
27
|
+
# call dispatches to, or the method a literal `method(:name)` chain names. The receiver is a node, `:implicit`
|
|
28
|
+
# for a receiverless call, or nil where Rigor cannot say (an `instance_method` chain). The name is nil when a
|
|
29
|
+
# `send` names its method with anything but a valid literal.
|
|
30
|
+
def dispatch(call)
|
|
31
|
+
reader = method_reader(call.receiver) if METHOD_OBJECT_CALLS.include?(call.name)
|
|
32
|
+
if reader
|
|
33
|
+
receiver = reader.receiver || :implicit if BOUND_METHOD_READERS.include?(reader.name)
|
|
34
|
+
return [RebindingBlockCallees.literal_name(reader.arguments&.arguments&.first), receiver]
|
|
35
|
+
end
|
|
36
|
+
[RebindingBlockCallees.send_target(call), call.receiver || :implicit]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def method_reader(receiver)
|
|
40
|
+
receiver = receiver.receiver while wrapper?(receiver)
|
|
41
|
+
receiver if receiver.is_a?(Prism::CallNode) && METHOD_READERS.include?(receiver.name)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Whether `receiver` may be a Method, UnboundMethod or Proc, whose `call` runs a callable Rigor cannot follow.
|
|
45
|
+
def callable?(receiver, scope, scope_index)
|
|
46
|
+
return false unless receiver.is_a?(Prism::Node)
|
|
47
|
+
|
|
48
|
+
type = receiver_type(receiver, scope, scope_index)
|
|
49
|
+
members = type.is_a?(Type::Union) ? type.members : [type]
|
|
50
|
+
members.any? do |member|
|
|
51
|
+
member.is_a?(Type::BoundMethod) ||
|
|
52
|
+
(member.is_a?(Type::Nominal) && CALLABLE_CLASSES.include?(member.class_name.to_s))
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def wrapper?(node) = node.is_a?(Prism::CallNode) && METHOD_WRAPPERS.include?(node.name)
|
|
57
|
+
|
|
58
|
+
# A `call` (or the like) on a receiver that may be a Method, UnboundMethod or Proc.
|
|
59
|
+
def callable_call?(name, receiver, scope, scope_index)
|
|
60
|
+
METHOD_OBJECT_CALLS.include?(name) && callable?(receiver, scope, scope_index)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def receiver_type(receiver, scope, scope_index)
|
|
64
|
+
(scope_index&.[](receiver) || scope).type_of(receiver)
|
|
65
|
+
rescue StandardError
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Whether the receiver's type names a class whose RBS or project table gives it `name`, on every member of a
|
|
70
|
+
# union. A receiverless call at the top level asks `Object`, `main`'s class. Any other type cannot say.
|
|
71
|
+
def resolves_on_receiver?(receiver, name, scope, scope_index)
|
|
72
|
+
return scope.toplevel? && class_defines?("Object", :instance, name, scope) if receiver == :implicit
|
|
73
|
+
return false if receiver.nil?
|
|
74
|
+
|
|
75
|
+
type = receiver_type(receiver, scope, scope_index)
|
|
76
|
+
!type.nil? && type_resolves?(type, name, scope)
|
|
77
|
+
rescue StandardError
|
|
78
|
+
false
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def type_resolves?(type, name, scope)
|
|
82
|
+
case type
|
|
83
|
+
when Type::Union then type.members.all? { |member| type_resolves?(member, name, scope) }
|
|
84
|
+
when Type::Singleton then class_defines?(type.class_name, :singleton, name, scope)
|
|
85
|
+
else
|
|
86
|
+
class_name = receiver_class_name(type)
|
|
87
|
+
!class_name.nil? && class_defines?(class_name, :instance, name, scope)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def receiver_class_name(type)
|
|
92
|
+
case type
|
|
93
|
+
when Type::Nominal, Type::StructInstance, Type::DataInstance then type.class_name&.to_s
|
|
94
|
+
when Type::Tuple then "Array"
|
|
95
|
+
when Type::HashShape then "Hash"
|
|
96
|
+
when Type::Constant then type.value.class.name
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def class_defines?(class_name, side, name, scope)
|
|
101
|
+
rbs = if side == :singleton
|
|
102
|
+
Rigor::Reflection.singleton_method_definition(class_name, name, scope: scope)
|
|
103
|
+
else
|
|
104
|
+
Rigor::Reflection.instance_method_definition(class_name, name, scope: scope)
|
|
105
|
+
end
|
|
106
|
+
!rbs.nil? || scope.discovered_method?(class_name, name, side)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../dependency_recorder"
|
|
6
|
+
require_relative "../../inference/scope_indexer"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module Analysis
|
|
10
|
+
module CheckRules
|
|
11
|
+
# Issue #1817 — `call.unresolved-toplevel` declines inside a block handed to a project method that may run it
|
|
12
|
+
# under another `self`. `def self.define(&blk) = module_eval(&blk)` makes `Ext.define { refine(String) { … } }`
|
|
13
|
+
# call `refine` on `Ext`, not on `main`, so the call is not a top-level one even though the block sits at the
|
|
14
|
+
# top of the file.
|
|
15
|
+
#
|
|
16
|
+
# The reading is by method NAME across every project definition, never by the receiver's type: any project
|
|
17
|
+
# `def` of the block's callee name that may hand the block to something else declines the rule. That
|
|
18
|
+
# over-reads (an unrelated class's same-named method declines too), which only loses a report; resolving the
|
|
19
|
+
# receiver would read one class and could miss the definition Ruby actually dispatches to, which reports on
|
|
20
|
+
# correct code. A definition "may hand the block on" unless every use of it is provably a plain call:
|
|
21
|
+
#
|
|
22
|
+
# - its body only `yield`s, or reads the block parameter only as the receiver of `call` / `()` / `[]` /
|
|
23
|
+
# `yield` / `===` / `arity` / `lambda?` / `parameters` / `nil?` / `!`, or as an `if` / `unless` / `while` /
|
|
24
|
+
# `until` predicate or the left side of `&&` — each of which runs the block with its own `self` or not at
|
|
25
|
+
# all;
|
|
26
|
+
# - any other read (`module_eval(&blk)`, `other(&blk)`, `@blk = blk`, `return blk`), an anonymous `&` or a
|
|
27
|
+
# `...` forwarded anywhere, a `super` that passes the block on implicitly, or a `binding` / `eval` that
|
|
28
|
+
# could reach it, may rebind `self`. Where in the body it sits does not matter: an eval under an `if` may
|
|
29
|
+
# run.
|
|
30
|
+
#
|
|
31
|
+
# A callee name the project gives a method without a readable `def` (`attr_*`, `alias_method`,
|
|
32
|
+
# `define_method`, `define_singleton_method` on any receiver, a `def` written inside a block), or patches
|
|
33
|
+
# through `pre_eval:`, cannot be read and declines too. So does a `send` / `__send__` / `public_send` whose
|
|
34
|
+
# method name is not a literal. Issue #2184 — a project `method_missing` that may hand its block on answers
|
|
35
|
+
# for every name, since Ruby reaches it for any name the receiver lacks; and the block of a `define_method` /
|
|
36
|
+
# `define_singleton_method` call is a method body, run under whatever receives the method. A callee no project
|
|
37
|
+
# file defines keeps the rule's old answer otherwise: Rigor reads only the project's own bodies here.
|
|
38
|
+
module RebindingBlockCallees
|
|
39
|
+
SEND_CALLS = %i[send __send__ public_send].freeze
|
|
40
|
+
# Reading the block through these runs it with its own `self`, or does not run it.
|
|
41
|
+
PLAIN_PROC_CALLS = %i[call yield [] === ! arity lambda? parameters nil?].freeze
|
|
42
|
+
# Calls, on any receiver, that can reach a local by name: `binding`, `eval`, `local_variable_get`. The string
|
|
43
|
+
# forms of the `*_eval` family (`STRING_EVAL_CALLS`, with an argument) run in the caller's scope too.
|
|
44
|
+
BINDING_CALLS = %i[binding eval local_variable_get].freeze
|
|
45
|
+
STRING_EVAL_CALLS = %i[instance_eval class_eval module_eval].freeze
|
|
46
|
+
private_constant :STRING_EVAL_CALLS
|
|
47
|
+
PREDICATE_PARENTS = [Prism::IfNode, Prism::UnlessNode, Prism::WhileNode, Prism::UntilNode].freeze
|
|
48
|
+
# `proc { … }`, `lambda { … }` and `Proc.new { … }` passed with `&` are the block of the call they go to.
|
|
49
|
+
PROC_LITERAL_CALLS = %i[proc lambda].freeze
|
|
50
|
+
# Core methods that run their block under another `self`: as a method body, or under the receiver (#2186:
|
|
51
|
+
# also when the block is a proc passed through a local, `Ext.instance_exec(&blk)`).
|
|
52
|
+
REBINDING_CORE_CALLS = %i[
|
|
53
|
+
define_method define_singleton_method
|
|
54
|
+
instance_eval instance_exec class_eval class_exec module_eval module_exec
|
|
55
|
+
].freeze
|
|
56
|
+
private_constant :REBINDING_CORE_CALLS
|
|
57
|
+
private_constant :SEND_CALLS, :PLAIN_PROC_CALLS, :BINDING_CALLS, :PREDICATE_PARENTS, :PROC_LITERAL_CALLS
|
|
58
|
+
|
|
59
|
+
# The calls in one file that pass a literal block, each with the offsets that block's body spans, plus the
|
|
60
|
+
# file's memo of callee answers. Built on the first ask, so a file whose would-fire calls all decline earlier
|
|
61
|
+
# never walks. One file is one ADR-46 consumer, so an answer's dependency edges, recorded on its first ask,
|
|
62
|
+
# hold for every later call in the file that reuses it.
|
|
63
|
+
#
|
|
64
|
+
# Issue #2186 — a proc literal that is a value rather than a call's block (`blk = proc { … }`) adds a row for
|
|
65
|
+
# each call that receives it as `&blk`, when every other read of that local only calls or tests it. A literal
|
|
66
|
+
# called in place (`proc { … }.call`) runs with its own `self` and adds none. Any other proc value (stored,
|
|
67
|
+
# passed positionally, read another way, or in a method (or the code around it) that may read locals by name
|
|
68
|
+
# through `binding` / `eval` / `local_variable_get` or a computed `send`) cannot be followed to the call that
|
|
69
|
+
# runs it, so its row is {UNKNOWN}, which declines. A heredoc that opens inside a block adds a row over its
|
|
70
|
+
# body: Prism puts the body after the block's closing brace, but Ruby runs its interpolation inside the block.
|
|
71
|
+
class Sites
|
|
72
|
+
# The target of a row whose block Rigor cannot follow to the call that runs it.
|
|
73
|
+
UNKNOWN = :unknown
|
|
74
|
+
|
|
75
|
+
attr_reader :answers
|
|
76
|
+
|
|
77
|
+
def initialize(root)
|
|
78
|
+
@root = root
|
|
79
|
+
@rows = nil
|
|
80
|
+
@answers = {}
|
|
81
|
+
@def_rows = nil
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# The block-passing calls (or {UNKNOWN}) whose block contains `node`. Rows are sorted by start, so the scan
|
|
85
|
+
# stops at the first block that starts after `node`.
|
|
86
|
+
def enclosing(node)
|
|
87
|
+
build
|
|
88
|
+
start = node.location.start_offset
|
|
89
|
+
stop = node.location.end_offset
|
|
90
|
+
found = []
|
|
91
|
+
@rows.each do |from, to, call|
|
|
92
|
+
break if from > start
|
|
93
|
+
|
|
94
|
+
found << call if stop <= to
|
|
95
|
+
end
|
|
96
|
+
found
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# The project's named `def` row counts, keyed by method name and then by owner and side; built once per file.
|
|
100
|
+
def def_rows(scope)
|
|
101
|
+
@def_rows ||= RebindingBlockCallees.def_row_counts(scope)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def build
|
|
107
|
+
return if @rows
|
|
108
|
+
|
|
109
|
+
@rows = []
|
|
110
|
+
@bodies = []
|
|
111
|
+
@heredocs = []
|
|
112
|
+
@proc_values = []
|
|
113
|
+
@local_reads = Hash.new { |hash, name| hash[name] = [] }
|
|
114
|
+
@block_passes = Hash.new { |hash, name| hash[name] = [] }
|
|
115
|
+
@scopes_reading_locals = Set.new.compare_by_identity
|
|
116
|
+
walk unless @root.nil?
|
|
117
|
+
# A block call chained on another (`m { }.then { }`) is met after the call whose block starts later.
|
|
118
|
+
@rows.sort_by!(&:first)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def walk
|
|
122
|
+
collect(@root, nil, @root)
|
|
123
|
+
@proc_values.each do |body, parent, local_scope|
|
|
124
|
+
value_targets(parent, local_scope).each { |target| @bodies << [body, target] }
|
|
125
|
+
end
|
|
126
|
+
@heredocs.sort_by!(&:first)
|
|
127
|
+
@bodies.each { |body, target| add_rows(body, target) }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def collect(node, parent, local_scope)
|
|
131
|
+
heredoc = RebindingBlockCallees.heredoc_range(node)
|
|
132
|
+
@heredocs << heredoc if heredoc
|
|
133
|
+
note(node, parent, local_scope)
|
|
134
|
+
# A `def` opens a new local-variable scope; blocks and lambdas share the enclosing one. A class body is
|
|
135
|
+
# read as part of the scope around it, which only widens the set a `binding` there may reach.
|
|
136
|
+
inner = node.is_a?(Prism::DefNode) ? node : local_scope
|
|
137
|
+
node.compact_child_nodes.each { |child| collect(child, node, inner) }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def note(node, parent, local_scope)
|
|
141
|
+
case node
|
|
142
|
+
when Prism::CallNode then note_call(node, parent, local_scope)
|
|
143
|
+
when Prism::LambdaNode then note_value(node, node, parent, local_scope)
|
|
144
|
+
when Prism::LocalVariableReadNode
|
|
145
|
+
@local_reads[node.name] << [node, parent] unless parent.is_a?(Prism::BlockArgumentNode)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def note_call(node, parent, local_scope)
|
|
150
|
+
block = node.block
|
|
151
|
+
body = RebindingBlockCallees.block_body(block)
|
|
152
|
+
@bodies << [body, node] if body
|
|
153
|
+
if block.is_a?(Prism::BlockArgumentNode) && block.expression.is_a?(Prism::LocalVariableReadNode)
|
|
154
|
+
@block_passes[block.expression.name] << node
|
|
155
|
+
end
|
|
156
|
+
value = RebindingBlockCallees.proc_literal(node)
|
|
157
|
+
note_value(node, value, parent, local_scope) if value
|
|
158
|
+
target = RebindingBlockCallees.send_target(node)
|
|
159
|
+
@scopes_reading_locals << local_scope if target.nil? || BINDING_CALLS.include?(target)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# `literal` is the proc expression and `body` its block. Passed with `&`, it is that call's block.
|
|
163
|
+
def note_value(literal, body, parent, local_scope)
|
|
164
|
+
return if parent.is_a?(Prism::BlockArgumentNode) || RebindingBlockCallees.plain_read?(literal, parent)
|
|
165
|
+
|
|
166
|
+
@proc_values << [body, parent, local_scope]
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# The calls a proc value assigned to a local reaches as `&local`, or {UNKNOWN}.
|
|
170
|
+
def value_targets(parent, local_scope)
|
|
171
|
+
return [UNKNOWN] if @scopes_reading_locals.include?(local_scope)
|
|
172
|
+
return [UNKNOWN] unless parent.is_a?(Prism::LocalVariableWriteNode)
|
|
173
|
+
|
|
174
|
+
name = parent.name
|
|
175
|
+
followed = @local_reads[name].all? do |read, read_parent|
|
|
176
|
+
RebindingBlockCallees.plain_read?(read, read_parent)
|
|
177
|
+
end
|
|
178
|
+
followed ? @block_passes[name] : [UNKNOWN]
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# The block's own row, plus one over the body of each heredoc that opens inside it.
|
|
182
|
+
def add_rows(body, target)
|
|
183
|
+
from = body.location.start_offset
|
|
184
|
+
to = body.location.end_offset
|
|
185
|
+
@rows << [from, to, target]
|
|
186
|
+
index = @heredocs.bsearch_index { |(opening, _start, _stop)| opening >= from }
|
|
187
|
+
return if index.nil?
|
|
188
|
+
|
|
189
|
+
while index < @heredocs.size && @heredocs[index].first < to
|
|
190
|
+
_opening, start, stop = @heredocs[index]
|
|
191
|
+
@rows << [start, stop, target]
|
|
192
|
+
index += 1
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
module_function
|
|
198
|
+
|
|
199
|
+
# Does `call_node` sit inside a block that some project definition of its callee may run under another `self`?
|
|
200
|
+
def inside_rebound_block?(call_node, scope, sites, scope_index = nil)
|
|
201
|
+
sites.enclosing(call_node).any? do |block_call|
|
|
202
|
+
block_call == Sites::UNKNOWN || callee_may_rebind?(block_call, scope, sites, scope_index)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# `[opening, body start, end]` offsets of a heredoc `node` opens, or nil.
|
|
207
|
+
def heredoc_range(node)
|
|
208
|
+
opening = node.opening_loc if node.respond_to?(:opening_loc)
|
|
209
|
+
closing = node.closing_loc if node.respond_to?(:closing_loc)
|
|
210
|
+
return nil unless opening&.slice&.start_with?("<<") && closing
|
|
211
|
+
|
|
212
|
+
start = case node
|
|
213
|
+
when Prism::StringNode, Prism::XStringNode then node.content_loc.start_offset
|
|
214
|
+
when Prism::InterpolatedStringNode, Prism::InterpolatedXStringNode
|
|
215
|
+
node.parts.first&.location&.start_offset
|
|
216
|
+
end
|
|
217
|
+
[opening.start_offset, start || closing.start_offset, closing.end_offset]
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# The node whose span is the block `call` passes: its literal block, or a lambda / proc literal passed with `&`.
|
|
221
|
+
def block_body(block)
|
|
222
|
+
case block
|
|
223
|
+
when Prism::BlockNode then block
|
|
224
|
+
when Prism::BlockArgumentNode then proc_literal(block.expression)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def proc_literal(node)
|
|
229
|
+
case node
|
|
230
|
+
when Prism::LambdaNode then node
|
|
231
|
+
when Prism::CallNode
|
|
232
|
+
node.block if node.block.is_a?(Prism::BlockNode) && proc_literal_call?(node)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def proc_literal_call?(node)
|
|
237
|
+
return PROC_LITERAL_CALLS.include?(node.name) if node.receiver.nil?
|
|
238
|
+
|
|
239
|
+
node.name == :new && node.receiver.is_a?(Prism::ConstantReadNode) && node.receiver.name == :Proc
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def callee_may_rebind?(block_call, scope, sites, scope_index = nil)
|
|
243
|
+
name, receiver = Dispatch.dispatch(block_call)
|
|
244
|
+
return true if name.nil? || REBINDING_CORE_CALLS.include?(name)
|
|
245
|
+
return true if Dispatch.callable_call?(name, receiver, scope, scope_index)
|
|
246
|
+
return true if answer(name, scope, sites)
|
|
247
|
+
# Ruby reaches `method_missing` only for a name the receiver lacks.
|
|
248
|
+
return false if Dispatch.resolves_on_receiver?(receiver, name, scope, scope_index)
|
|
249
|
+
|
|
250
|
+
answer(:method_missing, scope, sites)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def answer(name, scope, sites)
|
|
254
|
+
answers = sites.answers
|
|
255
|
+
return answers[name] if answers.key?(name)
|
|
256
|
+
|
|
257
|
+
answers[name] = project_may_rebind?(name, scope, sites)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# The call's own name, or the literal name a `send` family call dispatches to (nil when it is not one).
|
|
261
|
+
def send_target(call)
|
|
262
|
+
return call.name unless SEND_CALLS.include?(call.name)
|
|
263
|
+
|
|
264
|
+
literal_name(call.arguments&.arguments&.first)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def literal_name(first)
|
|
268
|
+
case first
|
|
269
|
+
when Prism::SymbolNode, Prism::StringNode
|
|
270
|
+
text = first.unescaped
|
|
271
|
+
text.to_sym if text.valid_encoding?
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Reads every project table for `name`, so it records a `defines:<name>` edge whichever way it answers (a file
|
|
276
|
+
# that starts or stops defining the name re-checks this one), plus each read body's own method edge through the
|
|
277
|
+
# scope accessors (an edit of that body re-checks this one).
|
|
278
|
+
def project_may_rebind?(name, scope, sites)
|
|
279
|
+
DependencyRecorder.read_missing(:defines, name) if DependencyRecorder.active?
|
|
280
|
+
return true if patched_anywhere?(name, scope)
|
|
281
|
+
|
|
282
|
+
slots = Set.new
|
|
283
|
+
readers = {
|
|
284
|
+
[scope.discovered_def_nodes, :instance] => ->(owner) { instance_def(scope, owner, name) },
|
|
285
|
+
[scope.discovered_singleton_def_nodes, :singleton] => ->(owner) { scope.singleton_def_for(owner, name) }
|
|
286
|
+
}
|
|
287
|
+
readers.each do |(tables, side), reader|
|
|
288
|
+
tables.each do |owner, table|
|
|
289
|
+
next unless table.key?(name)
|
|
290
|
+
|
|
291
|
+
slots << [owner, side]
|
|
292
|
+
node = reader.call(owner)
|
|
293
|
+
return true if !node.is_a?(Prism::DefNode) || def_may_rebind?(node)
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
unread_definition?(sites.def_rows(scope)[name], slots) || bodiless_definition?(name, scope)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def instance_def(scope, owner, name)
|
|
300
|
+
return scope.top_level_def_for(name) if owner == Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY
|
|
301
|
+
|
|
302
|
+
scope.user_def_for(owner, name)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# A def table keeps one body per class and side, so a second `def` of the name for the same slot (in a
|
|
306
|
+
# sibling file, whose order is not load order, or in the other arm of an `if`) is one the tables never
|
|
307
|
+
# showed, and so is a `def` whose class and side the tables hold no slot for. Every `def` outside a deferred
|
|
308
|
+
# body has a named row in the deferred-range table; a `module_function` row stands for both sides. A row with
|
|
309
|
+
# no side (a `def` inside a block, a `define_singleton_method` name) counts under a nil side, which no slot has.
|
|
310
|
+
def unread_definition?(rows, slots)
|
|
311
|
+
return false if rows.nil?
|
|
312
|
+
|
|
313
|
+
rows.any? { |slot, count| count > 1 || !slots.include?(slot) }
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def def_row_counts(scope)
|
|
317
|
+
counts = Hash.new { |hash, name| hash[name] = Hash.new(0) }
|
|
318
|
+
scope.discovered_deferred_ranges.each_value do |list|
|
|
319
|
+
list.each do |(_start, _stop, name, kind, owner)|
|
|
320
|
+
next if name.nil?
|
|
321
|
+
|
|
322
|
+
key = owner || Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY
|
|
323
|
+
sides = kind == :both ? %i[instance singleton] : [kind]
|
|
324
|
+
sides.each { |side| counts[name][[key, side]] += 1 }
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
counts.default_proc = nil
|
|
328
|
+
counts
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# A name some class gives a method with no `def` body Rigor holds (`attr_*`, `alias_method`, `define_method`).
|
|
332
|
+
def bodiless_definition?(name, scope)
|
|
333
|
+
scope.discovered_methods.any? do |owner, table|
|
|
334
|
+
table.key?(name) &&
|
|
335
|
+
!scope.discovered_def_nodes[owner]&.key?(name) &&
|
|
336
|
+
!scope.discovered_singleton_def_nodes[owner]&.key?(name)
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def patched_anywhere?(name, scope)
|
|
341
|
+
patched = scope.environment&.project_patched_methods
|
|
342
|
+
!patched.nil? && patched.by_key.any? { |(_owner, patched_name, _kind), _entry| patched_name == name }
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def def_may_rebind?(def_node)
|
|
346
|
+
return false if def_node.body.nil?
|
|
347
|
+
|
|
348
|
+
block = def_node.parameters&.block
|
|
349
|
+
escapes?(def_node.body, nil, block&.name)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# `block_name` is the def's named `&` parameter, or nil.
|
|
353
|
+
def escapes?(node, parent, block_name)
|
|
354
|
+
return true if hands_block_on?(node, parent, block_name)
|
|
355
|
+
|
|
356
|
+
node.compact_child_nodes.any? { |child| escapes?(child, node, block_name) }
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def hands_block_on?(node, parent, block_name)
|
|
360
|
+
case node
|
|
361
|
+
when Prism::ForwardingSuperNode then !node.block.is_a?(Prism::BlockNode)
|
|
362
|
+
when Prism::SuperNode then node.block.nil?
|
|
363
|
+
when Prism::BlockArgumentNode then node.expression.nil?
|
|
364
|
+
when Prism::ForwardingArgumentsNode then true
|
|
365
|
+
when Prism::LocalVariableReadNode then node.name == block_name && !plain_read?(node, parent)
|
|
366
|
+
when Prism::CallNode then reaches_locals?(node)
|
|
367
|
+
else false
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# Issue #2186 — a call that can reach the block local by name, on any receiver and through `send`:
|
|
372
|
+
# `Kernel.eval("module_eval(&b)")`, `send(:binding).local_variable_get(:b)`, `instance_eval("module_eval(&b)")`.
|
|
373
|
+
# A `send` whose method name is not a literal may be any of them.
|
|
374
|
+
def reaches_locals?(call)
|
|
375
|
+
name = send_target(call)
|
|
376
|
+
return true if name.nil? || BINDING_CALLS.include?(name)
|
|
377
|
+
|
|
378
|
+
STRING_EVAL_CALLS.include?(name) && !call.arguments.nil?
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def plain_read?(node, parent)
|
|
382
|
+
case parent
|
|
383
|
+
when Prism::CallNode then parent.receiver.equal?(node) && PLAIN_PROC_CALLS.include?(parent.name)
|
|
384
|
+
when Prism::AndNode then parent.left.equal?(node)
|
|
385
|
+
when *PREDICATE_PARENTS then parent.predicate.equal?(node)
|
|
386
|
+
else false
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
require_relative "rebinding_block_callees/dispatch"
|
|
@@ -55,8 +55,7 @@ module Rigor
|
|
|
55
55
|
collect_ensure_returns(begin_node)
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
-
#
|
|
59
|
-
# `#collect` returns it.
|
|
58
|
+
# One `{return_node:}` entry per offending `return`, frozen the same way `#collect` returns them.
|
|
60
59
|
def results
|
|
61
60
|
@results.freeze
|
|
62
61
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
5
|
require_relative "rule_ids"
|
|
6
|
+
require_relative "../dependency_recorder"
|
|
6
7
|
require_relative "../diagnostic"
|
|
7
8
|
require_relative "../../inference/version_guard"
|
|
8
9
|
require_relative "../../source/constant_path"
|
|
@@ -243,7 +244,13 @@ module Rigor
|
|
|
243
244
|
|
|
244
245
|
# Whether a project or `pre_eval:` file defines a method called `name` anywhere — on any class, on either
|
|
245
246
|
# side, or at the top level. Asked once per name per file.
|
|
247
|
+
#
|
|
248
|
+
# Issue #2133 — the answer reads every project file's tables whole, so it records a `defines:<name>` edge
|
|
249
|
+
# whichever way it answers, as {Inference::BlockCallTiming.project_defines_anywhere?} does: an edit that makes
|
|
250
|
+
# a file start or stop defining the name re-checks the call. A `pre_eval:` patch moves the snapshot's own
|
|
251
|
+
# fingerprint.
|
|
246
252
|
def project_defines?(scope, name)
|
|
253
|
+
Analysis::DependencyRecorder.read_missing(:defines, name) if Analysis::DependencyRecorder.active?
|
|
247
254
|
return true if scope.discovered_methods.any? { |_owner, table| table.key?(name) }
|
|
248
255
|
return true if scope.discovered_def_nodes.any? { |_owner, table| table.key?(name) }
|
|
249
256
|
|
|
@@ -178,7 +178,7 @@ module Rigor
|
|
|
178
178
|
|
|
179
179
|
# Resolves a user-supplied rule token (`undefined-method`, `call.undefined-method`, or the family wildcard
|
|
180
180
|
# `call`) to the set of canonical rule identifiers it disables. Returns `nil` for `"all"` (the existing
|
|
181
|
-
# wildcard meaning "every rule"),
|
|
181
|
+
# wildcard meaning "every rule"), and an empty array for an unknown token.
|
|
182
182
|
def self.resolve_rule_token(token)
|
|
183
183
|
return nil if token == "all"
|
|
184
184
|
return [LEGACY_RULE_ALIASES.fetch(token)] if LEGACY_RULE_ALIASES.key?(token)
|
|
@@ -10,7 +10,7 @@ module Rigor
|
|
|
10
10
|
module CheckRules
|
|
11
11
|
# ADR-53 Track B — the engine-owned AST walk that hosts CheckRules' per-node collectors, so a file is
|
|
12
12
|
# traversed once for all of them instead of once per collector (the ADR-52 WD4 model applied to the
|
|
13
|
-
# built-in rules;
|
|
13
|
+
# built-in rules; {Plugin::NodeRuleWalk} drives the same collectors, ADR-53 B4).
|
|
14
14
|
#
|
|
15
15
|
# The walk is a single visit-before-descend DFS over `compact_child_nodes`. It threads the union of the
|
|
16
16
|
# context the hosted collectors need — the per-collector hand-rolled walks each tracked some subset of
|
|
@@ -146,12 +146,14 @@ module Rigor
|
|
|
146
146
|
qualified_prefix = extend_prefix(node, context.qualified_prefix)
|
|
147
147
|
detached_ivar_facet = context.detached_ivar_facet || detached_ivar_facet?(node)
|
|
148
148
|
|
|
149
|
-
Context
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
149
|
+
# Nearly every node leaves all four fields as they were, and a {Context} is immutable, so the parent's
|
|
150
|
+
# is the child's. This runs once per node and was a top allocation site (a Data per node).
|
|
151
|
+
if in_loop_or_block == context.in_loop_or_block && inside_def == context.inside_def &&
|
|
152
|
+
qualified_prefix.equal?(context.qualified_prefix) && detached_ivar_facet == context.detached_ivar_facet
|
|
153
|
+
return context
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
Context.new(in_loop_or_block, qualified_prefix, inside_def, detached_ivar_facet)
|
|
155
157
|
end
|
|
156
158
|
|
|
157
159
|
# True for a node whose descendants write ivars to a store the enclosing class name does not name:
|
|
@@ -123,10 +123,9 @@ module Rigor
|
|
|
123
123
|
@scope_index[node] || @scope_index[node.statements] || @scope_index[node.rescue_clause]
|
|
124
124
|
end
|
|
125
125
|
|
|
126
|
-
# Resolves one clause to `{clause:, names:
|
|
127
|
-
#
|
|
128
|
-
#
|
|
129
|
-
# `StandardError`.
|
|
126
|
+
# Resolves one clause to `{clause:, names:}`, where `names` are the certified exception CLASS names it
|
|
127
|
+
# rescues, or nil when the clause is opaque (any entry unresolved / dynamic / splat / module — the
|
|
128
|
+
# comparisons involving it stay silent). A bare `rescue` names the implicit `StandardError`.
|
|
130
129
|
def clause_info(clause, prefix, scope)
|
|
131
130
|
exceptions = clause.exceptions
|
|
132
131
|
if exceptions.empty?
|
|
@@ -9,9 +9,9 @@ module Rigor
|
|
|
9
9
|
# the envelope `global.write-type-mismatch` and `global.readonly-write` judge a write against.
|
|
10
10
|
#
|
|
11
11
|
# The setter is the authority, not the RBS declaration of the global. Where the two differ, the setter wins:
|
|
12
|
-
# `$;` is declared
|
|
13
|
-
# declared
|
|
14
|
-
# that enforces it and was confirmed on Ruby 4.0.5, whose error the comment quotes.
|
|
12
|
+
# `$;` is declared as a Regexp, a String or nil, but its setter also takes any object with `to_str`, and
|
|
13
|
+
# `$stdout` is declared as an IO, but its setter takes anything that responds to `write`. Each entry names
|
|
14
|
+
# the CRuby function that enforces it and was confirmed on Ruby 4.0.5, whose error the comment quotes.
|
|
15
15
|
#
|
|
16
16
|
# A special whose setter accepts every value is absent, so a write to it is never checked: `$stdin`
|
|
17
17
|
# (ADR-117 WD2), `$_`, `$VERBOSE` / `$-v` / `$-w`, `$DEBUG` / `$-d` and `$=`. So is `$@`, whose setter raises
|