rigortype 0.3.9 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/data/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/10-sorbet.md +9 -10
- data/docs/handbook/11-sig-gen.md +454 -13
- data/docs/manual/02-cli-reference.md +63 -2
- data/docs/manual/03-configuration.md +7 -0
- data/docs/manual/04-diagnostics.md +4 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/10-mcp-server.md +3 -2
- 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 +323 -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 +520 -49
- data/lib/rigor/analysis/dependency_recorder.rb +23 -0
- data/lib/rigor/analysis/fact_store.rb +9 -0
- data/lib/rigor/analysis/incremental.rb +26 -0
- data/lib/rigor/analysis/incremental_session.rb +36 -4
- 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 +1 -0
- data/lib/rigor/analysis/rule_catalog.rb +127 -0
- data/lib/rigor/analysis/run_cache_key.rb +20 -11
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +167 -11
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
- data/lib/rigor/analysis/runner/pool_coordinator.rb +107 -15
- data/lib/rigor/analysis/runner/project_pre_passes.rb +26 -7
- data/lib/rigor/analysis/runner.rb +278 -20
- data/lib/rigor/analysis/template_unit_collector.rb +303 -0
- data/lib/rigor/analysis/template_unit_paths.rb +91 -0
- data/lib/rigor/analysis/template_unit_positions.rb +293 -0
- data/lib/rigor/analysis/template_units.rb +399 -0
- data/lib/rigor/analysis/worker_session.rb +53 -10
- 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 +6 -1
- data/lib/rigor/cache/engine_source.rb +29 -1
- data/lib/rigor/cache/incremental_snapshot.rb +33 -1
- 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 +0 -1
- data/lib/rigor/ci_detector.rb +1 -0
- data/lib/rigor/cli/doc_links.rb +1 -1
- data/lib/rigor/cli/docs_command.rb +4 -4
- 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.rb +21 -3
- data/lib/rigor/configuration/severity_profile.rb +19 -3
- data/lib/rigor/configuration.rb +66 -3
- 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 +43 -31
- 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/lockfile_resolver.rb +17 -0
- data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
- data/lib/rigor/environment/member_consistency.rb +298 -0
- data/lib/rigor/environment/rbs_loader.rb +399 -133
- data/lib/rigor/environment.rb +103 -38
- 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 +73 -27
- 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 +387 -15
- data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
- data/lib/rigor/inference/content_join.rb +200 -27
- 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/element_read_widening.rb +22 -10
- data/lib/rigor/inference/error_info.rb +196 -0
- data/lib/rigor/inference/expression_typer.rb +1532 -496
- data/lib/rigor/inference/external_ancestor_resolution.rb +267 -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 +447 -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 +167 -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 +881 -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/multi_target_binder.rb +340 -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 +540 -120
- 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 +3 -2
- 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.rb +4615 -551
- data/lib/rigor/inference/statement_evaluator.rb +3176 -490
- 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 +168 -5
- 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/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.rb +1 -0
- data/lib/rigor/protection/discovery_seed.rb +3 -1
- data/lib/rigor/protection/kill_signature.rb +0 -1
- data/lib/rigor/protection/mutation_cache.rb +1 -2
- data/lib/rigor/rbs_extended.rb +27 -0
- data/lib/rigor/reflection/constant_ancestors.rb +97 -0
- data/lib/rigor/reflection/constant_path.rb +19 -6
- data/lib/rigor/reflection.rb +72 -105
- data/lib/rigor/scope/discovery_index.rb +135 -2
- data/lib/rigor/scope.rb +859 -49
- 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 +558 -31
- 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 +88 -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-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 +12 -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 +1 -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 +135 -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 +9 -7
- data/skills/rigor-project-init/references/01-detect.md +13 -9
- data/skills/rigor-project-init/references/02-configure.md +33 -8
- 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 +108 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../type"
|
|
6
|
+
require_relative "block_call_timing"
|
|
7
|
+
require_relative "stored_block_call"
|
|
8
|
+
|
|
9
|
+
module Rigor
|
|
10
|
+
module Inference
|
|
11
|
+
# Issue #1360 — `$?`, the status of the last child process the thread waited for. Ruby keeps it per thread, not in
|
|
12
|
+
# the method frame's special-variable slot: a subprocess that a called method runs sets its caller's `$?`, a fiber
|
|
13
|
+
# shares its thread's, and a new thread starts without one. A subprocess that certainly ran ({.sets?}) sets it to
|
|
14
|
+
# a `Process::Status`: a backtick or `%x` command, `Kernel#system` (also when the command cannot run, with exit
|
|
15
|
+
# status 127), and `Process.wait`, `waitpid`, `wait2` or `waitpid2` without a flags argument, which blocks until a
|
|
16
|
+
# child exits.
|
|
17
|
+
#
|
|
18
|
+
# Ruby 4.0.5 sets it back to `nil` in four ways. One of those four waits given the `WNOHANG` flag sets it so when
|
|
19
|
+
# no child has exited, and `Process.waitall` when there is no child; either can run inside any method the thread
|
|
20
|
+
# calls, or in a signal handler, so a file that holds one ({.clears?}) binds `$?` nowhere. A backtick, `%x` or
|
|
21
|
+
# `system` sets it to nil before it runs the child, so an exception raised while it waits (`Timeout::Error`,
|
|
22
|
+
# `Interrupt`, `Thread#raise`) leaves it nil: a rescue clause or modifier fallback reads it unbound
|
|
23
|
+
# ({ErrorInfo.rescue_entry}), and so do the code past a rescue modifier and a body a `retry` re-enters. And
|
|
24
|
+
# `IO#close` on an `IO.popen` stream whose child was already reaped sets it to nil, which the analysis does not
|
|
25
|
+
# model. Otherwise a `$?` once set stays a `Process::Status`, so the binding survives later calls, blocks and loops
|
|
26
|
+
# until a join with a path that did not set it drops it. The root block of a new thread or ractor
|
|
27
|
+
# ({FreshFrameBlocks.entry}) and a closure's body ({FreshFrameBlocks.closure_entry}) start without it, and so does
|
|
28
|
+
# a method body.
|
|
29
|
+
module LastStatus
|
|
30
|
+
LAST_STATUS = :$?
|
|
31
|
+
# The `Process` functions that wait for a child and set `$?` to its status. Their optional flags argument may
|
|
32
|
+
# hold `WNOHANG`, which sets `$?` to nil instead when no child has exited.
|
|
33
|
+
WAITS = Set[:wait, :waitpid, :wait2, :waitpid2].freeze
|
|
34
|
+
CLEARERS = (WAITS | Set[:waitall]).freeze
|
|
35
|
+
# Compared as Strings: a literal with an invalid byte (`send("\xff")`) cannot become a Symbol.
|
|
36
|
+
CLEARER_NAMES = CLEARERS.to_set(&:to_s).freeze
|
|
37
|
+
SENDS = Set[:send, :__send__, :public_send].freeze
|
|
38
|
+
private_constant :LAST_STATUS, :WAITS, :CLEARERS, :CLEARER_NAMES, :SENDS
|
|
39
|
+
|
|
40
|
+
module_function
|
|
41
|
+
|
|
42
|
+
# `after`, the scope past `node`, with `$?` bound to `Process::Status` when `node` certainly ran a subprocess
|
|
43
|
+
# ({.certainly_sets?}) in a file that clears `$?` nowhere.
|
|
44
|
+
def after(node, after, scope)
|
|
45
|
+
return after if scope.discovery.clears_last_status || !certainly_sets?(node, scope)
|
|
46
|
+
|
|
47
|
+
after.with_global(LAST_STATUS, Type::Combinator.nominal_of("Process::Status"))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# True when running `node` certainly runs a subprocess that sets `$?` ({.sets?}): `node` itself, or a call whose
|
|
51
|
+
# receiver chain does, or whose positional arguments do unless a safe-navigation call may skip them
|
|
52
|
+
# (`%x(git rev-parse HEAD).strip`, `puts(%x(date))`). A subprocess in a branch, a block or an `&&` operand may not
|
|
53
|
+
# run, so it does not count.
|
|
54
|
+
def certainly_sets?(node, scope)
|
|
55
|
+
return true if sets?(node, scope)
|
|
56
|
+
|
|
57
|
+
case node
|
|
58
|
+
when Prism::CallNode
|
|
59
|
+
return true if certainly_sets?(node.receiver, scope)
|
|
60
|
+
return false if node.safe_navigation?
|
|
61
|
+
|
|
62
|
+
node.arguments&.arguments&.any? { |argument| certainly_sets?(argument, scope) } || false
|
|
63
|
+
when Prism::ParenthesesNode
|
|
64
|
+
body = node.body
|
|
65
|
+
body.is_a?(Prism::StatementsNode) && body.body.any? { |statement| certainly_sets?(statement, scope) }
|
|
66
|
+
else false
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# True when `node` itself runs a subprocess and waits for it, which leaves `$?` a `Process::Status`: a backtick or
|
|
71
|
+
# `%x` command, `system` on implicit `self`, `self.` or `Kernel`, or `wait`, `waitpid`, `wait2` or `waitpid2`
|
|
72
|
+
# on the core `Process` module with at most one plain argument, the child's pid. A program that defines a
|
|
73
|
+
# method of that name anywhere ({BlockCallTiming.project_defines_anywhere?}) may run Ruby code in its place, so
|
|
74
|
+
# the name sets nothing there.
|
|
75
|
+
def sets?(node, scope)
|
|
76
|
+
case node
|
|
77
|
+
when Prism::XStringNode, Prism::InterpolatedXStringNode
|
|
78
|
+
!BlockCallTiming.project_defines_anywhere?(:`, scope)
|
|
79
|
+
when Prism::CallNode then subprocess_call?(node, scope)
|
|
80
|
+
else false
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# True when `node` is a call that may set `$?` to nil: `wait`, `waitpid`, `wait2` or `waitpid2` with a flags
|
|
85
|
+
# argument, a splat, keywords or a forwarded `...`, on any receiver, `waitall`, or a `send`,
|
|
86
|
+
# `__send__` or `public_send` whose literal name is one of these. `Inference::ScopeIndexer` records whether a
|
|
87
|
+
# file holds one ({Scope::DiscoveryIndex#clears_last_status}).
|
|
88
|
+
def clears?(node)
|
|
89
|
+
return false unless node.is_a?(Prism::CallNode)
|
|
90
|
+
|
|
91
|
+
name = node.name
|
|
92
|
+
return !blocking_wait_arguments?(node) if WAITS.include?(name)
|
|
93
|
+
return true if name == :waitall
|
|
94
|
+
return false unless SENDS.include?(name)
|
|
95
|
+
|
|
96
|
+
sent = node.arguments&.arguments&.first
|
|
97
|
+
(sent.is_a?(Prism::SymbolNode) || sent.is_a?(Prism::StringNode)) && CLEARER_NAMES.include?(sent.unescaped)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# `after`, the scope an `ensure` clause leaves, with `$?` as `entry`, the scope the clause started from, binds it
|
|
101
|
+
# unless the clause bound it itself by running a subprocess: the clause read `$?` unbound, since it may run after
|
|
102
|
+
# a raise that came before the body's subprocess, but the code past it runs from the `begin` that finished.
|
|
103
|
+
def restore_unless_set(after, entry)
|
|
104
|
+
bound = entry.global(LAST_STATUS)
|
|
105
|
+
bound ? after.with_global(LAST_STATUS, bound) : after
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def subprocess_call?(call_node, scope)
|
|
109
|
+
name = call_node.name
|
|
110
|
+
certain =
|
|
111
|
+
if name == :system
|
|
112
|
+
StoredBlockCall.kernel_spelled?(call_node.receiver)
|
|
113
|
+
elsif WAITS.include?(name)
|
|
114
|
+
blocking_wait_arguments?(call_node) && process_module?(call_node.receiver, scope)
|
|
115
|
+
else
|
|
116
|
+
false
|
|
117
|
+
end
|
|
118
|
+
certain && !BlockCallTiming.project_defines_anywhere?(name, scope)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# True when a wait call passes no flags: at most one positional argument, the pid, and no splat, keywords or
|
|
122
|
+
# forwarded `...` that may hold more.
|
|
123
|
+
def blocking_wait_arguments?(call_node)
|
|
124
|
+
arguments = call_node.arguments&.arguments
|
|
125
|
+
arguments.nil? || (arguments.size == 1 && plain_argument?(arguments.first))
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def plain_argument?(argument)
|
|
129
|
+
!(argument.is_a?(Prism::SplatNode) || argument.is_a?(Prism::KeywordHashNode) ||
|
|
130
|
+
argument.is_a?(Prism::ForwardingArgumentsNode))
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def process_module?(receiver, scope)
|
|
134
|
+
return false unless StoredBlockCall.root_constant_name(receiver) == :Process
|
|
135
|
+
|
|
136
|
+
type = scope.type_of(receiver)
|
|
137
|
+
type.is_a?(Type::Singleton) && type.class_name == "Process"
|
|
138
|
+
rescue StandardError
|
|
139
|
+
false
|
|
140
|
+
end
|
|
141
|
+
private_class_method :subprocess_call?, :blocking_wait_arguments?, :plain_argument?, :process_module?
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -15,10 +15,11 @@ module Rigor
|
|
|
15
15
|
# - the underlying class `X` equals or inherits from the entry's `receiver_constraint`;
|
|
16
16
|
# - the call's method name is in the entry's `method_names`.
|
|
17
17
|
#
|
|
18
|
-
# On a match the helper returns the
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
18
|
+
# On a match the helper returns the narrowed `self_type` for the block body: the receiver class's
|
|
19
|
+
# instance type (`Nominal[X]`) for `:receiver_instance` entries — Sinatra's `generate_method`
|
|
20
|
+
# contract — or the entry's named `self_type` class when the DSL `instance_eval`s the block on a
|
|
21
|
+
# different object (Grape's `params` body on `Grape::Validations::ParamsScope`, `namespace` body on
|
|
22
|
+
# the `Grape::API::Instance` class object, verb bodies on `Grape::Endpoint`).
|
|
22
23
|
#
|
|
23
24
|
# Slice 1b ships the floor only (per ADR-16 § WD13): bare-identifier method lookups inside the
|
|
24
25
|
# block resolve through the inference engine's normal `self_type`-driven path, so methods
|
|
@@ -37,40 +38,189 @@ module Rigor
|
|
|
37
38
|
registry = environment&.plugin_registry
|
|
38
39
|
return nil if registry.nil? || registry.empty?
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
singleton_name = singleton_receiver_class_name(receiver_type)
|
|
42
|
+
nominal_name = nominal_receiver_class_name(receiver_type)
|
|
43
|
+
return nil if singleton_name.nil? && nominal_name.nil?
|
|
42
44
|
|
|
43
45
|
# ADR-52 WD1 — the verb-keyed table compiled at registry build. Entries arrive in
|
|
44
46
|
# (plugin registration, declaration) order; the method-name membership is guaranteed
|
|
45
47
|
# by the table key.
|
|
46
48
|
entries = registry.contribution_index.block_entries_for(call_node.name)
|
|
47
49
|
entries.each do |entry|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
narrowed = entry_self_type_for(entry, singleton_name, nominal_name, call_node,
|
|
51
|
+
scope, environment)
|
|
52
|
+
return narrowed if narrowed
|
|
51
53
|
end
|
|
52
54
|
nil
|
|
53
55
|
end
|
|
54
56
|
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
57
|
+
# The narrowed `self_type` one entry contributes for this receiver, or nil on a miss. Nominal
|
|
58
|
+
# receivers exist only inside an already-narrowed `instance_eval` body — they can only re-enter a
|
|
59
|
+
# *named instance*-binding entry (`params`-family nesting on `Nominal[ParamsScope]`).
|
|
60
|
+
# `:receiver_instance` and `singleton(...)` entries keep their Singleton-only contract.
|
|
61
|
+
def entry_self_type_for(entry, singleton_name, nominal_name, call_node, scope, environment)
|
|
62
|
+
return nil if singleton_name.nil? && !entry.named_instance_binding?
|
|
63
|
+
|
|
64
|
+
method_name = call_node.name
|
|
65
|
+
receiver_name = singleton_name || nominal_name
|
|
66
|
+
matched = receiver_class_inherits_from?(receiver_name, entry.receiver_constraint, environment, scope)
|
|
67
|
+
# `extend M` lifts M's instance surface onto the class object — `class F; extend T::Sig;
|
|
68
|
+
# sig { ... }; end` calls `sig` on `Singleton[F]` even though F does not INHERIT from
|
|
69
|
+
# T::Sig. Singleton receivers therefore also match through the extends edge — but only when
|
|
70
|
+
# the module that actually ANSWERS `method_name` is the constrained one: a nearer `extend`
|
|
71
|
+
# whose module defines the same name owns the call and picks the block's self at runtime.
|
|
72
|
+
if !matched && singleton_name
|
|
73
|
+
matched = singleton_extends_reach?(receiver_name, entry.receiver_constraint, method_name,
|
|
74
|
+
call_node, scope, environment)
|
|
75
|
+
end
|
|
76
|
+
return nil unless matched
|
|
77
|
+
|
|
78
|
+
narrowed_self_type(entry, receiver_name, environment)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The match contract stays narrow: `Singleton[X]` receivers (class-level DSL calls) for every
|
|
82
|
+
# entry, plus `Nominal[Y]` receivers only for entries whose declared `self_type` binds an
|
|
83
|
+
# instance — the `requires do ... requires do ... end` nesting shape inside a Grape `params`
|
|
84
|
+
# body, where `self` is already the ParamsScope instance the call evaluates on.
|
|
59
85
|
def singleton_receiver_class_name(receiver_type)
|
|
60
86
|
return nil unless receiver_type.is_a?(Type::Singleton)
|
|
61
87
|
|
|
62
88
|
receiver_type.class_name
|
|
63
89
|
end
|
|
64
90
|
|
|
65
|
-
def
|
|
66
|
-
return
|
|
91
|
+
def nominal_receiver_class_name(receiver_type)
|
|
92
|
+
return nil unless receiver_type.is_a?(Type::Nominal)
|
|
93
|
+
|
|
94
|
+
receiver_type.class_name
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# The narrowed `self_type` an entry contributes: `:receiver_instance` keeps the receiver class's
|
|
98
|
+
# instance type; a String `self_type` names the class the DSL `instance_eval`s the block on —
|
|
99
|
+
# `singleton(Foo)` for class-object evaluation (Grape's `namespace` body on
|
|
100
|
+
# `Grape::API::Instance`), `Foo` for instance evaluation (Grape's verb bodies on
|
|
101
|
+
# `Grape::Endpoint`, `params` bodies on `Grape::Validations::ParamsScope`).
|
|
102
|
+
def narrowed_self_type(entry, receiver_class_name, environment)
|
|
103
|
+
self_type = entry.self_type
|
|
104
|
+
return instance_type_for(receiver_class_name, environment) if self_type == :receiver_instance
|
|
105
|
+
|
|
106
|
+
if entry.singleton_binding?
|
|
107
|
+
return environment.singleton_for_name(entry.self_type_name) || Type::Singleton.new(entry.self_type_name)
|
|
108
|
+
end
|
|
67
109
|
|
|
68
|
-
|
|
69
|
-
|
|
110
|
+
instance_type_for(entry.self_type_name, environment)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def receiver_class_inherits_from?(class_name, constraint, environment, scope = nil)
|
|
114
|
+
name = class_name.to_s
|
|
115
|
+
return true if name == constraint
|
|
116
|
+
return true if rbs_inherits?(name, constraint, environment)
|
|
117
|
+
|
|
118
|
+
# Source-side ancestry — `class API < Grape::API` lives on the scope's discovery tables, not in
|
|
119
|
+
# the environment's RBS/registry ordering (the same reason ADR-43's bridge walks them).
|
|
120
|
+
source_ancestors_reach?(name, constraint, environment, scope)
|
|
70
121
|
rescue StandardError
|
|
71
122
|
false
|
|
72
123
|
end
|
|
73
124
|
|
|
125
|
+
# BFS over the source-side superclass table. The table stores names AS WRITTEN (`"::API::Base"`,
|
|
126
|
+
# bare `"Base"`), so each hop resolves through `ancestor_name_candidates` (rooted names, header
|
|
127
|
+
# nesting) rather than a raw lookup. Deliberately NOT `external_ancestor_name_candidates`: that
|
|
128
|
+
# walk records `ancestry_sources` edges through `record_class_dependency`, which would mislabel
|
|
129
|
+
# a DSL-call lookup as an ancestry dependency.
|
|
130
|
+
def source_ancestors_reach?(name, constraint, environment, scope)
|
|
131
|
+
supers = scope&.discovered_superclasses
|
|
132
|
+
queue = [name]
|
|
133
|
+
seen = {}
|
|
134
|
+
until queue.empty?
|
|
135
|
+
current = queue.shift
|
|
136
|
+
next if current.nil? || seen[current]
|
|
137
|
+
|
|
138
|
+
seen[current] = true
|
|
139
|
+
raw = supers&.[](current)
|
|
140
|
+
next if raw.nil?
|
|
141
|
+
|
|
142
|
+
scope.ancestor_name_candidates(current, raw).each do |candidate|
|
|
143
|
+
return true if candidate == constraint || rbs_inherits?(candidate, constraint, environment)
|
|
144
|
+
|
|
145
|
+
queue << candidate if supers.key?(candidate)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
false
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# The `extend`-edge twin of `source_ancestors_reach?`: true when the module that would answer
|
|
152
|
+
# `method_name` on `class_name`'s singleton — walked through `extend` edges, nearest first, then
|
|
153
|
+
# up the discovered superclass chain — resolves to `constraint`. Source `extend` targets are
|
|
154
|
+
# stored in singleton-ancestor search order and resolve through `ancestor_name_candidates`;
|
|
155
|
+
# RBS-side `extend` edges come from `Environment#singleton_extended_modules`, which is how
|
|
156
|
+
# `class Doc < T::ImmutableStruct` picks up `T::ImmutableStruct`'s own `extend T::Sig`.
|
|
157
|
+
def singleton_extends_reach?(class_name, constraint, method_name, call_node, scope, environment)
|
|
158
|
+
return false if scope.nil?
|
|
159
|
+
|
|
160
|
+
supers = scope.discovered_superclasses
|
|
161
|
+
extends = scope.discovered_extends
|
|
162
|
+
queue = [class_name.to_s]
|
|
163
|
+
seen = {}
|
|
164
|
+
until queue.empty?
|
|
165
|
+
current = queue.shift
|
|
166
|
+
next if current.nil? || seen[current]
|
|
167
|
+
|
|
168
|
+
seen[current] = true
|
|
169
|
+
# The class's own singleton defs sit ahead of EVERY `extend` in its singleton ancestry —
|
|
170
|
+
# `class F; extend T::Sig; def self.sig(&b); end; end` calls F's method, not T::Sig's —
|
|
171
|
+
# once the def has executed; a `def` written after this call has not run yet.
|
|
172
|
+
return current == constraint if scope.singleton_def_shadows_call?(current, method_name, call_node)
|
|
173
|
+
|
|
174
|
+
owner = extended_module_call_owner(current, extends, method_name, call_node, scope, environment)
|
|
175
|
+
return owner == constraint || rbs_inherits?(owner, constraint, environment) if owner
|
|
176
|
+
|
|
177
|
+
raw = supers[current]
|
|
178
|
+
scope.ancestor_name_candidates(current, raw).each { |c| queue << c } if raw
|
|
179
|
+
end
|
|
180
|
+
false
|
|
181
|
+
rescue StandardError
|
|
182
|
+
false
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# One hop of `singleton_extends_reach?`: the module that answers `method_name` among `current`'s
|
|
186
|
+
# `extend` edges, or nil when none of them define it (the walk then continues to the
|
|
187
|
+
# superclass). An `extend` edge binds the first resolution candidate that exists at runtime —
|
|
188
|
+
# a project class or an RBS-known name — and an edge whose bound module lacks the method simply
|
|
189
|
+
# does not answer, so the search moves to the next edge, exactly like the singleton ancestry.
|
|
190
|
+
def extended_module_call_owner(current, extends, method_name, call_node, scope, environment)
|
|
191
|
+
(extends[current] || []).each do |mod_name|
|
|
192
|
+
owner = scope.ancestor_name_candidates(current, mod_name).find do |candidate|
|
|
193
|
+
scope.known_user_class?(candidate) ||
|
|
194
|
+
Rigor::Reflection.rbs_class_known?(candidate, environment: environment)
|
|
195
|
+
end
|
|
196
|
+
next if owner.nil? ||
|
|
197
|
+
!extended_module_defines?(owner, method_name, call_node, scope, environment)
|
|
198
|
+
|
|
199
|
+
return owner
|
|
200
|
+
end
|
|
201
|
+
(environment.singleton_extended_modules(current) || []).each do |mod_name|
|
|
202
|
+
return mod_name if extended_module_defines?(mod_name, method_name, call_node,
|
|
203
|
+
scope, environment)
|
|
204
|
+
end
|
|
205
|
+
nil
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# `extend M` answers through M's INSTANCE surface — a source `def` inside the module or an RBS
|
|
209
|
+
# instance declaration (which already resolves through M's own `include`s, so `T::Generic`'s
|
|
210
|
+
# `include T::Helpers` answers `abstract!` on the extend edge). The source `def` counts only
|
|
211
|
+
# once it has executed: a module reopened AFTER the call site still lacks the method at call
|
|
212
|
+
# time, so the walk moves on exactly as the runtime ancestry would.
|
|
213
|
+
def extended_module_defines?(mod_name, method_name, call_node, scope, environment)
|
|
214
|
+
return true if scope.instance_def_shadows_call?(mod_name, method_name, call_node)
|
|
215
|
+
|
|
216
|
+
!Rigor::Reflection.instance_method_definition(mod_name, method_name,
|
|
217
|
+
environment: environment).nil?
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def rbs_inherits?(class_name, constraint, environment)
|
|
221
|
+
%i[equal subclass].include?(environment.class_ordering(class_name, constraint))
|
|
222
|
+
end
|
|
223
|
+
|
|
74
224
|
def instance_type_for(class_name, environment)
|
|
75
225
|
environment.nominal_for_name(class_name) || Type::Nominal.new(class_name)
|
|
76
226
|
end
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module MatchRebinding
|
|
8
|
+
# Whether a call a statement runs rebinds the `$~` of the frame it is made in by the method it calls, on any
|
|
9
|
+
# receiver (issue #1365). Two readings, split by what the analyzer did before, so that no position newly keeps
|
|
10
|
+
# a narrowing Ruby rebinds and none newly forgets one it keeps on a guess:
|
|
11
|
+
#
|
|
12
|
+
# - a call the statement-level name table forgot on ({MATCH_CAPABLE_METHODS}, and for an implicit-self call
|
|
13
|
+
# {SelfCalls.named_match?}) still forgets unless its syntax proves it cannot match ({.forgets_by_name?}):
|
|
14
|
+
# `row[:name]`, `csv.split(",")` and `list.index(3)` keep the narrowing, `row[key]` does not. A flow type
|
|
15
|
+
# is not proof, because it can be stale while the value is a Regexp (#1380);
|
|
16
|
+
# - a call in a position that never forgot — an operand, an explicit-receiver builtin outside the table, an
|
|
17
|
+
# index write, a `send` or an `eval` — forgets only on evidence that it matches ({.rebinds?}).
|
|
18
|
+
module Calls
|
|
19
|
+
# These rebind `$~` whatever their argument: `sub` / `gsub` / `scan` with a String pattern still set it,
|
|
20
|
+
# `match` compiles one, and `!~` runs `=~`.
|
|
21
|
+
ALWAYS_MATCHING = Set[:=~, :!~, :match, :sub, :sub!, :gsub, :gsub!, :scan].freeze
|
|
22
|
+
# These rebind it only when an argument is a Regexp: `"a,b".split(",")`, `"abc"["z"]`, `list.index(3)`,
|
|
23
|
+
# `{name: 1}[:name]` and `%w[a].any?(String)` leave it alone.
|
|
24
|
+
LOOKUPS = Set[
|
|
25
|
+
:[], :slice, :slice!, :index, :rindex, :byteindex, :byterindex, :partition, :rpartition, :split,
|
|
26
|
+
:start_with?, :any?, :all?, :none?, :one?
|
|
27
|
+
].freeze
|
|
28
|
+
# `grep` / `grep_v` rebind the caller's `$~` only in their block form.
|
|
29
|
+
BLOCK_FORM_LOOKUPS = Set[:grep, :grep_v].freeze
|
|
30
|
+
# `a === b` is `a`'s method, and `~re` matches `$_`, so these read the receiver: `String === s` and `~1`
|
|
31
|
+
# run no match.
|
|
32
|
+
RECEIVER_PATTERNS = Set[:===, :~].freeze
|
|
33
|
+
# These run a String of code in the frame that calls them: `eval` (`Kernel.eval`, `binding.eval`), and the
|
|
34
|
+
# other three in their String form, since their block form is a block.
|
|
35
|
+
EVALS = Set[:eval, :instance_eval, :class_eval, :module_eval].freeze
|
|
36
|
+
# These run the method they name.
|
|
37
|
+
SENDS = Set[:send, :__send__, :public_send].freeze
|
|
38
|
+
# A literal name a `send` compares by its bytes, which an invalid one (`send("\xff")`) cannot turn into a
|
|
39
|
+
# Symbol.
|
|
40
|
+
NAMES_BY_STRING = (
|
|
41
|
+
ALWAYS_MATCHING | LOOKUPS | BLOCK_FORM_LOOKUPS | RECEIVER_PATTERNS | EVALS | SENDS | Set[:[]=]
|
|
42
|
+
).to_h { |name| [name.to_s, name] }.freeze
|
|
43
|
+
# An interpolated String of code is parsed with each interpolation standing for this identifier.
|
|
44
|
+
PLACEHOLDER = "__rigor_interpolation__"
|
|
45
|
+
# Code past these bounds is not read: parsing and scanning it would cost more than it tells, and a deeply
|
|
46
|
+
# nested literal (`[[[…]]]`) overflows the scan's recursion.
|
|
47
|
+
MAX_CODE_BYTES = 64 * 1024
|
|
48
|
+
MAX_CODE_NESTING = 256
|
|
49
|
+
OPENERS = "([{".bytes.to_set.freeze
|
|
50
|
+
CLOSERS = ")]}".bytes.to_set.freeze
|
|
51
|
+
# Operators a computed `send` name may end in that are not attribute writers.
|
|
52
|
+
OPERATOR_SUFFIXES = %w[== != <= >=].freeze
|
|
53
|
+
# `split` without a separator splits on `$;` (also spelled `$-F`), which may hold a Regexp.
|
|
54
|
+
FIELD_SEPARATORS = %i[$; $-F].freeze
|
|
55
|
+
EMPTY = [].freeze
|
|
56
|
+
private_constant :ALWAYS_MATCHING, :LOOKUPS, :BLOCK_FORM_LOOKUPS, :RECEIVER_PATTERNS, :EVALS, :SENDS,
|
|
57
|
+
:NAMES_BY_STRING, :PLACEHOLDER, :MAX_CODE_BYTES, :MAX_CODE_NESTING, :OPENERS,
|
|
58
|
+
:CLOSERS, :OPERATOR_SUFFIXES, :FIELD_SEPARATORS, :EMPTY
|
|
59
|
+
|
|
60
|
+
module_function
|
|
61
|
+
|
|
62
|
+
# True when the statement-level name table forgot on `call_node` before issue #1365: one of its names on any
|
|
63
|
+
# receiver, or, for an implicit-self call and a call in its arguments, a name {SelfCalls.named_match?} reads.
|
|
64
|
+
def base_named?(call_node, implicit:)
|
|
65
|
+
MATCH_CAPABLE_METHODS.include?(call_node.name) || (implicit && SelfCalls.named_match?(call_node))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# True when a call {.base_named?} names may still rebind `$~`: every such call does unless its syntax proves
|
|
69
|
+
# it cannot. `match?` never matches; `grep` / `grep_v` without a block do not rebind the caller's `$~`; a
|
|
70
|
+
# lookup, `start_with?`, `byteindex`, `byterindex` or pattern predicate whose arguments are all non-Regexp
|
|
71
|
+
# literals ({Operands.keep_literal?}), and `[]=` whose index is, run no match; nor does `===` on such a
|
|
72
|
+
# literal or on a constant naming a class or module. Any other argument counts as it did, whatever its flow
|
|
73
|
+
# type (#1380), and so do `=~`, `!~`, `match`, `sub`, `gsub`, `scan`, `~`, an eval and a `send`.
|
|
74
|
+
def forgets_by_name?(call_node, scope)
|
|
75
|
+
name = call_node.name
|
|
76
|
+
arguments = call_node.arguments&.arguments || EMPTY
|
|
77
|
+
return true if ALWAYS_MATCHING.include?(name) || name == :~
|
|
78
|
+
return false if name == :match?
|
|
79
|
+
return !keep_receiver?(call_node.receiver, scope) if name == :===
|
|
80
|
+
return !call_node.block.nil? && !keep_arguments?(arguments) if BLOCK_FORM_LOOKUPS.include?(name)
|
|
81
|
+
|
|
82
|
+
lookup_forgets?(name, arguments, scope)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# A lookup keeps the narrowing on literal arguments alone, and `split` on none (or a nil separator) only
|
|
86
|
+
# while this file never writes `$;`; `[]=` is read by its index; any other name forgets.
|
|
87
|
+
def lookup_forgets?(name, arguments, scope)
|
|
88
|
+
return !keep_arguments?(arguments[0...-1]) if name == :[]=
|
|
89
|
+
return true unless LOOKUPS.include?(name) && keep_arguments?(arguments)
|
|
90
|
+
|
|
91
|
+
default_separator?(name, arguments) && field_separator_written?(scope)
|
|
92
|
+
end
|
|
93
|
+
private_class_method :lookup_forgets?
|
|
94
|
+
|
|
95
|
+
# True when `call_node`, a statement's own call, may rebind the match globals of the frame it is made in once
|
|
96
|
+
# its operands have run. A call {.base_named?} names still forgets unless its syntax proves it cannot
|
|
97
|
+
# ({.forgets_by_name?}); any other forgets when it is known to match ({.rebinds?}). An implicit-self or
|
|
98
|
+
# `self.` call also forgets where it may reach the frame's slot although it does not match itself: where the
|
|
99
|
+
# frame hands its slot to code the analyzer does not trace ({Frame#self_call_fallback?}), where no body
|
|
100
|
+
# stamped a frame, or where its arguments hold something {MatchRebinding.operand_may_match?} counts. Any
|
|
101
|
+
# other call runs a method in a frame of its own (issue #1364) or a C method that does not match.
|
|
102
|
+
def statement_rebinds?(call_node, scope)
|
|
103
|
+
receiver = call_node.receiver
|
|
104
|
+
implicit = receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
105
|
+
if base_named?(call_node, implicit: implicit)
|
|
106
|
+
return true if forgets_by_name?(call_node, scope)
|
|
107
|
+
elsif rebinds?(call_node, scope)
|
|
108
|
+
return true
|
|
109
|
+
end
|
|
110
|
+
return false unless implicit
|
|
111
|
+
|
|
112
|
+
frame = scope&.match_frame
|
|
113
|
+
frame.nil? || frame.self_call_fallback?(scope) ||
|
|
114
|
+
MatchRebinding.operand_may_match?(call_node.arguments, scope)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# True when `node`, a call, is known to rebind the `$~` of the frame it is made in by the method it calls:
|
|
118
|
+
# an {ALWAYS_MATCHING} name; a {LOOKUPS} name with an argument known to be a Regexp
|
|
119
|
+
# ({Operands.known_regexp_operand?}), and `grep` / `grep_v` on the same terms in their block form; `[]=` with
|
|
120
|
+
# such an index (`s[re] = v`); `===` or unary `~` on such a receiver; an eval whose code may match
|
|
121
|
+
# ({.eval_may_match?}); or a {SENDS} call that names one of these with the arguments it is sent, or whose
|
|
122
|
+
# computed name is sent a known Regexp (not an interpolated attribute writer, `"#{name}="`). `match?` never
|
|
123
|
+
# counts, and `Dynamic[top]` is not a known Regexp. `node` may also be an index `||=` / `&&=` / `op=` write,
|
|
124
|
+
# read by its index (`s[re] ||= v`). `scope` types the operands.
|
|
125
|
+
def rebinds?(node, scope)
|
|
126
|
+
arguments = node.arguments&.arguments || EMPTY
|
|
127
|
+
return known_arguments?(arguments, scope) unless node.is_a?(Prism::CallNode)
|
|
128
|
+
|
|
129
|
+
named?(node.name, node.receiver, arguments, !node.block.nil?, scope)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def named?(name, receiver, arguments, block, scope)
|
|
133
|
+
return true if ALWAYS_MATCHING.include?(name)
|
|
134
|
+
return !receiver.nil? && Operands.known_regexp_operand?(receiver, scope) if RECEIVER_PATTERNS.include?(name)
|
|
135
|
+
return field_separator_regexp?(scope) if default_separator?(name, arguments)
|
|
136
|
+
return false if arguments.empty?
|
|
137
|
+
return eval_may_match?(name, receiver, arguments.first, scope) if EVALS.include?(name)
|
|
138
|
+
return sent?(receiver, arguments, block, scope) if SENDS.include?(name)
|
|
139
|
+
return known_arguments?(arguments[0...-1], scope) if name == :[]=
|
|
140
|
+
return block && known_arguments?(arguments, scope) if BLOCK_FORM_LOOKUPS.include?(name)
|
|
141
|
+
|
|
142
|
+
LOOKUPS.include?(name) && known_arguments?(arguments, scope)
|
|
143
|
+
end
|
|
144
|
+
private_class_method :named?
|
|
145
|
+
|
|
146
|
+
def sent?(receiver, arguments, block, scope)
|
|
147
|
+
name_node, *rest = arguments
|
|
148
|
+
if name_node.is_a?(Prism::SymbolNode) || name_node.is_a?(Prism::StringNode)
|
|
149
|
+
name = NAMES_BY_STRING[name_node.unescaped]
|
|
150
|
+
return !name.nil? && named?(name, receiver, rest, block, scope)
|
|
151
|
+
end
|
|
152
|
+
return false if attribute_writer_name?(name_node)
|
|
153
|
+
|
|
154
|
+
known_arguments?(rest, scope)
|
|
155
|
+
end
|
|
156
|
+
private_class_method :sent?
|
|
157
|
+
|
|
158
|
+
# `"#{name}="` or `:"#{name}="` names a writer, which runs in a frame of its own.
|
|
159
|
+
def attribute_writer_name?(node)
|
|
160
|
+
return false unless node.is_a?(Prism::InterpolatedStringNode) || node.is_a?(Prism::InterpolatedSymbolNode)
|
|
161
|
+
|
|
162
|
+
tail = node.parts.last
|
|
163
|
+
text = tail.is_a?(Prism::StringNode) ? tail.unescaped : ""
|
|
164
|
+
text.end_with?("=") && OPERATOR_SUFFIXES.none? { |suffix| text.end_with?(suffix) }
|
|
165
|
+
end
|
|
166
|
+
private_class_method :attribute_writer_name?
|
|
167
|
+
|
|
168
|
+
# `split` with no argument, or a nil first one (`split(nil, 2)`), splits on `$;`.
|
|
169
|
+
def default_separator?(name, arguments)
|
|
170
|
+
name == :split && (arguments.empty? || arguments.first.is_a?(Prism::NilNode))
|
|
171
|
+
end
|
|
172
|
+
private_class_method :default_separator?
|
|
173
|
+
|
|
174
|
+
# True when this file writes `$;` anywhere ({Scope#program_globals}), so a `split` on it may split on a
|
|
175
|
+
# Regexp. `$;` set in another file, by `ruby -F`, or only through an operator write is not seen.
|
|
176
|
+
def field_separator_written?(scope)
|
|
177
|
+
globals = scope&.program_globals
|
|
178
|
+
!globals.nil? && FIELD_SEPARATORS.any? { |name| globals.key?(name) }
|
|
179
|
+
end
|
|
180
|
+
private_class_method :field_separator_written?
|
|
181
|
+
|
|
182
|
+
# True when this file writes a known Regexp to `$;`.
|
|
183
|
+
def field_separator_regexp?(scope)
|
|
184
|
+
globals = scope&.program_globals
|
|
185
|
+
!globals.nil? && FIELD_SEPARATORS.any? { |name| Operands.regexp_type?(globals[name], scope) }
|
|
186
|
+
end
|
|
187
|
+
private_class_method :field_separator_regexp?
|
|
188
|
+
|
|
189
|
+
# An eval of a literal or interpolated String counts when the analyzer reads its code and it may match
|
|
190
|
+
# ({.code_reading}). Code it cannot read — a variable, or a literal past the bounds — counts only for
|
|
191
|
+
# `binding.eval` (any receiver ending in a `binding` call) and `Kernel.eval`, whose whole purpose is to run
|
|
192
|
+
# code in this frame; `instance_eval`, `class_eval` and `module_eval` of such code are the metaprogramming
|
|
193
|
+
# idiom that defines methods, and counting them would forget on correct code.
|
|
194
|
+
def eval_may_match?(name, receiver, source, scope)
|
|
195
|
+
answer = code_literal?(source) ? code_reading(source, scope) : nil
|
|
196
|
+
return answer unless answer.nil?
|
|
197
|
+
|
|
198
|
+
name == :eval && frame_eval_receiver?(receiver)
|
|
199
|
+
end
|
|
200
|
+
private_class_method :eval_may_match?
|
|
201
|
+
|
|
202
|
+
def code_literal?(node)
|
|
203
|
+
node.is_a?(Prism::StringNode) || node.is_a?(Prism::InterpolatedStringNode)
|
|
204
|
+
end
|
|
205
|
+
private_class_method :code_literal?
|
|
206
|
+
|
|
207
|
+
def frame_eval_receiver?(receiver)
|
|
208
|
+
case receiver
|
|
209
|
+
when Prism::CallNode then receiver.name == :binding
|
|
210
|
+
when Prism::ConstantReadNode then receiver.name == :Kernel
|
|
211
|
+
when Prism::ConstantPathNode then receiver.parent.nil? && receiver.name == :Kernel
|
|
212
|
+
else false
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
private_class_method :frame_eval_receiver?
|
|
216
|
+
|
|
217
|
+
# Whether the String of code an eval runs may match, or nil when the analyzer cannot read it. A literal that
|
|
218
|
+
# parses is read on the block scan's terms ({MatchRebinding.program_may_match?}), and an interpolated one
|
|
219
|
+
# the same way with each interpolation replaced by an identifier. A literal that does not parse raises
|
|
220
|
+
# before it runs, so it does not match. An interpolated one that does not parse once so replaced, code
|
|
221
|
+
# longer than {MAX_CODE_BYTES} or nested deeper than {MAX_CODE_NESTING}, and code whose scan raises is not
|
|
222
|
+
# read. The answer is kept on the frame as {Frame#memo} keeps a scan, since every pass over the call asks
|
|
223
|
+
# again.
|
|
224
|
+
def code_reading(node, scope)
|
|
225
|
+
MatchRebinding.remember(node, scope, :code) { read_code(node, scope) }
|
|
226
|
+
end
|
|
227
|
+
private_class_method :code_reading
|
|
228
|
+
|
|
229
|
+
def read_code(node, scope)
|
|
230
|
+
interpolated = node.is_a?(Prism::InterpolatedStringNode)
|
|
231
|
+
text = if interpolated
|
|
232
|
+
node.parts.map { |part| part.is_a?(Prism::StringNode) ? part.unescaped : PLACEHOLDER }.join
|
|
233
|
+
else
|
|
234
|
+
node.unescaped
|
|
235
|
+
end
|
|
236
|
+
return nil if oversized?(text)
|
|
237
|
+
|
|
238
|
+
result = Prism.parse(text)
|
|
239
|
+
return (interpolated ? nil : false) unless result.errors.empty?
|
|
240
|
+
|
|
241
|
+
MatchRebinding.program_may_match?(result.value, scope)
|
|
242
|
+
rescue SystemStackError, StandardError
|
|
243
|
+
nil
|
|
244
|
+
end
|
|
245
|
+
private_class_method :read_code
|
|
246
|
+
|
|
247
|
+
# Longer than {MAX_CODE_BYTES}, or with brackets nested deeper than {MAX_CODE_NESTING}.
|
|
248
|
+
def oversized?(text)
|
|
249
|
+
return true if text.bytesize > MAX_CODE_BYTES
|
|
250
|
+
|
|
251
|
+
depth = 0
|
|
252
|
+
text.each_byte do |byte|
|
|
253
|
+
if OPENERS.include?(byte)
|
|
254
|
+
depth += 1
|
|
255
|
+
return true if depth > MAX_CODE_NESTING
|
|
256
|
+
elsif CLOSERS.include?(byte)
|
|
257
|
+
depth -= 1
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
false
|
|
261
|
+
end
|
|
262
|
+
private_class_method :oversized?
|
|
263
|
+
|
|
264
|
+
def keep_arguments?(arguments)
|
|
265
|
+
arguments.all? { |argument| Operands.keep_literal?(argument) }
|
|
266
|
+
end
|
|
267
|
+
private_class_method :keep_arguments?
|
|
268
|
+
|
|
269
|
+
def keep_receiver?(receiver, scope)
|
|
270
|
+
!receiver.nil? && (Operands.keep_literal?(receiver) || Operands.class_constant?(receiver, scope))
|
|
271
|
+
end
|
|
272
|
+
private_class_method :keep_receiver?
|
|
273
|
+
|
|
274
|
+
def known_arguments?(arguments, scope)
|
|
275
|
+
arguments.any? { |argument| Operands.known_regexp_operand?(argument, scope) }
|
|
276
|
+
end
|
|
277
|
+
private_class_method :known_arguments?
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|