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,708 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbs"
|
|
4
|
+
|
|
5
|
+
require_relative "../../type"
|
|
6
|
+
require_relative "../../inference/acceptance"
|
|
7
|
+
require_relative "../../inference/rbs_type_translator"
|
|
8
|
+
require_relative "../../rbs_extended"
|
|
9
|
+
|
|
10
|
+
module Rigor
|
|
11
|
+
class Environment
|
|
12
|
+
module MemberConsistency
|
|
13
|
+
# Compares two declarations of one member (ADR-112 WD5), and a member's refinements with its own
|
|
14
|
+
# declared types. See {MemberConsistency} for the rule; this class holds the type work.
|
|
15
|
+
#
|
|
16
|
+
# A contradiction is reported only where it is PROVEN: no value, and no call, satisfies both sides.
|
|
17
|
+
# Everything short of a proof is undecided. Four places carry that burden:
|
|
18
|
+
#
|
|
19
|
+
# - {#disjoint?} proves two types share no value, from the RBS hierarchy of Ruby core and stdlib
|
|
20
|
+
# classes ({RbsProof}) only: two such classes neither of which is an RBS ancestor of the other, or a
|
|
21
|
+
# literal whose class is not below such a class. Never Ruby constants the analyzer process happens to
|
|
22
|
+
# have loaded (rbs declares `Tempfile < File`, the `tempfile` library `Tempfile < Delegator`), and
|
|
23
|
+
# never a class the project declares anywhere. A module, an interface, a carrier without a class of
|
|
24
|
+
# its own, or a class-name refinement payload proves nothing; and without an {RbsProof} (the
|
|
25
|
+
# environment build) nothing is ever proven.
|
|
26
|
+
# - {#provable_names?} admits only class names written absolutely (`::String`). A relative name may be
|
|
27
|
+
# a class the project defines only in Ruby (`String` inside `module App` may be `App::String`), and
|
|
28
|
+
# seeing that would take a scan of the run's files, which a path-set-independent answer cannot use.
|
|
29
|
+
# - {#faithful?} refuses a type position whose Rigor translation could mean something else: an alias,
|
|
30
|
+
# an interface, `self`, a type variable, a proc, and a relative class name the project's RBS inputs
|
|
31
|
+
# declare (a relative `Data` inside `module App` may be `App::Data`).
|
|
32
|
+
# - A position a call may leave empty — an optional or rest parameter, an optional keyword — never
|
|
33
|
+
# contradicts, since the call that omits it satisfies both sides; nor does any block position, since
|
|
34
|
+
# a body that never yields, or an untyped block, satisfies both.
|
|
35
|
+
# - Overloads are paired by correspondence, never by position, and a member whose overloads do not
|
|
36
|
+
# pair one to one is undecided.
|
|
37
|
+
#
|
|
38
|
+
# Which side binds ({#subset?}) is decided by names alone — the same class, a literal of exactly that
|
|
39
|
+
# class, a refinement of that class — with no hierarchy at all. The environment build takes that
|
|
40
|
+
# decision before the environment exists, and the reported rows re-derive it on a cache hit, so it
|
|
41
|
+
# must read nothing but the two declarations. A subclass relation (`Integer` against `Numeric`) is
|
|
42
|
+
# therefore undecided, as it was before the rule existed.
|
|
43
|
+
class Comparator # rubocop:disable Metrics/ClassLength
|
|
44
|
+
# A type position: the declared RBS type, and the `rigor:v1:` refinement that overrides it (or nil).
|
|
45
|
+
Slot = Data.define(:rbs_type, :override)
|
|
46
|
+
private_constant :Slot
|
|
47
|
+
|
|
48
|
+
# The RBS leaf types whose Rigor translation loses nothing a precision comparison could depend on.
|
|
49
|
+
FAITHFUL_LEAVES = [
|
|
50
|
+
::RBS::Types::Bases::Any, ::RBS::Types::Bases::Bool, ::RBS::Types::Bases::Nil,
|
|
51
|
+
::RBS::Types::Bases::Top, ::RBS::Types::Bases::Bottom, ::RBS::Types::Bases::Void,
|
|
52
|
+
::RBS::Types::Literal
|
|
53
|
+
].freeze
|
|
54
|
+
private_constant :FAITHFUL_LEAVES
|
|
55
|
+
|
|
56
|
+
# RBS's three spellings of the top type ("they are all equivalent for the type system" — rbs
|
|
57
|
+
# `docs/syntax.md`). None says anything about the value, so each is the least precise answer a
|
|
58
|
+
# position can give, and two of them are equal.
|
|
59
|
+
TOP_SPELLINGS = [::RBS::Types::Bases::Any, ::RBS::Types::Bases::Top, ::RBS::Types::Bases::Void].freeze
|
|
60
|
+
private_constant :TOP_SPELLINGS
|
|
61
|
+
|
|
62
|
+
# Classes whose instances are exactly one value, read as that value so `bool` and `TrueClass`
|
|
63
|
+
# compare as a union and its member rather than as two unrelated names.
|
|
64
|
+
SINGLE_VALUE_CLASSES = { "TrueClass" => true, "FalseClass" => false, "NilClass" => nil }.freeze
|
|
65
|
+
private_constant :SINGLE_VALUE_CLASSES
|
|
66
|
+
|
|
67
|
+
EQUAL = [:equal, nil].freeze
|
|
68
|
+
SIGNATURE_MORE_PRECISE = [:signature, nil].freeze
|
|
69
|
+
CONSISTENT = %i[equal signature inline].freeze
|
|
70
|
+
NO_STRINGS = [].freeze
|
|
71
|
+
private_constant :EQUAL, :SIGNATURE_MORE_PRECISE, :CONSISTENT, :NO_STRINGS
|
|
72
|
+
|
|
73
|
+
RETURN_DIRECTIVE_PREFIX = "rigor:v1:return:"
|
|
74
|
+
PARAM_DIRECTIVE_PREFIX = "rigor:v1:param:"
|
|
75
|
+
INFERRED_SIGNATURE_DIRECTIVE = RbsExtended::INFERRED_SIGNATURE_DIRECTIVE
|
|
76
|
+
private_constant :RETURN_DIRECTIVE_PREFIX, :PARAM_DIRECTIVE_PREFIX, :INFERRED_SIGNATURE_DIRECTIVE
|
|
77
|
+
|
|
78
|
+
# More overloads than this per member are left undecided rather than paired: the pairing compares
|
|
79
|
+
# every overload with every other.
|
|
80
|
+
PAIRING_LIMIT = 16
|
|
81
|
+
private_constant :PAIRING_LIMIT
|
|
82
|
+
|
|
83
|
+
# The carriers {#refinement_subset?} hands to `Inference::Acceptance`: a literal or a refinement
|
|
84
|
+
# against a refinement, which it answers from the value and the refinement alone.
|
|
85
|
+
REFINEMENT_OR_VALUE = [Type::Constant, Type::Refined, Type::Difference, Type::IntegerRange,
|
|
86
|
+
Type::FloatRange].freeze
|
|
87
|
+
# The class a range or record carrier's values all belong to.
|
|
88
|
+
CARRIER_CLASSES = { Type::IntegerRange => "Integer", Type::FloatRange => "Float",
|
|
89
|
+
Type::HashShape => "Hash" }.freeze
|
|
90
|
+
private_constant :REFINEMENT_OR_VALUE, :CARRIER_CLASSES
|
|
91
|
+
|
|
92
|
+
# @param shadowable — the first `::` segment of every class / module name the project's RBS inputs
|
|
93
|
+
# declare. A relative class name starting with one of them may resolve inside an enclosing
|
|
94
|
+
# namespace, so it is not compared.
|
|
95
|
+
# @param proof — an {RbsProof}, or nil when nothing may be proven disjoint (the environment build,
|
|
96
|
+
# whose decisions do not depend on it).
|
|
97
|
+
def initialize(shadowable, proof: nil)
|
|
98
|
+
@shadowable = shadowable
|
|
99
|
+
@proof = proof
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @return `[outcome, detail]` — `outcome` is `:equal`, `:signature` (`sig/` is more precise),
|
|
103
|
+
# `:inline` (the inline side is more precise), `:contradiction` or `:undecided`; `detail` names the
|
|
104
|
+
# deciding position for the two reported outcomes and is nil otherwise.
|
|
105
|
+
def compare(signature_member, inline_member, method_name)
|
|
106
|
+
return EQUAL if same_declaration?(signature_member, inline_member)
|
|
107
|
+
return SIGNATURE_MORE_PRECISE if inferred_signature?(inline_member)
|
|
108
|
+
|
|
109
|
+
if signature_member.is_a?(::RBS::AST::Members::Alias) || inline_member.is_a?(::RBS::AST::Members::Alias)
|
|
110
|
+
return compare_aliases(signature_member, inline_member)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
compare_overloads(
|
|
114
|
+
overloads_for(signature_member, method_name), overloads_for(inline_member, method_name),
|
|
115
|
+
refinements_of(signature_member), refinements_of(inline_member)
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# The details of every member-level `rigor:v1:return:` / `rigor:v1:param:` refinement on `member`
|
|
120
|
+
# that is provably disjoint from its own declared type in every overload it applies to. Only
|
|
121
|
+
# member-level annotations count: `RBS::Definition::Method#annotations`, which call sites read, does
|
|
122
|
+
# not carry an overload's own annotations, so the engine never honours those.
|
|
123
|
+
def refinement_conflicts(member)
|
|
124
|
+
return NO_STRINGS unless member_annotation_strings(member).any? { |string| refinement_directive?(string) }
|
|
125
|
+
|
|
126
|
+
return_override, param_overrides = refinements_of(member)
|
|
127
|
+
method_types = member_method_types(member)
|
|
128
|
+
return NO_STRINGS if method_types.empty?
|
|
129
|
+
|
|
130
|
+
conflicts = []
|
|
131
|
+
conflicts << return_conflict(method_types, return_override) if return_override
|
|
132
|
+
param_overrides.each { |name, override| conflicts << param_conflict(method_types, name, override) }
|
|
133
|
+
conflicts.compact
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Every annotation string on the member and its overloads, in declaration order.
|
|
137
|
+
def member_annotation_strings(member)
|
|
138
|
+
strings = nil
|
|
139
|
+
if member.respond_to?(:annotations)
|
|
140
|
+
Array(member.annotations).each { |annotation| (strings ||= []) << annotation.string }
|
|
141
|
+
end
|
|
142
|
+
if member.is_a?(::RBS::AST::Members::MethodDefinition)
|
|
143
|
+
member.overloads.each do |overload|
|
|
144
|
+
Array(overload.annotations).each { |annotation| (strings ||= []) << annotation.string }
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
strings || NO_STRINGS
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def refinement_directive?(string)
|
|
151
|
+
string.start_with?(RETURN_DIRECTIVE_PREFIX, PARAM_DIRECTIVE_PREFIX)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
private
|
|
155
|
+
|
|
156
|
+
# The common case after `rigor sig-gen` has written an annotated member into `sig/`: the two
|
|
157
|
+
# declarations are the same text, annotations included, and nothing needs translating.
|
|
158
|
+
def same_declaration?(signature_member, inline_member)
|
|
159
|
+
return false unless signature_member.instance_of?(inline_member.class)
|
|
160
|
+
return false unless signature_member.annotations == inline_member.annotations
|
|
161
|
+
|
|
162
|
+
case signature_member
|
|
163
|
+
when ::RBS::AST::Members::MethodDefinition then signature_member.overloads == inline_member.overloads
|
|
164
|
+
when ::RBS::AST::Members::AttrReader, ::RBS::AST::Members::AttrWriter, ::RBS::AST::Members::AttrAccessor
|
|
165
|
+
signature_member.type == inline_member.type
|
|
166
|
+
else false
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# rbs-inline's skeleton for a `def` whose annotations asserted nothing about it (ADR-93 WD6,
|
|
171
|
+
# `%a{rigor:v1:inferred-signature}`): no author wrote this inline side, so it has nothing to be
|
|
172
|
+
# consistent or inconsistent with, and `sig/` binds without a word.
|
|
173
|
+
def inferred_signature?(member)
|
|
174
|
+
member_annotation_strings(member).include?(INFERRED_SIGNATURE_DIRECTIVE)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def compare_aliases(signature_member, inline_member)
|
|
178
|
+
both = signature_member.is_a?(::RBS::AST::Members::Alias) && inline_member.is_a?(::RBS::AST::Members::Alias)
|
|
179
|
+
return EQUAL if both && signature_member.old_name == inline_member.old_name
|
|
180
|
+
|
|
181
|
+
[:undecided, "an alias cannot be compared with another declaration"]
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# One overload each: compared directly, and the only shape where a contradiction can be reported. A
|
|
185
|
+
# contradiction between two overload SETS would need every pairing refuted, and "these two overloads
|
|
186
|
+
# disagree" says nothing when a third one covers the call; so several overloads either pair one to
|
|
187
|
+
# one — each overload consistent with exactly one on the other side — or stay undecided.
|
|
188
|
+
def compare_overloads(sig_types, inline_types, sig_refinements, inline_refinements)
|
|
189
|
+
return [:undecided, "the two declare a different number of overloads"] if sig_types.size != inline_types.size
|
|
190
|
+
if sig_types.size == 1
|
|
191
|
+
return combine(compare_method_types(sig_types[0], inline_types[0], sig_refinements, inline_refinements))
|
|
192
|
+
end
|
|
193
|
+
return [:undecided, "too many overloads to pair"] if sig_types.size > PAIRING_LIMIT
|
|
194
|
+
|
|
195
|
+
matrix = sig_types.map do |sig_type|
|
|
196
|
+
inline_types.map do |inline_type|
|
|
197
|
+
combine(compare_method_types(sig_type, inline_type, sig_refinements, inline_refinements))
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
paired = unique_pairing(matrix, [:equal]) || unique_pairing(matrix, CONSISTENT)
|
|
201
|
+
return [:undecided, "the overloads do not pair one to one"] if paired.nil?
|
|
202
|
+
|
|
203
|
+
combine(paired)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# The verdicts of a one-to-one pairing in which every row has exactly one column whose outcome is in
|
|
207
|
+
# `accepted`, all columns distinct; nil when there is no such pairing.
|
|
208
|
+
def unique_pairing(matrix, accepted)
|
|
209
|
+
columns = matrix.map do |row|
|
|
210
|
+
candidates = row.each_index.select { |j| accepted.include?(row[j].first) }
|
|
211
|
+
return nil unless candidates.size == 1
|
|
212
|
+
|
|
213
|
+
candidates.first
|
|
214
|
+
end
|
|
215
|
+
return nil unless columns.uniq.size == columns.size
|
|
216
|
+
|
|
217
|
+
columns.each_with_index.map { |j, i| matrix[i][j] }
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# The method types one member declares for `method_name`: an attribute reads as `() -> T` (reader)
|
|
221
|
+
# or `(T) -> T` (writer), which is the method RBS builds from it.
|
|
222
|
+
def overloads_for(member, method_name)
|
|
223
|
+
case member
|
|
224
|
+
when ::RBS::AST::Members::MethodDefinition then member.overloads.map(&:method_type)
|
|
225
|
+
when ::RBS::AST::Members::AttrReader, ::RBS::AST::Members::AttrWriter, ::RBS::AST::Members::AttrAccessor
|
|
226
|
+
[attribute_method_type(member.type, writer: method_name.to_s.end_with?("="))]
|
|
227
|
+
else []
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def member_method_types(member)
|
|
232
|
+
case member
|
|
233
|
+
when ::RBS::AST::Members::MethodDefinition then member.overloads.map(&:method_type)
|
|
234
|
+
when ::RBS::AST::Members::AttrReader, ::RBS::AST::Members::AttrAccessor
|
|
235
|
+
[attribute_method_type(member.type, writer: false)]
|
|
236
|
+
when ::RBS::AST::Members::AttrWriter then [attribute_method_type(member.type, writer: true)]
|
|
237
|
+
else []
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def attribute_method_type(type, writer:)
|
|
242
|
+
positionals = writer ? [::RBS::Types::Function::Param.new(type: type, name: nil)] : []
|
|
243
|
+
function = ::RBS::Types::Function.new(
|
|
244
|
+
required_positionals: positionals, optional_positionals: [], rest_positionals: nil,
|
|
245
|
+
trailing_positionals: [], required_keywords: {}, optional_keywords: {}, rest_keywords: nil,
|
|
246
|
+
return_type: type
|
|
247
|
+
)
|
|
248
|
+
::RBS::MethodType.new(type_params: [], type: function, block: nil, location: nil)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def compare_method_types(sig_type, inline_type, sig_refinements, inline_refinements)
|
|
252
|
+
return [EQUAL] if sig_refinements == inline_refinements && sig_type == inline_type
|
|
253
|
+
unless sig_type.type_params.empty? && inline_type.type_params.empty?
|
|
254
|
+
return [[:undecided, "a generic method type"]]
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
compare_functions(sig_type.type, inline_type.type, sig_refinements, inline_refinements, "",
|
|
258
|
+
block: false, provable: true) +
|
|
259
|
+
compare_blocks(sig_type.block, inline_type.block)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def compare_blocks(sig_block, inline_block)
|
|
263
|
+
return [] if sig_block.nil? && inline_block.nil?
|
|
264
|
+
return [[:undecided, "only one side declares a block"]] if sig_block.nil? || inline_block.nil?
|
|
265
|
+
return [[:undecided, "the block is required on one side only"]] if sig_block.required != inline_block.required
|
|
266
|
+
|
|
267
|
+
# A block position never contradicts: a call without an optional block, a body that never yields,
|
|
268
|
+
# or an untyped block satisfies both sides.
|
|
269
|
+
compare_functions(sig_block.type, inline_block.type, [nil, {}], [nil, {}], "block ",
|
|
270
|
+
block: true, provable: false)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def compare_functions(sig_fn, inline_fn, sig_refinements, inline_refinements, prefix, block:, provable:)
|
|
274
|
+
results = compare_parameters(sig_fn, inline_fn, [sig_refinements[1], inline_refinements[1]], prefix,
|
|
275
|
+
block, provable)
|
|
276
|
+
results << compare_slot(
|
|
277
|
+
"#{prefix}return type",
|
|
278
|
+
Slot.new(rbs_type: sig_fn.return_type, override: sig_refinements[0]),
|
|
279
|
+
Slot.new(rbs_type: inline_fn.return_type, override: inline_refinements[0]),
|
|
280
|
+
provable: provable
|
|
281
|
+
)
|
|
282
|
+
results
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# `(?) -> T` declares nothing about its parameters, so the other side's parameter list is the more
|
|
286
|
+
# precise one — or they are equal when both are untyped.
|
|
287
|
+
def compare_parameters(sig_fn, inline_fn, param_refinements, prefix, block, provable)
|
|
288
|
+
sig_params, inline_params = param_refinements
|
|
289
|
+
sig_untyped = sig_fn.is_a?(::RBS::Types::UntypedFunction)
|
|
290
|
+
inline_untyped = inline_fn.is_a?(::RBS::Types::UntypedFunction)
|
|
291
|
+
return [] if sig_untyped && inline_untyped
|
|
292
|
+
return [[:inline, nil]] if sig_untyped
|
|
293
|
+
return [[:signature, nil]] if inline_untyped
|
|
294
|
+
|
|
295
|
+
unless function_shape(sig_fn) == function_shape(inline_fn)
|
|
296
|
+
return [(!block && shape_contradiction(sig_fn, inline_fn, prefix)) || shape_undecided(block)]
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
parameter_pairs(sig_fn, inline_fn).map do |label, sig_param, inline_param, required|
|
|
300
|
+
compare_slot(
|
|
301
|
+
"#{prefix}#{label}",
|
|
302
|
+
Slot.new(rbs_type: sig_param.type, override: sig_param.name && sig_params[sig_param.name]),
|
|
303
|
+
Slot.new(rbs_type: inline_param.type, override: inline_param.name && inline_params[inline_param.name]),
|
|
304
|
+
provable: provable && required
|
|
305
|
+
)
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def shape_undecided(block)
|
|
310
|
+
[:undecided,
|
|
311
|
+
block ? "the block's parameter lists have different shapes" : "the parameter lists have different shapes"]
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def function_shape(function)
|
|
315
|
+
[function.required_positionals.size, function.optional_positionals.size, !function.rest_positionals.nil?,
|
|
316
|
+
function.trailing_positionals.size, function.required_keywords.keys.sort,
|
|
317
|
+
function.optional_keywords.keys.sort, !function.rest_keywords.nil?]
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Aligned `[label, sig_param, inline_param, required]` for two functions of the same shape. `required`
|
|
321
|
+
# is false for a position a call may leave empty.
|
|
322
|
+
def parameter_pairs(sig_fn, inline_fn)
|
|
323
|
+
positional_pairs(sig_fn, inline_fn) + keyword_pairs(sig_fn, inline_fn)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def positional_pairs(sig_fn, inline_fn)
|
|
327
|
+
inline_leading = inline_fn.required_positionals + inline_fn.optional_positionals
|
|
328
|
+
required = sig_fn.required_positionals.size
|
|
329
|
+
pairs = (sig_fn.required_positionals + sig_fn.optional_positionals).each_with_index.map do |param, i|
|
|
330
|
+
["parameter #{i + 1}", param, inline_leading[i], i < required]
|
|
331
|
+
end
|
|
332
|
+
if sig_fn.rest_positionals
|
|
333
|
+
pairs << ["rest parameter", sig_fn.rest_positionals, inline_fn.rest_positionals, false]
|
|
334
|
+
end
|
|
335
|
+
sig_fn.trailing_positionals.each_with_index do |param, i|
|
|
336
|
+
pairs << ["trailing parameter #{i + 1}", param, inline_fn.trailing_positionals[i], true]
|
|
337
|
+
end
|
|
338
|
+
pairs
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def keyword_pairs(sig_fn, inline_fn)
|
|
342
|
+
inline_keywords = inline_fn.required_keywords.merge(inline_fn.optional_keywords)
|
|
343
|
+
pairs = sig_fn.required_keywords.merge(sig_fn.optional_keywords).map do |name, param|
|
|
344
|
+
["keyword `#{name}:`", param, inline_keywords[name], sig_fn.required_keywords.key?(name)]
|
|
345
|
+
end
|
|
346
|
+
if sig_fn.rest_keywords
|
|
347
|
+
pairs << ["keyword rest parameter", sig_fn.rest_keywords, inline_fn.rest_keywords, false]
|
|
348
|
+
end
|
|
349
|
+
pairs
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# A shape difference is a contradiction only where no call can satisfy both. Ruby passes keywords to
|
|
353
|
+
# a method without keyword parameters as a trailing positional Hash, so the positional counts only
|
|
354
|
+
# mean something when neither side takes keywords; and a keyword one side requires is refused by the
|
|
355
|
+
# other only when the other has no positional parameter a Hash could land in and no `**rest`. A
|
|
356
|
+
# block's parameters never contradict by count: a block is called with whatever its caller passes.
|
|
357
|
+
def shape_contradiction(sig_fn, inline_fn, prefix)
|
|
358
|
+
if keywordless?(sig_fn) && keywordless?(inline_fn)
|
|
359
|
+
sig_range = positional_range(sig_fn)
|
|
360
|
+
inline_range = positional_range(inline_fn)
|
|
361
|
+
if sig_range.last < inline_range.first || inline_range.last < sig_range.first
|
|
362
|
+
return [:contradiction, "#{prefix}`sig/` accepts #{describe_range(sig_range)} positional " \
|
|
363
|
+
"argument(s) and the inline annotation #{describe_range(inline_range)}"]
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
keyword = unacceptable_keyword(sig_fn, inline_fn) || unacceptable_keyword(inline_fn, sig_fn)
|
|
368
|
+
return nil if keyword.nil?
|
|
369
|
+
|
|
370
|
+
[:contradiction, "#{prefix}keyword `#{keyword}:` is required by one side and not accepted by the other"]
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def keywordless?(function)
|
|
374
|
+
function.required_keywords.empty? && function.optional_keywords.empty? && function.rest_keywords.nil?
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def positional_range(function)
|
|
378
|
+
minimum = function.required_positionals.size + function.trailing_positionals.size
|
|
379
|
+
maximum = function.rest_positionals ? Float::INFINITY : minimum + function.optional_positionals.size
|
|
380
|
+
(minimum..maximum)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def describe_range(range)
|
|
384
|
+
return range.first.to_s if range.first == range.last
|
|
385
|
+
return "#{range.first} or more" if range.last == Float::INFINITY
|
|
386
|
+
|
|
387
|
+
"#{range.first} to #{range.last}"
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def unacceptable_keyword(requiring, other)
|
|
391
|
+
return nil unless other.rest_keywords.nil? && other.rest_positionals.nil?
|
|
392
|
+
unless (other.required_positionals + other.optional_positionals + other.trailing_positionals).empty?
|
|
393
|
+
return nil
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
requiring.required_keywords.keys.find do |name|
|
|
397
|
+
!other.required_keywords.key?(name) && !other.optional_keywords.key?(name)
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# One type position. The top spellings are the least precise answer; a position Rigor cannot
|
|
402
|
+
# translate faithfully is undecided unless the two are spelled identically. `provable` is false for a
|
|
403
|
+
# position a call may leave empty, which can never contradict.
|
|
404
|
+
def compare_slot(label, sig_slot, inline_slot, provable:)
|
|
405
|
+
return EQUAL if sig_slot == inline_slot
|
|
406
|
+
|
|
407
|
+
sig_top = top_slot?(sig_slot)
|
|
408
|
+
inline_top = top_slot?(inline_slot)
|
|
409
|
+
return EQUAL if sig_top && inline_top
|
|
410
|
+
return [:inline, nil] if sig_top
|
|
411
|
+
return SIGNATURE_MORE_PRECISE if inline_top
|
|
412
|
+
|
|
413
|
+
sig_type = slot_type(sig_slot)
|
|
414
|
+
inline_type = slot_type(inline_slot)
|
|
415
|
+
return [:undecided, "#{label} is a type Rigor cannot compare"] if sig_type.nil? || inline_type.nil?
|
|
416
|
+
|
|
417
|
+
slot_verdict(label, [sig_slot, inline_slot], sig_type, inline_type, provable)
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def slot_verdict(label, slots, sig_type, inline_type, provable)
|
|
421
|
+
inline_fits = subset?(inline_type, sig_type)
|
|
422
|
+
sig_fits = subset?(sig_type, inline_type)
|
|
423
|
+
return EQUAL if inline_fits && sig_fits
|
|
424
|
+
return [:inline, nil] if inline_fits
|
|
425
|
+
return SIGNATURE_MORE_PRECISE if sig_fits
|
|
426
|
+
unless provable && provable_names?(slots) && disjoint?(sig_type, inline_type)
|
|
427
|
+
return [:undecided, "Rigor cannot rank or separate the two #{label}s"]
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
[:contradiction,
|
|
431
|
+
"#{label} is `#{describe_slot(slots[0])}` in `sig/` and `#{describe_slot(slots[1])}` inline, " \
|
|
432
|
+
"and no value is both"]
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def top_slot?(slot)
|
|
436
|
+
slot.override.nil? && TOP_SPELLINGS.any? { |klass| slot.rbs_type.is_a?(klass) }
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def slot_type(slot)
|
|
440
|
+
type = slot.override || (faithful?(slot.rbs_type) ? translate(slot.rbs_type) : nil)
|
|
441
|
+
type && single_values(type)
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# `TrueClass` / `FalseClass` / `NilClass` as the one value each holds, through a union.
|
|
445
|
+
def single_values(type)
|
|
446
|
+
case type
|
|
447
|
+
when Type::Nominal
|
|
448
|
+
return type unless type.type_args.empty? && SINGLE_VALUE_CLASSES.key?(type.class_name)
|
|
449
|
+
|
|
450
|
+
Type::Combinator.constant_of(SINGLE_VALUE_CLASSES.fetch(type.class_name))
|
|
451
|
+
when Type::Union then Type::Combinator.union(*type.members.map { |member| single_values(member) })
|
|
452
|
+
else type
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def describe_slot(slot)
|
|
457
|
+
slot.override ? slot.override.describe(:short) : slot.rbs_type.to_s
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# Whether the Rigor translation of `type` keeps everything a subtype answer could depend on.
|
|
461
|
+
# `RbsTypeTranslator` reads an alias, an interface, `self`, `instance`, `class` and a type variable as
|
|
462
|
+
# `untyped`, a proc as the bare `Proc` class, and an intersection as one of its members; and it reads
|
|
463
|
+
# a relative class name as a top-level one, which it is not when an enclosing namespace declares the
|
|
464
|
+
# same name. Comparing any of those would let a lost detail decide which side binds, or report a
|
|
465
|
+
# contradiction between two spellings of one class.
|
|
466
|
+
def faithful?(type)
|
|
467
|
+
case type
|
|
468
|
+
when *FAITHFUL_LEAVES then true
|
|
469
|
+
when ::RBS::Types::ClassInstance then faithful_name?(type.name) && type.args.all? { |arg| faithful?(arg) }
|
|
470
|
+
when ::RBS::Types::ClassSingleton then faithful_name?(type.name)
|
|
471
|
+
when ::RBS::Types::Optional then faithful?(type.type)
|
|
472
|
+
when ::RBS::Types::Union, ::RBS::Types::Tuple then type.types.all? { |member| faithful?(member) }
|
|
473
|
+
when ::RBS::Types::Record then (type.fields.values + type.optional_fields.values).all? { |v| faithful?(v) }
|
|
474
|
+
else false
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# An absolute name means what it says. A relative one resolves outward from the member's class, so it
|
|
479
|
+
# is read as top-level only when the project declares no class or module whose name starts with the
|
|
480
|
+
# same segment — which is also what makes the `sig/` and the inline spelling of it comparable.
|
|
481
|
+
def faithful_name?(name)
|
|
482
|
+
return true if name.absolute?
|
|
483
|
+
|
|
484
|
+
head = name.namespace.path.first || name.name
|
|
485
|
+
!@shadowable.include?(head.to_s)
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
def translate(type)
|
|
489
|
+
Inference::RbsTypeTranslator.translate(type)
|
|
490
|
+
rescue StandardError
|
|
491
|
+
nil
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# Whether `sub` is provably a subset of `sup`, decided by names alone (see the class comment): equal
|
|
495
|
+
# types, a union member by member, the same class with element types that are each a subset (or
|
|
496
|
+
# unstated on `sup`), a literal of exactly that class, a refinement or a range of that class, a tuple
|
|
497
|
+
# of an `Array`, a record of a `Hash`; `untyped` either side. A refinement on the `sup` side is left
|
|
498
|
+
# to `Inference::Acceptance`, which answers it from the value and the refinement alone.
|
|
499
|
+
def subset?(sub, sup)
|
|
500
|
+
return true if sub == sup || gradual?(sub) || gradual?(sup) || sub.is_a?(Type::Bot)
|
|
501
|
+
return sub.members.all? { |member| subset?(member, sup) } if sub.is_a?(Type::Union)
|
|
502
|
+
return sup.members.any? { |member| subset?(sub, member) } if sup.is_a?(Type::Union)
|
|
503
|
+
|
|
504
|
+
subset_of_member?(sub, sup)
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
def subset_of_member?(sub, sup)
|
|
508
|
+
case sup
|
|
509
|
+
when Type::Nominal then subset_of_class?(sub, sup)
|
|
510
|
+
when Type::Refined, Type::Difference, Type::IntegerRange, Type::FloatRange then refinement_subset?(sub, sup)
|
|
511
|
+
when Type::Tuple then sub.is_a?(Type::Tuple) && pairwise_subset?(sub.elements, sup.elements)
|
|
512
|
+
else false
|
|
513
|
+
end
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
def pairwise_subset?(subs, sups)
|
|
517
|
+
subs.size == sups.size && subs.zip(sups).all? { |left, right| subset?(left, right) }
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
def gradual?(type)
|
|
521
|
+
type.is_a?(Type::Dynamic) || type.is_a?(Type::Top)
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
def subset_of_class?(sub, sup)
|
|
525
|
+
case sub
|
|
526
|
+
when Type::Constant then bare_class?(sup, literal_class_name(sub.value))
|
|
527
|
+
when Type::Nominal
|
|
528
|
+
sub.class_name == sup.class_name && (sup.type_args.empty? || pairwise_subset?(sub.type_args, sup.type_args))
|
|
529
|
+
when Type::Refined, Type::Difference then subset?(sub.base, sup)
|
|
530
|
+
when Type::Tuple then tuple_of_array?(sub, sup)
|
|
531
|
+
else bare_class?(sup, CARRIER_CLASSES[sub.class])
|
|
532
|
+
end
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
def tuple_of_array?(tuple, sup)
|
|
536
|
+
sup.class_name == "Array" &&
|
|
537
|
+
(sup.type_args.empty? || tuple.elements.all? { |element| subset?(element, sup.type_args.first) })
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
# A literal's class name. The literal's own class is exact, and its name is compared with the RBS
|
|
541
|
+
# name the other side spells — no constant is resolved.
|
|
542
|
+
def literal_class_name(value)
|
|
543
|
+
value.class.name
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
def bare_class?(type, name)
|
|
547
|
+
!name.nil? && type.class_name == name && type.type_args.empty?
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def refinement_subset?(sub, sup)
|
|
551
|
+
return false unless REFINEMENT_OR_VALUE.any? { |klass| sub.is_a?(klass) }
|
|
552
|
+
|
|
553
|
+
Inference::Acceptance.accepts(sup, sub).yes?
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
# Whether the class names a disjointness proof would lean on mean what they say: every class name in
|
|
557
|
+
# both declared types is written absolutely, and a refinement override is a value or a built-in
|
|
558
|
+
# refinement (whose base is a core class), never a class-name payload, which resolves like a
|
|
559
|
+
# relative name.
|
|
560
|
+
def provable_names?(slots)
|
|
561
|
+
return false if @proof.nil?
|
|
562
|
+
|
|
563
|
+
slots.all? do |slot|
|
|
564
|
+
slot.override ? provable_override?(slot.override) : absolute_names_only?(slot.rbs_type)
|
|
565
|
+
end
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
def absolute_names_only?(type)
|
|
569
|
+
case type
|
|
570
|
+
when ::RBS::Types::ClassInstance, ::RBS::Types::ClassSingleton
|
|
571
|
+
return false unless type.name.absolute?
|
|
572
|
+
end
|
|
573
|
+
type.each_type { |inner| return false unless absolute_names_only?(inner) }
|
|
574
|
+
true
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
def provable_override?(type)
|
|
578
|
+
members = type.is_a?(Type::Union) ? type.members : [type]
|
|
579
|
+
members.all? { |member| REFINEMENT_OR_VALUE.any? { |klass| member.is_a?(klass) } }
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
# Whether `left` and `right` provably share no value: every pairing of their members is two distinct
|
|
583
|
+
# literals, a literal whose class is not below an RBS class, or two RBS classes neither of which is
|
|
584
|
+
# an RBS ancestor of the other. Anything else is left unproven, so the answer is false.
|
|
585
|
+
def disjoint?(left, right)
|
|
586
|
+
return false if @proof.nil?
|
|
587
|
+
|
|
588
|
+
left_atoms = atoms(left)
|
|
589
|
+
right_atoms = atoms(right)
|
|
590
|
+
return false if left_atoms.nil? || right_atoms.nil?
|
|
591
|
+
|
|
592
|
+
left_atoms.all? { |a| right_atoms.all? { |b| disjoint_atoms?(a, b) } }
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
# `[:value, v]` or `[:class, name]` per union member, or nil when a member has neither.
|
|
596
|
+
def atoms(type)
|
|
597
|
+
members = type.is_a?(Type::Union) ? type.members : [type]
|
|
598
|
+
result = members.map { |member| atom(member) }
|
|
599
|
+
result.include?(nil) ? nil : result
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
def atom(type)
|
|
603
|
+
case type
|
|
604
|
+
when Type::Constant then [:value, type.value]
|
|
605
|
+
when Type::Nominal then [:class, type.class_name]
|
|
606
|
+
when Type::Refined, Type::Difference then atom(type.base)
|
|
607
|
+
when Type::Tuple then [:class, "Array"]
|
|
608
|
+
else
|
|
609
|
+
name = CARRIER_CLASSES[type.class]
|
|
610
|
+
name && [:class, name]
|
|
611
|
+
end
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
def disjoint_atoms?(left, right)
|
|
615
|
+
case [left[0], right[0]]
|
|
616
|
+
when %i[value value] then !left[1].eql?(right[1])
|
|
617
|
+
when %i[value class] then outside_class?(left[1], right[1])
|
|
618
|
+
when %i[class value] then outside_class?(right[1], left[1])
|
|
619
|
+
else unrelated_classes?(left[1], right[1])
|
|
620
|
+
end
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
# A literal's own class is exact (`1` is an `Integer`, not a subclass), so it is outside `name` when
|
|
624
|
+
# RBS places that class nowhere below `name`.
|
|
625
|
+
def outside_class?(value, name)
|
|
626
|
+
ancestors = @proof.class_ancestors(literal_class_name(value))
|
|
627
|
+
!ancestors.nil? && !@proof.class_ancestors(name).nil? && !ancestors.include?(name)
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
def unrelated_classes?(left, right)
|
|
631
|
+
left_ancestors = @proof.class_ancestors(left)
|
|
632
|
+
right_ancestors = @proof.class_ancestors(right)
|
|
633
|
+
return false if left_ancestors.nil? || right_ancestors.nil?
|
|
634
|
+
|
|
635
|
+
!left_ancestors.include?(right) && !right_ancestors.include?(left)
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
# Folds per-position results into one verdict: any contradiction wins, then any undecided position,
|
|
639
|
+
# then the direction every precise position agrees on. Positions pointing both ways are undecided —
|
|
640
|
+
# each side says something the other does not.
|
|
641
|
+
def combine(results)
|
|
642
|
+
contradiction = results.find { |outcome, _| outcome == :contradiction }
|
|
643
|
+
return contradiction if contradiction
|
|
644
|
+
|
|
645
|
+
undecided = results.find { |outcome, _| outcome == :undecided }
|
|
646
|
+
return undecided if undecided
|
|
647
|
+
|
|
648
|
+
directions = results.map(&:first)
|
|
649
|
+
if directions.include?(:inline) && directions.include?(:signature)
|
|
650
|
+
return [:undecided, "each declaration is more precise than the other in a different position"]
|
|
651
|
+
end
|
|
652
|
+
return [:inline, nil] if directions.include?(:inline)
|
|
653
|
+
return SIGNATURE_MORE_PRECISE if directions.include?(:signature)
|
|
654
|
+
|
|
655
|
+
EQUAL
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
# `[return_override, {param_name => override}]` read off the MEMBER's annotations — the ones
|
|
659
|
+
# `RBS::Definition::Method#annotations` carries and call sites honour — with `RbsExtended`'s own
|
|
660
|
+
# parsers, so a refinement means here what it means at a call site.
|
|
661
|
+
def refinements_of(member)
|
|
662
|
+
strings = Array(member.annotations).map(&:string).select { |string| refinement_directive?(string) }
|
|
663
|
+
return [nil, {}.freeze] if strings.empty?
|
|
664
|
+
|
|
665
|
+
return_override = strings.lazy.filter_map { |string| RbsExtended.parse_return_type_override(string) }.first
|
|
666
|
+
params = strings.filter_map { |string| RbsExtended.parse_param_annotation(string) }
|
|
667
|
+
[return_override, params.to_h { |override| [override.param_name, override.type] }]
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
def return_conflict(method_types, override)
|
|
671
|
+
declared = method_types.map { |method_type| method_type.type.return_type }
|
|
672
|
+
return nil unless declared.all? { |type| exceeds?(type, override) }
|
|
673
|
+
|
|
674
|
+
"`rigor:v1:return: #{override.describe(:short)}` is disjoint from the declared return type " \
|
|
675
|
+
"`#{declared.map(&:to_s).uniq.join('` / `')}`"
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
def param_conflict(method_types, name, override)
|
|
679
|
+
declared = method_types.flat_map do |method_type|
|
|
680
|
+
function = method_type.type
|
|
681
|
+
function.is_a?(::RBS::Types::UntypedFunction) ? [] : named_params(function, name)
|
|
682
|
+
end
|
|
683
|
+
return nil if declared.empty? || !declared.all? { |param| exceeds?(param.type, override) }
|
|
684
|
+
|
|
685
|
+
"`rigor:v1:param: #{name} #{override.describe(:short)}` is disjoint from the declared parameter type " \
|
|
686
|
+
"`#{declared.map { |param| param.type.to_s }.uniq.join('` / `')}`"
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
def named_params(function, name)
|
|
690
|
+
(function.required_positionals + function.optional_positionals + function.trailing_positionals +
|
|
691
|
+
[function.rest_positionals, function.rest_keywords].compact +
|
|
692
|
+
function.required_keywords.values + function.optional_keywords.values).select { |p| p.name == name }
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
# A refinement exceeds its declared type when the two provably share no value. A top spelling holds
|
|
696
|
+
# every value, and a declared type Rigor cannot read faithfully proves nothing.
|
|
697
|
+
def exceeds?(declared, refinement)
|
|
698
|
+
return false if TOP_SPELLINGS.any? { |klass| declared.is_a?(klass) } || !faithful?(declared)
|
|
699
|
+
return false unless provable_names?([Slot.new(rbs_type: declared, override: nil)]) &&
|
|
700
|
+
provable_override?(refinement)
|
|
701
|
+
|
|
702
|
+
declared_type = translate(declared)
|
|
703
|
+
!declared_type.nil? && disjoint?(single_values(declared_type), single_values(refinement))
|
|
704
|
+
end
|
|
705
|
+
end
|
|
706
|
+
end
|
|
707
|
+
end
|
|
708
|
+
end
|