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
data/lib/rigor/sig_gen/writer.rb
CHANGED
|
@@ -31,12 +31,16 @@ module Rigor
|
|
|
31
31
|
|
|
32
32
|
# Per-`update_existing` accumulator. The merge_class helper mutates `source` / `decls` / `applied` /
|
|
33
33
|
# `skipped` in place as each class is processed so the next class sees the latest byte positions.
|
|
34
|
-
MergeState = Struct.new(:source, :decls, :applied, :skipped, keyword_init: true)
|
|
34
|
+
MergeState = Struct.new(:source, :decls, :applied, :skipped, :left_unreadable, :replaced, keyword_init: true)
|
|
35
35
|
private_constant :MergeState
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
# @param dry_run — assemble and validate every target exactly as a write would, and report it, without
|
|
38
|
+
# creating or changing a file. `sig-gen --check` (ADR-112 WD4) is this, so it can only ever disagree
|
|
39
|
+
# with `--write` about the disk, never about the content.
|
|
40
|
+
def initialize(path_mapper:, overwrite: false, dry_run: false)
|
|
38
41
|
@path_mapper = path_mapper
|
|
39
42
|
@overwrite = overwrite
|
|
43
|
+
@dry_run = dry_run
|
|
40
44
|
# Run-level (cross-file) namespace-kind view, populated per `#write_all` from every candidate's per-file
|
|
41
45
|
# map. Empty until then so the single-target `#write` path falls back to per-candidate kinds only.
|
|
42
46
|
@global_namespace_kinds = {}
|
|
@@ -74,6 +78,15 @@ module Rigor
|
|
|
74
78
|
write_target(target, emittable, source_path: source_path)
|
|
75
79
|
end
|
|
76
80
|
|
|
81
|
+
# Whether `proposed` has STRICTLY FEWER bare `untyped` tokens than `existing`. A count, not a slot-by-slot
|
|
82
|
+
# comparison: it reads as "at least one more position is concrete", and does not prove that no concrete
|
|
83
|
+
# position became `untyped` elsewhere. Word-boundary matching counts `untyped` only as a type token, not
|
|
84
|
+
# as a substring inside identifiers. Public because the generator asks the same question before it lists a
|
|
85
|
+
# declared `initialize` at all (#1436).
|
|
86
|
+
def self.fewer_untyped?(proposed, existing)
|
|
87
|
+
proposed.scan(/\buntyped\b/).size < existing.scan(/\buntyped\b/).size
|
|
88
|
+
end
|
|
89
|
+
|
|
77
90
|
private
|
|
78
91
|
|
|
79
92
|
# Shared per-target write path used by both `#write` and `#write_all`. Picks a representative
|
|
@@ -91,7 +104,7 @@ module Rigor
|
|
|
91
104
|
# Deliberately NARROWER than {Classification::EMITTABLE}: the write path has never produced a
|
|
92
105
|
# `:new_file` row (the generator does not classify one), and admitting it here would let a
|
|
93
106
|
# classification with no writer behind it decide that a target file must be created.
|
|
94
|
-
EMITTABLE = [Classification::NEW_METHOD, Classification::TIGHTER_RETURN].freeze
|
|
107
|
+
EMITTABLE = [Classification::NEW_METHOD, Classification::TIGHTER_RETURN, Classification::INLINE_OVERWRITE].freeze
|
|
95
108
|
private_constant :EMITTABLE
|
|
96
109
|
|
|
97
110
|
def inside_sig_root?(target)
|
|
@@ -116,8 +129,10 @@ module Rigor
|
|
|
116
129
|
action: :skipped_invalid_rbs, error: error)
|
|
117
130
|
end
|
|
118
131
|
|
|
119
|
-
|
|
120
|
-
|
|
132
|
+
unless @dry_run
|
|
133
|
+
FileUtils.mkdir_p(target.dirname)
|
|
134
|
+
target.write(content)
|
|
135
|
+
end
|
|
121
136
|
WriteResult.new(source_path: source_path, target_path: target,
|
|
122
137
|
action: :created, applied: candidates)
|
|
123
138
|
end
|
|
@@ -245,7 +260,7 @@ module Rigor
|
|
|
245
260
|
|
|
246
261
|
def render_tree_node_body(node, kinds, supers, depth, prefix)
|
|
247
262
|
inner_indent = INDENT * (depth + 1)
|
|
248
|
-
method_lines = node[:methods].
|
|
263
|
+
method_lines = node[:methods].flat_map { |c| c.rbs_lines.map { |line| "#{inner_indent}#{line}\n" } }.join
|
|
249
264
|
child_blocks = node[:children].values.map do |child|
|
|
250
265
|
render_tree_node(child, kinds, supers, depth + 1, prefix + [node[:name]])
|
|
251
266
|
end.join
|
|
@@ -291,13 +306,15 @@ module Rigor
|
|
|
291
306
|
decls = parse_signature(source)
|
|
292
307
|
return WriteResult.new(source_path: source_path, target_path: target, action: :noop) if decls.nil?
|
|
293
308
|
|
|
294
|
-
state = MergeState.new(source: source, decls: decls, applied: [], skipped: []
|
|
309
|
+
state = MergeState.new(source: source, decls: decls, applied: [], skipped: [], left_unreadable: [],
|
|
310
|
+
replaced: [])
|
|
295
311
|
merge_candidates(state, candidates)
|
|
296
312
|
|
|
297
313
|
action = state.applied.empty? ? :noop : :updated
|
|
298
314
|
unless action == :updated
|
|
299
315
|
return WriteResult.new(source_path: source_path, target_path: target, action: action,
|
|
300
|
-
applied: state.applied, skipped: state.skipped
|
|
316
|
+
applied: state.applied, skipped: state.skipped,
|
|
317
|
+
left_unreadable: state.left_unreadable)
|
|
301
318
|
end
|
|
302
319
|
|
|
303
320
|
# The merge splices text into an existing file by byte offset, so a bug here can produce a file neither
|
|
@@ -310,9 +327,10 @@ module Rigor
|
|
|
310
327
|
action: :skipped_invalid_rbs, error: error)
|
|
311
328
|
end
|
|
312
329
|
|
|
313
|
-
target.write(state.source)
|
|
330
|
+
target.write(state.source) unless @dry_run
|
|
314
331
|
WriteResult.new(source_path: source_path, target_path: target,
|
|
315
|
-
action: action, applied: state.applied, skipped: state.skipped
|
|
332
|
+
action: action, applied: state.applied, skipped: state.skipped,
|
|
333
|
+
left_unreadable: state.left_unreadable, replaced: state.replaced)
|
|
316
334
|
end
|
|
317
335
|
|
|
318
336
|
# Applies every class group, then every requested shell, then normalises the layout the three steps
|
|
@@ -544,7 +562,7 @@ module Rigor
|
|
|
544
562
|
state.applied.concat(methods)
|
|
545
563
|
insert_namespace_chain(state, class_name, kinds, supers, methods)
|
|
546
564
|
else
|
|
547
|
-
state.source = merge_into_existing_class(state.source, decl, methods, state
|
|
565
|
+
state.source = merge_into_existing_class(state.source, decl, methods, state)
|
|
548
566
|
state.decls = parse_signature(state.source) || state.decls
|
|
549
567
|
end
|
|
550
568
|
end
|
|
@@ -575,24 +593,29 @@ module Rigor
|
|
|
575
593
|
source.end_with?("\n")
|
|
576
594
|
end
|
|
577
595
|
|
|
578
|
-
def merge_into_existing_class(source, decl, methods,
|
|
596
|
+
def merge_into_existing_class(source, decl, methods, state)
|
|
579
597
|
existing_pairs = collect_member_pairs(decl)
|
|
580
598
|
new_methods, conflicting = partition_against_existing(methods, existing_pairs)
|
|
581
599
|
|
|
582
600
|
source = insert_into_class(source, decl, new_methods)
|
|
583
|
-
applied.concat(new_methods)
|
|
601
|
+
state.applied.concat(new_methods)
|
|
584
602
|
|
|
585
603
|
if @overwrite
|
|
586
|
-
source, replaced = replace_eligible_conflicts(source, decl, conflicting)
|
|
587
|
-
applied.concat(replaced)
|
|
588
|
-
|
|
604
|
+
source, replaced = replace_eligible_conflicts(source, decl, conflicting, state)
|
|
605
|
+
state.applied.concat(replaced)
|
|
606
|
+
state.replaced.concat(replaced)
|
|
607
|
+
state.skipped.concat(conflicting.reject { |c| replaced.include?(c) }.map { |c| [c, :user_authored] })
|
|
589
608
|
else
|
|
590
|
-
skipped.concat(conflicting.map { |c| [c, :user_authored] })
|
|
609
|
+
state.skipped.concat(conflicting.map { |c| [c, :user_authored] })
|
|
591
610
|
end
|
|
592
611
|
|
|
593
612
|
source
|
|
594
613
|
end
|
|
595
614
|
|
|
615
|
+
def inline_overwrite?(candidate)
|
|
616
|
+
candidate.classification == Classification::INLINE_OVERWRITE
|
|
617
|
+
end
|
|
618
|
+
|
|
596
619
|
# Returns a list of `[method_name (Symbol), kind (Symbol)]` pairs for every method-like member in the
|
|
597
620
|
# declaration. ADR-14 slice 4 recognises `MethodDefinition`'s `:instance` / `:singleton` kind plus the
|
|
598
621
|
# three `attr_*` declaration kinds so a source-side `attr_reader :name` and an RBS-side
|
|
@@ -628,7 +651,8 @@ module Rigor
|
|
|
628
651
|
return source if new_methods.empty?
|
|
629
652
|
|
|
630
653
|
indent = INDENT * member_indent_depth(decl)
|
|
631
|
-
|
|
654
|
+
lines = new_methods.flat_map { |c| c.rbs_lines.map { |line| "#{indent}#{line}\n" } }.join
|
|
655
|
+
insert_before_end(source, decl, lines)
|
|
632
656
|
end
|
|
633
657
|
|
|
634
658
|
# Walks the class's existing method declarations; for each replaceable candidate that matches a member
|
|
@@ -643,21 +667,32 @@ module Rigor
|
|
|
643
667
|
# canonical case is `initialize_stub_candidate`, which bypasses the existing-RBS comparison and always
|
|
644
668
|
# classifies as `NEW_METHOD` — when sig-gen's `--params=observed` upgrades a `(path: untyped) -> void`
|
|
645
669
|
# declaration to `(path: String) -> void` we want `--overwrite` to apply it.
|
|
646
|
-
def replace_eligible_conflicts(source, decl, candidates)
|
|
670
|
+
def replace_eligible_conflicts(source, decl, candidates, state)
|
|
647
671
|
eligible = candidates.select { |c| eligible_for_replacement?(c, decl, source) }
|
|
648
672
|
return [source, []] if eligible.empty?
|
|
649
673
|
|
|
650
674
|
replaced = []
|
|
675
|
+
done = {}
|
|
651
676
|
# Apply replacements from highest byte position downward so earlier byte offsets remain valid as the
|
|
652
677
|
# source grows or shrinks.
|
|
653
|
-
sorted = eligible.sort_by { |c| -member_position(decl, c
|
|
678
|
+
sorted = eligible.sort_by { |c| -member_position(decl, c) }
|
|
654
679
|
sorted.each do |candidate|
|
|
655
|
-
|
|
680
|
+
member = replaceable_member(decl, candidate)
|
|
681
|
+
# An `attr_accessor` answers for two methods; its one replacement covers both candidates.
|
|
682
|
+
next replaced << candidate if member && done[member.location.start_pos]
|
|
683
|
+
|
|
684
|
+
result = apply_replacement(source, decl, candidate, state)
|
|
685
|
+
next if result.nil?
|
|
686
|
+
|
|
687
|
+
done[member.location.start_pos] = true
|
|
688
|
+
source = result
|
|
689
|
+
replaced << candidate
|
|
656
690
|
end
|
|
657
691
|
[source, replaced]
|
|
658
692
|
end
|
|
659
693
|
|
|
660
694
|
def eligible_for_replacement?(candidate, decl, source)
|
|
695
|
+
return true if inline_overwrite?(candidate)
|
|
661
696
|
return false if replaces_declared_void?(candidate, decl)
|
|
662
697
|
|
|
663
698
|
case candidate.classification
|
|
@@ -684,25 +719,37 @@ module Rigor
|
|
|
684
719
|
declared_void && !candidate.rbs.to_s.end_with?(" -> void")
|
|
685
720
|
end
|
|
686
721
|
|
|
687
|
-
# Compares the existing member's source-side RBS text against the candidate's proposed RBS text
|
|
688
|
-
#
|
|
689
|
-
# least one `untyped` slot becomes a concrete type AND no concrete slot becomes `untyped`. Word-boundary
|
|
690
|
-
# matching ensures we count `untyped` only as a type token, not as a substring inside identifiers.
|
|
722
|
+
# Compares the existing member's source-side RBS text against the candidate's proposed RBS text
|
|
723
|
+
# ({.fewer_untyped?}).
|
|
691
724
|
def tightens_untyped?(candidate, decl, source)
|
|
692
725
|
member = find_method_member(decl, candidate.method_name, candidate.kind)
|
|
693
726
|
return false if member.nil?
|
|
694
727
|
|
|
695
|
-
|
|
696
|
-
count_untyped(candidate.rbs) < count_untyped(existing_rbs)
|
|
728
|
+
self.class.fewer_untyped?(candidate.rbs, source[member.location.start_pos...member.location.end_pos])
|
|
697
729
|
end
|
|
698
730
|
|
|
699
|
-
def
|
|
700
|
-
|
|
731
|
+
def member_position(decl, candidate)
|
|
732
|
+
member = replaceable_member(decl, candidate)
|
|
733
|
+
member ? member.location.start_pos : -1
|
|
701
734
|
end
|
|
702
735
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
736
|
+
# The member a replacement overwrites. An inline overwrite may also land on an `attr_*` declaration of
|
|
737
|
+
# the same method (ADR-112 WD4), which it replaces in the inline attribute's own spelling; an
|
|
738
|
+
# `overloading?` member (`| ...`) is never its target.
|
|
739
|
+
def replaceable_member(decl, candidate)
|
|
740
|
+
member = find_method_member(decl, candidate.method_name, candidate.kind)
|
|
741
|
+
return member unless candidate.classification == Classification::INLINE_OVERWRITE
|
|
742
|
+
|
|
743
|
+
member = nil if member.respond_to?(:overloading?) && member.overloading?
|
|
744
|
+
member || find_attr_member(decl, candidate.method_name, candidate.kind)
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
def find_attr_member(decl, method_name, kind)
|
|
748
|
+
decl.members.find do |m|
|
|
749
|
+
pairs = []
|
|
750
|
+
collect_pairs_for_member(m, pairs) unless m.is_a?(RBS::AST::Members::MethodDefinition)
|
|
751
|
+
m.respond_to?(:kind) && m.kind == kind && pairs.any? { |name, _| name == method_name }
|
|
752
|
+
end
|
|
706
753
|
end
|
|
707
754
|
|
|
708
755
|
def find_method_member(decl, method_name, kind)
|
|
@@ -714,12 +761,96 @@ module Rigor
|
|
|
714
761
|
# Splices the new RBS one-liner over the existing declaration's byte range. `RBS::Parser`'s location
|
|
715
762
|
# starts at the `def` keyword, NOT at the column zero of the line, so the leading whitespace stays inside
|
|
716
763
|
# `source[0...start_pos]` and we do not re-emit it.
|
|
717
|
-
def apply_replacement(source, decl, candidate)
|
|
718
|
-
member =
|
|
764
|
+
def apply_replacement(source, decl, candidate, state)
|
|
765
|
+
member = replaceable_member(decl, candidate)
|
|
719
766
|
return nil if member.nil?
|
|
720
767
|
|
|
721
768
|
loc = member.location
|
|
722
|
-
source[0...loc.start_pos] + candidate.rbs + source[loc.end_pos..]
|
|
769
|
+
replaced = source[0...loc.start_pos] + kept_comments(source, loc) + candidate.rbs + source[loc.end_pos..]
|
|
770
|
+
# Both splices insert above the member, the effect annotation at its own line and the declared ones at
|
|
771
|
+
# its first annotation's, so running the lower insertion first keeps the positions the parse gave valid.
|
|
772
|
+
replaced = splice_annotations(replaced, member, candidate, state)
|
|
773
|
+
splice_declared_annotations(replaced, member, candidate)
|
|
774
|
+
end
|
|
775
|
+
|
|
776
|
+
# A member written across lines (`def f: () -> A` / ` # why` / ` | (B) -> C`) can hold comments inside its
|
|
777
|
+
# own location, which the splice above would delete with the text it replaces. Each such comment line is
|
|
778
|
+
# kept, moved above the new line at the member's indentation. Only whole comment lines are recognised: RBS
|
|
779
|
+
# has no string type that spans a line, so a line whose first non-blank character is `#` is a comment.
|
|
780
|
+
def kept_comments(source, loc)
|
|
781
|
+
text = source[loc.start_pos...loc.end_pos].to_s
|
|
782
|
+
return "" unless text.include?("\n")
|
|
783
|
+
|
|
784
|
+
indent = source[line_start_index(source, loc.start_pos)...loc.start_pos].to_s
|
|
785
|
+
indent = "" unless indent.match?(/\A[ \t]*\z/)
|
|
786
|
+
text.lines.drop(1).filter_map { |line| line.strip if line.match?(/\A\s*#/) }
|
|
787
|
+
.map { |comment| "#{comment}\n#{indent}" }.join
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
# ADR-112 WD4 — the annotations an inline declaration carries that its `sig/` copy lacks, added above the
|
|
791
|
+
# member. Additive only: an annotation already there stays, whoever wrote it, so a copy never loses
|
|
792
|
+
# anything to regeneration (an annotation later deleted inline is therefore not deleted here).
|
|
793
|
+
def splice_declared_annotations(source, member, candidate)
|
|
794
|
+
missing = missing_declared_annotations(member, candidate)
|
|
795
|
+
return source if missing.empty?
|
|
796
|
+
|
|
797
|
+
line_start = line_start_index(source, member.location.start_pos)
|
|
798
|
+
indent = source[line_start...member.location.start_pos].to_s
|
|
799
|
+
return source unless indent.match?(/\A[ \t]*\z/)
|
|
800
|
+
|
|
801
|
+
anchor = annotation_block_start(source, member, line_start)
|
|
802
|
+
source[0...anchor] + missing.map { |line| "#{indent}#{line}\n" }.join + source[anchor..]
|
|
803
|
+
end
|
|
804
|
+
|
|
805
|
+
# A declared annotation line is `%a` plus a one-character delimiter pair around its content.
|
|
806
|
+
def missing_declared_annotations(member, candidate)
|
|
807
|
+
present = member.annotations.map { |annotation| annotation.string.to_s.strip }
|
|
808
|
+
candidate.declared_annotations.reject { |line| present.include?(line[3...-1].to_s.strip) }
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
# The start of the line the member's first annotation sits on, or of the member's own line.
|
|
812
|
+
def annotation_block_start(source, member, line_start)
|
|
813
|
+
starts = member.annotations.filter_map { |annotation| annotation.location&.start_pos }
|
|
814
|
+
([line_start] + starts.map { |pos| line_start_index(source, pos) }).min
|
|
815
|
+
end
|
|
816
|
+
|
|
817
|
+
# ADR-103 WD9 — the annotation half of a replacement, and the one place this slice is allowed to
|
|
818
|
+
# write bytes ABOVE an existing declaration.
|
|
819
|
+
#
|
|
820
|
+
# `RBS::Parser` puts a member's annotations OUTSIDE its `location`, so {#apply_replacement} above
|
|
821
|
+
# already leaves every annotation on the declaration byte-untouched — the `%a{deprecated}` and the
|
|
822
|
+
# `%a{implicitly-returns-nil}` a user wrote survive an `--overwrite` unchanged. What is left is the
|
|
823
|
+
# question of adding ours, and the answer is: only onto a declaration that carries none.
|
|
824
|
+
#
|
|
825
|
+
# A declaration that already carries an annotation is left exactly as it is, and the candidate is
|
|
826
|
+
# reported as `sig.effect.left-unreadable`. The writer cannot tell an annotation it authored from
|
|
827
|
+
# one the user wrote, and it has no grammar for merging two — so rewriting the region could silently
|
|
828
|
+
# replace an author's own `%a{pure}` with a labelled envelope, or strip a directive Rigor does not
|
|
829
|
+
# read at all. Leaving it and saying so is the only move that cannot destroy an authored fact.
|
|
830
|
+
#
|
|
831
|
+
# An annotation on one of the member's overloads (`def m: %a{pure} (Integer) -> String`) counts too:
|
|
832
|
+
# it sits inside the location, and a tighter return keeps it (#1436), so ours would be a second one.
|
|
833
|
+
def splice_annotations(source, member, candidate, state)
|
|
834
|
+
return source if candidate.annotations.empty?
|
|
835
|
+
|
|
836
|
+
if annotated?(member)
|
|
837
|
+
# Rebuilt so the reported row carries the reason it was actually given. The generator stamped
|
|
838
|
+
# `:emitted` when it decided the annotation; only the writer knows the declaration refused it.
|
|
839
|
+
state.left_unreadable << candidate.with_effect_annotation(candidate.annotations, :left_unreadable)
|
|
840
|
+
return source
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
line_start = line_start_index(source, member.location.start_pos)
|
|
844
|
+
indent = source[line_start...member.location.start_pos].to_s
|
|
845
|
+
return source unless indent.match?(/\A[ \t]*\z/)
|
|
846
|
+
|
|
847
|
+
source[0...line_start] + candidate.annotations.map { |line| "#{indent}#{line}\n" }.join + source[line_start..]
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
def annotated?(member)
|
|
851
|
+
return true unless member.annotations.empty?
|
|
852
|
+
|
|
853
|
+
member.respond_to?(:overloads) && member.overloads.any? { |overload| overload.annotations.any? }
|
|
723
854
|
end
|
|
724
855
|
end
|
|
725
856
|
end
|
data/lib/rigor/sig_gen.rb
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "sig_gen/alias_index"
|
|
3
4
|
require_relative "sig_gen/classification"
|
|
5
|
+
require_relative "sig_gen/effect_annotation"
|
|
6
|
+
require_relative "sig_gen/inline_declarations"
|
|
4
7
|
require_relative "sig_gen/method_candidate"
|
|
5
8
|
require_relative "sig_gen/observed_call"
|
|
6
9
|
require_relative "sig_gen/meta_class_shape"
|
|
@@ -32,7 +32,7 @@ module Rigor
|
|
|
32
32
|
# requiring the loader from here fails outright. `signature_path_audit_spec` pins the two
|
|
33
33
|
# anchors equal, and pins this discovery against `Loader.bundled_plugin_sig_path`, so the
|
|
34
34
|
# duplication cannot drift unnoticed.
|
|
35
|
-
BUNDLED_PLUGINS_ROOT = File.expand_path("../../plugins", __dir__)
|
|
35
|
+
BUNDLED_PLUGINS_ROOT = File.expand_path("../../plugins", __dir__).freeze
|
|
36
36
|
|
|
37
37
|
# One configured `signature_paths:` entry's resolution status.
|
|
38
38
|
#
|
|
@@ -25,7 +25,6 @@ module Rigor
|
|
|
25
25
|
module NodeWalker
|
|
26
26
|
module_function
|
|
27
27
|
|
|
28
|
-
# @return when no block is given.
|
|
29
28
|
def each(root, &)
|
|
30
29
|
return to_enum(__method__, root) unless block_given?
|
|
31
30
|
|
|
@@ -46,8 +45,6 @@ module Rigor
|
|
|
46
45
|
# itself). The yielded `ancestors` array is the live descent stack — callers that retain it past the
|
|
47
46
|
# block invocation MUST copy it (`Plugin::NodeContext` does). Used by the plugin engine to give
|
|
48
47
|
# `node_rule` blocks their enclosing class / method / block context (ADR-37 slice 1d).
|
|
49
|
-
#
|
|
50
|
-
# @return when no block is given.
|
|
51
48
|
def each_with_ancestors(root, &)
|
|
52
49
|
return to_enum(__method__, root) unless block_given?
|
|
53
50
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Source
|
|
7
|
+
# Issue #992 — the positional-argument envelope of a `Prism::DefNode`, and the ONE place a `def` nobody
|
|
8
|
+
# declared is turned into something `call.wrong-arity` can check a call against.
|
|
9
|
+
#
|
|
10
|
+
# Both undeclared lanes read it. A method known only to the discovery walk has no other shape, and a
|
|
11
|
+
# method `rigor-rbs-inline` synthesized a fully defaulted skeleton for (`%a{rigor:v1:inferred-signature}`)
|
|
12
|
+
# is sent here too rather than having its synthesized member's arity read, so a `def` can never be
|
|
13
|
+
# judged by two parsers of its parameter list that disagree.
|
|
14
|
+
#
|
|
15
|
+
# An envelope is plain frozen data, `[min, max, required_keywords]`, because it rides the ADR-85 seed
|
|
16
|
+
# bundle through `Marshal`: `max` is nil when a rest or `...` parameter leaves it unbounded, and
|
|
17
|
+
# `required_keywords` is true when the def takes a required keyword — a call that passes positionals
|
|
18
|
+
# only then fails on the missing keyword whatever its count, which is a different diagnostic than the
|
|
19
|
+
# one this envelope backs, so the rule declines on it the way `compute_arity_envelope` declines an RBS
|
|
20
|
+
# function with required keywords.
|
|
21
|
+
#
|
|
22
|
+
# {OPAQUE} is the value every other contribution to a name records — `define_method`, `attr_*`, an alias,
|
|
23
|
+
# a macro that names the method, a second `def` whose envelope differs — and it absorbs under {.merge}.
|
|
24
|
+
# That makes the per-name fold a join: order-independent, idempotent, and unable to keep one of several
|
|
25
|
+
# shapes silently.
|
|
26
|
+
module ParameterEnvelope
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
OPAQUE = :opaque
|
|
30
|
+
|
|
31
|
+
# The envelope of `def_node`'s own parameter list. Block, keyword-optional and keyword-rest parameters
|
|
32
|
+
# never change how many positionals the method accepts; `super`, `yield` and `&block` in the body do
|
|
33
|
+
# not either, so the body is not read at all.
|
|
34
|
+
def of(def_node)
|
|
35
|
+
params = def_node.parameters
|
|
36
|
+
return [0, 0, false].freeze if params.nil?
|
|
37
|
+
|
|
38
|
+
required = params.requireds.size + params.posts.size
|
|
39
|
+
[required, maximum(params, required), required_keywords?(params)].freeze
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def maximum(params, required)
|
|
43
|
+
return nil if params.rest || params.keyword_rest.is_a?(Prism::ForwardingParameterNode)
|
|
44
|
+
|
|
45
|
+
required + params.optionals.size
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def required_keywords?(params)
|
|
49
|
+
params.keywords.any?(Prism::RequiredKeywordParameterNode)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The join of two contributions to one name: equal envelopes stay, anything else is {OPAQUE}.
|
|
53
|
+
def merge(left, right)
|
|
54
|
+
return right if left.nil?
|
|
55
|
+
return left if right.nil?
|
|
56
|
+
|
|
57
|
+
left == right ? left : OPAQUE
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def opaque?(envelope) = envelope == OPAQUE
|
|
61
|
+
|
|
62
|
+
# Folds `overlay`'s per-class entries into `base` with {.merge}, returning a new table. Both are
|
|
63
|
+
# `{class name => {key => envelope}}`.
|
|
64
|
+
def merge_tables(base, overlay)
|
|
65
|
+
return overlay if base.nil? || base.empty?
|
|
66
|
+
return base if overlay.nil? || overlay.empty?
|
|
67
|
+
|
|
68
|
+
base.merge(overlay) { |_class, left, right| left.merge(right) { |_key, a, b| merge(a, b) } }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
data/lib/rigor/source.rb
CHANGED
|
@@ -825,24 +825,72 @@ module Rigor
|
|
|
825
825
|
|
|
826
826
|
return [top] if flattened.any?(Top)
|
|
827
827
|
|
|
828
|
-
|
|
828
|
+
absorb_contained_members(unique_members(flattened))
|
|
829
|
+
end
|
|
830
|
+
|
|
831
|
+
# Drops every member another member already contains, so the join after a guard names one set
|
|
832
|
+
# instead of several readings of it. The normative relation is
|
|
833
|
+
# docs/type-specification/normalization.md § "Member absorption"; {absorbed_by?} is that list.
|
|
834
|
+
#
|
|
835
|
+
# Only a `FloatRange`, `Tuple` or `HashShape` member can be dropped, so the O(n²) scan is
|
|
836
|
+
# skipped for every union that carries none of them — this runs on the hot path of every join.
|
|
837
|
+
#
|
|
838
|
+
# The `!absorbed_by?(other, member)` re-check costs one predicate call per pair that absorbed
|
|
839
|
+
# at all, and it is what makes "both arms dropped" unrepresentable should a future clause make
|
|
840
|
+
# the relation symmetric on a pair of distinct members.
|
|
841
|
+
def absorb_contained_members(members)
|
|
842
|
+
return members unless members.any? { |m| absorbable_member?(m) }
|
|
843
|
+
|
|
844
|
+
members.reject do |member|
|
|
845
|
+
absorbable_member?(member) &&
|
|
846
|
+
members.any? do |other|
|
|
847
|
+
!other.equal?(member) && absorbed_by?(member, other) && !absorbed_by?(other, member)
|
|
848
|
+
end
|
|
849
|
+
end
|
|
829
850
|
end
|
|
830
851
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
# later guard would narrow member by member into a growing union (`Float[...1.0] |
|
|
835
|
-
# Float[0.0...1.0]`) that names one set. `IntegerRange` keeps its pre-ADR-109 behaviour
|
|
836
|
-
# (`Integer | Integer[0..5]` stays as written) because both of its edges narrow.
|
|
837
|
-
def absorb_float_ranges(members)
|
|
838
|
-
return members unless members.any?(FloatRange)
|
|
852
|
+
def absorbable_member?(type)
|
|
853
|
+
type.is_a?(FloatRange) || type.is_a?(Tuple) || type.is_a?(HashShape)
|
|
854
|
+
end
|
|
839
855
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
856
|
+
# `narrower`'s inhabitants are a subset of `wider`'s under a rule the union already applies to
|
|
857
|
+
# a direct member. The structural clauses only lift that same relation element-wise over a
|
|
858
|
+
# fixed spine; none of them introduces an absorption the flat union does not already perform,
|
|
859
|
+
# which is why a value-pinned element never collapses (normalization.md keeps `1 | Integer`,
|
|
860
|
+
# so `[1, String] | [Integer, String]` keeps both arms) and an `IntegerRange` or `Refined`
|
|
861
|
+
# element never does either.
|
|
862
|
+
def absorbed_by?(narrower, wider)
|
|
863
|
+
return true if wider.is_a?(Top) || narrower.is_a?(Bot)
|
|
864
|
+
return true if listed_in_union?(narrower, wider)
|
|
865
|
+
|
|
866
|
+
case narrower
|
|
867
|
+
when FloatRange then float_range_absorbed_by?(narrower, wider)
|
|
868
|
+
when Tuple then wider.is_a?(Tuple) && tuple_absorbed_by?(narrower, wider)
|
|
869
|
+
when HashShape then wider.is_a?(HashShape) && hash_shape_absorbed_by?(narrower, wider)
|
|
870
|
+
when Union then union_absorbed_by?(narrower, wider)
|
|
871
|
+
else false
|
|
843
872
|
end
|
|
844
873
|
end
|
|
845
874
|
|
|
875
|
+
# Flatten-and-dedupe read one level down: the member is already one of the arms `wider` lists.
|
|
876
|
+
def listed_in_union?(narrower, wider)
|
|
877
|
+
wider.is_a?(Union) && wider.members.any? { |member| element_absorbed_or_equal?(narrower, member) }
|
|
878
|
+
end
|
|
879
|
+
|
|
880
|
+
def union_absorbed_by?(narrower, wider)
|
|
881
|
+
narrower.members.all? { |member| element_absorbed_or_equal?(member, wider) }
|
|
882
|
+
end
|
|
883
|
+
|
|
884
|
+
def element_absorbed_or_equal?(narrower, wider)
|
|
885
|
+
narrower.equal?(wider) || narrower == wider || absorbed_by?(narrower, wider)
|
|
886
|
+
end
|
|
887
|
+
|
|
888
|
+
# ADR-109 WD5 — a `FloatRange` is absorbed by a bare `Nominal[Float]` or by a `FloatRange` that
|
|
889
|
+
# contains it. The truthy edge of `x > c` is a range and the falsy edge keeps `Float`, so the
|
|
890
|
+
# join after every Float guard would otherwise carry both and every later guard would narrow
|
|
891
|
+
# member by member into a growing union (`Float[...1.0] | Float[0.0...1.0]`) that names one set.
|
|
892
|
+
# `IntegerRange` keeps its pre-ADR-109 behaviour (`Integer | Integer[0..5]` stays as written)
|
|
893
|
+
# because both of its edges narrow.
|
|
846
894
|
def float_range_absorbed_by?(range, other)
|
|
847
895
|
case other
|
|
848
896
|
when Nominal then other.class_name == "Float" && other.type_args.empty?
|
|
@@ -851,6 +899,34 @@ module Rigor
|
|
|
851
899
|
end
|
|
852
900
|
end
|
|
853
901
|
|
|
902
|
+
# Differing arity is never absorbed: `[A]` and `[A, B]` have disjoint inhabitants, so neither
|
|
903
|
+
# arm is a reading of the other.
|
|
904
|
+
def tuple_absorbed_by?(narrower, wider)
|
|
905
|
+
return false unless narrower.elements.size == wider.elements.size
|
|
906
|
+
|
|
907
|
+
narrower.elements.each_with_index.all? do |element, index|
|
|
908
|
+
element_absorbed_or_equal?(element, wider.elements[index])
|
|
909
|
+
end
|
|
910
|
+
end
|
|
911
|
+
|
|
912
|
+
def hash_shape_absorbed_by?(narrower, wider)
|
|
913
|
+
return false unless hash_shape_spine_equal?(narrower, wider)
|
|
914
|
+
|
|
915
|
+
narrower.pairs.all? { |key, value| element_absorbed_or_equal?(value, wider.pairs[key]) }
|
|
916
|
+
end
|
|
917
|
+
|
|
918
|
+
# The spine is everything but the value types: the key set and the openness / optionality /
|
|
919
|
+
# read-only policy. Two shapes that disagree on any of it describe differently-shaped hashes,
|
|
920
|
+
# and an element-wise reading of their values would not be a containment.
|
|
921
|
+
def hash_shape_spine_equal?(narrower, wider)
|
|
922
|
+
narrower.pairs.size == wider.pairs.size &&
|
|
923
|
+
narrower.extra_keys == wider.extra_keys &&
|
|
924
|
+
narrower.required_keys == wider.required_keys &&
|
|
925
|
+
narrower.optional_keys == wider.optional_keys &&
|
|
926
|
+
narrower.read_only_keys == wider.read_only_keys &&
|
|
927
|
+
narrower.pairs.each_key.all? { |key| wider.pairs.key?(key) }
|
|
928
|
+
end
|
|
929
|
+
|
|
854
930
|
def unique_members(types)
|
|
855
931
|
types.each_with_object([]) do |type, unique|
|
|
856
932
|
unique << type unless unique.any? { |member| member.equal?(type) || member == type }
|
|
@@ -70,6 +70,7 @@ module Rigor
|
|
|
70
70
|
"Array" => ->(removed) { removed.is_a?(Tuple) && removed.elements.empty? },
|
|
71
71
|
"Hash" => ->(removed) { removed.is_a?(HashShape) && removed.pairs.empty? }
|
|
72
72
|
}.freeze
|
|
73
|
+
Ractor.make_shareable(EMPTY_WITNESS_PREDICATES)
|
|
73
74
|
private_constant :EMPTY_WITNESS_PREDICATES
|
|
74
75
|
|
|
75
76
|
def top
|
data/lib/rigor/type/refined.rb
CHANGED
|
@@ -176,6 +176,7 @@ module Rigor
|
|
|
176
176
|
# recovered by inspecting an arbitrary string.
|
|
177
177
|
literal_string: ->(v) { v.is_a?(String) }
|
|
178
178
|
}.freeze
|
|
179
|
+
Ractor.make_shareable(PREDICATES)
|
|
179
180
|
|
|
180
181
|
# Maps `[base_class_name, predicate_id]` pairs to their kebab-case canonical name. Registered
|
|
181
182
|
# shapes print through `describe`; unregistered combinations fall back to the operator form.
|
data/lib/rigor/version.rb
CHANGED
data/lib/rigor.rb
CHANGED
|
@@ -10,6 +10,7 @@ require_relative "rigor/environment"
|
|
|
10
10
|
require_relative "rigor/rbs_extended"
|
|
11
11
|
require_relative "rigor/testing"
|
|
12
12
|
require_relative "rigor/inference/budget_trace"
|
|
13
|
+
require_relative "rigor/inference/external_ancestor_resolution"
|
|
13
14
|
require_relative "rigor/inference/dynamic_origin"
|
|
14
15
|
require_relative "rigor/inference/fallback"
|
|
15
16
|
require_relative "rigor/inference/fallback_tracer"
|
|
@@ -29,6 +29,7 @@ module Rigor
|
|
|
29
29
|
|
|
30
30
|
# The `_later` twins enqueue an ActiveJob that broadcasts, so they carry the enqueue meaning too.
|
|
31
31
|
TURBO_LATER_SELECTORS = TURBO_SELECTORS.map { |name| "#{name.delete_suffix('_to')}_later_to" }.freeze
|
|
32
|
+
Ractor.make_shareable(TURBO_LATER_SELECTORS)
|
|
32
33
|
|
|
33
34
|
LATER = (BROADCASTING + ["rails.activejob.enqueue", "job.enqueue"]).freeze
|
|
34
35
|
|