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
|
@@ -56,8 +56,7 @@ module Rigor
|
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
# @param root — the local-variable scope to scan.
|
|
59
|
-
# @param layout_lookup — a
|
|
60
|
-
# struct member list.
|
|
59
|
+
# @param layout_lookup — a resolver mapping a constant receiver name to its struct member list.
|
|
61
60
|
# @return the fold-safe local names.
|
|
62
61
|
def fold_safe_locals(root, layout_lookup)
|
|
63
62
|
return EMPTY if root.nil?
|
|
@@ -283,8 +282,7 @@ module Rigor
|
|
|
283
282
|
# `[1].each { s.x = v }; s.x` would read the construction value — verified unsound on #525.
|
|
284
283
|
#
|
|
285
284
|
# `for` shares the enclosing locals rather than opening a scope, so the block rationale does not apply
|
|
286
|
-
# to it; it is a boundary for the same reason `while` is (below)
|
|
287
|
-
# wrong reason for it.
|
|
285
|
+
# to it; it is a boundary for the same reason `while` is (below).
|
|
288
286
|
#
|
|
289
287
|
# `while` / `until` are boundaries because a single static pass cannot model a member setter across
|
|
290
288
|
# ITERATIONS. Issue #597 tried to lift this: `eval_loop` does join the body's exit scope with the
|
|
@@ -327,8 +325,8 @@ module Rigor
|
|
|
327
325
|
return nil unless meta_constant?(call_node.receiver, :Struct)
|
|
328
326
|
|
|
329
327
|
args = call_node.arguments&.arguments || []
|
|
330
|
-
# `[0..-2]` cannot return nil for a start of 0, but `Array#[]
|
|
331
|
-
# the `|| []` keeps the read well-typed on that worst case.
|
|
328
|
+
# `[0..-2]` cannot return nil for a start of 0, but the RBS signature of `Array#[]` with a Range says it
|
|
329
|
+
# can; the `|| []` keeps the read well-typed on that worst case.
|
|
332
330
|
positional = args.last.is_a?(Prism::KeywordHashNode) ? (args[0..-2] || []) : args
|
|
333
331
|
positional = positional[1..] if positional.first.is_a?(Prism::StringNode)
|
|
334
332
|
return nil if positional.nil? || positional.empty?
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
3
5
|
require_relative "synthetic_method"
|
|
4
6
|
|
|
5
7
|
module Rigor
|
|
@@ -65,8 +67,29 @@ module Rigor
|
|
|
65
67
|
{ "entries" => entries.map(&:to_h), "class_names" => class_names }
|
|
66
68
|
end
|
|
67
69
|
|
|
70
|
+
# What a dispatch can read from the index, digested (nil for the empty index): each key's rows in their
|
|
71
|
+
# first-wins order, with the return type and the Tier B `origin_module` the dispatcher promotes through,
|
|
72
|
+
# and the synthesised class names. The rest of the provenance (the call site's path and line) is left
|
|
73
|
+
# out, so an edit that only moves the `devise` line keeps the digest. `IncrementalSession` compares it
|
|
74
|
+
# across runs (PR #2286 review M1): the index is built from class-body calls that no declaration
|
|
75
|
+
# signature records, so a recheck that sees it move cannot trust its served files.
|
|
76
|
+
def surface_digest
|
|
77
|
+
return nil if empty?
|
|
78
|
+
|
|
79
|
+
rows = entries.each_with_index
|
|
80
|
+
.sort_by { |entry, index| [entry.kind.to_s, entry.class_name, entry.method_name.to_s, index] }
|
|
81
|
+
.map { |entry, _| surface_row(entry) }
|
|
82
|
+
Digest::SHA256.hexdigest((rows + class_names.sort.map { |name| "class\x1f#{name}" }).join("\x00"))
|
|
83
|
+
end
|
|
84
|
+
|
|
68
85
|
EMPTY_ROW = [].freeze
|
|
69
86
|
|
|
87
|
+
def surface_row(entry)
|
|
88
|
+
[entry.kind, entry.class_name, entry.method_name, entry.return_type,
|
|
89
|
+
entry.provenance[:origin_module], entry.provenance[:plugin_id]].join("\x1f")
|
|
90
|
+
end
|
|
91
|
+
private :surface_row
|
|
92
|
+
|
|
70
93
|
def bucket(entries, kind)
|
|
71
94
|
h = {}
|
|
72
95
|
entries.each do |entry|
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require_relative "../../source/literals"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module Inference
|
|
8
|
+
module SyntheticMethodScanner
|
|
9
|
+
# The instance-method names a project class answers ahead of a module a trait registry includes into it
|
|
10
|
+
# (PR #2286 review S1). Ruby looks a method up on the class itself before any module it includes, and a
|
|
11
|
+
# module the class itself includes or prepends sits ahead of one `devise` included earlier — so where
|
|
12
|
+
# the project names a member, the Tier B entry for it would answer the call from the wrong rung:
|
|
13
|
+
#
|
|
14
|
+
# class User < ApplicationRecord
|
|
15
|
+
# devise :confirmable
|
|
16
|
+
# include PasswordPolicy # defines password_required?
|
|
17
|
+
# def confirmed? = :yes
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# `User.new.confirmed?` is `:yes` and `password_required?` is the policy's, not `bool` from the Devise
|
|
21
|
+
# module's RBS. {SyntheticMethodScanner} therefore drops a trait entry whose name this index reports.
|
|
22
|
+
#
|
|
23
|
+
# A name counts as the class's when any reopening of the class body (in any project file) writes `def`,
|
|
24
|
+
# `alias` / `alias_method`, `attr_*`, `define_method` or `delegate` for it, or when a project module the
|
|
25
|
+
# class includes or prepends (followed through that module's own `include` / `prepend`) does. The MRO
|
|
26
|
+
# order of an `include` relative to the `devise` call is NOT consulted: a module included before the
|
|
27
|
+
# call sits behind the Devise module at runtime, but the name is declined anyway. Declining is the
|
|
28
|
+
# conservative direction — the call then reaches the tiers below the synthetic one (the discovered
|
|
29
|
+
# method, the project mixin), which answer it as they did before Tier B was reachable. A mixin constant
|
|
30
|
+
# is matched to a project module by name or by `::`-suffix, again widening only what is declined.
|
|
31
|
+
class ProjectMembers
|
|
32
|
+
# The calls that name members through their literal arguments, and the suffixes each name takes.
|
|
33
|
+
SUFFIXES_BY_CALL = Ractor.make_shareable(
|
|
34
|
+
{
|
|
35
|
+
attr_reader: [""], attr: [""], attr_writer: ["="], attr_accessor: ["", "="], delegate: [""]
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
NAME_ARG_CALLS = %i[define_method alias_method].freeze
|
|
39
|
+
MIXIN_CALLS = %i[include prepend].freeze
|
|
40
|
+
|
|
41
|
+
Entry = Struct.new(:names, :mixins)
|
|
42
|
+
private_constant :Entry
|
|
43
|
+
|
|
44
|
+
def initialize(asts)
|
|
45
|
+
@classes = Hash.new { |hash, key| hash[key] = Entry.new(Set.new, []) }
|
|
46
|
+
@modules = Hash.new { |hash, key| hash[key] = Entry.new(Set.new, []) }
|
|
47
|
+
asts.each_value { |ast| walk_namespaces(ast, []) unless ast.nil? }
|
|
48
|
+
@declared = {}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# True when `class_name`'s own body, or a project module it includes or prepends, names `method_name`.
|
|
52
|
+
def declares?(class_name, method_name)
|
|
53
|
+
(@declared[class_name] ||= declared_names(class_name)).include?(method_name.to_sym)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def declared_names(class_name)
|
|
59
|
+
entry = @classes.fetch(class_name, nil)
|
|
60
|
+
return Set.new.freeze if entry.nil?
|
|
61
|
+
|
|
62
|
+
names = entry.names.dup
|
|
63
|
+
visited = Set.new
|
|
64
|
+
queue = entry.mixins.dup
|
|
65
|
+
until queue.empty?
|
|
66
|
+
module_keys_for(queue.shift).each do |key|
|
|
67
|
+
next unless visited.add?(key)
|
|
68
|
+
|
|
69
|
+
names.merge(@modules[key].names)
|
|
70
|
+
queue.concat(@modules[key].mixins)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
names.freeze
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def module_keys_for(name)
|
|
77
|
+
@modules.keys.select { |key| key == name || key.end_with?("::#{name}") }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def walk_namespaces(node, scope_stack)
|
|
81
|
+
case node
|
|
82
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
83
|
+
name = SyntheticMethodScanner.class_name_from(node, scope_stack)
|
|
84
|
+
if name
|
|
85
|
+
table = node.is_a?(Prism::ClassNode) ? @classes : @modules
|
|
86
|
+
collect_members(node.body, table[name])
|
|
87
|
+
end
|
|
88
|
+
inner = scope_stack + [node]
|
|
89
|
+
node.body&.rigor_each_child { |child| walk_namespaces(child, inner) }
|
|
90
|
+
else
|
|
91
|
+
node.rigor_each_child { |child| walk_namespaces(child, scope_stack) }
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Every member-naming form under the namespace body, at any depth short of a nested namespace (a
|
|
96
|
+
# `private def`, a `def` under a conditional and an `included do ... def` all count). A nested `class`,
|
|
97
|
+
# `module` or `class << self` is another namespace's body.
|
|
98
|
+
def collect_members(node, entry)
|
|
99
|
+
return if node.nil?
|
|
100
|
+
|
|
101
|
+
case node
|
|
102
|
+
when Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode then return
|
|
103
|
+
when Prism::DefNode then entry.names << node.name if node.receiver.nil?
|
|
104
|
+
when Prism::AliasMethodNode then add_symbol(entry, node.new_name)
|
|
105
|
+
when Prism::CallNode then collect_call(node, entry)
|
|
106
|
+
end
|
|
107
|
+
node.rigor_each_child { |child| collect_members(child, entry) }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def collect_call(call, entry)
|
|
111
|
+
return unless call.receiver.nil? || call.receiver.is_a?(Prism::SelfNode)
|
|
112
|
+
|
|
113
|
+
args = call.arguments&.arguments || []
|
|
114
|
+
if MIXIN_CALLS.include?(call.name)
|
|
115
|
+
entry.mixins.concat(args.filter_map { |arg| SyntheticMethodScanner.const_name_string(arg) })
|
|
116
|
+
elsif NAME_ARG_CALLS.include?(call.name)
|
|
117
|
+
add_symbol(entry, args.first)
|
|
118
|
+
elsif (suffixes = SUFFIXES_BY_CALL[call.name])
|
|
119
|
+
args.each { |arg| suffixes.each { |suffix| add_symbol(entry, arg, suffix: suffix) } }
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def add_symbol(entry, node, suffix: "")
|
|
124
|
+
value = Source::Literals.symbol_or_string_name(node)
|
|
125
|
+
entry.names << :"#{value}#{suffix}" unless value.nil?
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -8,6 +8,7 @@ require_relative "../source/literals"
|
|
|
8
8
|
require_relative "../source/node_children"
|
|
9
9
|
require_relative "synthetic_method"
|
|
10
10
|
require_relative "synthetic_method_index"
|
|
11
|
+
require_relative "synthetic_method_scanner/project_members"
|
|
11
12
|
|
|
12
13
|
module Rigor
|
|
13
14
|
module Inference
|
|
@@ -63,18 +64,30 @@ module Rigor
|
|
|
63
64
|
registries = collect_trait_registries(plugin_registry)
|
|
64
65
|
nested_templates = collect_nested_class_templates(plugin_registry)
|
|
65
66
|
return SyntheticMethodIndex::EMPTY if templates.empty? && registries.empty? && nested_templates.empty?
|
|
66
|
-
# Tier B alone cannot emit without an environment: every trait entry
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
# gate mirrors that nil guard and MUST be removed in the change that threads a real environment.
|
|
67
|
+
# Tier B alone cannot emit without an environment: every trait entry funnels through
|
|
68
|
+
# `module_instance_method_names`, which answers `[]` for a nil environment. The production pre-pass always
|
|
69
|
+
# passes one (#476), so this gate guards the direct callers that pass none (a spec, a probe): a
|
|
70
|
+
# registry-only scan would otherwise pay a whole-project parse to build a provably empty index. It mirrors
|
|
71
|
+
# that nil guard and goes with it.
|
|
72
72
|
return SyntheticMethodIndex::EMPTY if environment.nil? && templates.empty? && nested_templates.empty?
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
sources = read_sources(paths, buffer: buffer)
|
|
75
|
+
# With only trait registries contributing, a project that never writes a registry's method name (no
|
|
76
|
+
# `devise` anywhere) cannot produce an entry: a call node named `devise` needs the text `devise` in its
|
|
77
|
+
# file. Skip the parse (the dominant cost) then, and the environment build it would have asked for.
|
|
78
|
+
return SyntheticMethodIndex::EMPTY if templates.empty? && nested_templates.empty? &&
|
|
79
|
+
none_mention_registry_method?(sources, registries)
|
|
80
|
+
|
|
81
|
+
asts = parse_sources(sources)
|
|
82
|
+
entries, class_names = collect_all_entries(asts, templates, registries, nested_templates, environment,
|
|
83
|
+
fact_store)
|
|
84
|
+
SyntheticMethodIndex.new(entries: decline_project_declared(entries, asts), class_names: class_names)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The match + emit walk over every parsed file: `[entries, class_names]`.
|
|
88
|
+
def collect_all_entries(asts, templates, registries, nested_templates, environment, fact_store)
|
|
75
89
|
hierarchy = build_hierarchy(asts)
|
|
76
90
|
concern_index = build_concern_index(asts)
|
|
77
|
-
|
|
78
91
|
entries = []
|
|
79
92
|
class_names = []
|
|
80
93
|
asts.each do |path, ast|
|
|
@@ -88,8 +101,19 @@ module Rigor
|
|
|
88
101
|
end
|
|
89
102
|
collect_nested_class_entries(entries, class_names, nested_templates, ast, path) unless nested_templates.empty?
|
|
90
103
|
end
|
|
104
|
+
[entries, class_names]
|
|
105
|
+
end
|
|
91
106
|
|
|
92
|
-
|
|
107
|
+
# PR #2286 review S1 — a Tier B entry whose name the class itself declares, or a project module it includes
|
|
108
|
+
# or prepends declares, is dropped: Ruby answers that call from the project's member, ahead of the module
|
|
109
|
+
# the registry included ({ProjectMembers}). Tier C entries (no `origin_module`) are left as they were.
|
|
110
|
+
def decline_project_declared(entries, asts)
|
|
111
|
+
return entries unless entries.any? { |entry| entry.provenance[:origin_module] }
|
|
112
|
+
|
|
113
|
+
members = ProjectMembers.new(asts)
|
|
114
|
+
entries.reject do |entry|
|
|
115
|
+
entry.provenance[:origin_module] && members.declares?(entry.class_name, entry.method_name)
|
|
116
|
+
end
|
|
93
117
|
end
|
|
94
118
|
|
|
95
119
|
# Aggregates `(plugin_id, template)` pairs across every plugin's `manifest.heredoc_templates` in registration
|
|
@@ -249,16 +273,32 @@ module Rigor
|
|
|
249
273
|
call_node.arguments&.arguments&.first
|
|
250
274
|
end
|
|
251
275
|
|
|
252
|
-
|
|
276
|
+
# `path => source text` (nil when the file cannot be read), reading an editor buffer's in-flight bytes
|
|
277
|
+
# for the logical path it is bound to.
|
|
278
|
+
def read_sources(paths, buffer: nil)
|
|
253
279
|
paths.to_h do |path|
|
|
254
280
|
physical = buffer ? buffer.resolve(path) : path
|
|
255
|
-
|
|
256
|
-
[path, Prism.parse(source, filepath: path).value]
|
|
281
|
+
[path, File.read(physical)]
|
|
257
282
|
rescue StandardError
|
|
258
283
|
[path, nil]
|
|
259
284
|
end
|
|
260
285
|
end
|
|
261
286
|
|
|
287
|
+
def parse_sources(sources)
|
|
288
|
+
sources.to_h do |path, source|
|
|
289
|
+
[path, source.nil? ? nil : Prism.parse(source, filepath: path).value]
|
|
290
|
+
rescue StandardError
|
|
291
|
+
[path, nil]
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def none_mention_registry_method?(sources, registries)
|
|
296
|
+
names = registries.map { |(_plugin_id, registry)| registry.method_name.to_s }
|
|
297
|
+
sources.each_value.none? do |source|
|
|
298
|
+
!source.nil? && names.any? { |name| source.include?(name) }
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
262
302
|
# ADR-16 slice 4 — Concern re-targeting index.
|
|
263
303
|
#
|
|
264
304
|
# Walks every top-level / nested `module M` decl looking for the ActiveSupport::Concern shape:
|
|
@@ -493,7 +533,7 @@ module Rigor
|
|
|
493
533
|
# (resolved through `registry.modules_by_symbol`; unknown
|
|
494
534
|
# symbols silently skipped per design decision (2)).
|
|
495
535
|
#
|
|
496
|
-
# Returns
|
|
536
|
+
# Returns the module names: the `always_included` ones first, then those for the symbol arguments in order.
|
|
497
537
|
def resolve_trait_modules(registry, call_node)
|
|
498
538
|
modules = registry.always_included.dup
|
|
499
539
|
positional_symbols(call_node, registry).each do |symbol|
|
|
@@ -534,6 +574,18 @@ module Rigor
|
|
|
534
574
|
# Returns the Symbol method-name list defined on `module_name`'s RBS instance definition. Empty Array when the
|
|
535
575
|
# module is not in the RBS env (silent skip — the synthetic emit produces nothing rather than fabricating method
|
|
536
576
|
# names).
|
|
577
|
+
#
|
|
578
|
+
# RBS builds a module's instance definition over its self types, or over `Object` when it declares none:
|
|
579
|
+
# `Kernel#puts`, `Object#hash`, the pretty-printer's mixin — and, for `module Devise::Models::X :
|
|
580
|
+
# ActiveRecord::Base`, every `ActiveRecord::Base` method (`save`, `update`) — appear in its method table
|
|
581
|
+
# beside its own members. They are not what including the module adds, and synthesising them would answer
|
|
582
|
+
# the class's own or inherited methods from the module's view of them. A method is kept only when its
|
|
583
|
+
# defining owner is the module itself or one of the module's own ancestors (its `include` / `prepend`
|
|
584
|
+
# chain, which `definition.ancestors` lists without the self types or `Object`).
|
|
585
|
+
#
|
|
586
|
+
# A non-public method is skipped: the tier answers a call by name, and a private module method answered
|
|
587
|
+
# for `user.secret` would silence the call Ruby rejects. A definition that carries no owner (the specs'
|
|
588
|
+
# stubs) keeps every name.
|
|
537
589
|
def module_instance_method_names(module_name, environment)
|
|
538
590
|
return [] if environment.nil?
|
|
539
591
|
|
|
@@ -543,7 +595,14 @@ module Rigor
|
|
|
543
595
|
definition = loader.instance_definition(module_name)
|
|
544
596
|
return [] if definition.nil?
|
|
545
597
|
|
|
546
|
-
|
|
598
|
+
own_ancestors = nil
|
|
599
|
+
definition.methods.filter_map do |name, method|
|
|
600
|
+
next name unless method.respond_to?(:defined_in)
|
|
601
|
+
next unless method.public?
|
|
602
|
+
|
|
603
|
+
own_ancestors ||= definition.ancestors.ancestors.to_set { |ancestor| ancestor.name.to_s }
|
|
604
|
+
name if own_ancestors.include?(method.defined_in.to_s)
|
|
605
|
+
end
|
|
547
606
|
rescue StandardError
|
|
548
607
|
[]
|
|
549
608
|
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../run_scoped_slots"
|
|
4
|
+
require_relative "memo_audit"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module Inference
|
|
8
|
+
# Issue #2515 — whether the `<toplevel>` def table's answer for a name IS the method a receiverless call of that
|
|
9
|
+
# name runs, so the callee mutation floor may read the def as written. {.reliable?} is false (the floor reads every
|
|
10
|
+
# argument as written, `StatementEvaluator::FLOOR_EVERY`) when any of these holds for `name`:
|
|
11
|
+
#
|
|
12
|
+
# - {Scope#toplevel_def_ambiguity} names a reason (the slot is contested, several ownerless definition rows of the
|
|
13
|
+
# name exist, a `def self.name` defines it on `main`, an `alias` introduces it, an anonymous class body defines
|
|
14
|
+
# it, `Object` defines it or has a module prepended);
|
|
15
|
+
# - a `pre_eval:` patch defines `name` on `Object`, `Kernel` or `BasicObject`.
|
|
16
|
+
#
|
|
17
|
+
# A visibility toggle (`private :name`, `public :name`) does not change which def answers a receiverless call, and
|
|
18
|
+
# `method_missing` on `Object` only answers a name nothing defines, so neither is a hazard.
|
|
19
|
+
#
|
|
20
|
+
# The answer is a function of the discovery index and the name (the environment is the run's), so it is memoised
|
|
21
|
+
# per index. A recorder is told the `defines:<name>` name edge on every call, hit or not
|
|
22
|
+
# ({Scope#toplevel_def_ambiguity}), so a file that starts or stops defining the name re-checks the caller; the memo
|
|
23
|
+
# is bypassed while one is active, as the callee floor's other memos are.
|
|
24
|
+
module ToplevelDefCensus
|
|
25
|
+
OBJECT_OWNERS = %w[Object Kernel BasicObject].freeze
|
|
26
|
+
RELIABLE_KEY = RunScopedSlots.register(:rigor_toplevel_def_reliable)
|
|
27
|
+
private_constant :OBJECT_OWNERS, :RELIABLE_KEY
|
|
28
|
+
|
|
29
|
+
module_function
|
|
30
|
+
|
|
31
|
+
# True when the `<toplevel>` table's def for `name` is the method a receiverless call of `name` runs.
|
|
32
|
+
def reliable?(scope, name)
|
|
33
|
+
return compute?(scope, name) if Analysis::DependencyRecorder.active? || MemoAudit.suspended?
|
|
34
|
+
|
|
35
|
+
table = (Thread.current[RELIABLE_KEY] ||= ObjectSpace::WeakKeyMap.new)
|
|
36
|
+
by_name = (table[scope.discovery] ||= {})
|
|
37
|
+
return by_name[name] = compute?(scope, name) unless by_name.key?(name)
|
|
38
|
+
|
|
39
|
+
MemoAudit.check("top-level def census", by_name[name], name) { compute?(scope, name) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def compute?(scope, name)
|
|
43
|
+
return false unless scope.toplevel_def_ambiguity(name).nil?
|
|
44
|
+
|
|
45
|
+
!patched?(scope, name)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# True when a `pre_eval:` patch defines `name` on an `Object`-level class.
|
|
49
|
+
def patched?(scope, name)
|
|
50
|
+
patched = scope.environment&.project_patched_methods
|
|
51
|
+
return false if patched.nil?
|
|
52
|
+
|
|
53
|
+
patched.by_key.each_key.any? do |(owner, method_name, _kind)|
|
|
54
|
+
method_name == name && OBJECT_OWNERS.include?(owner)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -29,9 +29,10 @@ module Rigor
|
|
|
29
29
|
# written in; a refinement seed (`non-empty-array[0]`) still can, since its remover does not close a literal.
|
|
30
30
|
#
|
|
31
31
|
# A site whose widening declines leaves the binding as it is, exactly as the straight-line seam does: a
|
|
32
|
-
# precise nominal (a declared or inferred `Array[String]` is a claim
|
|
33
|
-
#
|
|
34
|
-
#
|
|
32
|
+
# precise nominal under a site that stores nothing it can see (a declared or inferred `Array[String]` is a claim
|
|
33
|
+
# this seam may not grow; an element rewriter such as `map!` replaces it with the gradual arm, issue #2427), a
|
|
34
|
+
# receiver that is no carrier, or a name the mutator tables do not list for that carrier (`store` on a `Tuple`, a
|
|
35
|
+
# method its class does not define). The caller MUST therefore not read an unchanged binding as describing later
|
|
35
36
|
# iterations. An empty-witness refinement under a storing mutator that keeps the witness is not such a decline:
|
|
36
37
|
# the straight-line widening grows it (issue #936), and gives a site whose arguments describe nothing it stores
|
|
37
38
|
# (`non-empty-array[String]` under `map!`) the gradual arm itself ({RewriteMutation}),
|
|
@@ -64,10 +65,11 @@ module Rigor
|
|
|
64
65
|
CalleeStore = Data.define(:call, :arguments)
|
|
65
66
|
|
|
66
67
|
COLLECTION_CLASSES = %w[Array Hash].freeze
|
|
68
|
+
CONTAINER_CLASSES = %w[Array Hash Set].freeze
|
|
67
69
|
FLOORED_CLASSES = %w[Array Hash String].freeze
|
|
68
70
|
|
|
69
71
|
NO_ARG_NODES = [].freeze
|
|
70
|
-
private_constant :COLLECTION_CLASSES, :FLOORED_CLASSES, :NO_ARG_NODES
|
|
72
|
+
private_constant :COLLECTION_CLASSES, :CONTAINER_CLASSES, :FLOORED_CLASSES, :NO_ARG_NODES
|
|
71
73
|
|
|
72
74
|
module_function
|
|
73
75
|
|
|
@@ -99,6 +101,103 @@ module Rigor
|
|
|
99
101
|
VALUE_PRESERVING.include?(method_name) && !literal_carrier?(type) ? widened : gradual_content(widened)
|
|
100
102
|
end
|
|
101
103
|
|
|
104
|
+
# Issue #2296 — what the caller's argument `type` can hold after a callee that mutates the parameter inside a
|
|
105
|
+
# block (`def poke(acc) = 2.times { |i| acc[i] = "x" }`). Nothing at the call says what the block stores, so
|
|
106
|
+
# each carrier takes one unknown store and keeps its element type beside the gradual arm, as a block-captured
|
|
107
|
+
# binding does ({.widen}): `[7, 9]` reads `Array[Dynamic[top] | Integer]`, `{ a: 1 }`
|
|
108
|
+
# `Hash[Dynamic[top] | Symbol, Dynamic[top] | Integer]`, `Set[1]` `Set[Dynamic[top] | Integer]`, a literal
|
|
109
|
+
# String `String`. Not {.content_floor}'s bare carrier: dropping the element type there widened
|
|
110
|
+
# `ScopeIndexer.finalize_constant_writes` to `Hash[untyped, untyped]` under sig-gen (#2295). A class-level
|
|
111
|
+
# nominal (`Hash[String, Set[String]]`) is a claim this seam may not grow, and is left alone; a value-pinned one
|
|
112
|
+
# (`Hash["a" | "b", 1]`) holds a first-store answer the unknown store falsifies, and takes the gradual arm, as a
|
|
113
|
+
# block's unknown-store binding does.
|
|
114
|
+
def callee_block_floor(type)
|
|
115
|
+
widened = widen_store(type, :[]=, unknown_arguments(2)) || type
|
|
116
|
+
widened = callee_set_floor(widened) || widened
|
|
117
|
+
dynamic_elements(value_pinned_collection?(widened) ? gradual_content(widened) : widened)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Issue #2296 — `type` after a callee that grows the parameter through a Set-only adder (`s.add(x)`,
|
|
121
|
+
# `s.merge(xs)`): a Set carrier takes one unknown store, and anything else, a Hash under `merge` included, is
|
|
122
|
+
# returned as nil (unchanged).
|
|
123
|
+
def callee_set_floor(type)
|
|
124
|
+
widened = widen_store(type, :add, unknown_arguments(1))
|
|
125
|
+
widened && dynamic_elements(widened)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Issue #2647 — `type` with each container element the unknown store widened (`Dynamic[top] | String`, which an
|
|
129
|
+
# element read back out fails a literal-typed parameter with, although the callee stored a value the call never
|
|
130
|
+
# shows) made a gradual `Dynamic[String]`: the floor may forget a fact, never claim the nominal class. An element
|
|
131
|
+
# that carries no `Dynamic[top]` arm (a class-level `Array[Integer]`, which the floor leaves alone) is returned as
|
|
132
|
+
# it is. Scoped to the callee floor's two entry points, so a local mutation's unknown-store widening keeps its
|
|
133
|
+
# answer.
|
|
134
|
+
def dynamic_elements(type)
|
|
135
|
+
return type unless needs_dynamic?(type)
|
|
136
|
+
|
|
137
|
+
case type
|
|
138
|
+
when Type::Union then Type::Combinator.union(*type.members.map { |member| dynamic_elements(member) })
|
|
139
|
+
when Type::Difference then Type::Combinator.difference(dynamic_elements(type.base), type.removed)
|
|
140
|
+
when Type::Nominal
|
|
141
|
+
return type unless CONTAINER_CLASSES.include?(type.class_name) && !type.type_args.empty?
|
|
142
|
+
|
|
143
|
+
Type::Combinator.nominal_of(type.class_name, type_args: type.type_args.map { |arg| dynamic_element(arg) })
|
|
144
|
+
else type
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# True when {.dynamic_elements} would change `type`: a container element with a `Dynamic[top]` member and a class
|
|
149
|
+
# member (neither a literal nor `Dynamic[top]`). Reads with `any?` only, so the common unchanged type is not
|
|
150
|
+
# rebuilt.
|
|
151
|
+
def needs_dynamic?(type)
|
|
152
|
+
case type
|
|
153
|
+
when Type::Union then type.members.any? { |member| needs_dynamic?(member) }
|
|
154
|
+
when Type::Difference then needs_dynamic?(type.base)
|
|
155
|
+
when Type::Nominal
|
|
156
|
+
CONTAINER_CLASSES.include?(type.class_name) && type.type_args.any? { |arg| needs_dynamic_element?(arg) }
|
|
157
|
+
else false
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def needs_dynamic_element?(arg)
|
|
162
|
+
return false unless arg.is_a?(Type::Union)
|
|
163
|
+
|
|
164
|
+
arg.members.any? { |member| gradual_top?(member) } &&
|
|
165
|
+
arg.members.any? { |member| !member.is_a?(Type::Constant) && !gradual_top?(member) }
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def gradual_top?(member) = member.is_a?(Type::Dynamic) && member.static_facet.is_a?(Type::Top)
|
|
169
|
+
|
|
170
|
+
# `Dynamic[top] | A | B` as `Dynamic[A | B]` for the class members; a literal member (a value-pinned key) and any
|
|
171
|
+
# other element stay as they are.
|
|
172
|
+
def dynamic_element(arg)
|
|
173
|
+
members = arg.is_a?(Type::Union) ? arg.members : [arg]
|
|
174
|
+
gradual, known = members.partition { |member| gradual_top?(member) }
|
|
175
|
+
return arg if gradual.empty?
|
|
176
|
+
|
|
177
|
+
literals, classes = known.partition { |member| member.is_a?(Type::Constant) }
|
|
178
|
+
return arg if classes.empty?
|
|
179
|
+
|
|
180
|
+
Type::Combinator.union(*literals, Type::Combinator.dynamic(Type::Combinator.union(*classes)))
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def unknown_arguments(count) = Array.new(count) { Type::Combinator.untyped }
|
|
184
|
+
|
|
185
|
+
# False only when {.callee_block_floor} and {.callee_set_floor} are sure to leave `type` as it is: a type that is
|
|
186
|
+
# no String, Array, Hash or Set carrier in any form. A cheap gate, so a call passing only such locals skips the
|
|
187
|
+
# census closures those floors read.
|
|
188
|
+
def callee_floorable?(type)
|
|
189
|
+
case type
|
|
190
|
+
when Type::Union then type.members.any? { |member| callee_floorable?(member) }
|
|
191
|
+
when Type::Tuple, Type::HashShape, Type::Difference, Type::Refined, Type::Intersection then true
|
|
192
|
+
when Type::Constant then type.value.is_a?(String) || type.value.is_a?(::Set)
|
|
193
|
+
when Type::Nominal then FLOORABLE_NOMINALS.include?(type.class_name)
|
|
194
|
+
else false
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
FLOORABLE_NOMINALS = %w[Array Hash Set String].freeze
|
|
199
|
+
private_constant :FLOORABLE_NOMINALS
|
|
200
|
+
|
|
102
201
|
# Every `Array` / `Hash` / `String` carrier `type` can be as its bare carrier: `Array[Dynamic[top]]`,
|
|
103
202
|
# `Hash[Dynamic[top], Dynamic[top]]`, `String`. A literal, a nominal (a precise one included) and a refinement
|
|
104
203
|
# over one ({.carrier_class}) all floor; so `non-empty-string`, which a callee can empty, reads `String`.
|
|
@@ -124,9 +124,8 @@ module Rigor
|
|
|
124
124
|
|
|
125
125
|
# How a walk position reaches the exit scope: whether a local rebind there is threaded, whether an
|
|
126
126
|
# instance variable's is, whether a class variable's or global's is, the offset below which a rebind
|
|
127
|
-
# precedes a jump,
|
|
128
|
-
#
|
|
129
|
-
# other binding construct, false otherwise), and the locals a nested block shadows.
|
|
127
|
+
# precedes a jump, whether the position binds targets only (`:multi_assign` under a multi-assign,
|
|
128
|
+
# `:capture` under any other binding construct, false otherwise), and the locals a nested block shadows.
|
|
130
129
|
Position = Data.define(:local, :ivar, :other, :horizon, :binding, :shadowed)
|
|
131
130
|
private_constant :Position
|
|
132
131
|
|
|
@@ -5,6 +5,7 @@ require "prism"
|
|
|
5
5
|
require_relative "void_origin"
|
|
6
6
|
require_relative "../reflection"
|
|
7
7
|
require_relative "../source/node_children"
|
|
8
|
+
require_relative "../run_scoped_slots"
|
|
8
9
|
|
|
9
10
|
module Rigor
|
|
10
11
|
module Inference
|
|
@@ -62,7 +63,7 @@ module Rigor
|
|
|
62
63
|
# graceful-degradation branch is needed. `def_node` identity is stable per run (the ADR-85
|
|
63
64
|
# {DefNodeResolver} yields one node object per `(path, node_id)`), the same identity the ADR-84
|
|
64
65
|
# return memo relies on.
|
|
65
|
-
MEMO_KEY = :__rigor_void_tail_summary_memo__
|
|
66
|
+
MEMO_KEY = RunScopedSlots.register(:__rigor_void_tail_summary_memo__)
|
|
66
67
|
private_constant :MEMO_KEY
|
|
67
68
|
|
|
68
69
|
def initialize(scope)
|
|
@@ -13,9 +13,9 @@ module Rigor
|
|
|
13
13
|
# with no `range`) stays supported — it is still legal under incremental sync and is what `didOpen` and a
|
|
14
14
|
# client-side resync send.
|
|
15
15
|
class BufferTable
|
|
16
|
-
# @!attribute uri
|
|
17
|
-
# @!attribute bytes
|
|
18
|
-
# @!attribute version
|
|
16
|
+
# @!attribute uri — the LSP DocumentUri (e.g. `file:///abs/path/lib/foo.rb`).
|
|
17
|
+
# @!attribute bytes — the current full text of the buffer.
|
|
18
|
+
# @!attribute version — the monotonically increasing LSP version number.
|
|
19
19
|
Entry = Data.define(:uri, :bytes, :version)
|
|
20
20
|
|
|
21
21
|
def initialize
|
|
@@ -98,6 +98,12 @@ module Rigor
|
|
|
98
98
|
@dirty.key?(uri)
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
+
# @return true when `uri` is open, its bytes are the ones on disk, and the table holds them faithfully: the
|
|
102
|
+
# buffers a whole-project round analysed as they are.
|
|
103
|
+
def on_disk?(uri)
|
|
104
|
+
open?(uri) && !dirty?(uri) && !desynchronized?(uri)
|
|
105
|
+
end
|
|
106
|
+
|
|
101
107
|
def [](uri)
|
|
102
108
|
@entries[uri]
|
|
103
109
|
end
|