rigortype 0.3.9 → 0.4.1
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 +5 -4
- data/data/core_overlay/enumerable.rbs +51 -0
- data/data/core_overlay/enumerator.rbs +84 -0
- data/data/core_overlay/hash_rbs3.rbs +41 -0
- data/data/core_overlay/process.rbs +40 -0
- data/data/core_overlay/string_io.rbs +33 -0
- data/data/effects/core.yml +3 -3
- data/data/gem_overlay/activesupport/core_ext.rbs +127 -4
- data/docs/handbook/03-narrowing.md +95 -10
- data/docs/handbook/04-tuples-and-shapes.md +8 -6
- data/docs/handbook/07-rbs-and-extended.md +16 -11
- data/docs/handbook/08-understanding-errors.md +3 -2
- data/docs/handbook/10-sorbet.md +9 -10
- data/docs/handbook/11-sig-gen.md +454 -13
- data/docs/install.md +87 -15
- data/docs/manual/02-cli-reference.md +65 -4
- data/docs/manual/03-configuration.md +12 -0
- data/docs/manual/04-diagnostics.md +4 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/08-skills.md +4 -3
- data/docs/manual/10-mcp-server.md +3 -2
- data/docs/manual/12-caching.md +43 -4
- data/docs/manual/14-rails-quickstart.md +19 -11
- data/docs/manual/16-rbs-extended-annotations.md +40 -7
- data/docs/manual/19-effect-labels.md +10 -0
- data/docs/manual/plugins/README.md +7 -0
- data/docs/manual/plugins/rigor-actioncable.md +8 -1
- data/docs/manual/plugins/rigor-actionmailer.md +7 -0
- data/docs/manual/plugins/rigor-actionpack.md +243 -0
- data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
- data/docs/manual/plugins/rigor-activejob.md +7 -0
- data/docs/manual/plugins/rigor-activerecord.md +110 -4
- data/docs/manual/plugins/rigor-activestorage.md +8 -1
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
- data/docs/manual/plugins/rigor-grape.md +106 -0
- data/docs/manual/plugins/rigor-graphql.md +23 -2
- data/docs/manual/plugins/rigor-pundit.md +8 -1
- data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
- data/docs/manual/plugins/rigor-rails-routes.md +7 -0
- data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
- data/docs/manual/plugins/rigor-sidekiq.md +8 -1
- data/docs/manual/plugins/rigor-sorbet.md +21 -5
- data/exe/rigor +19 -4
- data/lib/rigor/analysis/baseline.rb +1 -1
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
- data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
- data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +427 -0
- data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
- data/lib/rigor/analysis/check_rules.rb +638 -136
- data/lib/rigor/analysis/dependency_recorder.rb +222 -17
- data/lib/rigor/analysis/fact_store.rb +9 -0
- data/lib/rigor/analysis/incremental.rb +26 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +266 -0
- data/lib/rigor/analysis/incremental_run_slot.rb +171 -0
- data/lib/rigor/analysis/incremental_session.rb +309 -53
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
- data/lib/rigor/analysis/project_scan.rb +11 -1
- data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
- data/lib/rigor/analysis/reachability/scan_cache.rb +3 -1
- data/lib/rigor/analysis/rule_catalog.rb +127 -0
- data/lib/rigor/analysis/run_cache_key.rb +46 -18
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +168 -12
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
- data/lib/rigor/analysis/runner/pool_coordinator.rb +109 -17
- data/lib/rigor/analysis/runner/project_pre_passes.rb +44 -17
- data/lib/rigor/analysis/runner.rb +603 -54
- data/lib/rigor/analysis/shadow_harness.rb +27 -0
- data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
- data/lib/rigor/analysis/template_unit_collector.rb +303 -0
- data/lib/rigor/analysis/template_unit_paths.rb +93 -0
- data/lib/rigor/analysis/template_unit_positions.rb +293 -0
- data/lib/rigor/analysis/template_units.rb +409 -0
- data/lib/rigor/analysis/worker_session.rb +57 -13
- data/lib/rigor/bleeding_edge.rb +0 -2
- data/lib/rigor/builtins/hkt_builtins.rb +1 -0
- data/lib/rigor/builtins/imported_refinements.rb +4 -0
- data/lib/rigor/builtins/regex_refinement.rb +17 -9
- data/lib/rigor/builtins/static_return_refinements.rb +2 -0
- data/lib/rigor/cache/descriptor.rb +45 -1
- data/lib/rigor/cache/engine_source.rb +29 -1
- data/lib/rigor/cache/file_digest.rb +28 -0
- data/lib/rigor/cache/incremental_snapshot.rb +102 -8
- data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
- data/lib/rigor/cache/rbs_descriptor.rb +37 -7
- data/lib/rigor/cache/store.rb +63 -4
- data/lib/rigor/cache/value_digest.rb +325 -0
- data/lib/rigor/ci_detector.rb +1 -0
- data/lib/rigor/cli/check_command.rb +83 -13
- data/lib/rigor/cli/doc_links.rb +1 -1
- data/lib/rigor/cli/docs_command.rb +4 -4
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/effects_report.rb +2 -2
- data/lib/rigor/cli/plugin_command.rb +3 -3
- data/lib/rigor/cli/prism_colorizer.rb +0 -1
- data/lib/rigor/cli/sig_gen_command.rb +210 -29
- data/lib/rigor/cli/skill_command.rb +1 -1
- data/lib/rigor/cli/skill_describe.rb +0 -1
- data/lib/rigor/cli/type_of_command.rb +19 -5
- data/lib/rigor/cli/type_of_renderer.rb +20 -9
- data/lib/rigor/cli/type_of_template_probe.rb +189 -0
- data/lib/rigor/cli/unused_command.rb +3 -3
- data/lib/rigor/cli.rb +21 -3
- data/lib/rigor/config_audit.rb +3 -2
- data/lib/rigor/config_path.rb +29 -0
- data/lib/rigor/configuration/severity_profile.rb +24 -3
- data/lib/rigor/configuration.rb +71 -7
- data/lib/rigor/effects/ancestry_recorder.rb +191 -0
- data/lib/rigor/effects/attribution.rb +11 -2
- data/lib/rigor/effects/callee_rule.rb +368 -0
- data/lib/rigor/effects/catalog.rb +7 -4
- data/lib/rigor/effects/collector.rb +11 -5
- data/lib/rigor/effects/config_envelopes.rb +9 -2
- data/lib/rigor/effects/definition_context.rb +179 -0
- data/lib/rigor/effects/effect_table.rb +11 -3
- data/lib/rigor/effects/envelope_check.rb +1 -1
- data/lib/rigor/effects/envelope_index.rb +15 -0
- data/lib/rigor/effects/file_collection.rb +59 -5
- data/lib/rigor/effects/framework_units.rb +1 -1
- data/lib/rigor/effects/identity.rb +16 -0
- data/lib/rigor/effects/local_ownership.rb +38 -12
- data/lib/rigor/effects/method_key.rb +21 -0
- data/lib/rigor/effects/mutation_classifier.rb +23 -12
- data/lib/rigor/effects/plugin_facts.rb +49 -37
- data/lib/rigor/effects/propagator.rb +295 -16
- data/lib/rigor/effects/registry.rb +1 -1
- data/lib/rigor/effects/scanner.rb +121 -72
- data/lib/rigor/effects/signature_sources.rb +1 -1
- data/lib/rigor/effects/snapshot.rb +2 -1
- data/lib/rigor/effects/summary.rb +27 -4
- data/lib/rigor/effects/unit_scan.rb +385 -30
- data/lib/rigor/effects/visibility.rb +101 -0
- data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
- data/lib/rigor/environment/lockfile_resolver.rb +27 -1
- data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
- data/lib/rigor/environment/member_consistency.rb +298 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
- data/lib/rigor/environment/rbs_loader.rb +400 -134
- data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
- data/lib/rigor/environment.rb +104 -96
- data/lib/rigor/hashing/xxh3.rb +264 -0
- data/lib/rigor/inference/acceptance.rb +139 -9
- data/lib/rigor/inference/block_auto_splat.rb +216 -0
- data/lib/rigor/inference/block_call_timing.rb +338 -0
- data/lib/rigor/inference/block_parameter_binder.rb +88 -26
- data/lib/rigor/inference/block_repetition.rb +71 -0
- data/lib/rigor/inference/body_fixpoint.rb +2 -1
- data/lib/rigor/inference/budget_trace.rb +2 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
- data/lib/rigor/inference/captured_locals.rb +393 -15
- data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
- data/lib/rigor/inference/content_join.rb +200 -27
- data/lib/rigor/inference/declaration_walk/context.rb +195 -0
- data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
- data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
- data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
- data/lib/rigor/inference/declaration_walk.rb +7 -0
- data/lib/rigor/inference/def_node_resolver.rb +3 -2
- data/lib/rigor/inference/def_return_typer.rb +11 -7
- data/lib/rigor/inference/define_method_block_self.rb +64 -0
- data/lib/rigor/inference/definer_resolution.rb +266 -0
- data/lib/rigor/inference/element_read_widening.rb +22 -10
- data/lib/rigor/inference/error_info.rb +196 -0
- data/lib/rigor/inference/expression_typer.rb +1665 -538
- data/lib/rigor/inference/external_ancestor_resolution.rb +264 -0
- data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
- data/lib/rigor/inference/global_write_census.rb +239 -0
- data/lib/rigor/inference/guard_rebinding.rb +488 -0
- data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
- data/lib/rigor/inference/index_write_widening.rb +16 -3
- data/lib/rigor/inference/indexed_narrowing.rb +61 -9
- data/lib/rigor/inference/jump_targets.rb +82 -0
- data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
- data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
- data/lib/rigor/inference/last_line.rb +340 -0
- data/lib/rigor/inference/last_status.rb +144 -0
- data/lib/rigor/inference/macro_block_self_type.rb +179 -17
- data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
- data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
- data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
- data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
- data/lib/rigor/inference/match_rebinding.rb +392 -0
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
- data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
- data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
- data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
- data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +901 -38
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher.rb +214 -13
- data/lib/rigor/inference/method_parameter_binder.rb +8 -3
- data/lib/rigor/inference/module_function_state.rb +213 -0
- data/lib/rigor/inference/multi_target_binder.rb +345 -52
- data/lib/rigor/inference/mutation_rejoin.rb +4 -1
- data/lib/rigor/inference/mutation_widening.rb +70 -48
- data/lib/rigor/inference/narrowing.rb +674 -124
- data/lib/rigor/inference/operand_effects.rb +167 -0
- data/lib/rigor/inference/operand_walk.rb +88 -0
- data/lib/rigor/inference/optimistic_origin.rb +152 -9
- data/lib/rigor/inference/parameter_inference_collector.rb +13 -11
- data/lib/rigor/inference/project_method_ownership.rb +136 -0
- data/lib/rigor/inference/project_patched_methods.rb +7 -2
- data/lib/rigor/inference/project_patched_scanner.rb +7 -3
- data/lib/rigor/inference/receiver_alias.rb +90 -1
- data/lib/rigor/inference/receiver_blind_block.rb +219 -0
- data/lib/rigor/inference/refinement_mutation.rb +15 -11
- data/lib/rigor/inference/repeated_or_writes.rb +463 -0
- data/lib/rigor/inference/return_barrier.rb +54 -0
- data/lib/rigor/inference/rewrite_mutation.rb +120 -0
- data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
- data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
- data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
- data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +130 -0
- data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
- data/lib/rigor/inference/scope_indexer.rb +5913 -701
- data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
- data/lib/rigor/inference/statement_evaluator.rb +3249 -497
- data/lib/rigor/inference/stored_block_call.rb +54 -0
- data/lib/rigor/inference/string_mutation.rb +44 -7
- data/lib/rigor/inference/unknown_store_widening.rb +200 -0
- data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
- data/lib/rigor/language_server/debouncer.rb +0 -1
- data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
- data/lib/rigor/language_server/hover_renderer.rb +3 -3
- data/lib/rigor/language_server/project_context.rb +5 -3
- data/lib/rigor/mcp/server.rb +2 -1
- data/lib/rigor/plugin/base.rb +196 -13
- data/lib/rigor/plugin/box_probe.rb +91 -0
- data/lib/rigor/plugin/bundled_catalog.rb +1 -1
- data/lib/rigor/plugin/effect_attribution.rb +58 -4
- data/lib/rigor/plugin/io_boundary.rb +82 -4
- data/lib/rigor/plugin/isolation.rb +1 -1
- data/lib/rigor/plugin/loader.rb +2 -1
- data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
- data/lib/rigor/plugin/manifest.rb +71 -10
- data/lib/rigor/plugin/registry.rb +35 -1
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
- data/lib/rigor/plugin/template_unit.rb +196 -0
- data/lib/rigor/plugin/trust_policy.rb +3 -3
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/protection/discovery_seed.rb +20 -9
- data/lib/rigor/protection/kill_signature.rb +0 -1
- data/lib/rigor/protection/mutation_cache.rb +4 -5
- data/lib/rigor/rbs_extended.rb +27 -0
- data/lib/rigor/reflection/constant_ancestors.rb +146 -0
- data/lib/rigor/reflection/constant_path.rb +41 -13
- data/lib/rigor/reflection.rb +73 -107
- data/lib/rigor/scope/discovery_index.rb +351 -2
- data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
- data/lib/rigor/scope/resolution_chain.rb +884 -0
- data/lib/rigor/scope.rb +981 -198
- data/lib/rigor/sig_gen/alias_index.rb +289 -0
- data/lib/rigor/sig_gen/classification.rb +22 -5
- data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
- data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
- data/lib/rigor/sig_gen/generator.rb +580 -47
- data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
- data/lib/rigor/sig_gen/method_candidate.rb +47 -3
- data/lib/rigor/sig_gen/observation_collector.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +124 -9
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
- data/lib/rigor/sig_gen/write_result.rb +19 -3
- data/lib/rigor/sig_gen/writer.rb +166 -35
- data/lib/rigor/sig_gen.rb +3 -0
- data/lib/rigor/signature_path_audit.rb +1 -1
- data/lib/rigor/source/node_walker.rb +0 -3
- data/lib/rigor/source/parameter_envelope.rb +72 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/combinator.rb +112 -12
- data/lib/rigor/type/difference.rb +1 -0
- data/lib/rigor/type/hash_shape.rb +1 -1
- data/lib/rigor/type/refined.rb +1 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
- data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
- data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
- data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
- data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +10 -0
- data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
- data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
- data/plugins/rigor-grape/sig/grape.rbs +263 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
- data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
- data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
- data/sig/rigor/analysis/baseline.rbs +69 -7
- data/sig/rigor/analysis/fact_store.rbs +1 -1
- data/sig/rigor/analysis/project_scan.rbs +74 -0
- data/sig/rigor/effects/config_envelopes.rbs +100 -0
- data/sig/rigor/effects/effect_table.rbs +60 -0
- data/sig/rigor/effects/envelope.rbs +97 -0
- data/sig/rigor/effects/envelope_index.rbs +33 -0
- data/sig/rigor/effects/file_collection.rbs +81 -0
- data/sig/rigor/effects/label.rbs +26 -0
- data/sig/rigor/effects/label_set.rbs +46 -0
- data/sig/rigor/effects/method_key.rbs +24 -0
- data/sig/rigor/effects/origin.rbs +50 -0
- data/sig/rigor/effects/plugin_facts.rbs +141 -0
- data/sig/rigor/effects/registry.rbs +68 -0
- data/sig/rigor/effects/summary.rbs +50 -0
- data/sig/rigor/effects/taint_cause.rbs +12 -0
- data/sig/rigor/environment.rbs +14 -10
- data/sig/rigor/inference/optimistic_origin.rbs +10 -0
- data/sig/rigor/inference.rbs +6 -4
- data/sig/rigor/plugin/additional_initializer.rbs +36 -0
- data/sig/rigor/plugin/base.rbs +30 -7
- data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
- data/sig/rigor/plugin/effect_attribution.rbs +66 -0
- data/sig/rigor/plugin/effect_edge.rbs +33 -0
- data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
- data/sig/rigor/plugin/io_boundary.rbs +2 -1
- data/sig/rigor/plugin/loader.rbs +3 -3
- data/sig/rigor/plugin/manifest.rbs +50 -11
- data/sig/rigor/plugin/protocol_contract.rbs +68 -0
- data/sig/rigor/plugin/registry.rbs +62 -1
- data/sig/rigor/plugin.rbs +1 -1
- data/sig/rigor/rbs_extended.rbs +1 -1
- data/sig/rigor/reflection.rbs +7 -6
- data/sig/rigor/scope.rbs +177 -20
- data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
- data/sig/rigor/source.rbs +4 -4
- data/sig/rigor/testing.rbs +10 -4
- data/sig/rigor/type.rbs +6 -0
- data/sig/rigor.rbs +38 -20
- data/skills/rigor-ask/SKILL.md +8 -5
- data/skills/rigor-baseline-reduce/SKILL.md +4 -6
- data/skills/rigor-ci-setup/SKILL.md +17 -21
- data/skills/rigor-doctor/SKILL.md +24 -22
- data/skills/rigor-doctor/references/01-checks.md +97 -33
- data/skills/rigor-editor-setup/SKILL.md +6 -4
- data/skills/rigor-mcp-setup/SKILL.md +5 -4
- data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
- data/skills/rigor-next-steps/SKILL.md +4 -2
- data/skills/rigor-plugin-author/SKILL.md +19 -23
- data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
- data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
- data/skills/rigor-plugin-review/SKILL.md +6 -4
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
- data/skills/rigor-plugin-tune/SKILL.md +4 -2
- data/skills/rigor-project-init/SKILL.md +46 -18
- data/skills/rigor-project-init/references/01-detect.md +13 -9
- data/skills/rigor-project-init/references/02-configure.md +40 -10
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
- data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
- data/skills/rigor-protection-uplift/SKILL.md +4 -6
- data/skills/rigor-rbs-setup/SKILL.md +4 -2
- data/skills/rigor-type-oracle/SKILL.md +4 -6
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
- data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
- data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
- data/skills/rigor-upgrade/SKILL.md +13 -8
- metadata +132 -1
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../../source/node_children"
|
|
6
|
+
require_relative "context"
|
|
7
|
+
require_relative "errors"
|
|
8
|
+
require_relative "shadow"
|
|
9
|
+
|
|
10
|
+
module Rigor
|
|
11
|
+
module Inference
|
|
12
|
+
# ADR-116 WD5 (ADR-53 Theme B): one traversal owns the declaration-context rules that `ScopeIndexer`'s table
|
|
13
|
+
# walkers each copied — what `self`, the cref and `Module.nesting` become under `class`/`module`, `class <<`,
|
|
14
|
+
# a `K = Class.new { … }`-family write, a bare factory block, and an eval-family block. A discovery table
|
|
15
|
+
# becomes a collector that receives events and never tracks context itself; the {Context} it is handed is
|
|
16
|
+
# the walk's.
|
|
17
|
+
#
|
|
18
|
+
# Require `rigor/inference/declaration_walk`, not this file: the rules the walk applies are `ScopeIndexer`
|
|
19
|
+
# functions, and that entry point loads them.
|
|
20
|
+
#
|
|
21
|
+
# The arms, in the order the walk tries them:
|
|
22
|
+
#
|
|
23
|
+
# - `class` / `module` — {Collector#on_declaration}, then the body under {Context#declaration_body}. The
|
|
24
|
+
# header's constant path and superclass expression are not walked. A header that renders no name raises
|
|
25
|
+
# no event and follows the `unrendered_header` rule (below).
|
|
26
|
+
# - `class << expr` — the expression under the enclosing context, the body under
|
|
27
|
+
# {Context#singleton_class_body}.
|
|
28
|
+
# - a constant write (the four spellings that can name a class) — {Collector#on_constant_write}; then, when
|
|
29
|
+
# the rvalue is the meta-new idiom, ONLY the factory call's receiver and arguments, under the enclosing
|
|
30
|
+
# context, and its block body, rebound to the class the write names. Nothing else of the write is
|
|
31
|
+
# walked: not the target's parent (`P` in `P::K = …`), not the guard's read in `K = K || …`, not a
|
|
32
|
+
# `.freeze` tail's call, not the block's parameters, and the factory call itself raises no
|
|
33
|
+
# {Collector#on_call}. A write whose rvalue is not the idiom walks its children, as any node does.
|
|
34
|
+
# - `def` — {Collector#on_def}, then the children.
|
|
35
|
+
# - a call — {Collector#on_call}; then a bare factory block (`Class.new { … }`, `Module.new`, `Struct.new`,
|
|
36
|
+
# `Data.define`) walks its receiver and arguments under the enclosing context and its body with an
|
|
37
|
+
# unnamed `self`, and an eval-family block (`class_eval`, `module_eval`, `class_exec`, `module_exec`,
|
|
38
|
+
# `instance_eval`, `instance_exec`) its receiver and arguments under the enclosing context and its body
|
|
39
|
+
# with `self` rebound to the receiver. Neither walks the block's parameters. Any other call walks its
|
|
40
|
+
# children. A collector following the `:ordinary_call` variant of the `factory_block` rule (below) sees
|
|
41
|
+
# a bare factory block walked as any other call instead.
|
|
42
|
+
#
|
|
43
|
+
# Every other node walks its children under the context it was reached with, in `compact_child_nodes`
|
|
44
|
+
# order, so a collector sees its events in the order the legacy walker it replaces accumulated them.
|
|
45
|
+
#
|
|
46
|
+
# ## The collector protocol
|
|
47
|
+
#
|
|
48
|
+
# A collector includes {Collector} and overrides the events it needs. Each handler answers {DESCEND} or
|
|
49
|
+
# {DECLINE}: {DECLINE} takes the node's whole subtree away from THIS collector, while the other collectors
|
|
50
|
+
# of the same run continue into it. Any other answer descends. The walk dispatches only the events some
|
|
51
|
+
# collector of the run overrides.
|
|
52
|
+
#
|
|
53
|
+
# ## Variants
|
|
54
|
+
#
|
|
55
|
+
# Where the legacy walkers disagree on a context rule, a port keeps its walker's answer by naming a
|
|
56
|
+
# variant of the rule in its class's `VARIANTS` (ADR-116 WD5; {RULE_VARIANTS} lists them). Collectors on
|
|
57
|
+
# different variants of a rule still share a run: where the variants give a subtree different contexts
|
|
58
|
+
# (a bare factory block, a meta-new or eval-family body, a header that renders no name) the walk goes down
|
|
59
|
+
# once per variant in use, each collector only in its own, and once for all of them everywhere else. A
|
|
60
|
+
# variant is a legacy answer kept on purpose, and #1521 tracks converging each one.
|
|
61
|
+
#
|
|
62
|
+
# The rule walk stays separate (ADR-53 rejected folding rule collectors into indexing): this walk only
|
|
63
|
+
# builds discovery tables.
|
|
64
|
+
module DeclarationWalk
|
|
65
|
+
# A handler's answers: go on into the node, or leave its subtree to the run's other collectors.
|
|
66
|
+
DESCEND = :descend
|
|
67
|
+
DECLINE = :decline
|
|
68
|
+
|
|
69
|
+
# Each context rule a collector may follow a legacy variant of, with its variants. The first is the
|
|
70
|
+
# walk's own rule, which a collector follows unless its `VARIANTS` names another.
|
|
71
|
+
#
|
|
72
|
+
# - `factory_block` — a bare `Class.new { … }` / `Module.new` / `Struct.new` / `Data.define` block.
|
|
73
|
+
# `:unnamed_self` walks the factory's receiver and arguments, then the body with an unnamed `self`,
|
|
74
|
+
# and skips the block's parameters (`walk_class_cvars`' rule). `:ordinary_call` walks the call's
|
|
75
|
+
# children like any call's — receiver, arguments, the block's parameters and body — under the
|
|
76
|
+
# enclosing context, so a `self::` header in the body anchors on the enclosing self
|
|
77
|
+
# (`walk_class_superclasses`' rule; `class C; Class.new { class self::E < S; end }; end` files
|
|
78
|
+
# `C::E`, a class Ruby never creates; #1521 item 8).
|
|
79
|
+
# - `anonymous_class_path` — the file path an anonymous class's synthetic name carries; answered by
|
|
80
|
+
# {Context#anonymous_class_path}, which documents `:whole_file` and `:outside_class_bodies` (#1521
|
|
81
|
+
# item 11).
|
|
82
|
+
# - `unrendered_header` — a `class` / `module` whose header renders no name, which only a parse error
|
|
83
|
+
# produces (`class foo`, a `module` keyword followed by a `def`). `:children` walks every child, the
|
|
84
|
+
# header's parts included, under the enclosing context (`walk_class_superclasses`' rule).
|
|
85
|
+
# `:skip` walks nothing below it (the member-layout walkers'). `:body_with_lost_nesting` walks the body
|
|
86
|
+
# alone under {Context#lost_header_body} (`walk_def_nestings`'). #1521 item 3.
|
|
87
|
+
# - `lexical_prefix` — the prefix meta-new and eval-family splits resolve against, answered by
|
|
88
|
+
# {Context#lexical_prefix}: `:prefix` or `walk_def_nestings`' `:nesting_head` (#1521 item 1).
|
|
89
|
+
RULE_VARIANTS = {
|
|
90
|
+
factory_block: %i[unnamed_self ordinary_call].freeze,
|
|
91
|
+
anonymous_class_path: %i[whole_file outside_class_bodies].freeze,
|
|
92
|
+
unrendered_header: %i[children skip body_with_lost_nesting].freeze,
|
|
93
|
+
lexical_prefix: %i[prefix nesting_head].freeze
|
|
94
|
+
}.freeze
|
|
95
|
+
|
|
96
|
+
# The event handlers a collector may override; each answers {DESCEND} until overridden.
|
|
97
|
+
module Collector
|
|
98
|
+
EVENTS = %i[on_declaration on_def on_call on_constant_write].freeze
|
|
99
|
+
|
|
100
|
+
# The legacy variants of {RULE_VARIANTS} this collector follows, `rule => variant`. A collector
|
|
101
|
+
# overrides the constant, documenting each entry where it declares it.
|
|
102
|
+
VARIANTS = {}.freeze
|
|
103
|
+
|
|
104
|
+
# A `class` / `module` header. `body` is the context its body is walked under; it exists even when
|
|
105
|
+
# the declaration has no body.
|
|
106
|
+
def on_declaration(_node, _context, _body)
|
|
107
|
+
DESCEND
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def on_def(_node, _context)
|
|
111
|
+
DESCEND
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Every call, the factory and eval-family calls included, before the walk enters its block.
|
|
115
|
+
def on_call(_node, _context)
|
|
116
|
+
DESCEND
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# `K = …`, `P::K = …`, `K ||= …` and `P::K ||= …`, before the walk decides whether the rvalue opens a
|
|
120
|
+
# meta-new body.
|
|
121
|
+
def on_constant_write(_node, _context)
|
|
122
|
+
DESCEND
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# The {EVENTS} `klass` overrides. Answered once per class: the table is a module ivar, which only the
|
|
126
|
+
# main Ractor may touch, so a pool worker on the Ractor backend recomputes it instead (the #1055
|
|
127
|
+
# pattern in `FactStore::Target.local`); the answer is the same either way.
|
|
128
|
+
def self.events_of(klass)
|
|
129
|
+
return overridden_events(klass) unless Ractor.main?
|
|
130
|
+
|
|
131
|
+
@events_by_class[klass] ||= overridden_events(klass)
|
|
132
|
+
end
|
|
133
|
+
@events_by_class = {}.compare_by_identity
|
|
134
|
+
|
|
135
|
+
def self.overridden_events(klass)
|
|
136
|
+
EVENTS.reject { |event| klass.instance_method(event).owner.equal?(self) }.freeze
|
|
137
|
+
end
|
|
138
|
+
private_class_method :overridden_events
|
|
139
|
+
|
|
140
|
+
# The variant of `rule` `klass` follows: its `VARIANTS` entry, or the walk's own rule.
|
|
141
|
+
def self.variant_of(klass, rule)
|
|
142
|
+
klass::VARIANTS.fetch(rule) { RULE_VARIANTS.fetch(rule).first }
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Raises {UnknownVariant} unless every `VARIANTS` entry of `klass` names a rule and one of its
|
|
146
|
+
# variants, so a misspelt variant is a {ContractError} rather than the walk's rule followed silently:
|
|
147
|
+
# an error row on the file in a file's own index, and an aborted run in the project pre-pass.
|
|
148
|
+
def self.check_variants!(klass)
|
|
149
|
+
klass::VARIANTS.each do |rule, variant|
|
|
150
|
+
next if RULE_VARIANTS.fetch(rule, EMPTY).include?(variant)
|
|
151
|
+
|
|
152
|
+
raise UnknownVariant, "#{klass}: no #{rule.inspect} variant #{variant.inspect} (ADR-116 WD5)"
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
EMPTY = [].freeze
|
|
156
|
+
private_constant :EMPTY
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
NO_COLLECTORS = [].freeze
|
|
160
|
+
private_constant :NO_COLLECTORS
|
|
161
|
+
|
|
162
|
+
module_function
|
|
163
|
+
|
|
164
|
+
# Walks `root` once for every collector, from `context` (a file's top level by default).
|
|
165
|
+
def run(root, collectors, context = Context.root)
|
|
166
|
+
Traversal.for(collectors).walk(root, collectors, context)
|
|
167
|
+
collectors
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# One run's dispatch: which events any collector overrides, so an event none of them handles costs no
|
|
171
|
+
# call per node.
|
|
172
|
+
class Traversal
|
|
173
|
+
# The traversal for `collectors`. A single-collector run's depends on the collector's class alone, so
|
|
174
|
+
# the main Ractor builds it once per class (the #1055 pattern in `FactStore::Target.local`); every
|
|
175
|
+
# other run builds its own.
|
|
176
|
+
def self.for(collectors)
|
|
177
|
+
return new(collectors) unless collectors.size == 1 && Ractor.main?
|
|
178
|
+
|
|
179
|
+
@single_runs[collectors.first.class] ||= new(collectors)
|
|
180
|
+
end
|
|
181
|
+
@single_runs = {}.compare_by_identity
|
|
182
|
+
|
|
183
|
+
def initialize(collectors)
|
|
184
|
+
@declarations = handles?(collectors, :on_declaration)
|
|
185
|
+
@defs = handles?(collectors, :on_def)
|
|
186
|
+
@calls = handles?(collectors, :on_call)
|
|
187
|
+
@constant_writes = handles?(collectors, :on_constant_write)
|
|
188
|
+
collectors.each { |collector| Collector.check_variants!(collector.class) }
|
|
189
|
+
@ordinary_factories = collectors.any? { |collector| ordinary_factory?(collector) }
|
|
190
|
+
@unnamed_factories = !collectors.all? { |collector| ordinary_factory?(collector) }
|
|
191
|
+
@unrendered_variants = collectors.any? { |collector| variant(collector, :unrendered_header) != :children }
|
|
192
|
+
@nesting_heads = collectors.any? { |collector| variant(collector, :lexical_prefix) == :nesting_head }
|
|
193
|
+
# Only a multi-collector run keeps its collectors, which a cached single-collector traversal must
|
|
194
|
+
# not hold on to, and the subsets it builds from them as it first needs each ({#subset}).
|
|
195
|
+
if collectors.size > 1
|
|
196
|
+
@run = collectors
|
|
197
|
+
@subsets = {}
|
|
198
|
+
end
|
|
199
|
+
freeze
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def walk(node, collectors, context) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
203
|
+
return unless node.is_a?(Prism::Node)
|
|
204
|
+
|
|
205
|
+
# The arms are disjoint node classes, so their order is only a cost: the commonest kind is tried first.
|
|
206
|
+
case node
|
|
207
|
+
when Prism::CallNode
|
|
208
|
+
collectors = descending(collectors) { |c| c.on_call(node, context) } if @calls
|
|
209
|
+
return if collectors.empty?
|
|
210
|
+
# Both block arms need a literal block, which most calls lack.
|
|
211
|
+
return if node.block.is_a?(Prism::BlockNode) &&
|
|
212
|
+
(walk_factory_call?(node, collectors, context) || walk_eval_call?(node, collectors, context))
|
|
213
|
+
when Prism::DefNode
|
|
214
|
+
collectors = descending(collectors) { |c| c.on_def(node, context) } if @defs
|
|
215
|
+
return if collectors.empty?
|
|
216
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
217
|
+
return if walk_declaration?(node, collectors, context)
|
|
218
|
+
when Prism::SingletonClassNode
|
|
219
|
+
return walk_singleton_class(node, collectors, context)
|
|
220
|
+
when Prism::ConstantWriteNode, Prism::ConstantPathWriteNode,
|
|
221
|
+
Prism::ConstantOrWriteNode, Prism::ConstantPathOrWriteNode
|
|
222
|
+
collectors = descending(collectors) { |c| c.on_constant_write(node, context) } if @constant_writes
|
|
223
|
+
return if collectors.empty? || walk_meta_new_write?(node, collectors, context)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
node.rigor_each_child { |child| walk(child, collectors, context) }
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
private
|
|
230
|
+
|
|
231
|
+
def handles?(collectors, event)
|
|
232
|
+
collectors.any? { |collector| Collector.events_of(collector.class).include?(event) }
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# A subset of a multi-collector run that its nodes keep asking for, built the first time and reused:
|
|
236
|
+
# each collector alone (a pair in which one declines), the run without each collector (a larger run in
|
|
237
|
+
# which one declines), and the run split by `factory_block` and `lexical_prefix` variant where it mixes
|
|
238
|
+
# them. `key` is a Symbol for a split, or `[kind, collector]` for the other two.
|
|
239
|
+
def subset(kind, collector = nil)
|
|
240
|
+
table = (@subsets[kind] ||= {}.compare_by_identity)
|
|
241
|
+
key = collector || kind
|
|
242
|
+
table.fetch(key) { table[key] = yield.freeze }
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def alone(collector)
|
|
246
|
+
subset(:alone, collector) { [collector] }
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def variant_split(rule, &)
|
|
250
|
+
subset(rule) do
|
|
251
|
+
second, first = @run.partition(&)
|
|
252
|
+
[first.freeze, second.freeze]
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def variant(collector, rule)
|
|
257
|
+
Collector.variant_of(collector.class, rule)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def ordinary_factory?(collector)
|
|
261
|
+
variant(collector, :factory_block) == :ordinary_call
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# The collectors that let the walk into the node the block asks each of them about. Every collector is
|
|
265
|
+
# asked, in order, whatever the others answer.
|
|
266
|
+
#
|
|
267
|
+
# Asking allocates nothing, so a node where no collector declines costs no object. Where exactly one
|
|
268
|
+
# declines, the answer is a {#subset} memoised for the run (the one survivor of a pair, or the whole run
|
|
269
|
+
# without the decliner), built the first time and free after that. Two cases copy instead: a decline
|
|
270
|
+
# inside a group of three or more that is not the whole run (a variant fork's group, or what is left of
|
|
271
|
+
# a run of four or more after an earlier decline), and two or more declines at one node of a group of
|
|
272
|
+
# three or more. Each allocates one Array.
|
|
273
|
+
def descending(collectors, &)
|
|
274
|
+
case collectors.size
|
|
275
|
+
when 0 then collectors
|
|
276
|
+
when 1 then yield(collectors.first) == DECLINE ? NO_COLLECTORS : collectors
|
|
277
|
+
when 2 then descending_pair(collectors, yield(collectors.first), yield(collectors.last))
|
|
278
|
+
else descending_many(collectors, &)
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# An index loop rather than `each_with_index`, which allocates a block frame object (a T_IMEMO) per
|
|
283
|
+
# call: this runs at every node a run of three or more dispatches an event for.
|
|
284
|
+
def descending_many(collectors)
|
|
285
|
+
declined = nil
|
|
286
|
+
kept = nil
|
|
287
|
+
index = 0
|
|
288
|
+
while index < collectors.size
|
|
289
|
+
collector = collectors[index]
|
|
290
|
+
if yield(collector) != DECLINE
|
|
291
|
+
kept&.push(collector)
|
|
292
|
+
elsif declined.nil? && kept.nil?
|
|
293
|
+
declined = index
|
|
294
|
+
else
|
|
295
|
+
kept ||= collectors.take(index).tap { |list| list.delete_at(declined) }
|
|
296
|
+
end
|
|
297
|
+
index += 1
|
|
298
|
+
end
|
|
299
|
+
kept || (declined.nil? ? collectors : without(collectors, declined))
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def without(collectors, index)
|
|
303
|
+
declined = collectors[index]
|
|
304
|
+
if collectors.equal?(@run)
|
|
305
|
+
return subset(:without, declined) { @run.reject { |collector| collector.equal?(declined) } }
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
collectors.dup.tap { |list| list.delete_at(index) }
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def descending_pair(collectors, first, last)
|
|
312
|
+
if first == DECLINE
|
|
313
|
+
last == DECLINE ? NO_COLLECTORS : alone(collectors.last)
|
|
314
|
+
else
|
|
315
|
+
last == DECLINE ? alone(collectors.first) : collectors
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# False only for a header that renders no prefix while every collector here follows the walk's
|
|
320
|
+
# `unrendered_header` rule, which then walks the node's children like any node's.
|
|
321
|
+
def walk_declaration?(node, collectors, context)
|
|
322
|
+
body_context = context.declaration_body(node)
|
|
323
|
+
return walk_unrendered_header?(node, collectors, context) if body_context.nil?
|
|
324
|
+
|
|
325
|
+
collectors = descending(collectors) { |c| c.on_declaration(node, context, body_context) } if @declarations
|
|
326
|
+
body = node.body
|
|
327
|
+
walk(body, collectors, body_context) if body && !collectors.empty?
|
|
328
|
+
true
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# The `unrendered_header` rule: `:children` collectors walk every child under the enclosing context,
|
|
332
|
+
# `:body_with_lost_nesting` ones the body alone under {Context#lost_header_body}, `:skip` ones nothing.
|
|
333
|
+
def walk_unrendered_header?(node, collectors, context)
|
|
334
|
+
return false unless @unrendered_variants
|
|
335
|
+
|
|
336
|
+
children = collectors.select { |collector| variant(collector, :unrendered_header) == :children }
|
|
337
|
+
node.rigor_each_child { |child| walk(child, children, context) } unless children.empty?
|
|
338
|
+
lost = collectors.select { |collector| variant(collector, :unrendered_header) == :body_with_lost_nesting }
|
|
339
|
+
walk(node.body, lost, context.lost_header_body) if node.body && !lost.empty?
|
|
340
|
+
true
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def walk_singleton_class(node, collectors, context)
|
|
344
|
+
walk(node.expression, collectors, context)
|
|
345
|
+
body = node.body
|
|
346
|
+
walk(body, collectors, context.singleton_class_body) if body
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def walk_meta_new_write?(node, collectors, context)
|
|
350
|
+
enclosing, body, body_self = context.meta_new_split(node)
|
|
351
|
+
return false if enclosing.nil?
|
|
352
|
+
|
|
353
|
+
enclosing.each { |part| walk(part, collectors, context) }
|
|
354
|
+
walk_rebound_body(node, body, collectors, context, body_self, :meta_new) if body
|
|
355
|
+
true
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# A meta-new or eval-family body, with `self` rebound to `owner`. The `lexical_prefix` rule decides
|
|
359
|
+
# `owner`, so where the run mixes its variants the `:nesting_head` collectors take the owner their
|
|
360
|
+
# prefix gives, and the body is walked once per distinct owner — once in all where the two agree.
|
|
361
|
+
def walk_rebound_body(node, body, collectors, context, owner, kind)
|
|
362
|
+
return walk(body, collectors, rebound(context, owner, kind)) unless @nesting_heads
|
|
363
|
+
|
|
364
|
+
prefixes, heads = prefix_groups(collectors)
|
|
365
|
+
head_owner = heads.empty? ? owner : split(node, context, kind, :nesting_head)[2]
|
|
366
|
+
if head_owner == owner
|
|
367
|
+
walk(body, collectors, rebound(context, owner, kind))
|
|
368
|
+
else
|
|
369
|
+
walk(body, prefixes, rebound(context, owner, kind)) unless prefixes.empty?
|
|
370
|
+
walk(body, heads, rebound(context, head_owner, kind))
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def rebound(context, owner, kind)
|
|
375
|
+
kind == :eval ? context.eval_body(owner) : context.meta_new_body(owner)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def split(node, context, kind, lexical_variant)
|
|
379
|
+
kind == :eval ? context.eval_split(node, lexical_variant) : context.meta_new_split(node, lexical_variant)
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# `[prefix, nesting_head]` collectors of a run that mixes the `lexical_prefix` variants.
|
|
383
|
+
def prefix_groups(collectors)
|
|
384
|
+
if collectors.equal?(@run)
|
|
385
|
+
return variant_split(:lexical_prefix) { |collector| variant(collector, :lexical_prefix) == :nesting_head }
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
heads, prefixes = collectors.partition { |collector| variant(collector, :lexical_prefix) == :nesting_head }
|
|
389
|
+
[prefixes, heads]
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# The `factory_block` rule, for a call with a literal block. False when every collector here follows
|
|
393
|
+
# `:ordinary_call`, so the call walks its children like any call. Otherwise the receiver and arguments
|
|
394
|
+
# are walked once, for every collector, since both variants walk them under the enclosing context;
|
|
395
|
+
# the variants part only at the block: the `:unnamed_self` collectors walk its body with an unnamed
|
|
396
|
+
# `self`, the `:ordinary_call` ones the whole block, parameters included, under the enclosing context.
|
|
397
|
+
# Each collector sees the same events in the same order as in a run of its own.
|
|
398
|
+
def walk_factory_call?(node, collectors, context)
|
|
399
|
+
return false unless @unnamed_factories && ScopeIndexer.meta_new_constant_rvalue?(node)
|
|
400
|
+
|
|
401
|
+
unnamed = unnamed_factory_group(collectors)
|
|
402
|
+
return false if unnamed.empty?
|
|
403
|
+
|
|
404
|
+
walk(node.receiver, collectors, context)
|
|
405
|
+
node.arguments&.arguments&.each { |argument| walk(argument, collectors, context) }
|
|
406
|
+
block = node.block
|
|
407
|
+
walk(block.body, unnamed, context.factory_body) if block.body
|
|
408
|
+
walk(block, ordinary_factory_group(collectors), context) if unnamed.size < collectors.size
|
|
409
|
+
true
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def unnamed_factory_group(collectors)
|
|
413
|
+
return collectors unless @ordinary_factories
|
|
414
|
+
return factory_split.first if collectors.equal?(@run)
|
|
415
|
+
|
|
416
|
+
collectors.reject { |collector| ordinary_factory?(collector) }
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def ordinary_factory_group(collectors)
|
|
420
|
+
return factory_split.last if collectors.equal?(@run)
|
|
421
|
+
|
|
422
|
+
collectors.select { |collector| ordinary_factory?(collector) }
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# `[unnamed_self, ordinary_call]` collectors of the whole run.
|
|
426
|
+
def factory_split
|
|
427
|
+
variant_split(:factory_block) { |collector| ordinary_factory?(collector) }
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def walk_eval_call?(node, collectors, context)
|
|
431
|
+
enclosing, body, eval_self = context.eval_split(node)
|
|
432
|
+
return false if enclosing.nil?
|
|
433
|
+
|
|
434
|
+
enclosing.each { |part| walk(part, collectors, context) }
|
|
435
|
+
walk_rebound_body(node, body, collectors, context, eval_self, :eval) if body
|
|
436
|
+
true
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# ADR-116 WD5 — the entry point for {Rigor::Inference::DeclarationWalk}. The walk lives in
|
|
4
|
+
# `declaration_walk/traversal`, and the context rules it applies are `ScopeIndexer` functions, so this file loads
|
|
5
|
+
# the indexer, which loads the walk. `ScopeIndexer` itself requires the traversal file rather than this one, which
|
|
6
|
+
# keeps the two free of a require cycle.
|
|
7
|
+
require_relative "scope_indexer"
|
|
@@ -76,8 +76,9 @@ module Rigor
|
|
|
76
76
|
# walk over the analyzer's own parse produced. This holds per NODE and needs nothing about files.
|
|
77
77
|
# 2. AGREEMENT — where both tables answer for the same DEF (through different objects), they answer the
|
|
78
78
|
# same chain. A bundle is only reused when the file's content SHA-256 matches
|
|
79
|
-
# ({Cache::FileDigest.hexdigest}, the digest tier, not the stat one), and
|
|
80
|
-
# pure function of that file's AST, so identical bytes
|
|
79
|
+
# ({Cache::FileDigest.hexdigest}, the digest tier, not the stat one), and the def-nesting table
|
|
80
|
+
# (`ScopeIndexer.declaration_walk_tables`) is a pure function of that file's AST, so identical bytes
|
|
81
|
+
# yield an identical chain.
|
|
81
82
|
#
|
|
82
83
|
# What is NOT true — and was asserted here before it was measured — is that a file takes exactly ONE of
|
|
83
84
|
# the two branches per run. It does so in the cross-file pre-pass only. An UNCHANGED file re-analysed as
|
|
@@ -4,6 +4,7 @@ require "prism"
|
|
|
4
4
|
|
|
5
5
|
require_relative "../type"
|
|
6
6
|
require_relative "../source/node_children"
|
|
7
|
+
require_relative "return_barrier"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
9
10
|
module Inference
|
|
@@ -13,15 +14,14 @@ module Rigor
|
|
|
13
14
|
# The inferred type is the union of:
|
|
14
15
|
#
|
|
15
16
|
# - the body's last-statement type, and
|
|
16
|
-
# - the type of every explicit `return value`
|
|
17
|
-
#
|
|
17
|
+
# - the type of every explicit `return value` that exits the method. A `return` inside an ordinary block does
|
|
18
|
+
# (control-flow-analysis.md § "Non-local exits"); one inside a {ReturnBarrier} — a nested `def`, a lambda,
|
|
19
|
+
# or a `define_method` body — does not. The barriers are the ones `StatementEvaluator`'s return sink stops
|
|
20
|
+
# at, so the proposal and `rigor check`'s view of callers agree (issue #1382).
|
|
18
21
|
#
|
|
19
22
|
# Extracted from `Rigor::SigGen::Generator#infer_return_type` so `LineTypeCollector` (`rigor annotate`'s
|
|
20
23
|
# def-line annotator) and the sig-generator share one source of truth.
|
|
21
24
|
module DefReturnTyper
|
|
22
|
-
RETURN_BARRIER_NODES = [Prism::DefNode, Prism::LambdaNode, Prism::BlockNode].freeze
|
|
23
|
-
private_constant :RETURN_BARRIER_NODES
|
|
24
|
-
|
|
25
25
|
module_function
|
|
26
26
|
|
|
27
27
|
def call(def_node, scope_index)
|
|
@@ -60,10 +60,14 @@ module Rigor
|
|
|
60
60
|
|
|
61
61
|
def collect_return_types(node, scope_index, out)
|
|
62
62
|
return unless node.is_a?(Prism::Node)
|
|
63
|
-
return if
|
|
63
|
+
return if ReturnBarrier.node?(node)
|
|
64
64
|
|
|
65
65
|
type_return_node(node, scope_index, out) if node.is_a?(Prism::ReturnNode)
|
|
66
|
-
|
|
66
|
+
# A barrier call's receiver and arguments still run in the method; only its block is cut off.
|
|
67
|
+
barrier_block = node.block if node.is_a?(Prism::CallNode) && ReturnBarrier.block_call?(node)
|
|
68
|
+
node.rigor_each_child do |c|
|
|
69
|
+
collect_return_types(c, scope_index, out) unless c.equal?(barrier_block)
|
|
70
|
+
end
|
|
67
71
|
end
|
|
68
72
|
|
|
69
73
|
def type_return_node(return_node, scope_index, out)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../type"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
# Issue #963 — the lexical `self` of a `define_method(:name) { ... }` block.
|
|
8
|
+
#
|
|
9
|
+
# A block has no `self` of its own, so every block body enters with `Scope#entering_opaque_block`
|
|
10
|
+
# and the enclosing class body's `self_type` (`Singleton[C]`) rides along unchanged. For
|
|
11
|
+
# `define_method` that carrier is the wrong side of the class: `Module#define_method` turns the
|
|
12
|
+
# block into an INSTANCE method of the receiver, and Ruby runs its body with `self` bound to the
|
|
13
|
+
# receiving instance. A bare `text` inside the block therefore reaches `C`'s member / attr reader,
|
|
14
|
+
# while `Singleton[C]` answers only `C`'s class methods.
|
|
15
|
+
#
|
|
16
|
+
# The consequence #963 records is #618's veto going the wrong way: `ExpressionTyper#self_type_answers?`
|
|
17
|
+
# asked the singleton side, found nothing, and let a same-named top-level `def text` bind ahead of the
|
|
18
|
+
# reader — typing `text.upcase` inside `class Line < Struct.new(:text); define_method(:shout) { text.upcase }`
|
|
19
|
+
# as the top-level def's `nil`. Narrowing the block body's `self_type` to `Nominal[C]` makes the
|
|
20
|
+
# existing veto ask the instance side, which is the side Ruby dispatches on; nothing in the veto
|
|
21
|
+
# itself changes.
|
|
22
|
+
#
|
|
23
|
+
# The match is deliberately narrow — the receiver must be the class body's own `self`, and that `self`
|
|
24
|
+
# must be a known `Singleton[C]` on the instance side of the class. `Foo.define_method(...)` on some other
|
|
25
|
+
# receiver keeps the unmodelled-self answer, and so does a `class << ...` BODY, where `self` is the
|
|
26
|
+
# singleton class and the call defines a CLASS method: an instance reader is not in that MRO, MRI reaches
|
|
27
|
+
# the top-level `def` there, and Rigor must answer the same. The body is the exclusion, not the lexical
|
|
28
|
+
# singleton frame — a `def` reached from that body has the class object as its `self`, so the call defines
|
|
29
|
+
# an instance method and the narrowing applies. `Scope#singleton_class_body?` is what draws that line, and
|
|
30
|
+
# carrying it on the scope is what lets both block-entry paths apply it identically.
|
|
31
|
+
# A `module_function` module is the one shape where both answers are right at once: MRI makes the block
|
|
32
|
+
# BOTH an instance method, where the reader resolves, and a module function on the module object, where it
|
|
33
|
+
# does not. Narrowing silences the module-function half, which is the false-positive-averse direction
|
|
34
|
+
# AGENTS.md § Implementation Guidelines asks for.
|
|
35
|
+
module DefineMethodBlockSelf
|
|
36
|
+
module_function
|
|
37
|
+
|
|
38
|
+
# @return the block body's narrowed `self_type`, or `nil` when the call shape does not match.
|
|
39
|
+
def narrow_self_type_for(scope:, call_node:)
|
|
40
|
+
return nil if scope&.singleton_class_body?
|
|
41
|
+
return nil unless define_method_on_lexical_self?(call_node)
|
|
42
|
+
|
|
43
|
+
self_type = scope&.self_type
|
|
44
|
+
return nil unless self_type.is_a?(Type::Singleton)
|
|
45
|
+
|
|
46
|
+
class_name = self_type.class_name
|
|
47
|
+
return nil if class_name.nil?
|
|
48
|
+
|
|
49
|
+
instance_type_for(class_name, scope.environment)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def define_method_on_lexical_self?(call_node)
|
|
53
|
+
return false unless call_node.is_a?(Prism::CallNode)
|
|
54
|
+
return false unless call_node.name == :define_method
|
|
55
|
+
|
|
56
|
+
call_node.receiver.nil? || call_node.receiver.is_a?(Prism::SelfNode)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def instance_type_for(class_name, environment)
|
|
60
|
+
environment&.nominal_for_name(class_name) || Type::Nominal.new(class_name)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|