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,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../type"
|
|
4
|
+
require_relative "../../rbs_extended"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module Inference
|
|
8
|
+
module MethodDispatcher
|
|
9
|
+
# Issue #1350 — how `OverloadSelector` reads a `Dynamic[T]` argument. The internal spec keeps a `Dynamic` whose
|
|
10
|
+
# static facet is not `top` out of the imprecise arguments because its facet discriminates, but acceptance
|
|
11
|
+
# short-circuits on the wrapper and takes it against any parameter, so the receiver-affinity pre-sort's first arm
|
|
12
|
+
# won: `Money#add(Integer(v))` typed `Money` and `.even?` reported `call.undefined-method`.
|
|
13
|
+
#
|
|
14
|
+
# Selection therefore reads a narrow facet's members ({.select}), leaving out `nil`: a facet's `nil` is usually
|
|
15
|
+
# the join of an overload that returns it for some untyped input (`Integer(v)`'s `exception:` form), and chosen
|
|
16
|
+
# by it `Integer | nil` fell through to the only arm that takes `nil` and `Complex(Integer(v), 1)` turned
|
|
17
|
+
# `Complex?`. One member left stands in for the argument, pass 0 included, and the return stays unwrapped; two
|
|
18
|
+
# are selected one by one, and the distinct picks come back together for the dispatch layer's join, wrapped in
|
|
19
|
+
# `Dynamic` when their returns differ (#521).
|
|
20
|
+
#
|
|
21
|
+
# Only sealed members are read ({.sealed?}): a literal, or a plain `Integer`, `Float`, `Rational`, `Complex` or
|
|
22
|
+
# `Symbol`, core classes whose `new` and `allocate` are undefined. Such a member's runtime value is of exactly its
|
|
23
|
+
# class, where any other member's may be of a subclass and take an arm the member itself skips: `Numeric` in
|
|
24
|
+
# `2 ** v`'s `Complex | Numeric` skipped a project `(real) -> Float`, whose `real` alias names `Integer`, for a
|
|
25
|
+
# catch-all `(untyped) -> nil`, and `.floor` on the call reported `call.undefined-method`. Scanning the parameters
|
|
26
|
+
# for a subclass of the member caught a plain class name and missed aliases, `instance`, type variables,
|
|
27
|
+
# intersections, singletons and modules.
|
|
28
|
+
#
|
|
29
|
+
# Even a sealed member is read only where acceptance can rule it out ({.provable?}). Acceptance reads class
|
|
30
|
+
# relations from the analyzer's own process, so it answers `no` for `Integer` against a `(Printable)` that project
|
|
31
|
+
# RBS or source includes into `Integer` (#1352). A plain argument shares that, but the wrapper hid it from a
|
|
32
|
+
# `Dynamic` one: with `(Printable) -> String | (Integer) -> Integer`, reading the member typed
|
|
33
|
+
# `fmt(Integer(v)).upcase` as `Integer` and fired `call.undefined-method` where master read `String`. So every
|
|
34
|
+
# overload's positional parameters must be spelled as a class RBS declares, `untyped`, `top` or `nil`, through `?`
|
|
35
|
+
# and `|`, which leaves acceptance only a sealed class's fixed ancestry to read. A literal or `bool` parameter
|
|
36
|
+
# asks for a value, which a class member cannot prove it holds: `Symbol` skipped `(:json) -> String` for
|
|
37
|
+
# `(untyped) -> nil`, and a `TrueClass` member was refused by `bool` itself. Anything else, a module, a stubbed
|
|
38
|
+
# name, an alias, an interface, a type variable, `instance`, `self`, an intersection, a singleton or an untyped
|
|
39
|
+
# `(?)`, keeps the wrapper too. The list names what is provable rather than what is not, so a form it has not met
|
|
40
|
+
# falls back to master's reading.
|
|
41
|
+
#
|
|
42
|
+
# A wider facet keeps the wrapper and the receiver's arm, as before. It is usually itself a #521 join
|
|
43
|
+
# (`Dynamic[BigDecimal | Complex | Float | Integer | Rational]` from `n * untyped`), and read member by member it
|
|
44
|
+
# joined every arm again: 256 call sites across the survey corpus lost a precise type to `Dynamic`, and a
|
|
45
|
+
# recursion's fuel-exhausted `n * of(n - 1)` stopped reading `Integer`.
|
|
46
|
+
module FacetDistribution
|
|
47
|
+
# The most facet members, `nil` aside, that selection reads one by one.
|
|
48
|
+
MEMBER_LIMIT = 2
|
|
49
|
+
# Member-wise argument lists beyond this keep their wrappers.
|
|
50
|
+
CAP = 8
|
|
51
|
+
# Core classes whose `new` and `allocate` are undefined, so plain Ruby cannot make an instance of a subclass
|
|
52
|
+
# (`Integer`, `Float` and `Symbol` have no allocator at all; a `Rational` or `Complex` subclass instance takes
|
|
53
|
+
# `Marshal.load` or a rebound `Class#allocate`). `NilClass` leaves with the facet's `nil`, and `true` and
|
|
54
|
+
# `false` are read only as literals.
|
|
55
|
+
SEALED_CLASSES = %w[Integer Float Rational Complex Symbol].freeze
|
|
56
|
+
|
|
57
|
+
module_function
|
|
58
|
+
|
|
59
|
+
# Whether any argument is a `Dynamic` whose facet selection reads; the cheap guard of the hot path.
|
|
60
|
+
def faceted?(arg_types) = arg_types.any? { |arg| arg.is_a?(Type::Dynamic) && facet_members(arg) }
|
|
61
|
+
|
|
62
|
+
# Selects through the block, called as `yield(arg_types, member)`. It yields once per member-wise list, one
|
|
63
|
+
# member standing in for each faceted argument, with `member` true so the block answers only a genuine match.
|
|
64
|
+
# It yields once with the arguments as given instead when the overloads are not {.provable?}, when an argument
|
|
65
|
+
# has two members and the caller is the singular `select` (`member_wise` false, whose one answer a member order
|
|
66
|
+
# would decide), when there are more than {CAP} lists, or when any list matches nothing.
|
|
67
|
+
def select(arg_types, definition, member_wise:, environment:, &)
|
|
68
|
+
choices = arg_types.map { |arg| facet_members(arg) || [arg] }
|
|
69
|
+
return yield(arg_types, false) unless member_wise || choices.all? { |members| members.size == 1 }
|
|
70
|
+
return yield(arg_types, false) unless provable?(definition, environment)
|
|
71
|
+
|
|
72
|
+
picks = picks_by_member(choices, &)
|
|
73
|
+
picks.empty? ? yield(arg_types, false) : picks
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# The distinct overloads the member-wise lists pick; none past {CAP} or when any list picks nothing.
|
|
77
|
+
def picks_by_member(choices)
|
|
78
|
+
return [] if choices.reduce(1) { |count, members| count * members.size } > CAP
|
|
79
|
+
|
|
80
|
+
lists = choices.first.product(*choices.drop(1)).map { |combination| yield(combination, true) }
|
|
81
|
+
lists.any?(&:empty?) ? [] : lists.flatten(1).uniq(&:object_id)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Whether every overload's positional parameters, rest and trailing included, are in a provable form
|
|
85
|
+
# ({.provable_param?}), where acceptance's answer for a sealed member rests only on the member class's fixed
|
|
86
|
+
# ancestry; a `rigor:v1:param:` override proves nothing either.
|
|
87
|
+
def provable?(definition, environment)
|
|
88
|
+
loader = environment&.rbs_loader
|
|
89
|
+
return false if loader.nil?
|
|
90
|
+
return false unless RbsExtended.param_type_override_map(definition, environment: environment).empty?
|
|
91
|
+
|
|
92
|
+
definition.method_types.all? do |method_type|
|
|
93
|
+
fun = method_type.type
|
|
94
|
+
next false unless fun.respond_to?(:required_positionals)
|
|
95
|
+
|
|
96
|
+
params = fun.required_positionals + fun.optional_positionals + fun.trailing_positionals
|
|
97
|
+
params += [fun.rest_positionals] if fun.rest_positionals
|
|
98
|
+
params.all? { |param| provable_param?(param.type, loader) }
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def provable_param?(rbs_type, loader)
|
|
103
|
+
case rbs_type
|
|
104
|
+
when RBS::Types::ClassInstance then declared_class?(rbs_type.name.to_s.delete_prefix("::"), loader)
|
|
105
|
+
when RBS::Types::Optional then provable_param?(rbs_type.type, loader)
|
|
106
|
+
when RBS::Types::Union then rbs_type.types.all? { |member| provable_param?(member, loader) }
|
|
107
|
+
when RBS::Types::Bases::Any, RBS::Types::Bases::Top, RBS::Types::Bases::Nil then true
|
|
108
|
+
else false
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# A class RBS declares, not a module and not a name Rigor stubbed because no RBS declares it: a class cannot be
|
|
113
|
+
# included, and a sealed member's class has a fixed ancestry, so acceptance's answer against it holds.
|
|
114
|
+
def declared_class?(name, loader)
|
|
115
|
+
loader.class_known?(name) && !loader.rbs_module?(name) && !loader.synthesized_type_names.include?(name)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# A `Dynamic` argument's facet members other than `nil`, or nil when selection keeps the wrapper: not a
|
|
119
|
+
# `Dynamic`, the untyped carrier, a facet that is only `nil`, one wider than {MEMBER_LIMIT}, or one with a
|
|
120
|
+
# member that is not sealed, an untyped member included.
|
|
121
|
+
def facet_members(arg)
|
|
122
|
+
return nil unless arg.is_a?(Type::Dynamic) && !arg.static_facet.is_a?(Type::Top)
|
|
123
|
+
|
|
124
|
+
facet = arg.static_facet
|
|
125
|
+
members = (facet.is_a?(Type::Union) ? facet.members : [facet]).reject { |member| nil_value?(member) }
|
|
126
|
+
members if members.size.between?(1, MEMBER_LIMIT) && members.all? { |member| sealed?(member) }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# A member whose runtime value is of exactly its class: a `Constant`, which is a value of that exact class, or a
|
|
130
|
+
# plain instance of {SEALED_CLASSES}.
|
|
131
|
+
def sealed?(member)
|
|
132
|
+
case member
|
|
133
|
+
when Type::Constant then true
|
|
134
|
+
when Type::Nominal then member.type_args.empty? && SEALED_CLASSES.include?(member.class_name)
|
|
135
|
+
else false
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def nil_value?(type)
|
|
140
|
+
return type.value.nil? if type.is_a?(Type::Constant)
|
|
141
|
+
|
|
142
|
+
type.is_a?(Type::Nominal) && type.class_name == "NilClass"
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../type"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module MethodDispatcher
|
|
8
|
+
# `Hash#transform_keys(mapping)` return type, with or without a block.
|
|
9
|
+
#
|
|
10
|
+
# CRuby's `transform_keys_hash_i` looks each old key up in the mapping with `rb_hash_lookup2`, which
|
|
11
|
+
# ignores a default. A hit takes the mapping's value as the new key and never reaches the block. A miss
|
|
12
|
+
# yields the old key to the block, or keeps it when no block is given. The new key type is therefore the
|
|
13
|
+
# mapping's value type joined with the block's return type, or with the receiver's key type when there is
|
|
14
|
+
# no block. The values are the receiver's, untouched.
|
|
15
|
+
#
|
|
16
|
+
# Neither RBS line says that. rbs 4.2 declares `[K2] (hash[_Key, K2] replacements) { (K old_key) -> K2 }
|
|
17
|
+
# -> Hash[K2, V]`, and {RbsDispatch} leaves `K2` untyped because the mapping and the block both decide it.
|
|
18
|
+
# The blockless `[K2] (hash[_Key, K2]) -> Hash[K | K2, V]` leaves `K2` unbound too. rbs 3.x declares no
|
|
19
|
+
# mapping overload at all; `data/core_overlay/hash_rbs3.rbs` supplies rbs 4.2's on that line, with the same gaps.
|
|
20
|
+
# This tier answers ahead of both, so the forms it accepts get one answer on either RBS line.
|
|
21
|
+
#
|
|
22
|
+
# A mapping the analysis cannot read contributes a `Dynamic[top]` key arm instead of nothing. That covers an
|
|
23
|
+
# untyped argument, a `to_hash`-convertible object, a Hash subclass, the unlisted entries of an open shape,
|
|
24
|
+
# and an empty closed shape, which is what a mapping filled through an alias the engine does not track still
|
|
25
|
+
# reads as. A block the call carries but the block pass could not type contributes `Dynamic[top]` too. A
|
|
26
|
+
# literal with a `**splat` entry needs no arm of its own: its type joins the splatted hash's pairs and a
|
|
27
|
+
# `Dynamic[top]` arm, which also keeps a copy the code writes into (`m = { **o }; m[:b] = :w`) growing. A
|
|
28
|
+
# non-empty shape filled through an alias still reads narrower than it is: that is a gap in the binding's
|
|
29
|
+
# own type.
|
|
30
|
+
#
|
|
31
|
+
# Declines, leaving the RBS answer, when:
|
|
32
|
+
#
|
|
33
|
+
# - the method is not `transform_keys`. `transform_keys!` returns `self` and stays with RBS.
|
|
34
|
+
# - the argument count is not one, or is unknown with no block. A `*splat`, a forwarded `...`, or a lone
|
|
35
|
+
# `**splat` may pass no argument, and with no block that call answers an `Enumerator`. With a block, the
|
|
36
|
+
# call is the block form or the mapping form, and a `Dynamic[top]` mapping arm covers both.
|
|
37
|
+
# - a receiver member is not a `Hash` carrier: a `HashShape` other than the empty closed one, a `Hash`
|
|
38
|
+
# nominal, or a difference over one (`non-empty-hash[K, V]`). A subclass may override the method
|
|
39
|
+
# (`ActiveSupport::HashWithIndifferentAccess` does), and an empty closed shape answers `{}` whatever the
|
|
40
|
+
# mapping says.
|
|
41
|
+
module HashTransformKeysFolding
|
|
42
|
+
module_function
|
|
43
|
+
|
|
44
|
+
def try_dispatch(context)
|
|
45
|
+
return nil unless context.method_name == :transform_keys
|
|
46
|
+
|
|
47
|
+
block = block_arm(context)
|
|
48
|
+
renamed = mapping_arm(context, block)
|
|
49
|
+
return nil if renamed.nil?
|
|
50
|
+
|
|
51
|
+
receiver = context.receiver
|
|
52
|
+
members = receiver.is_a?(Type::Union) ? receiver.members : [receiver]
|
|
53
|
+
answers = members.map { |member| answer_for(member, renamed, block) }
|
|
54
|
+
return nil if answers.any?(&:nil?)
|
|
55
|
+
|
|
56
|
+
Type::Combinator.union(*answers)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The mapping form's block parameter: the receiver's key type, as rbs 4.2's `{ (K old_key) -> K2 }`
|
|
60
|
+
# declares it, and as `data/core_overlay/hash_rbs3.rbs` restates it on the rbs 3.x line. The RBS probe
|
|
61
|
+
# does not bind it for a union of receiver shapes. Reached through {IteratorDispatch.block_param_types};
|
|
62
|
+
# nil falls through to the RBS probe.
|
|
63
|
+
def block_param_types(context)
|
|
64
|
+
return nil unless context.args.size == 1
|
|
65
|
+
|
|
66
|
+
receiver = context.receiver
|
|
67
|
+
members = receiver.is_a?(Type::Union) ? receiver.members : [receiver]
|
|
68
|
+
keys = members.map { |member| receiver_key_value(member)&.first }
|
|
69
|
+
return nil if keys.any?(&:nil?)
|
|
70
|
+
|
|
71
|
+
[Type::Combinator.union(*keys)]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# `Hash[unmapped | renamed, V]` for one receiver member, or nil when the member is not a `Hash` carrier.
|
|
75
|
+
def answer_for(receiver, renamed, block)
|
|
76
|
+
key_value = receiver_key_value(receiver)
|
|
77
|
+
return nil if key_value.nil?
|
|
78
|
+
|
|
79
|
+
key, value = key_value
|
|
80
|
+
new_key = Type::Combinator.union(block || key, renamed)
|
|
81
|
+
Type::Combinator.nominal_of("Hash", type_args: [new_key, value])
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# The receiver's `[K, V]`, projected the way {RbsDispatch} projects a shape, or nil when the receiver
|
|
85
|
+
# is not a `Hash` carrier. An open shape may hold entries it does not list, so its key and value take
|
|
86
|
+
# a `Dynamic[top]` arm.
|
|
87
|
+
def receiver_key_value(receiver)
|
|
88
|
+
case receiver
|
|
89
|
+
when Type::HashShape then shape_key_value(receiver)
|
|
90
|
+
when Type::Nominal then nominal_key_value(receiver)
|
|
91
|
+
when Type::Difference then receiver_key_value(receiver.base)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def shape_key_value(shape)
|
|
96
|
+
return nil if shape.pairs.empty? && shape.closed?
|
|
97
|
+
|
|
98
|
+
key = Type::Combinator.union(*shape.pairs.keys.map { |k| Type::Combinator.constant_of(k) })
|
|
99
|
+
value = Type::Combinator.union(*shape.pairs.values)
|
|
100
|
+
return [key, value] if shape.closed?
|
|
101
|
+
|
|
102
|
+
[Type::Combinator.union(key, unknown), Type::Combinator.union(value, unknown)]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def nominal_key_value(nominal)
|
|
106
|
+
return nil unless nominal.class_name == "Hash"
|
|
107
|
+
return [unknown, unknown] unless nominal.type_args.size == 2
|
|
108
|
+
|
|
109
|
+
nominal.type_args
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# The new keys the mapping supplies, or nil when the call is not one this tier answers (see the module
|
|
113
|
+
# comment for the argument-count rule).
|
|
114
|
+
def mapping_arm(context, block)
|
|
115
|
+
arguments = argument_nodes(context.call_node)
|
|
116
|
+
return block.nil? ? nil : unknown if unresolved_argument_count?(arguments)
|
|
117
|
+
return nil unless context.args.size == 1
|
|
118
|
+
|
|
119
|
+
mapping_value_type(context.args.first)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# The mapping's value type. Anything whose value type cannot be read contributes `Dynamic[top]`,
|
|
123
|
+
# including a member that is not a Hash at all (`to_hash` may still convert it, and a `TypeError` for one
|
|
124
|
+
# that cannot is not this tier's to report).
|
|
125
|
+
def mapping_value_type(mapping)
|
|
126
|
+
case mapping
|
|
127
|
+
when Type::HashShape then shape_value_type(mapping)
|
|
128
|
+
when Type::Nominal then nominal_value_type(mapping)
|
|
129
|
+
when Type::Difference then mapping_value_type(mapping.base)
|
|
130
|
+
when Type::Union then Type::Combinator.union(*mapping.members.map { |member| mapping_value_type(member) })
|
|
131
|
+
else unknown
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# An empty closed shape contributes `Dynamic[top]`, not `bot`: the engine records no aliasing, so
|
|
136
|
+
# `m = {}; m.tap { |x| x[:a] = :z }` still reads `{}` after the store, and a `bot` arm would drop `:z`.
|
|
137
|
+
# A non-empty shape filled through an alias has the same gap, which this rule does not close.
|
|
138
|
+
def shape_value_type(shape)
|
|
139
|
+
return unknown if shape.pairs.empty? && shape.closed?
|
|
140
|
+
|
|
141
|
+
value = Type::Combinator.union(*shape.pairs.values)
|
|
142
|
+
shape.closed? ? value : Type::Combinator.union(value, unknown)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def nominal_value_type(nominal)
|
|
146
|
+
return unknown unless nominal.class_name == "Hash" && nominal.type_args.size == 2
|
|
147
|
+
|
|
148
|
+
nominal.type_args[1]
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# The block's return type; `Dynamic[top]` when the call carries a block the block pass left untyped (it
|
|
152
|
+
# rescues to nil); nil when there is no block, so the unmapped keys keep the receiver's key type. `&nil`
|
|
153
|
+
# passes no block, whatever type the block pass gave it.
|
|
154
|
+
def block_arm(context)
|
|
155
|
+
block_node = context.call_node.is_a?(Prism::CallNode) ? context.call_node.block : nil
|
|
156
|
+
return nil if block_node.is_a?(Prism::BlockArgumentNode) && block_node.expression.is_a?(Prism::NilNode)
|
|
157
|
+
return context.block_type if context.block_type
|
|
158
|
+
|
|
159
|
+
block_node ? unknown : nil
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# The call's argument nodes, or an empty list for a nil or non-call node (an internal caller, whose
|
|
163
|
+
# argument types are authoritative).
|
|
164
|
+
def argument_nodes(call_node)
|
|
165
|
+
return EMPTY_ARGUMENTS unless call_node.is_a?(Prism::CallNode)
|
|
166
|
+
|
|
167
|
+
call_node.arguments&.arguments || EMPTY_ARGUMENTS
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
EMPTY_ARGUMENTS = [].freeze
|
|
171
|
+
private_constant :EMPTY_ARGUMENTS
|
|
172
|
+
|
|
173
|
+
# A `*splat`, a forwarded `...`, or keyword arguments made only of `**splat` entries: each may pass no
|
|
174
|
+
# argument at all (`transform_keys(**{})` passes none), so whether there is a mapping is not static.
|
|
175
|
+
def unresolved_argument_count?(arguments)
|
|
176
|
+
arguments.any? do |argument|
|
|
177
|
+
case argument
|
|
178
|
+
when Prism::SplatNode, Prism::ForwardingArgumentsNode then true
|
|
179
|
+
when Prism::KeywordHashNode then argument.elements.all?(Prism::AssocSplatNode)
|
|
180
|
+
else false
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def unknown
|
|
186
|
+
Type::Combinator.untyped
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../../type"
|
|
4
4
|
require_relative "singleton_folding"
|
|
5
|
+
require_relative "hash_transform_keys_folding"
|
|
5
6
|
|
|
6
7
|
module Rigor
|
|
7
8
|
module Inference
|
|
@@ -22,6 +23,8 @@ module Rigor
|
|
|
22
23
|
# bound from the argument.
|
|
23
24
|
# - `a.downto(b) { |i| … }` yields the same domain `[b, a]`, just iterated in reverse. Lower bound
|
|
24
25
|
# from the argument, upper bound from the receiver.
|
|
26
|
+
# - `h.transform_keys(mapping) { |k| … }` yields the receiver's keys, a rule the RBS probe does not state for
|
|
27
|
+
# a union of receiver shapes ({HashTransformKeysFolding.block_param_types}).
|
|
25
28
|
module IteratorDispatch
|
|
26
29
|
module_function
|
|
27
30
|
|
|
@@ -41,6 +44,7 @@ module Rigor
|
|
|
41
44
|
when :group_by, :partition then single_element_block_params(receiver)
|
|
42
45
|
when :each_slice, :each_cons then slice_block_params(receiver)
|
|
43
46
|
when :new then class_new_block_params(receiver, args)
|
|
47
|
+
when :transform_keys then HashTransformKeysFolding.block_param_types(context)
|
|
44
48
|
end
|
|
45
49
|
end
|
|
46
50
|
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../type"
|
|
4
|
+
require_relative "regexp_folding"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module Inference
|
|
8
|
+
module MethodDispatcher
|
|
9
|
+
# Issue #1381 — slice reads of the proven `$~`. On a proven-match edge the flow engine narrows `$~` to
|
|
10
|
+
# `MatchData` and each unconditional group's `$N` to `String` (see `Narrowing#regex_match_predicate_scopes`).
|
|
11
|
+
# The Integer overload of RBS `MatchData#[]` already reads `String`, but the Range and `(start, length)`
|
|
12
|
+
# overloads and `values_at` read `Array[String?]`, and destructuring that binds every slot `String?`.
|
|
13
|
+
# This tier answers those reads with a Tuple, one slot per group, using the rule
|
|
14
|
+
# {RegexpFolding.fold_last_match_group} applies to `Regexp.last_match(N)`: `String` when `$N` is bound,
|
|
15
|
+
# `String?` when it is not (an optional or alternation-reachable group is nil on a successful match).
|
|
16
|
+
# Index 0 is the whole match, always a `String`.
|
|
17
|
+
#
|
|
18
|
+
# Supported, all with compile-time constant Integer arguments:
|
|
19
|
+
#
|
|
20
|
+
# * `$~[a..b]` / `$~[a...b]`
|
|
21
|
+
# * `$~[start, length]`
|
|
22
|
+
# * `$~.values_at(i, j, ...)`
|
|
23
|
+
#
|
|
24
|
+
# The receiver must be the `$~` read itself or a no-argument `Regexp.last_match`: a local MatchData carries
|
|
25
|
+
# no group facts, and the scope's `$N` describe only the frame's current match.
|
|
26
|
+
#
|
|
27
|
+
# The scope does not record the regex's group count, and a slice past the last group is shorter than the
|
|
28
|
+
# range asked for (`$~[1..3]` on a one-group match is `["x"]`) while `values_at` pads with nil. The highest
|
|
29
|
+
# bound `$K` is a lower bound on the count — `Scope#join_bindings` keeps a key only when both sides bind
|
|
30
|
+
# it — so every index must be at most K. Declines (RBS answers) when:
|
|
31
|
+
#
|
|
32
|
+
# - `$~` is not proven non-nil, or no scope is threaded through,
|
|
33
|
+
# - no `$1`..`$9` narrows to `String` (every group optional, no group at all, `when /a/, /b/`),
|
|
34
|
+
# - any index is above K, negative, or not a constant Integer, or a Range bound is nil (endless /
|
|
35
|
+
# beginless),
|
|
36
|
+
# - the slice is empty.
|
|
37
|
+
#
|
|
38
|
+
# K is read from `$1`..`$9` only: `Scope#forget_match_globals` does not clear `$10` and above (#1384), so a
|
|
39
|
+
# binding there may belong to an earlier match.
|
|
40
|
+
module MatchDataFolding
|
|
41
|
+
MAX_GROUP_GLOBAL = 9
|
|
42
|
+
private_constant :MAX_GROUP_GLOBAL
|
|
43
|
+
|
|
44
|
+
module_function
|
|
45
|
+
|
|
46
|
+
# @return folded Tuple, or nil to defer.
|
|
47
|
+
def try_dispatch(context)
|
|
48
|
+
receiver = context.receiver
|
|
49
|
+
return nil unless receiver.is_a?(Type::Nominal) && receiver.class_name == "MatchData"
|
|
50
|
+
|
|
51
|
+
scope = context.scope
|
|
52
|
+
return nil if scope.nil?
|
|
53
|
+
return nil unless last_match_read?(context.call_node, scope)
|
|
54
|
+
return nil unless RegexpFolding.proven_match?(scope.global(:$~))
|
|
55
|
+
|
|
56
|
+
highest = highest_bound_group(scope)
|
|
57
|
+
return nil if highest.nil?
|
|
58
|
+
|
|
59
|
+
indices = slot_indices(context.method_name, context.args, highest)
|
|
60
|
+
return nil if indices.nil?
|
|
61
|
+
|
|
62
|
+
Type::Combinator.tuple_of(*indices.map { |index| slot_type(index, scope) })
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# The receiver expression reads the frame's current match: `$~`, or `last_match` with no arguments and no block
|
|
66
|
+
# on an expression that types as the core `Regexp` class. A project class that happens to be named `Regexp`
|
|
67
|
+
# (`Foo::Regexp` read as `Regexp` inside `module Foo`) types as its own singleton and does not count.
|
|
68
|
+
def last_match_read?(call_node, scope)
|
|
69
|
+
return false unless call_node.is_a?(Prism::CallNode)
|
|
70
|
+
|
|
71
|
+
receiver = call_node.receiver
|
|
72
|
+
case receiver
|
|
73
|
+
when Prism::GlobalVariableReadNode
|
|
74
|
+
receiver.name == :$~
|
|
75
|
+
when Prism::CallNode
|
|
76
|
+
receiver.name == :last_match && receiver.arguments.nil? && receiver.block.nil? &&
|
|
77
|
+
regexp_class?(receiver.receiver, scope)
|
|
78
|
+
else
|
|
79
|
+
false
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def regexp_class?(node, scope)
|
|
84
|
+
return false if node.nil?
|
|
85
|
+
|
|
86
|
+
type = scope.type_of(node)
|
|
87
|
+
type.is_a?(Type::Singleton) && type.class_name == "Regexp"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The highest N in 1..9 whose `$N` is narrowed to `String`, or nil when none is.
|
|
91
|
+
def highest_bound_group(scope)
|
|
92
|
+
MAX_GROUP_GLOBAL.downto(1).find { |index| string_global?(scope.global(:"$#{index}")) }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def string_global?(type)
|
|
96
|
+
type.is_a?(Type::Nominal) && type.class_name == "String"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# The group indices the call reads, in result order, or nil when the call shape is not folded or any index
|
|
100
|
+
# falls outside `0..highest`.
|
|
101
|
+
def slot_indices(method_name, args, highest)
|
|
102
|
+
indices =
|
|
103
|
+
case method_name
|
|
104
|
+
when :[] then index_slice(args)
|
|
105
|
+
when :values_at then values_at_indices(args)
|
|
106
|
+
end
|
|
107
|
+
return nil if indices.nil? || indices.none?
|
|
108
|
+
return nil unless indices.all? { |index| index.between?(0, highest) }
|
|
109
|
+
|
|
110
|
+
indices.to_a
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# `[range]` or `[start, length]`. A single Integer index is left to RBS, which already reads `String`.
|
|
114
|
+
def index_slice(args)
|
|
115
|
+
case args.size
|
|
116
|
+
when 1 then range_indices(args.first)
|
|
117
|
+
when 2 then start_length_indices(*args)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def range_indices(arg)
|
|
122
|
+
range = constant_value(arg)
|
|
123
|
+
return nil unless range.is_a?(Range)
|
|
124
|
+
|
|
125
|
+
first = range.begin
|
|
126
|
+
last = range.end
|
|
127
|
+
return nil unless first.is_a?(Integer) && last.is_a?(Integer)
|
|
128
|
+
return nil if first.negative? || last.negative?
|
|
129
|
+
|
|
130
|
+
range
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def start_length_indices(start_arg, length_arg)
|
|
134
|
+
start = constant_value(start_arg)
|
|
135
|
+
length = constant_value(length_arg)
|
|
136
|
+
return nil unless start.is_a?(Integer) && length.is_a?(Integer)
|
|
137
|
+
return nil if start.negative? || length.negative?
|
|
138
|
+
|
|
139
|
+
start...(start + length)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def values_at_indices(args)
|
|
143
|
+
indices = args.map { |arg| constant_value(arg) }
|
|
144
|
+
indices.all?(Integer) ? indices : nil
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def constant_value(type)
|
|
148
|
+
type.is_a?(Type::Constant) ? type.value : nil
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def slot_type(index, scope)
|
|
152
|
+
return Type::Combinator.nominal_of("String") if index.zero?
|
|
153
|
+
|
|
154
|
+
RegexpFolding.fold_last_match_group(Type::Combinator.constant_of(index), scope)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|