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,260 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../analysis/dependency_recorder"
|
|
6
|
+
require_relative "../source/node_children"
|
|
7
|
+
require_relative "../type"
|
|
8
|
+
require_relative "guard_rebinding"
|
|
9
|
+
require_relative "project_method_ownership"
|
|
10
|
+
|
|
11
|
+
module Rigor
|
|
12
|
+
module Inference
|
|
13
|
+
# Issue #2314 — whether running a node may reach a project method that writes a given global. Both places that
|
|
14
|
+
# end a global's binding at a call ask it: the statement walk past a statement call
|
|
15
|
+
# (`StatementEvaluator#forget_reached_globals`), and a `case` whose global subject a `when` test may rebind
|
|
16
|
+
# (`StatementEvaluator#when_test_rebinds_subject?`, issue #2211), so the two read a global alike.
|
|
17
|
+
#
|
|
18
|
+
# The writers come from {GlobalWriteCensus}: a `[:global_writer, name, method]` entry for each write of `name`
|
|
19
|
+
# inside a method body (`method` is the `def`'s name, or the literal name a `define_method` block defines), and
|
|
20
|
+
# `[:global_writer, name, nil]` for one inside a lambda or a block a call keeps to run later
|
|
21
|
+
# ({StoredBlockCall}), which any call may run. A write at the top level or in a class body runs where it is
|
|
22
|
+
# written, so no call reaches it.
|
|
23
|
+
#
|
|
24
|
+
# {.may_write?} answers true when the global has a writer and:
|
|
25
|
+
#
|
|
26
|
+
# - the node may run code other than Ruby core and the standard library ({GuardRebinding.may_rebind?}, the
|
|
27
|
+
# question #2260 asked of a `when` test), and
|
|
28
|
+
# - some writer is anonymous, or the node's own call has a receiver no target resolves (a `Dynamic` value), or a
|
|
29
|
+
# search by method name reaches a writer ({Search}).
|
|
30
|
+
#
|
|
31
|
+
# The search reads method names, not receivers: from the node it follows every call, by name, into every project
|
|
32
|
+
# `def` of that name on any class, transitively, and reaches a writer when it meets a call named after one. A
|
|
33
|
+
# call naming its method by a computed value (`send(name)`, `eval(code)`) reaches. A name no project `def` defines
|
|
34
|
+
# follows `method_missing`, `new` follows `initialize`, and an alias name (`alias`, `alias_method`, a
|
|
35
|
+
# `def_delegator` alias) follows the name it stands for. A nested `def` is not read, since defining a method runs
|
|
36
|
+
# nothing. Past {Search::DEF_BUDGET} bodies the search answers that it reaches. A `define_method` method that calls
|
|
37
|
+
# a writer is not followed (only its own writes count), and a method core code calls back without the program
|
|
38
|
+
# spelling it (`puts obj` runs `obj.to_s`) is read as the core method alone, as {GuardRebinding} reads it.
|
|
39
|
+
module GlobalWriterReach
|
|
40
|
+
EMPTY = Set.new.freeze
|
|
41
|
+
|
|
42
|
+
module_function
|
|
43
|
+
|
|
44
|
+
# The globals `scope` binds that some project method may write, other than one already `Dynamic[top]`. Each
|
|
45
|
+
# bound global's census key is recorded whatever the answer, so a file that starts writing one re-checks.
|
|
46
|
+
def candidates(scope)
|
|
47
|
+
table = writer_table(scope)
|
|
48
|
+
globals = scope.globals
|
|
49
|
+
globals.each_key { |name| record_writers(name) } if Analysis::DependencyRecorder.active?
|
|
50
|
+
return EMPTY if table.empty? || globals.empty?
|
|
51
|
+
|
|
52
|
+
table.each_key.select do |name|
|
|
53
|
+
type = globals[name]
|
|
54
|
+
!type.nil? && !type.is_a?(Type::Dynamic)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# True when running `node`, typed in `scope`, may reach a project method that writes `name` (see the module
|
|
59
|
+
# comment).
|
|
60
|
+
def may_write?(node, name, scope)
|
|
61
|
+
writers = writers(scope, name)
|
|
62
|
+
return false if writers.empty?
|
|
63
|
+
return false unless GuardRebinding.may_rebind?(node, scope)
|
|
64
|
+
return true if writers.include?(nil) || unresolved_call?(node, scope)
|
|
65
|
+
|
|
66
|
+
Search.new(scope, writers).reaches?(node)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# The method names that write `name` (nil for an anonymous writer). Records the census key, so a file that
|
|
70
|
+
# starts or stops writing the global re-checks the reader. A guard on the `case` site: every `when` test built so
|
|
71
|
+
# far also records the key through a statement call's {.candidates} or a callee's method edge.
|
|
72
|
+
def writers(scope, name)
|
|
73
|
+
record_writers(name) if Analysis::DependencyRecorder.active?
|
|
74
|
+
writer_table(scope)[name] || EMPTY
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def record_writers(name)
|
|
78
|
+
Analysis::DependencyRecorder.read_name(:global_census, "global_writer:#{name}")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# `{ global => Set[method name or nil] }` over the project's census and the `pre_eval:` files' own.
|
|
82
|
+
def writer_table(scope) = census_tables(scope).first
|
|
83
|
+
|
|
84
|
+
# `{ alias name => Set[the method names it stands for] }`: an `alias`, an `alias_method` or a `def_delegator`
|
|
85
|
+
# with a literal alias name, from the same census.
|
|
86
|
+
def alias_table(scope) = census_tables(scope).last
|
|
87
|
+
|
|
88
|
+
# The two tables above, kept for the census objects they were built from.
|
|
89
|
+
def census_tables(scope)
|
|
90
|
+
census = scope.discovered_global_write_census
|
|
91
|
+
pre_eval = scope.environment&.project_patched_methods&.write_census
|
|
92
|
+
cached = @census_tables
|
|
93
|
+
return cached[2] if cached && cached[0].equal?(census) && cached[1].equal?(pre_eval)
|
|
94
|
+
|
|
95
|
+
tables = build_tables([census, pre_eval])
|
|
96
|
+
@census_tables = [census, pre_eval, tables.freeze]
|
|
97
|
+
tables
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
TABLE_KINDS = { global_writer: 0, method_alias: 1 }.freeze
|
|
101
|
+
private_constant :TABLE_KINDS
|
|
102
|
+
|
|
103
|
+
def build_tables(censuses)
|
|
104
|
+
tables = [{}, {}]
|
|
105
|
+
censuses.each do |entries|
|
|
106
|
+
entries&.each do |kind, key, value|
|
|
107
|
+
index = TABLE_KINDS[kind]
|
|
108
|
+
(tables[index][key] ||= Set.new) << value if index
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
tables.each { |table| table.each_value(&:freeze).freeze }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# True when `node` is a call whose receiver no target resolves: the method it runs may be any.
|
|
115
|
+
def unresolved_call?(node, scope)
|
|
116
|
+
return false unless node.is_a?(Prism::CallNode)
|
|
117
|
+
|
|
118
|
+
receiver = node.receiver
|
|
119
|
+
return false if receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
120
|
+
|
|
121
|
+
targets = ProjectMethodOwnership.targets(GuardRebinding::ScanScope.dispatch_type(receiver, scope))
|
|
122
|
+
targets.nil? || targets.empty?
|
|
123
|
+
rescue StandardError
|
|
124
|
+
true
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# `{ method name => [[class name, kind], …] }` over the project's `def` tables, kept for the tables it was
|
|
128
|
+
# built from.
|
|
129
|
+
def def_index(scope)
|
|
130
|
+
instance = scope.discovered_def_nodes
|
|
131
|
+
singleton = scope.discovered_singleton_def_nodes
|
|
132
|
+
cached = @def_index
|
|
133
|
+
return cached[2] if cached && cached[0].equal?(instance) && cached[1].equal?(singleton)
|
|
134
|
+
|
|
135
|
+
index = {}
|
|
136
|
+
{ instance: instance, singleton: singleton }.each do |kind, table|
|
|
137
|
+
table.each do |class_name, methods|
|
|
138
|
+
methods.each_key { |method_name| (index[method_name] ||= []) << [class_name, kind] }
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
@def_index = [instance, singleton, index.freeze]
|
|
142
|
+
index
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# A breadth-first search by method name from a node to a writer's name.
|
|
146
|
+
class Search
|
|
147
|
+
# The most `def` bodies one search reads before it answers that it reaches.
|
|
148
|
+
DEF_BUDGET = 64
|
|
149
|
+
SEND_CALLS = %i[send __send__ public_send method public_method instance_method].to_set.freeze
|
|
150
|
+
CODE_CALLS = %i[eval instance_eval instance_exec class_eval class_exec module_eval module_exec].to_set.freeze
|
|
151
|
+
|
|
152
|
+
class Reached < StandardError
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def initialize(scope, writers)
|
|
156
|
+
@scope = scope
|
|
157
|
+
@writers = writers
|
|
158
|
+
@aliases = GlobalWriterReach.alias_table(scope)
|
|
159
|
+
@recording = Analysis::DependencyRecorder.active?
|
|
160
|
+
@seen = Set.new
|
|
161
|
+
@queue = []
|
|
162
|
+
@read = 0
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def reaches?(node)
|
|
166
|
+
scan(node)
|
|
167
|
+
until @queue.empty?
|
|
168
|
+
name = @queue.shift
|
|
169
|
+
definitions = definitions_of(name)
|
|
170
|
+
enqueue(:method_missing) if definitions.empty?
|
|
171
|
+
definitions.each { |definition| read_def(definition) }
|
|
172
|
+
end
|
|
173
|
+
false
|
|
174
|
+
rescue Reached
|
|
175
|
+
true
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
private
|
|
179
|
+
|
|
180
|
+
def read_def(definition)
|
|
181
|
+
@read += 1
|
|
182
|
+
reached! if @read > DEF_BUDGET
|
|
183
|
+
scan(definition.parameters)
|
|
184
|
+
scan(definition.body)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def definitions_of(name)
|
|
188
|
+
Analysis::DependencyRecorder.read_name(:defines, name) if Analysis::DependencyRecorder.active?
|
|
189
|
+
targets = GlobalWriterReach.def_index(@scope)[name] || []
|
|
190
|
+
targets.filter_map do |class_name, kind|
|
|
191
|
+
kind == :singleton ? @scope.singleton_def_for(class_name, name) : @scope.user_def_for(class_name, name)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def scan(node)
|
|
196
|
+
return unless node.is_a?(Prism::Node)
|
|
197
|
+
# A nested `def` defines a method; it runs nothing here, and its name is followed where it is called.
|
|
198
|
+
return if node.is_a?(Prism::DefNode)
|
|
199
|
+
|
|
200
|
+
case node
|
|
201
|
+
when Prism::CallNode then scan_call(node)
|
|
202
|
+
when Prism::BlockArgumentNode then scan_block_argument(node)
|
|
203
|
+
when Prism::ForNode then enqueue(:each)
|
|
204
|
+
end
|
|
205
|
+
implicit_names(node).each { |name| enqueue(name) }
|
|
206
|
+
node.rigor_each_child { |child| scan(child) }
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def scan_call(node)
|
|
210
|
+
enqueue(node.name)
|
|
211
|
+
# `new` runs the class's `initialize`.
|
|
212
|
+
enqueue(:initialize) if node.name == :new
|
|
213
|
+
arguments = node.arguments&.arguments || []
|
|
214
|
+
if SEND_CALLS.include?(node.name)
|
|
215
|
+
name = literal_name(arguments.first)
|
|
216
|
+
name ? enqueue(name) : reached!
|
|
217
|
+
elsif CODE_CALLS.include?(node.name) && !(arguments.empty? && node.block.is_a?(Prism::BlockNode))
|
|
218
|
+
reached!
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def scan_block_argument(node)
|
|
223
|
+
name = literal_name(node.expression)
|
|
224
|
+
enqueue(name) if name
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# The methods an operator or compound write calls without spelling them.
|
|
228
|
+
def implicit_names(node)
|
|
229
|
+
names = []
|
|
230
|
+
names << node.binary_operator if node.respond_to?(:binary_operator)
|
|
231
|
+
names.push(node.read_name, node.write_name) if node.respond_to?(:read_name) && node.respond_to?(:write_name)
|
|
232
|
+
names
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def enqueue(name)
|
|
236
|
+
return if name.nil? || @seen.include?(name)
|
|
237
|
+
|
|
238
|
+
reached! if @writers.include?(name)
|
|
239
|
+
@seen << name
|
|
240
|
+
@queue << name
|
|
241
|
+
# An alias runs the method it names.
|
|
242
|
+
Analysis::DependencyRecorder.read_name(:global_census, "method_alias:#{name}") if @recording
|
|
243
|
+
@aliases[name]&.each { |original| enqueue(original) }
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def literal_name(node)
|
|
247
|
+
return unless node.is_a?(Prism::SymbolNode) || node.is_a?(Prism::StringNode)
|
|
248
|
+
|
|
249
|
+
text = node.unescaped
|
|
250
|
+
text.to_sym if text.valid_encoding?
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def reached!
|
|
254
|
+
raise Reached
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
private_constant :Search
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
@@ -41,8 +41,7 @@ module Rigor
|
|
|
41
41
|
|
|
42
42
|
# Reference to a formal parameter the enclosing `Definition#params` declared. The reducer
|
|
43
43
|
# substitutes this node with the matching positional arg from the `App` being reduced; an
|
|
44
|
-
# unknown name raises during reduction
|
|
45
|
-
# earlier).
|
|
44
|
+
# unknown name raises during reduction.
|
|
46
45
|
Param = Data.define(:name) do
|
|
47
46
|
def initialize(name:)
|
|
48
47
|
raise ArgumentError, "name must be a Symbol, got #{name.class}" unless name.is_a?(Symbol)
|
|
@@ -43,7 +43,7 @@ module Rigor
|
|
|
43
43
|
# `Rigor::Type::*` carrier: `nil` / `true` / `false` produce `Constant` carriers; `bool`
|
|
44
44
|
# produces the `Constant<true> | Constant<false>` union; `untyped` produces
|
|
45
45
|
# `Combinator.untyped` (i.e. `Dynamic[Top]`). Nominal class names produce raw `Type::Nominal`
|
|
46
|
-
# carriers (no `name_scope` resolution
|
|
46
|
+
# carriers (no `name_scope` resolution; the reducer trusts the name verbatim).
|
|
47
47
|
module HktBodyParser
|
|
48
48
|
class ParseError < StandardError; end
|
|
49
49
|
|
|
@@ -151,10 +151,8 @@ module Rigor
|
|
|
151
151
|
# test := type_expr ("<:" | "==") type_expr
|
|
152
152
|
#
|
|
153
153
|
# Parens delimit a conditional unambiguously — bare `(type_expr)` grouping is not
|
|
154
|
-
# supported
|
|
155
|
-
#
|
|
156
|
-
# membership tests are programmatically supported via `HktBody::TestMembership` but the
|
|
157
|
-
# parser does not yet recognise the `in` keyword (no concrete demand yet).
|
|
154
|
+
# supported (no use case yet). Branches can be unions; test sides are single arms (wrap
|
|
155
|
+
# in `App[my_union, ...]` if you need a union there).
|
|
158
156
|
def parse_conditional
|
|
159
157
|
expect!(:lparen)
|
|
160
158
|
test = parse_test
|
|
@@ -152,8 +152,8 @@ module Rigor
|
|
|
152
152
|
def subtype_verdict(left, right)
|
|
153
153
|
return :yes if left == right
|
|
154
154
|
|
|
155
|
-
#
|
|
156
|
-
#
|
|
155
|
+
# Two Nominals of different class names are statically disjoint (`:no`); any other shape
|
|
156
|
+
# pair is `:maybe`.
|
|
157
157
|
return :no if disjoint_nominals?(left, right)
|
|
158
158
|
return :no if disjoint_constants?(left, right)
|
|
159
159
|
|
|
@@ -9,11 +9,9 @@ module Rigor
|
|
|
9
9
|
# annotations in shipped `.rbs` files.
|
|
10
10
|
#
|
|
11
11
|
# The registry stores registration metadata (arity, variance, bound) and the definition body
|
|
12
|
-
# as both a raw String and an evaluable `HktBody` node tree. `HktReducer`
|
|
13
|
-
# `
|
|
14
|
-
#
|
|
15
|
-
# carries its `bound` directly; the registry exists at this slice solely so the parser
|
|
16
|
-
# round-trip and downstream slices have a stable target API.
|
|
12
|
+
# as both a raw String and an evaluable `HktBody` node tree. `HktReducer` and `HktBodyParser`
|
|
13
|
+
# reduce `Type::App` instances against the definition. The carrier never needs to read from the
|
|
14
|
+
# registry because `Type::App` carries its `bound` directly.
|
|
17
15
|
#
|
|
18
16
|
# The registry is immutable after construction. Callers that need to extend it (e.g. plugin
|
|
19
17
|
# registrations layered on top of stdlib registrations) MUST build a new registry via `merge`
|
|
@@ -9,8 +9,7 @@ require_relative "unknown_store_widening"
|
|
|
9
9
|
module Rigor
|
|
10
10
|
module Inference
|
|
11
11
|
# Closes the "`params[:f] ||= []; params[:f] << x`" precision gap surfaced by the Redmine
|
|
12
|
-
# 6.1.2 `Query#as_params` survey
|
|
13
|
-
# "Indexed-collection narrowing through `Hash[k] ||= default`").
|
|
12
|
+
# 6.1.2 `Query#as_params` survey.
|
|
14
13
|
#
|
|
15
14
|
# After `receiver[key] ||= default` the next read at `receiver[key]` is known non-nil, but
|
|
16
15
|
# Rigor types each `Hash#[]` independently and the subsequent `<<` / `[]=` / other mutator
|
|
@@ -206,11 +205,9 @@ module Rigor
|
|
|
206
205
|
args&.size == 1 ? stable_address(node.receiver, args.first) : nil
|
|
207
206
|
end
|
|
208
207
|
|
|
209
|
-
# Companion invalidator for single-hop method-chain narrowings
|
|
210
|
-
#
|
|
211
|
-
#
|
|
212
|
-
# receiver — matching the ROADMAP's "any intervening method call against the same
|
|
213
|
-
# receiver" criterion. A call against `x.last` (the OUTER receiver is a `CallNode`, not a
|
|
208
|
+
# Companion invalidator for single-hop method-chain narrowings. Drops every `(receiver, *)`
|
|
209
|
+
# chain narrowing rooted at the call's OUTER stable receiver, for any intervening method call
|
|
210
|
+
# against the same receiver. A call against `x.last` (the OUTER receiver is a `CallNode`, not a
|
|
214
211
|
# stable root) does NOT drop narrowings keyed on `x`, so the worked-site `x.last << y`
|
|
215
212
|
# pattern correctly preserves the chain narrowing for any further `x.last` read in the same
|
|
216
213
|
# body. Always-safe (only forgets; never invents).
|
|
@@ -19,8 +19,8 @@ module Rigor
|
|
|
19
19
|
# the calls in a condition operand, where a narrowing an earlier operand or clause made would otherwise survive
|
|
20
20
|
# them: `if @x.nil? && reset!` read `@x` as `nil` in the body although `reset!` assigns it.
|
|
21
21
|
module InterveningIvarReset
|
|
22
|
-
# Nodes whose body does not run where it is written, or runs on another `self`: a `def`, and a class
|
|
23
|
-
# module body, where `@x` is the class's own instance variable.
|
|
22
|
+
# Nodes whose body does not run where it is written, or runs on another `self`: a `def`, and a class,
|
|
23
|
+
# module or `class << …` body, where `@x` is the class's own instance variable.
|
|
24
24
|
NON_RUNNING_NODES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
25
25
|
# Issue #1402 — the nodes that run code the program does not show on `self`'s behalf: `yield` runs the
|
|
26
26
|
# method's block, and `super` the superclass's method on `self`.
|
|
@@ -180,12 +180,17 @@ module Rigor
|
|
|
180
180
|
# takes the mark when widened, so a `nil` only the seed brings back is not fuel for
|
|
181
181
|
# `call.possible-nil-receiver` (ADR-58), as a declared one is not. A narrowing that kept `nil`
|
|
182
182
|
# (`return if @tag`, `@tag.nil?`) widens unmarked, so a read the program really makes on `nil` still reports.
|
|
183
|
-
|
|
183
|
+
# Issue #2039 — `mark: :unproven` marks as `mark: true` does only when the block answers nil (no write set is
|
|
184
|
+
# proven) and `direct_writes` (called at most once, answering a `Set`) does not name the variable: a call
|
|
185
|
+
# proven to write it, or whose callee visibly assigns it a possibly-nil value ({DirectIvarWrites}), widens it
|
|
186
|
+
# unmarked.
|
|
187
|
+
def widen(scope, mark: false, direct_writes: nil)
|
|
184
188
|
seed = class_ivar_seed(scope)
|
|
185
189
|
return scope if seed.nil? || seed.empty?
|
|
186
190
|
|
|
187
191
|
computed = false
|
|
188
192
|
writes = nil
|
|
193
|
+
direct = lazy_set(direct_writes)
|
|
189
194
|
seed.reduce(scope) do |widened, (name, seed_type)|
|
|
190
195
|
local_type = scope.ivar(name)
|
|
191
196
|
next widened if local_type.nil? || local_type == seed_type
|
|
@@ -198,11 +203,28 @@ module Rigor
|
|
|
198
203
|
|
|
199
204
|
joined = Type::Combinator.union(local_type, seed_type)
|
|
200
205
|
widened = widened.with_ivar(name, joined)
|
|
201
|
-
marked = (mark
|
|
206
|
+
marked = widen_marks?(mark, writes, direct, name, local_type) || scope.declaration_sourced?(:ivar, name)
|
|
202
207
|
marked ? widened.seed_declaration_sourced_ivar(name, joined) : widened
|
|
203
208
|
end
|
|
204
209
|
end
|
|
205
210
|
|
|
211
|
+
# Whether {.widen} stamps the declaration mark on `name`'s binding, narrowed to `local_type`: one whose
|
|
212
|
+
# narrowing ruled `nil` out, under `mark: true`, or under `mark: :unproven` when no write set is proven
|
|
213
|
+
# (`writes` nil) and the callee does not visibly write it (`direct`).
|
|
214
|
+
def widen_marks?(mark, writes, direct, name, local_type)
|
|
215
|
+
return false unless mark && nil_free?(local_type)
|
|
216
|
+
|
|
217
|
+
mark != :unproven || (writes.nil? && !direct.call.include?(name))
|
|
218
|
+
end
|
|
219
|
+
private_class_method :widen_marks?
|
|
220
|
+
|
|
221
|
+
# A callable answering `callable`'s `Set`, computed on the first call; an empty one when `callable` is nil.
|
|
222
|
+
def lazy_set(callable)
|
|
223
|
+
value = nil
|
|
224
|
+
-> { value ||= callable ? callable.call : Set.new }
|
|
225
|
+
end
|
|
226
|
+
private_class_method :lazy_set
|
|
227
|
+
|
|
206
228
|
# Issue #1963 — `scope` past the calls `node`, a condition operand, makes: {.widen} over the names every
|
|
207
229
|
# candidate call in it may write, or every name when one of them is not proven. A call's literal block may run,
|
|
208
230
|
# so its calls count; a `def`, a class or module body and (unless `run_lambdas`) a lambda literal run nothing
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
# Issue #1154 — the instance-variable writers {GlobalWriteCensus} records for the declared-parameter credit
|
|
8
|
+
# ({ScopeIndexer::DeclaredParamWrites}), which must not type an ivar a writer the class-ivar census never walks may
|
|
9
|
+
# replace. The standard is parity with a literal write: `@x = 1` in `initialize` already reads `1` in every sibling
|
|
10
|
+
# method whatever an unknown object's code does, so only the writers that literal writes would also have to answer
|
|
11
|
+
# for are recorded, and an unknown-receiver writer records nothing (as `ReflectiveIvarWrite` leaves
|
|
12
|
+
# `o.instance_variable_set` alone). Three entry kinds go into the census `Set`:
|
|
13
|
+
#
|
|
14
|
+
# - `[:ivar_set, name]` — a literal `o.instance_variable_set(:@x, v)` (or the same through a `send` that spells
|
|
15
|
+
# it) on an explicit receiver other than `self`, and an `@x` write in the literal block of an
|
|
16
|
+
# `o.instance_eval { … }` / `instance_exec`: code that names the ivar it writes.
|
|
17
|
+
# - `[:ivar_set_class, class_name]` — an open-world writer ({#open_world?}) with an implicit or `self` receiver
|
|
18
|
+
# inside an instance body of the class — a `def` or a `define_method` block in its class body, in the block of a
|
|
19
|
+
# `K = Struct.new` / `Data.define` / `Class.new` constant, or in a `K.class_eval` block (the host walk passes
|
|
20
|
+
# that class as `owner`): it may write any ivar of the class's instances. A writer in a class body, a
|
|
21
|
+
# `def self.` or at the top level records nothing.
|
|
22
|
+
# - `[:own_write, class_name, name]` — an `@x` write inside an instance body of the class.
|
|
23
|
+
# {ScopeIndexer#merge_constant_literal_tables} appends the file's path, so a reader can tell whether a second
|
|
24
|
+
# file writes the same ivar of the same class.
|
|
25
|
+
# - `[:own_write_type, class_name, name, value]` — issues #2079 / #2170: the same write with its coarse value, a
|
|
26
|
+
# literal's as `[:lit, value]` and anything else as {DYNAMIC}, and a class-body `attr_writer` /
|
|
27
|
+
# `attr_accessor` of the name as {DYNAMIC}. The path is appended as for `:own_write`. The class-ivar census
|
|
28
|
+
# of a file joins another file's entries, and those of the modules and superclasses on the class's chain,
|
|
29
|
+
# into the ivars it already holds ({ScopeIndexer::IvarContributions}).
|
|
30
|
+
class IvarWriterCensus
|
|
31
|
+
SEND_CALLS = %i[send __send__ public_send].to_set.freeze
|
|
32
|
+
# The methods a `send` reaching which is an open-world writer.
|
|
33
|
+
OPEN_WORLD_SENDS = %i[instance_variable_set instance_eval instance_exec extend
|
|
34
|
+
define_singleton_method].to_set.freeze
|
|
35
|
+
EVAL_CALLS = %i[instance_eval instance_exec].to_set.freeze
|
|
36
|
+
# The calls on a `singleton_class` receiver that give the object methods or code the census cannot read.
|
|
37
|
+
SINGLETON_CLASS_CALLS = %i[
|
|
38
|
+
class_eval class_exec module_eval module_exec define_method include prepend extend attr attr_reader
|
|
39
|
+
attr_writer attr_accessor send public_send __send__
|
|
40
|
+
].to_set.freeze
|
|
41
|
+
NO_ARGUMENTS = [].freeze
|
|
42
|
+
# Issue #2079 — the coarse value of a write that is not a literal.
|
|
43
|
+
DYNAMIC = :dynamic
|
|
44
|
+
# The writer declarations a class body records as a {DYNAMIC} write of each name it spells.
|
|
45
|
+
WRITER_DECLARATIONS = %i[attr_writer attr_accessor].to_set.freeze
|
|
46
|
+
|
|
47
|
+
# The call names any rule here reads, so every other call returns at one Set probe.
|
|
48
|
+
# (`define_method` is in {SINGLETON_CLASS_CALLS}.)
|
|
49
|
+
WATCHED_CALLS = (SEND_CALLS | OPEN_WORLD_SENDS | EVAL_CALLS | SINGLETON_CLASS_CALLS).freeze
|
|
50
|
+
|
|
51
|
+
def initialize(census)
|
|
52
|
+
@census = census
|
|
53
|
+
@instance_ranges = []
|
|
54
|
+
@owner_names = {}.compare_by_identity
|
|
55
|
+
@own_writes = {}
|
|
56
|
+
@typed_writes = Set.new
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Fed each node of a pre-order descent. `owner` is the class whose body the node sits in, as its qualified name
|
|
60
|
+
# or as the host walk's prefix Array (joined here only when an entry needs it), or nil where no class owns an
|
|
61
|
+
# instance (`class << self`, the top level, an opaque block).
|
|
62
|
+
def visit(node, owner)
|
|
63
|
+
case node
|
|
64
|
+
when Prism::DefNode then remember(node.location, owner) if owner && node.receiver.nil?
|
|
65
|
+
when Prism::SingletonClassNode then visit_singleton_class(node)
|
|
66
|
+
when Prism::CallNode then visit_call(node, owner) if WATCHED_CALLS.include?(node.name)
|
|
67
|
+
when Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
|
|
68
|
+
Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableTargetNode
|
|
69
|
+
record_own_write(node) unless @instance_ranges.empty?
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
# One entry per class and ivar: a repeated write costs a Hash probe, not a location and an Array.
|
|
76
|
+
def record_own_write(node)
|
|
77
|
+
owner = instance_owner(node)
|
|
78
|
+
return if owner.nil?
|
|
79
|
+
|
|
80
|
+
record_typed_write(owner, node.name, coarse_value(node))
|
|
81
|
+
seen = (@own_writes[owner] ||= {})
|
|
82
|
+
return if seen[node.name]
|
|
83
|
+
|
|
84
|
+
seen[node.name] = true
|
|
85
|
+
@census << [:own_write, owner_name(owner), node.name].freeze
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def record_typed_write(owner, name, value)
|
|
89
|
+
entry = [:own_write_type, owner_name(owner), name, value].freeze
|
|
90
|
+
@census << entry if @typed_writes.add?(entry)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Issue #2079 — a literal's value as `[:lit, value]`; any other value, an operator write and a multiple
|
|
94
|
+
# assignment target as {DYNAMIC}.
|
|
95
|
+
def coarse_value(node)
|
|
96
|
+
value = node.value if node.respond_to?(:value) && !node.is_a?(Prism::InstanceVariableOperatorWriteNode)
|
|
97
|
+
case value
|
|
98
|
+
when Prism::NilNode then [:lit, nil].freeze
|
|
99
|
+
when Prism::TrueNode then [:lit, true].freeze
|
|
100
|
+
when Prism::FalseNode then [:lit, false].freeze
|
|
101
|
+
when Prism::IntegerNode, Prism::FloatNode then [:lit, value.value].freeze
|
|
102
|
+
when Prism::SymbolNode, Prism::StringNode then literal_text(value)
|
|
103
|
+
else DYNAMIC
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def literal_text(node)
|
|
108
|
+
text = node.unescaped
|
|
109
|
+
return DYNAMIC unless text.valid_encoding?
|
|
110
|
+
|
|
111
|
+
[:lit, node.is_a?(Prism::SymbolNode) ? text.to_sym : text.dup.freeze].freeze
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Issue #2079 — `attr_writer :x` / `attr_accessor :x` in a class body writes `@x` with whatever the caller hands.
|
|
115
|
+
def record_writer_declaration(node, owner)
|
|
116
|
+
return unless owner && WRITER_DECLARATIONS.include?(node.name) && self_receiver?(node)
|
|
117
|
+
# A guard: an instance body's own `attr_writer` call raises (the method is `Module`'s), so only the class
|
|
118
|
+
# body's declares a writer.
|
|
119
|
+
return unless instance_owner(node).nil?
|
|
120
|
+
|
|
121
|
+
(node.arguments&.arguments || NO_ARGUMENTS).each do |argument|
|
|
122
|
+
name = literal_name(argument)
|
|
123
|
+
record_typed_write(owner, :"@#{name}", DYNAMIC) if name
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def owner_name(owner) = owner.is_a?(Array) ? (@owner_names[owner] ||= owner.join("::")) : owner
|
|
128
|
+
|
|
129
|
+
def visit_call(node, owner)
|
|
130
|
+
record_writer_declaration(node, owner)
|
|
131
|
+
remember(node.block.location, owner) if owner && define_method_block?(node)
|
|
132
|
+
if self_receiver?(node) || own_singleton_class_call?(node)
|
|
133
|
+
return if @instance_ranges.empty? || !(own_singleton_class_call?(node) || open_world?(node))
|
|
134
|
+
|
|
135
|
+
instance = instance_owner(node)
|
|
136
|
+
record_class(instance) if instance
|
|
137
|
+
else
|
|
138
|
+
visit_foreign(node)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# `class << self` inside an instance `def` opens the instance's singleton class.
|
|
143
|
+
def visit_singleton_class(node)
|
|
144
|
+
return unless node.expression.is_a?(Prism::SelfNode) && !@instance_ranges.empty?
|
|
145
|
+
|
|
146
|
+
instance = instance_owner(node)
|
|
147
|
+
record_class(instance) if instance
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def record_class(owner) = @census << [:ivar_set_class, owner_name(owner)].freeze
|
|
151
|
+
|
|
152
|
+
# The self writers whose target or code the census cannot read: a `send` with a computed method name or one
|
|
153
|
+
# naming {OPEN_WORLD_SENDS}; `instance_eval` / `instance_exec` given a block argument, and `instance_eval`
|
|
154
|
+
# given a string; `extend`; `define_singleton_method` with a body; and an `instance_variable_set` whose name no
|
|
155
|
+
# literal spells. A call in {SINGLETON_CLASS_CALLS} on the object's own `singleton_class` counts as well.
|
|
156
|
+
def open_world?(node)
|
|
157
|
+
name = node.name
|
|
158
|
+
arguments = node.arguments&.arguments || NO_ARGUMENTS
|
|
159
|
+
return open_world_send?(arguments) if SEND_CALLS.include?(name)
|
|
160
|
+
return open_world_eval?(node, arguments) if EVAL_CALLS.include?(name)
|
|
161
|
+
# #1931's census already makes every ivar of the class gradual for one in the same file; this entry covers one
|
|
162
|
+
# in a reopening in another file, which that census does not see.
|
|
163
|
+
return ivar_name(arguments.first).nil? if name == :instance_variable_set
|
|
164
|
+
|
|
165
|
+
name == :extend || (name == :define_singleton_method && (!node.block.nil? || arguments.size > 1))
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def open_world_send?(arguments)
|
|
169
|
+
return false if arguments.empty?
|
|
170
|
+
|
|
171
|
+
sent = literal_name(arguments.first)
|
|
172
|
+
sent.nil? || OPEN_WORLD_SENDS.include?(sent)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def open_world_eval?(node, arguments)
|
|
176
|
+
node.block.is_a?(Prism::BlockArgumentNode) || (node.name == :instance_eval && !arguments.empty?)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# A call on another receiver: a named foreign write, or `self.singleton_class.class_eval` and its kin.
|
|
180
|
+
def visit_foreign(node)
|
|
181
|
+
arguments = node.arguments&.arguments || NO_ARGUMENTS
|
|
182
|
+
name = node.name
|
|
183
|
+
if SEND_CALLS.include?(name) && arguments.first && literal_name(arguments.first) == :instance_variable_set
|
|
184
|
+
name = :instance_variable_set
|
|
185
|
+
arguments = arguments.drop(1)
|
|
186
|
+
end
|
|
187
|
+
if name == :instance_variable_set
|
|
188
|
+
ivar = ivar_name(arguments.first)
|
|
189
|
+
@census << [:ivar_set, ivar].freeze if ivar
|
|
190
|
+
elsif EVAL_CALLS.include?(name) && node.block.is_a?(Prism::BlockNode)
|
|
191
|
+
record_block_ivar_writes(node.block)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def record_block_ivar_writes(node)
|
|
196
|
+
return unless node.is_a?(Prism::Node)
|
|
197
|
+
return if node.is_a?(Prism::DefNode) || node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode)
|
|
198
|
+
|
|
199
|
+
case node
|
|
200
|
+
when Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
|
|
201
|
+
Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableTargetNode
|
|
202
|
+
@census << [:ivar_set, node.name].freeze
|
|
203
|
+
end
|
|
204
|
+
node.rigor_each_child { |child| record_block_ivar_writes(child) }
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def define_method_block?(node)
|
|
208
|
+
node.name == :define_method && self_receiver?(node) && node.block.is_a?(Prism::BlockNode)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# An instance body (`def` or `define_method` block) and the class it belongs to. A `define_method` block's own
|
|
212
|
+
# nodes may reach the walk with no owner (the walk reads its `self` as opaque), so the class rides the range.
|
|
213
|
+
def remember(location, owner)
|
|
214
|
+
@instance_ranges << [(location.start_offset...location.end_offset), owner]
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# The class of the innermost instance body covering `node` (pre-order, so the last covering range), or nil.
|
|
218
|
+
def instance_owner(node)
|
|
219
|
+
offset = node.location.start_offset
|
|
220
|
+
@instance_ranges.reverse_each { |(range, owner)| return owner if range.cover?(offset) }
|
|
221
|
+
nil
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def self_receiver?(node) = node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
|
225
|
+
|
|
226
|
+
# A call in {SINGLETON_CLASS_CALLS} on the object's own `singleton_class` (`singleton_class.class_eval { … }`).
|
|
227
|
+
def own_singleton_class_call?(node)
|
|
228
|
+
receiver = node.receiver
|
|
229
|
+
receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class && self_receiver?(receiver) &&
|
|
230
|
+
SINGLETON_CLASS_CALLS.include?(node.name)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def literal_name(argument)
|
|
234
|
+
return unless argument.is_a?(Prism::SymbolNode) || argument.is_a?(Prism::StringNode)
|
|
235
|
+
|
|
236
|
+
text = argument.unescaped
|
|
237
|
+
text.to_sym if text.valid_encoding?
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def ivar_name(argument)
|
|
241
|
+
name = literal_name(argument)
|
|
242
|
+
name if name.to_s.start_with?("@")
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
@@ -5,6 +5,7 @@ require "prism"
|
|
|
5
5
|
require_relative "../type"
|
|
6
6
|
require_relative "mutation_widening"
|
|
7
7
|
require_relative "key_presence_guard/scan"
|
|
8
|
+
require_relative "../run_scoped_slots"
|
|
8
9
|
|
|
9
10
|
module Rigor
|
|
10
11
|
module Inference
|
|
@@ -58,7 +59,7 @@ module Rigor
|
|
|
58
59
|
).to_set.freeze
|
|
59
60
|
|
|
60
61
|
STATE_KEY = :__rigor_key_presence_guard
|
|
61
|
-
CACHE_KEY = :__rigor_key_presence_guard_rewalk
|
|
62
|
+
CACHE_KEY = RunScopedSlots.register(:__rigor_key_presence_guard_rewalk)
|
|
62
63
|
private_constant :STATE_KEY, :CACHE_KEY
|
|
63
64
|
|
|
64
65
|
# The re-walk's state: the file's root, the end offset of each recorded guard, and the per-file scans.
|