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,427 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../reflection"
|
|
4
|
+
require_relative "../../inference/definer_resolution"
|
|
5
|
+
require_relative "../../source/parameter_envelope"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Analysis
|
|
9
|
+
module CheckRules
|
|
10
|
+
# Issue #992 — the positional envelope `call.wrong-arity` may check a call against when no signature
|
|
11
|
+
# declares the method: the {Source::ParameterEnvelope} the project's own `def` records in
|
|
12
|
+
# `Scope::DiscoveryIndex#discovered_parameter_envelopes`, and only when nothing the project can see
|
|
13
|
+
# could make a different definition of that name the one that runs.
|
|
14
|
+
#
|
|
15
|
+
# Asked in two stages, because every condition past the first can only WITHHOLD a firing:
|
|
16
|
+
#
|
|
17
|
+
# 1. {#owner_envelope} walks the receiver's superclass chain to the first class whose own body, or one of
|
|
18
|
+
# whose project mixins, records the name. Every record at that level must agree — deliberately wider
|
|
19
|
+
# than Ruby's order, so a mixin whose definition may shadow the class's own, or a load-order-dependent
|
|
20
|
+
# pair (#986), declines instead of answering — and none may be opaque. A call that fits this envelope
|
|
21
|
+
# is silent, and nothing below is consulted.
|
|
22
|
+
# 2. {#authoritative?} runs only for a call that would fire. It declines when a `method_missing` /
|
|
23
|
+
# `respond_to_missing?` hook or a dynamic-surface mark sits anywhere in the chain; when a mixin the
|
|
24
|
+
# project does not declare is not a known RBS module that lacks the name; when a `pre_eval:` patch or
|
|
25
|
+
# a plugin's synthetic method names it; when the owner does not make the method public; and when any
|
|
26
|
+
# project subclass of the receiver — whose instances the receiver's type admits — records a different
|
|
27
|
+
# or opaque envelope for it.
|
|
28
|
+
#
|
|
29
|
+
# ADR-119 C1b / C2-c — on both sides stage 1 and the subclass check ask `Inference::DefinerResolution`, and a
|
|
30
|
+
# chain it declines (`UNKNOWN`) answers no envelope: where Ruby's order depends on a fact the tables cannot
|
|
31
|
+
# prove, the rule is silent instead of checking the call against master's order (#1570, #1607).
|
|
32
|
+
#
|
|
33
|
+
# Deliberately NOT in scope: a constructor reached through `Class#new` (nothing records `initialize` as
|
|
34
|
+
# `[:singleton, :new]`, so `.new` finds no owner unless the class writes `def self.new` itself); keyword
|
|
35
|
+
# arity (the envelope's required-keyword flag declines, as `compute_arity_envelope` declines an RBS
|
|
36
|
+
# function with required keywords); and a `class_eval` whose receiver is not a constant, which the
|
|
37
|
+
# discovery walk cannot name.
|
|
38
|
+
class SourceArity # rubocop:disable Metrics/ClassLength
|
|
39
|
+
MISSING_HOOKS = %i[method_missing respond_to_missing?].freeze
|
|
40
|
+
private_constant :MISSING_HOOKS
|
|
41
|
+
|
|
42
|
+
# One class on the walk: its own name, the project modules its mixins resolve to (transitively through
|
|
43
|
+
# their own `include`s), and the candidate-name lists of the mixins that resolve to no project module.
|
|
44
|
+
#
|
|
45
|
+
# Issue #1570 — the level is a SEGMENT of the receiver's `Scope::ResolutionChain` (`:arity` flavor): the
|
|
46
|
+
# class with the modules Ruby inserts around it before the superclass's entries begin. Ruby skips an
|
|
47
|
+
# `include` of a module the superclass chain already carries, so `class C < Base; include M` where
|
|
48
|
+
# `Base` includes `M` leaves `M` at `Base`'s level; collecting every module a class's own `include`s
|
|
49
|
+
# name put it at `C`'s level instead, where its `def` answered a call `Base#foo` receives.
|
|
50
|
+
Level = Data.define(:class_name, :modules, :externals)
|
|
51
|
+
private_constant :Level
|
|
52
|
+
|
|
53
|
+
# ADR-119 C1b — the answer of a level whose records are not one non-opaque envelope. It equals itself in
|
|
54
|
+
# every world, so two worlds that both reach such a level agree, and no envelope equals it.
|
|
55
|
+
AMBIGUOUS = Object.new.freeze
|
|
56
|
+
private_constant :AMBIGUOUS
|
|
57
|
+
|
|
58
|
+
# What `Inference::DefinerResolution` reads off a candidate: the answer, the entry that gave it, its
|
|
59
|
+
# position on the chain and its side. The same four readers as `DefinerResolution::Hit`, which a call site
|
|
60
|
+
# may not name (the case/in contract spec allows no other reference to the module).
|
|
61
|
+
ArityHit = Data.define(:answer, :owner, :index, :side)
|
|
62
|
+
private_constant :ArityHit
|
|
63
|
+
|
|
64
|
+
# A subclass level the walk cannot read: `subclasses_agree?` reads the nil as a decline.
|
|
65
|
+
NO_LEVELS = [nil].freeze
|
|
66
|
+
private_constant :NO_LEVELS
|
|
67
|
+
|
|
68
|
+
def initialize(scope, method_name, kind)
|
|
69
|
+
@scope = scope
|
|
70
|
+
@method_name = method_name.to_sym
|
|
71
|
+
@kind = kind
|
|
72
|
+
@name_memo = {}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# The nearest definition's envelope, or nil when there is none or it is not one shape. Remembers the
|
|
76
|
+
# levels it walked for {#authoritative?}.
|
|
77
|
+
#
|
|
78
|
+
# Its ADR-46 reads are withheld ({Analysis::DependencyRecorder.withhold}) until the caller settles
|
|
79
|
+
# the verdict with {#settle_by_definitions} or {#settle_by_walk}.
|
|
80
|
+
def owner_envelope(class_name)
|
|
81
|
+
@owner_entries = []
|
|
82
|
+
@ambiguous = false
|
|
83
|
+
@unknown = false
|
|
84
|
+
envelope, @withheld = Analysis::DependencyRecorder.withhold { walk_to_owner(class_name) }
|
|
85
|
+
envelope
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# True when the nearest level recorded the name but not as one envelope.
|
|
89
|
+
def ambiguous? = @ambiguous
|
|
90
|
+
|
|
91
|
+
# For a silent verdict that only a definition can overturn — the call fits, the owner takes a required
|
|
92
|
+
# keyword, or nothing on the chain records the name: the owner `def`s themselves (their symbol edges)
|
|
93
|
+
# and a definition appearing at a level the walk passed over (a name-keyed negative edge). A mixin or
|
|
94
|
+
# reopening added at the owner's level makes it opaque, and everything {#authoritative?} reads can only
|
|
95
|
+
# withhold, so none of these needs the file-level class edge the bucket reads would otherwise record.
|
|
96
|
+
def settle_by_definitions
|
|
97
|
+
return if @withheld.nil?
|
|
98
|
+
# ADR-119 C1b — a declined chain (`@unknown`) that some level records the name on is silent only until
|
|
99
|
+
# another file's edit lifts the decline, so its verdict depends on everything the walk read, as an opaque
|
|
100
|
+
# level's does. A decline over a name no project level records is not `@unknown` (see
|
|
101
|
+
# {#decline_unless_recorded_nowhere}): it settles by definitions, as an `ABSENT` read does.
|
|
102
|
+
return settle_by_walk if @unknown
|
|
103
|
+
|
|
104
|
+
@owner_entries.each do |name, kind|
|
|
105
|
+
if kind == :singleton
|
|
106
|
+
@scope.user_singleton_def_site_for(name, @method_name)
|
|
107
|
+
else
|
|
108
|
+
@scope.user_def_site_for(name, @method_name)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
separator = @kind == :singleton ? "." : "#"
|
|
112
|
+
@passed.each { |name| Analysis::DependencyRecorder.read_missing(:method, "#{name}#{separator}#{@method_name}") }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# For every other verdict — a firing, or an opaque owner level whose opacity another file's edit can
|
|
116
|
+
# lift — the walk's reads are the dependency, so they are recorded as read.
|
|
117
|
+
def settle_by_walk
|
|
118
|
+
Analysis::DependencyRecorder.replay(@withheld) unless @withheld.nil?
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Stage 2, for the class {#owner_envelope} last answered.
|
|
122
|
+
def authoritative?(class_name)
|
|
123
|
+
!load_order_dependent?(class_name) && !object_extension_may_shadow? &&
|
|
124
|
+
@levels.all? { |level| clean_level?(level) && public_at?(level.class_name) } &&
|
|
125
|
+
chain_free_of_hooks?(class_name) &&
|
|
126
|
+
subclasses_agree?(class_name, @envelope)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
# ADR-119 C1b / C2-c, #1570 / #1607 — the nearest level is asked of {Inference::DefinerResolution}, on the
|
|
132
|
+
# receiver's own side: the chain stands only where every candidate level gives one envelope in every
|
|
133
|
+
# world Ruby may have run (a fork, an unproven mixin order, a `possible`-only definer, and on the
|
|
134
|
+
# singleton side a hook that could have put a definer ahead). Where it does not, the answer is NO
|
|
135
|
+
# envelope and `@unknown` — the rule stays silent — instead of master's order, which is what #1570 and
|
|
136
|
+
# #1607 fired on. The levels the walk keeps for {#authoritative?} are the chain's.
|
|
137
|
+
def walk_to_owner(class_name)
|
|
138
|
+
@levels = []
|
|
139
|
+
@passed = []
|
|
140
|
+
@recorded = false
|
|
141
|
+
|
|
142
|
+
case Inference::DefinerResolution.resolve(@scope, class_name, @method_name, side,
|
|
143
|
+
question: :arity) { |chain, from| arity_hit(chain, from) }
|
|
144
|
+
in Inference::DefinerResolution::Known(answer: _answer, owner: _owner) then nearest_envelope(class_name)
|
|
145
|
+
in Inference::DefinerResolution::ABSENT then nearest_envelope(class_name) # rubocop:disable Lint/DuplicateBranch
|
|
146
|
+
in Inference::DefinerResolution::UNKNOWN then decline_unless_recorded_nowhere(class_name)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# A declined read of a name no project level records (`arity_hit` found no level that does, in any world it
|
|
151
|
+
# was asked) answers no envelope whether or not it stands, so the walk's reads are not the verdict's
|
|
152
|
+
# dependency: only a `def` appearing can change it, which {#settle_by_definitions} files, as for an
|
|
153
|
+
# `ABSENT` read. It can leave a stale false negative in a warm run, never a firing. Without this, every
|
|
154
|
+
# `Widget.new` (the implicit `Class` tail may answer it) would replay the whole walk as a file-level
|
|
155
|
+
# class edge.
|
|
156
|
+
def decline_unless_recorded_nowhere(class_name)
|
|
157
|
+
nowhere = !@recorded && !arity_chain(class_name).truncated?
|
|
158
|
+
nowhere ? nearest_envelope(class_name) : decline
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# The chain's nearest level's envelope. `Known#answer` is that envelope, but {#authoritative?} and
|
|
162
|
+
# {#settle_by_definitions} also read the levels, passed names and owner entries this walk fills, so the
|
|
163
|
+
# answer is not used on its own. Run only once the candidate-set read has answered, so the chain stands
|
|
164
|
+
# for the name.
|
|
165
|
+
def nearest_envelope(class_name) = owner_in(chain_levels(arity_chain(class_name)).first)
|
|
166
|
+
|
|
167
|
+
def decline
|
|
168
|
+
@levels = []
|
|
169
|
+
@passed = []
|
|
170
|
+
@owner_entries = []
|
|
171
|
+
@envelope = nil
|
|
172
|
+
@unknown = true
|
|
173
|
+
nil
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# The first level at or after entry position `from` whose records name the method: its envelope (or
|
|
177
|
+
# {AMBIGUOUS}), the entry that recorded it and that entry's position and side on the chain. On the
|
|
178
|
+
# singleton side the class's own record is a `:singleton` entry and an extended module's an `:instance`
|
|
179
|
+
# one (`owner_entries` names the kind of each).
|
|
180
|
+
def arity_hit(chain, from)
|
|
181
|
+
start = 0
|
|
182
|
+
chain.levels.each_with_index do |raw, index|
|
|
183
|
+
level = Level.new(*raw)
|
|
184
|
+
entries = chain.level_entries(index)
|
|
185
|
+
first = start
|
|
186
|
+
start += entries.size
|
|
187
|
+
next if first < from
|
|
188
|
+
|
|
189
|
+
found = level_envelopes(level)
|
|
190
|
+
next if found.empty?
|
|
191
|
+
|
|
192
|
+
envelope = found.first
|
|
193
|
+
answer = found.all?(envelope) && !Source::ParameterEnvelope.opaque?(envelope) ? envelope : AMBIGUOUS
|
|
194
|
+
owner, owner_kind = owner_entries(level).first
|
|
195
|
+
position = first + (entries.index { |entry| entry.name == owner && entry.side == owner_kind } || 0)
|
|
196
|
+
@recorded = true
|
|
197
|
+
return ArityHit.new(answer, owner, position, owner_kind)
|
|
198
|
+
end
|
|
199
|
+
nil
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def owner_in(levels)
|
|
203
|
+
levels.each do |level|
|
|
204
|
+
@levels << level
|
|
205
|
+
found = level_envelopes(level)
|
|
206
|
+
if found.empty?
|
|
207
|
+
@passed.concat([level.class_name] + level.modules)
|
|
208
|
+
next
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
envelope = found.first
|
|
212
|
+
@owner_entries |= owner_entries(level)
|
|
213
|
+
unless found.all?(envelope) && !Source::ParameterEnvelope.opaque?(envelope)
|
|
214
|
+
@ambiguous = true
|
|
215
|
+
return nil
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
return @envelope = envelope
|
|
219
|
+
end
|
|
220
|
+
nil
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def owner_entries(level)
|
|
224
|
+
own = @scope.parameter_envelopes_of(level.class_name).key?([@kind, @method_name])
|
|
225
|
+
entries = own ? [[level.class_name, @kind]] : []
|
|
226
|
+
level.modules.each do |mod|
|
|
227
|
+
entries << [mod, :instance] if @scope.parameter_envelopes_of(mod).key?([:instance, @method_name])
|
|
228
|
+
end
|
|
229
|
+
entries
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# The survey corpus's own lesson. `PStore.new(path).transaction { … }` inside `module TDiary::IO`
|
|
233
|
+
# resolves `PStore` to the project's `TDiary::IO::PStore` when that class's file is loaded, and to the
|
|
234
|
+
# stdlib `::PStore` when it is not — and tdiary loads it only under one `io_class` setting, so under
|
|
235
|
+
# the default one the call is correct code. The analysis universe holds every file at once, so it
|
|
236
|
+
# always picks the inner class. Whenever the receiver's class is one of SEVERAL known classes the
|
|
237
|
+
# call site's `Module.nesting` spells with the same last segment, which one the receiver is depends
|
|
238
|
+
# on load order, and a verdict against the project class's `def` is not one the program backs.
|
|
239
|
+
def load_order_dependent?(class_name)
|
|
240
|
+
segment = class_name.to_s.split("::").last
|
|
241
|
+
candidates = Rigor::Reflection.lexical_nesting_chain(@scope).map { |entry| "#{entry}::#{segment}" }
|
|
242
|
+
candidates << segment
|
|
243
|
+
known = candidates.uniq.select { |name| known_class?(name) }
|
|
244
|
+
known.include?(class_name.to_s) && known.size > 1
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# A module some method body hands to `obj.extend` sits ahead of the object's class, so if it defines
|
|
248
|
+
# the name at all, that object answers with the module's definition.
|
|
249
|
+
def object_extension_may_shadow?
|
|
250
|
+
@scope.discovered_parameter_envelopes.any? do |_name, bucket|
|
|
251
|
+
bucket.key?(Scope::DiscoveryIndex::ENVELOPE_OBJECT_EXTENDED_MARK) && bucket.key?([:instance, @method_name])
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def known_class?(name)
|
|
256
|
+
@scope.discovered_classes.key?(name) || Rigor::Reflection.rbs_class_known?(name, scope: @scope)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Yields each level of `class_name`'s chain. False when the walk stopped at the ADR-41 budget rather than
|
|
260
|
+
# ending: budget exhaustion is uncertainty, and every caller reads it as a reason to decline.
|
|
261
|
+
def walked_whole_chain?(class_name, &)
|
|
262
|
+
levels, whole = chain_levels(arity_chain(class_name))
|
|
263
|
+
levels.each(&)
|
|
264
|
+
whole
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# `[levels, whole]` for one world of the chain, up to the first superclass the project does not declare.
|
|
268
|
+
def chain_levels(chain) = [chain.levels.map { |level| Level.new(*level) }, !chain.truncated?]
|
|
269
|
+
|
|
270
|
+
def side = @kind == :singleton ? :singleton : :instance
|
|
271
|
+
|
|
272
|
+
# Instance methods reach a receiver through `include` / `prepend`; class methods through `extend`, and
|
|
273
|
+
# an extended module's own `include`s reach the same singleton — the chain's singleton side.
|
|
274
|
+
#
|
|
275
|
+
# Issue #986 — the `:arity` flavor expands a name the compact-header rename collision left ambiguous
|
|
276
|
+
# to BOTH classes it names: both are ancestors at runtime and only their MRO order is unknowable. Taking
|
|
277
|
+
# both into the level is what keeps this rule alive for the rest of the receiver: a method only one of
|
|
278
|
+
# them declares still has one envelope and still fires, a method they disagree about lands two on the
|
|
279
|
+
# level's join and declines there, and the class's own `def`s, its superclass's and its unambiguous
|
|
280
|
+
# mixins are untouched. Letting the decline arrive as an unknown EXTERNAL mixin instead suppressed the
|
|
281
|
+
# rule for every level of the receiver.
|
|
282
|
+
def arity_chain(class_name) = Scope::ResolutionChain.for(@scope, class_name.to_s, side, :arity)
|
|
283
|
+
|
|
284
|
+
def resolve(owner, raw)
|
|
285
|
+
return nil if raw.nil?
|
|
286
|
+
|
|
287
|
+
bucket = (@name_memo[owner] ||= {})
|
|
288
|
+
return bucket[raw] if bucket.key?(raw)
|
|
289
|
+
|
|
290
|
+
bucket[raw] = @scope.ancestor_name_candidates(owner, raw).find { |name| project_class?(name) }
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def project_class?(name)
|
|
294
|
+
@scope.known_user_class?(name) || @scope.discovered_parameter_envelopes.key?(name)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# A module's methods reach the receiver as instance methods whichever side the receiver is on.
|
|
298
|
+
def level_envelopes(level)
|
|
299
|
+
own = @scope.parameter_envelopes_of(level.class_name)[[@kind, @method_name]]
|
|
300
|
+
mixed = level.modules.map { |mod| @scope.parameter_envelopes_of(mod)[[:instance, @method_name]] }
|
|
301
|
+
([own] + mixed).compact
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def clean_level?(level)
|
|
305
|
+
([level.class_name] + level.modules).none? { |name| dynamic_surface?(name) || project_patched?(name) } &&
|
|
306
|
+
level.externals.all? { |candidates| external_mixin_lacks_method?(candidates) }
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def dynamic_surface?(class_name)
|
|
310
|
+
Scope::DiscoveryIndex.rewritten_surface?(@scope.parameter_envelopes_of(class_name))
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def project_patched?(class_name)
|
|
314
|
+
environment = @scope.environment
|
|
315
|
+
patched = environment&.project_patched_methods
|
|
316
|
+
return true if patched && !patched.empty? && patch_names_method?(patched, class_name)
|
|
317
|
+
|
|
318
|
+
synthetic = environment&.synthetic_method_index
|
|
319
|
+
return false if synthetic.nil? || synthetic.empty?
|
|
320
|
+
|
|
321
|
+
synthetic.knows_class?(class_name) || !synthetic.lookup_instance(class_name, @method_name).empty? ||
|
|
322
|
+
!synthetic.lookup_singleton(class_name, @method_name).empty?
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def patch_names_method?(patched, class_name)
|
|
326
|
+
%i[instance singleton].any? do |kind|
|
|
327
|
+
!patched.lookup(class_name: class_name, method_name: @method_name, kind: kind).nil?
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# A mixin the project does not declare may define the name with any arity, and `prepend` lets it win
|
|
332
|
+
# over the class's own `def`. Only a module RBS knows, and whose declaration lacks the name, is
|
|
333
|
+
# evidence that it does not.
|
|
334
|
+
def external_mixin_lacks_method?(candidates)
|
|
335
|
+
name = candidates.find { |candidate| Rigor::Reflection.rbs_class_known?(candidate, scope: @scope) }
|
|
336
|
+
return false if name.nil?
|
|
337
|
+
|
|
338
|
+
Rigor::Reflection.instance_method_definition(name, @method_name, scope: @scope).nil?
|
|
339
|
+
rescue StandardError
|
|
340
|
+
false
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def public_at?(class_name)
|
|
344
|
+
visibility = @scope.discovered_method_visibility(class_name, @method_name)
|
|
345
|
+
visibility.nil? || visibility == :public
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# A hook anywhere in the chain, including above the owner, and on either side: a class that answers
|
|
349
|
+
# names it does not define is a class whose `def`s are not the whole story of what a call reaches.
|
|
350
|
+
def chain_free_of_hooks?(class_name)
|
|
351
|
+
walked_whole_chain?(class_name) do |level|
|
|
352
|
+
([level.class_name] + level.modules).each do |name|
|
|
353
|
+
envelopes = @scope.parameter_envelopes_of(name)
|
|
354
|
+
return false if dynamic_surface?(name)
|
|
355
|
+
return false if MISSING_HOOKS.any? { |hook| hook_recorded?(envelopes, hook) }
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def hook_recorded?(envelopes, hook)
|
|
361
|
+
envelopes.key?([:instance, hook]) || envelopes.key?([:singleton, hook])
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Every project class whose superclass chain reaches the receiver's class: a value typed as the
|
|
365
|
+
# receiver may be any of them, and each one's own definition of the name is what runs for it.
|
|
366
|
+
def subclasses_agree?(class_name, envelope)
|
|
367
|
+
each_subclass(class_name) do |subclass|
|
|
368
|
+
subclass_levels(subclass).each do |level|
|
|
369
|
+
# A subclass whose own level the budget cut cannot be read, which is a reason to decline.
|
|
370
|
+
return false if level.nil?
|
|
371
|
+
return false unless clean_level?(level)
|
|
372
|
+
return false unless level_envelopes(level).all?(envelope)
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
true
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# A subclass's own level (nil where the budget cut it or the chain does not stand for the name). It asks
|
|
379
|
+
# {Inference::DefinerResolution} on the receiver's side (ADR-119 C1b, C2-c): the subclass's own level
|
|
380
|
+
# answers, and a fork, an unproven mixin order, a `possible`-only definer or a hook there is `UNKNOWN`,
|
|
381
|
+
# which `subclasses_agree?` reads as a reason to decline.
|
|
382
|
+
def subclass_levels(subclass)
|
|
383
|
+
case Inference::DefinerResolution.resolve(@scope, subclass, @method_name, side, question: :arity,
|
|
384
|
+
&own_level_answer(subclass))
|
|
385
|
+
in Inference::DefinerResolution::Known(answer: _answer, owner: _owner) then [first_level(subclass)]
|
|
386
|
+
in Inference::DefinerResolution::ABSENT then [nil]
|
|
387
|
+
in Inference::DefinerResolution::UNKNOWN then NO_LEVELS
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# The subclass's own level, always a candidate (an empty answer where it records nothing), so that the
|
|
392
|
+
# read asks whether the chain stands for the subclass and never whether an ancestor defines the name.
|
|
393
|
+
def own_level_answer(subclass)
|
|
394
|
+
lambda do |chain, from|
|
|
395
|
+
next nil unless from.zero? && !chain.levels.empty?
|
|
396
|
+
|
|
397
|
+
level = Level.new(*chain.levels.first)
|
|
398
|
+
ArityHit.new(level_envelopes(level).uniq, subclass, 0, side)
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def first_level(class_name) = chain_levels(arity_chain(class_name)).first.first
|
|
403
|
+
|
|
404
|
+
def each_subclass(class_name)
|
|
405
|
+
children = children_by_parent
|
|
406
|
+
queue = children.fetch(class_name.to_s, []).dup
|
|
407
|
+
seen = {}
|
|
408
|
+
until queue.empty?
|
|
409
|
+
subclass = queue.shift
|
|
410
|
+
next if seen[subclass]
|
|
411
|
+
|
|
412
|
+
seen[subclass] = true
|
|
413
|
+
yield subclass
|
|
414
|
+
queue.concat(children.fetch(subclass, []))
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def children_by_parent
|
|
419
|
+
@scope.discovered_superclasses.each_with_object({}) do |(child, raw_parent), children|
|
|
420
|
+
parent = resolve(child, raw_parent)
|
|
421
|
+
(children[parent] ||= []) << child if parent
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Analysis
|
|
7
|
+
module CheckRules
|
|
8
|
+
# Issue #1367 (ADR-117 Decision point 1, WD2) — what the interpreter's setter for each special global accepts:
|
|
9
|
+
# the envelope `global.write-type-mismatch` and `global.readonly-write` judge a write against.
|
|
10
|
+
#
|
|
11
|
+
# The setter is the authority, not the RBS declaration of the global. Where the two differ, the setter wins:
|
|
12
|
+
# `$;` is declared `Regexp | String | nil`, but its setter also takes any object with `to_str`, and `$stdout` is
|
|
13
|
+
# declared `IO`, but its setter takes anything that responds to `write`. Each entry names the CRuby function
|
|
14
|
+
# that enforces it and was confirmed on Ruby 4.0.5, whose error the comment quotes.
|
|
15
|
+
#
|
|
16
|
+
# A special whose setter accepts every value is absent, so a write to it is never checked: `$stdin`
|
|
17
|
+
# (ADR-117 WD2), `$_`, `$VERBOSE` / `$-v` / `$-w`, `$DEBUG` / `$-d` and `$=`. So is `$@`, whose setter raises
|
|
18
|
+
# `ArgumentError` or `TypeError` depending on whether `$!` is set when it runs. `$&`, `` $` ``, `$'`, `$+`
|
|
19
|
+
# and `$1`..`$9` cannot be assigned at all: that is a syntax error, not a write.
|
|
20
|
+
module SpecialGlobalSetters
|
|
21
|
+
# A setter that raises `TypeError` unless the value is an instance of one of `classes` (a subclass counts,
|
|
22
|
+
# since the setter tests the object's built-in type) or, when `conversion` names a method, responds to it:
|
|
23
|
+
# the implicit conversion the setter calls (`to_str`, `to_int`) or the method it asks `respond_to?` about
|
|
24
|
+
# (`write`). `accepts` is the phrase the diagnostic quotes.
|
|
25
|
+
Contract = Data.define(:classes, :conversion, :accepts) do
|
|
26
|
+
# Whether an instance of the core class `class_name` is one the setter takes by type. Both sides are core
|
|
27
|
+
# classes, whose hierarchy is Ruby's own, so the running interpreter answers it.
|
|
28
|
+
def accepts_class?(class_name)
|
|
29
|
+
klass = ::Object.const_get(class_name)
|
|
30
|
+
classes.any? { |accepted| klass <= ::Object.const_get(accepted) }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# `rb_str_setter` (string.c), reached through `rb_deprecated_str_setter` and io.c's `deprecated_rs_setter`,
|
|
35
|
+
# tests `T_STRING` and calls no conversion: `$/ = 1` raises "value of $/ must be String", and so do a
|
|
36
|
+
# `Regexp` and an object with `to_str`.
|
|
37
|
+
STRING_OR_NIL = Contract.new(classes: %w[String NilClass].freeze, conversion: nil, accepts: "a String or nil")
|
|
38
|
+
# `rb_fs_setter` (string.c): nil, a String, a Regexp, or an object with `to_str` (`rb_fs_check`).
|
|
39
|
+
# `$; = 1` raises "value of $; must be String or Regexp".
|
|
40
|
+
FIELD_SEPARATOR = Contract.new(
|
|
41
|
+
classes: %w[String Regexp NilClass].freeze, conversion: :to_str,
|
|
42
|
+
accepts: "a String, a Regexp, nil, or an object with `to_str'"
|
|
43
|
+
)
|
|
44
|
+
# `match_setter` (re.c): nil or a MatchData (`Check_Type(val, T_MATCH)`). `$~ = 1` raises
|
|
45
|
+
# "wrong argument type Integer (expected MatchData)".
|
|
46
|
+
MATCH_DATA = Contract.new(classes: %w[MatchData NilClass].freeze, conversion: nil,
|
|
47
|
+
accepts: "a MatchData or nil")
|
|
48
|
+
# `set_arg0` (ruby.c) calls `StringValueCStr`: a String or an object with `to_str`, and not nil. `$0 = 1`
|
|
49
|
+
# raises "no implicit conversion of Integer into String".
|
|
50
|
+
PROGRAM_NAME = Contract.new(
|
|
51
|
+
classes: %w[String].freeze, conversion: :to_str, accepts: "a String or an object with `to_str'"
|
|
52
|
+
)
|
|
53
|
+
# `argf_lineno_setter` (io.c) calls `NUM2INT`: an Integer, a Float, or an object with `to_int`, and not nil.
|
|
54
|
+
# `$. = "1"` raises "no implicit conversion of String into Integer".
|
|
55
|
+
LINE_NUMBER = Contract.new(
|
|
56
|
+
classes: %w[Integer
|
|
57
|
+
Float].freeze, conversion: :to_int, accepts: "an Integer, a Float, or an object with `to_int'"
|
|
58
|
+
)
|
|
59
|
+
# `opt_i_set` (io.c) through `argf_inplace_mode_set`: nil or false turns in-place mode off; anything else
|
|
60
|
+
# goes through `StringValueCStr`. `$-i = 1` raises "no implicit conversion of Integer into String".
|
|
61
|
+
INPLACE_MODE = Contract.new(
|
|
62
|
+
classes: %w[String NilClass FalseClass].freeze, conversion: :to_str,
|
|
63
|
+
accepts: "a String, nil, false, or an object with `to_str'"
|
|
64
|
+
)
|
|
65
|
+
# `stdout_setter` / `stderr_setter` (io.c) call `must_respond_to(id_write, …)`, which asks `respond_to?` and so
|
|
66
|
+
# honours `respond_to_missing?`. `$stdout = 1` raises "$stdout must have write method, Integer given".
|
|
67
|
+
# ADR-117 WD2: this `_Writer` requirement is the only contract a stream write is checked against.
|
|
68
|
+
WRITER = Contract.new(classes: [].freeze, conversion: :write, accepts: "an object that responds to `write'")
|
|
69
|
+
|
|
70
|
+
CONTRACTS = {
|
|
71
|
+
"$/": STRING_OR_NIL,
|
|
72
|
+
"$-0": STRING_OR_NIL,
|
|
73
|
+
"$,": STRING_OR_NIL,
|
|
74
|
+
"$\\": STRING_OR_NIL,
|
|
75
|
+
"$;": FIELD_SEPARATOR,
|
|
76
|
+
"$-F": FIELD_SEPARATOR,
|
|
77
|
+
"$~": MATCH_DATA,
|
|
78
|
+
"$0": PROGRAM_NAME,
|
|
79
|
+
"$PROGRAM_NAME": PROGRAM_NAME,
|
|
80
|
+
"$.": LINE_NUMBER,
|
|
81
|
+
"$-i": INPLACE_MODE,
|
|
82
|
+
"$stdout": WRITER,
|
|
83
|
+
"$>": WRITER,
|
|
84
|
+
"$stderr": WRITER
|
|
85
|
+
}.freeze
|
|
86
|
+
|
|
87
|
+
# The specials whose setter is `rb_gvar_readonly_setter` — declared with `rb_define_readonly_variable`, as a
|
|
88
|
+
# virtual variable without a setter, or with that setter by name. Any write raises `NameError`
|
|
89
|
+
# ("$! is a read-only variable"), whatever the value.
|
|
90
|
+
#
|
|
91
|
+
# - `$!` (eval.c); `$$` and `$?` (process.c).
|
|
92
|
+
# - `$<`, `$FILENAME` and `$*` (io.c, ARGF).
|
|
93
|
+
# - `$:` / `$LOAD_PATH` / `$-I`, `$"` / `$LOADED_FEATURES` (load.c).
|
|
94
|
+
# - `$-W`, and the `-p` / `-l` / `-a` switches `$-p`, `$-l` and `$-a` (ruby.c).
|
|
95
|
+
READ_ONLY = Set[
|
|
96
|
+
:$!, :$$, :$?, :$<, :$FILENAME, :$*, :$:, :$LOAD_PATH, :$-I, :$", :$LOADED_FEATURES,
|
|
97
|
+
:$-W, :$-p, :$-l, :$-a
|
|
98
|
+
].freeze
|
|
99
|
+
|
|
100
|
+
# The literal nodes `global.write-type-mismatch` judges, by the core class of the object each evaluates to.
|
|
101
|
+
# An interpolated String, Symbol or Regexp is still a new instance of that class.
|
|
102
|
+
LITERAL_CLASSES = {
|
|
103
|
+
Prism::IntegerNode => "Integer", Prism::FloatNode => "Float", Prism::RationalNode => "Rational",
|
|
104
|
+
Prism::ImaginaryNode => "Complex", Prism::StringNode => "String", Prism::InterpolatedStringNode => "String",
|
|
105
|
+
Prism::SymbolNode => "Symbol", Prism::InterpolatedSymbolNode => "Symbol", Prism::ArrayNode => "Array",
|
|
106
|
+
Prism::HashNode => "Hash", Prism::RegularExpressionNode => "Regexp",
|
|
107
|
+
Prism::InterpolatedRegularExpressionNode => "Regexp", Prism::NilNode => "NilClass",
|
|
108
|
+
Prism::TrueNode => "TrueClass", Prism::FalseNode => "FalseClass"
|
|
109
|
+
}.freeze
|
|
110
|
+
|
|
111
|
+
# How the diagnostic names a literal of each class.
|
|
112
|
+
LITERAL_DESCRIPTIONS = { "NilClass" => "nil", "TrueClass" => "true", "FalseClass" => "false" }.freeze
|
|
113
|
+
|
|
114
|
+
module_function
|
|
115
|
+
|
|
116
|
+
# The setter contract a write of `name` is checked against, or nil when its setter accepts every value or the
|
|
117
|
+
# global is no special.
|
|
118
|
+
def contract_for(name)
|
|
119
|
+
CONTRACTS[name]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# The core class of the object `node` evaluates to when it is a literal ({LITERAL_CLASSES}), looking
|
|
123
|
+
# through parentheses around a single expression, or nil for any other expression.
|
|
124
|
+
def literal_class(node)
|
|
125
|
+
while node.is_a?(Prism::ParenthesesNode)
|
|
126
|
+
body = node.body
|
|
127
|
+
return nil unless body.is_a?(Prism::StatementsNode) && body.body.size == 1
|
|
128
|
+
|
|
129
|
+
node = body.body.first
|
|
130
|
+
end
|
|
131
|
+
LITERAL_CLASSES[node.class]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def literal_description(class_name)
|
|
135
|
+
LITERAL_DESCRIPTIONS.fetch(class_name) do
|
|
136
|
+
"#{class_name.start_with?('A', 'E', 'I', 'O', 'U') ? 'an' : 'a'} #{class_name} literal"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def read_only?(name)
|
|
141
|
+
READ_ONLY.include?(name)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
5
|
require_relative "../../source/node_children"
|
|
6
|
+
require_relative "../../inference/narrowing"
|
|
6
7
|
require_relative "inferred_param_guard"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
@@ -29,6 +30,12 @@ module Rigor
|
|
|
29
30
|
# The false-positive envelope mirrors `flow.always-truthy-condition`: clauses inside loops / blocks are
|
|
30
31
|
# skipped (mutation tracking through those is incomplete), and the rule reads the engine's own
|
|
31
32
|
# narrowing rather than recomputing it, so the diagnostic and the body typing can never diverge.
|
|
33
|
+
#
|
|
34
|
+
# Issue #1429 — a `:disjoint` clause is not reported when its `Bot` rests on a `Nominal` subject member whose
|
|
35
|
+
# class is disjoint from a condition's ({Inference::Narrowing.disjoint_nominal_guard?}): the `when C` or `in C`
|
|
36
|
+
# is code evidence that the subject can hold such an object (`io = STDOUT; case io when StringIO`), and Rigor's
|
|
37
|
+
# reading of the subject's type is what the evidence contradicts. The body scope stays `Bot`, so no call on the
|
|
38
|
+
# subject in it is checked. A subject that is a literal carrier (`x = 1; case x when String`) is still reported.
|
|
32
39
|
class UnreachableClauseCollector
|
|
33
40
|
LOOP_OR_BLOCK_NODE_CLASSES = [
|
|
34
41
|
Prism::WhileNode, Prism::UntilNode, Prism::ForNode, Prism::BlockNode
|
|
@@ -139,13 +146,30 @@ module Rigor
|
|
|
139
146
|
return if scope.nil?
|
|
140
147
|
return unless scope.local(subject_name).is_a?(Type::Bot)
|
|
141
148
|
|
|
149
|
+
kind = when_clause_kind(clause, subject_name)
|
|
150
|
+
return if kind == :disjoint &&
|
|
151
|
+
disjoint_nominal_subject?(@scope_index[clause.conditions.first], subject_name, clause.conditions)
|
|
152
|
+
|
|
142
153
|
@results << Result.new(
|
|
143
154
|
clause: clause, body: clause.statements, subject_name: subject_name,
|
|
144
155
|
condition_source: clause.conditions.map(&:slice).join(", "),
|
|
145
|
-
kind:
|
|
156
|
+
kind: kind, keyword: "when"
|
|
146
157
|
)
|
|
147
158
|
end
|
|
148
159
|
|
|
160
|
+
# True when the subject, as `entry` binds it, holds a non-literal `Nominal` whose class the class one of
|
|
161
|
+
# `patterns` names is disjoint from (issue #1429, see the class comment).
|
|
162
|
+
def disjoint_nominal_subject?(entry, subject_name, patterns)
|
|
163
|
+
subject_type = entry&.local(subject_name)
|
|
164
|
+
return false if subject_type.nil?
|
|
165
|
+
|
|
166
|
+
patterns.any? do |condition|
|
|
167
|
+
class_name = Inference::Narrowing.lexical_class_name(condition, entry)
|
|
168
|
+
class_name &&
|
|
169
|
+
Inference::Narrowing.disjoint_nominal_guard?(subject_type, class_name, environment: entry.environment)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
149
173
|
# A dead `when` is `:prior_exhaustion` when the subject was already narrowed to `bot` BEFORE this
|
|
150
174
|
# clause (an earlier clause covered it) — read from the entry scope the engine recorded on the
|
|
151
175
|
# clause's first condition node (ADR-47 WD2). Otherwise the subject was still concrete entering the
|
|
@@ -170,13 +194,23 @@ module Rigor
|
|
|
170
194
|
return if scope.nil?
|
|
171
195
|
return unless scope.local(subject_name).is_a?(Type::Bot)
|
|
172
196
|
|
|
197
|
+
kind = in_clause_kind(clause, subject_name)
|
|
198
|
+
return if kind == :disjoint && disjoint_nominal_pattern?(clause, subject_name)
|
|
199
|
+
|
|
173
200
|
@results << Result.new(
|
|
174
201
|
clause: clause, body: clause.statements, subject_name: subject_name,
|
|
175
202
|
condition_source: clause.pattern.slice,
|
|
176
|
-
kind:
|
|
203
|
+
kind: kind, keyword: "in"
|
|
177
204
|
)
|
|
178
205
|
end
|
|
179
206
|
|
|
207
|
+
# {#disjoint_nominal_subject?} for a bare class pattern, `in C` or `in C => x`.
|
|
208
|
+
def disjoint_nominal_pattern?(clause, subject_name)
|
|
209
|
+
pattern = clause.pattern
|
|
210
|
+
pattern = pattern.value if pattern.is_a?(Prism::CapturePatternNode)
|
|
211
|
+
disjoint_nominal_subject?(@scope_index[clause.pattern], subject_name, [pattern])
|
|
212
|
+
end
|
|
213
|
+
|
|
180
214
|
def in_clause_kind(clause, subject_name)
|
|
181
215
|
entry = @scope_index[clause.pattern]
|
|
182
216
|
return :prior_exhaustion if entry && entry.local(subject_name).is_a?(Type::Bot)
|