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,289 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../inference/rbs_type_translator"
|
|
4
|
+
require_relative "type_elaborator"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module SigGen
|
|
8
|
+
# Issue #1002 — the WRITE direction of a project's own type aliases.
|
|
9
|
+
#
|
|
10
|
+
# `Generator`'s `alias_expander:` wiring (PR #1000) reads a declared alias: `-> Type::t` in the project's
|
|
11
|
+
# `sig/` becomes the 22-member union so a proposal can be compared against it. Nothing went the other way,
|
|
12
|
+
# so a proposal that inferred exactly that union printed all 22 members — output an author cannot paste,
|
|
13
|
+
# because review rewrites it back into the alias the project already declares.
|
|
14
|
+
#
|
|
15
|
+
# This index answers the reverse lookup: given the RBS a proposal erased to, is there a project-declared
|
|
16
|
+
# alias whose own expansion erases to the same member SET, close enough to the method's own namespace to
|
|
17
|
+
# be the name a reader expects? The comparison is deliberately a set, not a string: a union missing one
|
|
18
|
+
# arm is a different type and must keep printing in full.
|
|
19
|
+
#
|
|
20
|
+
# A wrong alias name is worse than a long union — it is an RBS claim the author did not make and may not
|
|
21
|
+
# hold — so every rule below refuses rather than guesses.
|
|
22
|
+
#
|
|
23
|
+
# - **Project aliases only.** The candidate `.rbs` files come from the configuration's OWN resolved
|
|
24
|
+
# `signature_paths:` (what `Environment.for_project` calls `resolved_paths`), passed in by the caller.
|
|
25
|
+
# Deliberately NOT `RbsLoader#signature_paths`: that is `resolved_paths` PLUS plugin signature paths,
|
|
26
|
+
# bundler-discovered gem `sig/` directories, the `rbs collection` tree and Rigor's own gem overlays.
|
|
27
|
+
# Keyed off the loader, a project that merely bundles a gem shipping RBS aliases would find that gem's
|
|
28
|
+
# vocabulary in its proposals. A loaded plugin's own `sig/` is subtracted even when the project wired
|
|
29
|
+
# it through `signature_paths:` itself (the #697 shape) — it is the plugin's vocabulary either way.
|
|
30
|
+
# - **Lossless alias bodies only.** Neither translation nor erasure is injective. `Type::Intersection`
|
|
31
|
+
# erases to its FIRST member, `Refined` and `Difference` to their base, a proc type translates to a
|
|
32
|
+
# bare `Proc`, and an RBS intersection may not even survive translation. An alias
|
|
33
|
+
# `type inter = (Integer & Comparable) | String` therefore reaches the renderer indistinguishable from
|
|
34
|
+
# `Integer | String`, and folding a proposal that really is `Integer | String` into `inter` would claim
|
|
35
|
+
# an intersection the method never returns — RBS an author cannot accept. So the alias body is checked
|
|
36
|
+
# on BOTH sides of translation, each an ALLOW-list ({.lossless_rbs?} over the RBS AST,
|
|
37
|
+
# {.lossless?} over the translated carriers). Anything absent — including a form added after this was
|
|
38
|
+
# written, and a nested alias reference, whose own body this walk does not see — disqualifies the
|
|
39
|
+
# alias. Refusing to fold is always safe; naming the wrong type is not.
|
|
40
|
+
# - **Namespace proximity.** Type equality is not enough to make a name right for the reader.
|
|
41
|
+
# `:positive | :negative` is type-equal to this repository's `Analysis::FactStore::polarity` from
|
|
42
|
+
# anywhere in the tree, and naming it inside an unrelated class is a worse proposal than the union. An
|
|
43
|
+
# alias is only offered to a method whose owner is inside the alias's own namespace, and the NEAREST
|
|
44
|
+
# such alias wins.
|
|
45
|
+
# - **Unions only.** An alias whose expansion is a single type (`type name = String`) is never folded;
|
|
46
|
+
# the noise the issue is about is specifically the long union.
|
|
47
|
+
# - **Non-generic only.** A generic alias's expansion depends on its arguments, so there is no fixed
|
|
48
|
+
# member set to key on.
|
|
49
|
+
#
|
|
50
|
+
# A project with no aliases builds an empty index, and an empty index is a no-op: `#fold` returns its
|
|
51
|
+
# argument, so the rendered output is byte-identical to the pre-#1002 output.
|
|
52
|
+
class AliasIndex
|
|
53
|
+
# The carriers whose `erase_to_rbs` is faithful enough to key on. Everything absent is disqualifying,
|
|
54
|
+
# which is why this is a list of names resolved lazily rather than a `case`: a new carrier is excluded
|
|
55
|
+
# until someone establishes its erasure round-trips.
|
|
56
|
+
LOSSLESS_CARRIERS = %i[Top Bot Constant Nominal Singleton Union Tuple Maybe IntegerRange FloatRange].freeze
|
|
57
|
+
private_constant :LOSSLESS_CARRIERS
|
|
58
|
+
|
|
59
|
+
# A proc type translates to a bare `Proc` nominal, losing the signature, so an alias mentioning one
|
|
60
|
+
# cannot be keyed on its erasure either.
|
|
61
|
+
PROC_CLASS_NAMES = ["Proc", "::Proc"].freeze
|
|
62
|
+
private_constant :PROC_CLASS_NAMES
|
|
63
|
+
|
|
64
|
+
# The RBS forms whose meaning survives translation AND erasure. Notable absences, each a form that
|
|
65
|
+
# reaches the renderer looking like something narrower or wider than it is: `Intersection`, `Proc`,
|
|
66
|
+
# `Record`, `Interface`, `Alias` (its body is expanded during translation, out of this walk's sight),
|
|
67
|
+
# `untyped` / `void` / `top`, and every context-dependent base (`self`, `instance`, `class`).
|
|
68
|
+
LOSSLESS_RBS_TYPES = %w[
|
|
69
|
+
RBS::Types::ClassInstance RBS::Types::ClassSingleton RBS::Types::Union RBS::Types::Optional
|
|
70
|
+
RBS::Types::Tuple RBS::Types::Literal RBS::Types::Bases::Nil RBS::Types::Bases::Bool
|
|
71
|
+
RBS::Types::Bases::Bottom
|
|
72
|
+
].freeze
|
|
73
|
+
private_constant :LOSSLESS_RBS_TYPES
|
|
74
|
+
|
|
75
|
+
# @return an index with no entries — {#fold} is the identity.
|
|
76
|
+
def self.empty
|
|
77
|
+
new({})
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @param signature_paths — the project's OWN resolved signature paths.
|
|
81
|
+
# @param excluded_paths — signature paths that reached the list from somewhere other than the
|
|
82
|
+
# author: a loaded plugin's own `sig/`, which #697 lets a project wire through `signature_paths:`
|
|
83
|
+
# as well. Those aliases are the plugin's vocabulary, not this project's.
|
|
84
|
+
# Fail-soft in the ADR-5 sense: a loader that cannot enumerate aliases, and any alias whose body fails
|
|
85
|
+
# to translate, yields an entry-free index rather than an error, because a rendering nicety must never
|
|
86
|
+
# fail a run.
|
|
87
|
+
def self.build(environment:, signature_paths:, excluded_paths: [])
|
|
88
|
+
loader = environment&.rbs_loader
|
|
89
|
+
return empty unless loader.respond_to?(:each_type_alias_decl)
|
|
90
|
+
|
|
91
|
+
project_files = project_sig_files(signature_paths) - project_sig_files(excluded_paths)
|
|
92
|
+
return empty if project_files.empty?
|
|
93
|
+
|
|
94
|
+
new(collect_entries(loader, project_files, environment))
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Every `.rbs` file under the given signature paths, absolute. Directories only, matching `RbsLoader`,
|
|
98
|
+
# which skips a `signature_paths:` entry that is not a directory.
|
|
99
|
+
def self.project_sig_files(signature_paths)
|
|
100
|
+
Array(signature_paths).flat_map do |path|
|
|
101
|
+
dir = path.is_a?(Pathname) ? path : Pathname(path.to_s)
|
|
102
|
+
next [] unless dir.directory?
|
|
103
|
+
|
|
104
|
+
Dir.glob(dir.join("**", "*.rbs").to_s).map { |p| File.expand_path(p) }
|
|
105
|
+
end.to_set
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# `{ member set => entries sorted by declaration position }`. Every alias matching a set is kept, not
|
|
109
|
+
# just one: which of them is the right NAME depends on the method being rendered, and that is only
|
|
110
|
+
# known at {#fold} time.
|
|
111
|
+
def self.collect_entries(loader, project_files, environment)
|
|
112
|
+
entries = Hash.new { |h, k| h[k] = [] }
|
|
113
|
+
loader.each_type_alias_decl do |type_name, decl_entry|
|
|
114
|
+
entry = build_entry(loader, project_files, environment, type_name, decl_entry)
|
|
115
|
+
entries[entry[:members]] << entry unless entry.nil?
|
|
116
|
+
end
|
|
117
|
+
entries.each_value { |list| list.sort_by! { |entry| entry[:order] } }
|
|
118
|
+
entries.default_proc = nil
|
|
119
|
+
entries
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def self.build_entry(loader, project_files, environment, type_name, decl_entry)
|
|
123
|
+
decl = decl_entry.respond_to?(:decl) ? decl_entry.decl : nil
|
|
124
|
+
return nil if decl.nil?
|
|
125
|
+
return nil if decl.respond_to?(:type_params) && !Array(decl.type_params).empty?
|
|
126
|
+
|
|
127
|
+
file = declaration_file(decl)
|
|
128
|
+
return nil unless file && project_files.include?(file)
|
|
129
|
+
|
|
130
|
+
members = expanded_members(loader, environment, decl)
|
|
131
|
+
return nil if members.nil?
|
|
132
|
+
|
|
133
|
+
# The ABSOLUTE spelling. RBS resolves a relative type name innermost-first, so a bare
|
|
134
|
+
# `Deep::Inner::mood` written inside `Deep::Inner` would rebind if the project also declared a
|
|
135
|
+
# `Deep::Inner::Deep`. The leading `::` says exactly which alias the proposal means.
|
|
136
|
+
relative = type_name.to_s.delete_prefix("::")
|
|
137
|
+
{ members: members, name: "::#{relative}", namespace: relative.split("::")[0..-2].join("::"),
|
|
138
|
+
order: [file, declaration_line(decl), relative] }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def self.declaration_file(decl)
|
|
142
|
+
buffer_name = decl.location&.buffer&.name
|
|
143
|
+
return nil if buffer_name.nil?
|
|
144
|
+
|
|
145
|
+
File.expand_path(buffer_name.to_s)
|
|
146
|
+
rescue StandardError
|
|
147
|
+
nil
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def self.declaration_line(decl)
|
|
151
|
+
decl.location&.start_line || 0
|
|
152
|
+
rescue StandardError
|
|
153
|
+
0
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# nil whenever the alias is not usable as a rendering target: an untranslatable body, a body carrying a
|
|
157
|
+
# carrier whose erasure loses information, a body that erases to `untyped` (the gradual-consistency
|
|
158
|
+
# collapse in `Type::Union#erase_to_rbs`, which would otherwise key every such alias on the same
|
|
159
|
+
# one-member set), or a single-type body.
|
|
160
|
+
def self.expanded_members(loader, environment, decl)
|
|
161
|
+
return nil unless lossless_rbs?(decl.type)
|
|
162
|
+
|
|
163
|
+
translated = Inference::RbsTypeTranslator.translate(
|
|
164
|
+
decl.type, self_type: nil, instance_type: nil, type_vars: {}, alias_expander: loader
|
|
165
|
+
)
|
|
166
|
+
return nil if translated.nil? || !lossless?(translated)
|
|
167
|
+
|
|
168
|
+
erased = TypeElaborator.elaborate(translated, environment: environment).erase_to_rbs
|
|
169
|
+
members = split_top_level(erased)
|
|
170
|
+
members.size < 2 ? nil : members
|
|
171
|
+
rescue StandardError
|
|
172
|
+
nil
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# The RBS-AST half of the check, and the one that catches an intersection: `(Integer & Comparable)`
|
|
176
|
+
# does not necessarily survive translation as a `Type::Intersection`, so by the time the carrier walk
|
|
177
|
+
# runs there is nothing left to notice.
|
|
178
|
+
def self.lossless_rbs?(rbs_type)
|
|
179
|
+
return false unless LOSSLESS_RBS_TYPES.include?(rbs_type.class.name)
|
|
180
|
+
return true unless rbs_type.respond_to?(:each_type)
|
|
181
|
+
|
|
182
|
+
children = []
|
|
183
|
+
rbs_type.each_type { |child| children << child }
|
|
184
|
+
children.all? { |child| lossless_rbs?(child) }
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# The carrier half, a backstop for what translation itself invents (a `Refined` or `Difference` from a
|
|
188
|
+
# `rigor:` envelope, a bare `Proc` from a proc type reached some other way). Walks the TRANSLATED
|
|
189
|
+
# tree, so it also sees a construct the RBS walk could not reach.
|
|
190
|
+
def self.lossless?(type)
|
|
191
|
+
carrier = type.class.name.to_s.split("::").last&.to_sym
|
|
192
|
+
return false unless LOSSLESS_CARRIERS.include?(carrier)
|
|
193
|
+
return false if carrier == :Nominal && PROC_CLASS_NAMES.include?(type.class_name.to_s)
|
|
194
|
+
|
|
195
|
+
children_of(type).all? { |child| lossless?(child) }
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def self.children_of(type)
|
|
199
|
+
case type
|
|
200
|
+
when Type::Union then type.members
|
|
201
|
+
when Type::Tuple then type.elements
|
|
202
|
+
when Type::Maybe then [type.value_type]
|
|
203
|
+
when Type::Nominal then type.type_args
|
|
204
|
+
else []
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Splits an erased RBS string on its TOP-LEVEL `|` separators. Depth tracking keeps a nested union
|
|
209
|
+
# (`Array[A | B]`) whole, and the quote state keeps a literal string type (`"a | b"`) whole.
|
|
210
|
+
def self.split_top_level(rendered)
|
|
211
|
+
members = []
|
|
212
|
+
current = +""
|
|
213
|
+
depth = 0
|
|
214
|
+
quote = nil
|
|
215
|
+
rendered.each_char do |ch|
|
|
216
|
+
if quote
|
|
217
|
+
current << ch
|
|
218
|
+
quote = nil if ch == quote
|
|
219
|
+
next
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
case ch
|
|
223
|
+
when '"', "'" then quote = ch
|
|
224
|
+
when "(", "[", "{" then depth += 1
|
|
225
|
+
when ")", "]", "}" then depth -= 1
|
|
226
|
+
when "|"
|
|
227
|
+
if depth.zero?
|
|
228
|
+
members << current.strip
|
|
229
|
+
current = +""
|
|
230
|
+
next
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
current << ch
|
|
234
|
+
end
|
|
235
|
+
members << current.strip
|
|
236
|
+
members.reject(&:empty?).to_set
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# The namespaces a method owned by `owner` can name an alias from, INNERMOST FIRST: its own namespace,
|
|
240
|
+
# then each enclosing one, ending at the top level. `nil`/`""` for a top-level owner still reaches the
|
|
241
|
+
# top-level namespace, which is where an adopting project's `type` declarations usually live.
|
|
242
|
+
def self.namespace_chain(owner)
|
|
243
|
+
segments = owner.to_s.split("::")
|
|
244
|
+
segments.size.downto(0).map { |i| segments[0...i].join("::") }
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def initialize(entries)
|
|
248
|
+
@entries = entries
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# @param owner — the class or module the rendered member belongs to; `nil` renders unchanged,
|
|
252
|
+
# because proximity cannot be judged without it.
|
|
253
|
+
# @return the alias name when `rendered` is exactly some in-scope project alias's expansion,
|
|
254
|
+
# else `rendered` unchanged. Callers can apply this unconditionally.
|
|
255
|
+
def fold(rendered, owner)
|
|
256
|
+
return rendered if @entries.empty? || owner.nil? || !rendered.include?("|")
|
|
257
|
+
|
|
258
|
+
members = self.class.split_top_level(rendered)
|
|
259
|
+
return rendered if members.size < 2
|
|
260
|
+
|
|
261
|
+
candidates = @entries[members]
|
|
262
|
+
return rendered if candidates.nil? || candidates.empty?
|
|
263
|
+
|
|
264
|
+
nearest(candidates, owner) || rendered
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def empty?
|
|
268
|
+
@entries.empty?
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
private
|
|
272
|
+
|
|
273
|
+
# "Most specific, then declaration order", the rule the issue asked to have stated and pinned. The
|
|
274
|
+
# chain index is the distance from the method's own namespace outwards, so an alias declared beside the
|
|
275
|
+
# method beats one declared in an enclosing namespace, and `candidates` is already in declaration order
|
|
276
|
+
# ((file, line, name)), which `min_by` keeps as the tie-break. An alias whose namespace does not
|
|
277
|
+
# enclose the owner at all is not a candidate: it names the right type under a name this reader has no
|
|
278
|
+
# reason to expect.
|
|
279
|
+
def nearest(candidates, owner)
|
|
280
|
+
chain = self.class.namespace_chain(owner)
|
|
281
|
+
best = candidates.each_with_index.filter_map do |entry, position|
|
|
282
|
+
distance = chain.index(entry[:namespace])
|
|
283
|
+
distance.nil? ? nil : [[distance, position], entry[:name]]
|
|
284
|
+
end.min_by(&:first)
|
|
285
|
+
best&.last
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module Rigor
|
|
4
4
|
module SigGen
|
|
5
|
-
# The
|
|
6
|
-
#
|
|
5
|
+
# The classifications a candidate method falls into after the generator has compared the inferred return
|
|
6
|
+
# type — or, for a member declared inline, the inline declaration (ADR-112 WD4) — against the project's
|
|
7
|
+
# existing RBS.
|
|
7
8
|
#
|
|
8
9
|
# The strings are the diagnostic-family identifiers ADR-14 reserves under `sig.*`; the MVP carries them as
|
|
9
10
|
# plain symbols on the method candidate and renders the matching identifier in JSON / text output. They are
|
|
@@ -15,16 +16,22 @@ module Rigor
|
|
|
15
16
|
TIGHTER_RETURN = :tighter_return
|
|
16
17
|
EQUIVALENT = :equivalent
|
|
17
18
|
SKIPPED = :skipped
|
|
19
|
+
# ADR-112 WD4 — a member declared inline by `# @rbs` / `#:` whose `sig/` declaration disagrees with it,
|
|
20
|
+
# under `--overwrite`: the whole `sig/` member is replaced by the inline line. Produced only when the run
|
|
21
|
+
# asked for `--overwrite`; without it the same member is refused (`sig.skipped.inline-differs`), because
|
|
22
|
+
# neither side is presumed right and a slot-by-slot mix of two declarations need not be a declaration.
|
|
23
|
+
INLINE_OVERWRITE = :inline_overwrite
|
|
18
24
|
|
|
19
25
|
# The classifications that actually produce a line in a generated `sig/`. Consulted by the renderer,
|
|
20
26
|
# the writer and the generator's own post-passes; it lived as a private constant in the first two,
|
|
21
27
|
# which is one fork too many for a list this load-bearing.
|
|
22
|
-
EMITTABLE = [NEW_FILE, NEW_METHOD, TIGHTER_RETURN].freeze
|
|
28
|
+
EMITTABLE = [NEW_FILE, NEW_METHOD, TIGHTER_RETURN, INLINE_OVERWRITE].freeze
|
|
23
29
|
|
|
24
30
|
DIAGNOSTIC_IDS = {
|
|
25
31
|
NEW_FILE => "sig.generated.new-file",
|
|
26
32
|
NEW_METHOD => "sig.generated.new-method",
|
|
27
|
-
TIGHTER_RETURN => "sig.generated.tighter-return"
|
|
33
|
+
TIGHTER_RETURN => "sig.generated.tighter-return",
|
|
34
|
+
INLINE_OVERWRITE => "sig.generated.inline-overwrite"
|
|
28
35
|
}.freeze
|
|
29
36
|
|
|
30
37
|
SKIP_DIAGNOSTIC_IDS = {
|
|
@@ -40,7 +47,17 @@ module Rigor
|
|
|
40
47
|
unresolvable_superclass: "sig.skipped.unresolvable-superclass",
|
|
41
48
|
# Issue #744 — a project subclass overrides this method and its override is NOT emitted, so the
|
|
42
49
|
# declaration would be inherited by a subclass it does not describe.
|
|
43
|
-
overridden_by_unsigned_subclass: "sig.skipped.overridden-by-unsigned-subclass"
|
|
50
|
+
overridden_by_unsigned_subclass: "sig.skipped.overridden-by-unsigned-subclass",
|
|
51
|
+
# ADR-112 WD4 — `sig_gen.inline_declared: skip` is set and the inline reader declares this member, so
|
|
52
|
+
# a `sig/` copy would be a second declaration of it for a Steep that reads the inline annotations too.
|
|
53
|
+
inline_declared: "sig.skipped.inline-declared",
|
|
54
|
+
# ADR-112 WD4 — the member's class, or one it is nested in, is generic by an inline declaration (`# @rbs
|
|
55
|
+
# generic T`) and `sig/` does not declare it yet. sig-gen writes no class type parameters, and a header
|
|
56
|
+
# without them fails the class's definition build.
|
|
57
|
+
inline_generic_class: "sig.skipped.inline-generic-class",
|
|
58
|
+
# ADR-112 WD4 — the member is declared inline and in `sig/`, and the two disagree. A refusal, not a
|
|
59
|
+
# skip: `--write` and `--check` exit 1 until a person reconciles them or passes `--overwrite`.
|
|
60
|
+
inline_differs: "sig.skipped.inline-differs"
|
|
44
61
|
}.freeze
|
|
45
62
|
end
|
|
46
63
|
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbs"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module SigGen
|
|
7
|
+
# ADR-112 WD4 — whether a method's inline declaration and its `sig/` declaration say the same thing, so that
|
|
8
|
+
# `rigor sig-gen` refuses (or, under `--overwrite`, replaces) only a real disagreement (#1076).
|
|
9
|
+
#
|
|
10
|
+
# Two declarations are compared as types, not text. What is ignored, because RBS gives it no meaning:
|
|
11
|
+
#
|
|
12
|
+
# - parameter names (`(String)` and `(String s)`);
|
|
13
|
+
# - how a union is spelled: member order, `T?` for `T | nil`, `bool` for `true | false`, nesting;
|
|
14
|
+
# - a name written absolute or relative, when both resolve to the same constant — `::Foo` and `Foo` inside
|
|
15
|
+
# `module NS` are the same only if `NS` declares no `Foo` of its own ({#initialize}'s `resolve`).
|
|
16
|
+
#
|
|
17
|
+
# What is kept: overload order (RBS picks the first overload that matches, so `A | B` and `B | A` can
|
|
18
|
+
# answer a call differently), method type parameters as named, and everything else about the shape.
|
|
19
|
+
class DeclarationEquivalence
|
|
20
|
+
# @param resolve — maps a type name as written (`String`, `::Foo`, `Bar::Baz`) to the name it denotes,
|
|
21
|
+
# in the namespace both declarations sit in.
|
|
22
|
+
def initialize(resolve:)
|
|
23
|
+
@resolve = resolve
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def same?(left, right)
|
|
27
|
+
left.size == right.size && left.zip(right).all? { |a, b| method_type(a) == method_type(b) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The overloads with their returns left out, for comparing what an author wrote against a member whose
|
|
31
|
+
# return is inferred.
|
|
32
|
+
def same_parameters?(left, right)
|
|
33
|
+
left.size == right.size && left.zip(right).all? do |a, b|
|
|
34
|
+
method_type(a, with_return: false) == method_type(b, with_return: false)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def method_type(method_type, with_return: true)
|
|
41
|
+
[
|
|
42
|
+
method_type.type_params.map { |param| param.name.to_s },
|
|
43
|
+
function(method_type.type, with_return: with_return),
|
|
44
|
+
block(method_type.block)
|
|
45
|
+
]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def block(block)
|
|
49
|
+
return nil if block.nil?
|
|
50
|
+
|
|
51
|
+
[block.required, function(block.type), block.self_type && type(block.self_type)]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def function(function, with_return: true)
|
|
55
|
+
returned = with_return ? type(function.return_type) : nil
|
|
56
|
+
return [:untyped_function, returned] if function.is_a?(::RBS::Types::UntypedFunction)
|
|
57
|
+
|
|
58
|
+
[
|
|
59
|
+
function.required_positionals.map { |param| type(param.type) },
|
|
60
|
+
function.optional_positionals.map { |param| type(param.type) },
|
|
61
|
+
function.rest_positionals && type(function.rest_positionals.type),
|
|
62
|
+
function.trailing_positionals.map { |param| type(param.type) },
|
|
63
|
+
keywords(function.required_keywords),
|
|
64
|
+
keywords(function.optional_keywords),
|
|
65
|
+
function.rest_keywords && type(function.rest_keywords.type),
|
|
66
|
+
returned
|
|
67
|
+
]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def keywords(map)
|
|
71
|
+
map.map { |name, param| [name.to_s, type(param.type)] }.sort
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def type(node)
|
|
75
|
+
case node
|
|
76
|
+
when ::RBS::Types::Optional, ::RBS::Types::Union, ::RBS::Types::Bases::Bool then union(node)
|
|
77
|
+
when ::RBS::Types::ClassInstance then [:instance, @resolve.call(node.name.to_s), types(node.args)]
|
|
78
|
+
when ::RBS::Types::ClassSingleton then [:singleton, @resolve.call(node.name.to_s)]
|
|
79
|
+
when ::RBS::Types::Interface, ::RBS::Types::Alias then named(node)
|
|
80
|
+
else composite(node)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# An interface or alias is compared by its written name, a leading `::` aside: those names are rarely
|
|
85
|
+
# nested, and the environment question this class asks of a class name has no counterpart for them.
|
|
86
|
+
def named(node)
|
|
87
|
+
[:named, node.name.to_s.delete_prefix("::"), types(node.args)]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def composite(node)
|
|
91
|
+
case node
|
|
92
|
+
when ::RBS::Types::Tuple then [:tuple, types(node.types)]
|
|
93
|
+
when ::RBS::Types::Record then [:record, record_fields(node)]
|
|
94
|
+
when ::RBS::Types::Intersection then [:intersection, types(node.types).sort_by(&:inspect)]
|
|
95
|
+
when ::RBS::Types::Proc then [:proc, function(node.type), block(node.block)]
|
|
96
|
+
else [:other, node.to_s]
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def types(nodes)
|
|
101
|
+
nodes.map { |node| type(node) }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def record_fields(node)
|
|
105
|
+
required = node.fields.map { |key, field| [key.to_s, true, type(field)] }
|
|
106
|
+
optional = node.respond_to?(:optional_fields) ? node.optional_fields : {}
|
|
107
|
+
(required + optional.map { |key, field| [key.to_s, false, type(field)] }).sort_by(&:inspect)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def union(node)
|
|
111
|
+
members = union_members(node).uniq.sort_by(&:inspect)
|
|
112
|
+
members.size == 1 ? members.first : [:union, members]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# `true` / `false` / `nil` canonicalise as their literal spellings do (`[:other, "true"]`), so `bool` and
|
|
116
|
+
# `true | false`, or `T?` and `T | nil`, land on the same members.
|
|
117
|
+
def union_members(node)
|
|
118
|
+
case node
|
|
119
|
+
when ::RBS::Types::Union then node.types.flat_map { |member| union_members(member) }
|
|
120
|
+
when ::RBS::Types::Optional then union_members(node.type) + [[:other, "nil"]]
|
|
121
|
+
when ::RBS::Types::Bases::Bool then [[:other, "true"], [:other, "false"]]
|
|
122
|
+
else [type(node)]
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../effects/config_envelopes"
|
|
4
|
+
require_relative "../effects/label_set"
|
|
5
|
+
require_relative "../effects/method_key"
|
|
6
|
+
require_relative "../effects/summary"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module SigGen
|
|
10
|
+
# ADR-103 WD9 / ADR-14's reserved annotation-emission slot — decides whether a generated signature
|
|
11
|
+
# carries `%a{pure}` or `%a{rigor:v1:effect …}`, and records WHY when it does not.
|
|
12
|
+
#
|
|
13
|
+
# The whole module is one decision applied per method, and every branch of it exists to keep a WRONG
|
|
14
|
+
# annotation off the page. An emitted annotation is not a hint: the effects opt-in reads it back as an
|
|
15
|
+
# envelope and enforces it on the method and on everything the method reaches, so a `%a{pure}` sig-gen
|
|
16
|
+
# invented is a false contract that manufactures `effect.envelope-exceeded` on correct code. That is why
|
|
17
|
+
# the gates below are all one-directional — each of them can only suppress an emission.
|
|
18
|
+
#
|
|
19
|
+
# The gates, in order:
|
|
20
|
+
#
|
|
21
|
+
# 0. **Already declared** — the method's own signature, its class's, an `effects.envelopes:` entry
|
|
22
|
+
# (by `namespace:` OR by `match:`), or an rbs-inline `# @rbs %a{…}` states a bound. The author has
|
|
23
|
+
# spoken and sig-gen has nothing to add; writing its own answer over that would silently replace a
|
|
24
|
+
# contract with an inference. `withheld-declared`. This is a DIFFERENT lane from gate 5: an
|
|
25
|
+
# envelope on the method ITSELF goes to {Effects::EnvelopeCheck} and never enters the summary's
|
|
26
|
+
# `≤` lane, so the table cannot see it. An annotation carrying an unknown label counts: it reads
|
|
27
|
+
# as ⊤ and bounds nothing, but the author still wrote about this method, and replacing their typo
|
|
28
|
+
# with `%a{pure}` would delete the only thing `effect.unknown-label` has to point at.
|
|
29
|
+
# 1. **No summary** — the method is not an effect unit this run collected. Nothing is emitted and nothing
|
|
30
|
+
# is reported: the absence is about the run, not about the method.
|
|
31
|
+
# 2. **Non-exhaustive** ({Effects::Summary#exhaustive?} false) — the summary reads "these effects, and
|
|
32
|
+
# possibly more", which is precisely the claim an envelope must not make. `withheld-non-exhaustive`.
|
|
33
|
+
# 3. **Policy-discharged** — the transitive proven lane and the `effects.tolerated:` judgment disagree,
|
|
34
|
+
# so some part of this method's footprint is only invisible because the project agreed to ignore it.
|
|
35
|
+
# `docs/type-specification/effect-labels.md` § Discharge by policy, invariant 4 ("emission uses
|
|
36
|
+
# undischarged sets") and ADR-10 WD7's "opportunistic shapes never round-trip" are the same rule:
|
|
37
|
+
# a tolerated `telemetry` origin does not earn a written `%a{pure}`. `withheld-tolerated`.
|
|
38
|
+
# 4. **Unclaimed callee** ({Effects::EffectTable::Entry#unclaimed?}) — the method, or something it
|
|
39
|
+
# reaches, called something NOTHING described: no catalogue row, no plugin row, no envelope, and no
|
|
40
|
+
# project definition the closure could read. The summary is exhaustive, because every call
|
|
41
|
+
# RESOLVED — but "every call resolved" and "every callee's footprint is known" are different
|
|
42
|
+
# questions, and only the second one licenses a written bound. `withheld-unclaimed-callee`.
|
|
43
|
+
# 5. **A surviving declared label** ({Effects::EffectTable::Entry#trivial?}) — the proven lane is empty
|
|
44
|
+
# and the `≤` lane is not, which is a method whose callee stated a bound the analyzer never proved
|
|
45
|
+
# away. `%a{pure}` there would contradict a claim the project already carries.
|
|
46
|
+
# `withheld-declared`.
|
|
47
|
+
# 6. **Proven pure** — exhaustive, undischarged, claimed, and nothing outside `{mutate.local}` in
|
|
48
|
+
# either lane. `%a{pure}`, the ecosystem's existing purity spelling (design § 6.3), which hands
|
|
49
|
+
# Steep users better narrowing.
|
|
50
|
+
# 7. **Proven effectful** — the same, with real labels. `%a{rigor:v1:effect …}` only when the caller
|
|
51
|
+
# asked for envelopes; otherwise nothing, because the labelled spelling is Rigor's own and writing
|
|
52
|
+
# it into a project's `sig/` unbidden is a bigger commitment than a purity tag.
|
|
53
|
+
module EffectAnnotation
|
|
54
|
+
# The `sig.*` telemetry identifiers for this emission, alongside {Classification::DIAGNOSTIC_IDS} and
|
|
55
|
+
# {Classification::SKIP_DIAGNOSTIC_IDS}. Documented in `docs/type-specification/diagnostic-policy.md`
|
|
56
|
+
# and `docs/handbook/11-sig-gen.md` with the rest of the `sig.*` family.
|
|
57
|
+
DIAGNOSTIC_IDS = {
|
|
58
|
+
# An annotation was rendered onto the proposal.
|
|
59
|
+
emitted: "sig.effect.emitted",
|
|
60
|
+
# Withheld: the proven footprint and the `effects.tolerated:` judgment disagree.
|
|
61
|
+
withheld_tolerated: "sig.effect.withheld-tolerated",
|
|
62
|
+
# Withheld: some call this method reaches could not be resolved.
|
|
63
|
+
withheld_non_exhaustive: "sig.effect.withheld-non-exhaustive",
|
|
64
|
+
# Withheld: some call it reaches resolved, and nothing anywhere says what that callee does.
|
|
65
|
+
withheld_unclaimed_callee: "sig.effect.withheld-unclaimed-callee",
|
|
66
|
+
# Withheld: the `≤` lane carries a label the proven lane does not already admit.
|
|
67
|
+
withheld_declared: "sig.effect.withheld-declared",
|
|
68
|
+
# Write-time: the target declaration already carries annotations, so its bytes were left alone.
|
|
69
|
+
left_unreadable: "sig.effect.left-unreadable"
|
|
70
|
+
}.freeze
|
|
71
|
+
|
|
72
|
+
module_function
|
|
73
|
+
|
|
74
|
+
# The effect-unit key for a sig-gen candidate, in {Effects::MethodKey}'s spelling.
|
|
75
|
+
def key_for(class_name, method_name, kind)
|
|
76
|
+
return nil if class_name.nil?
|
|
77
|
+
|
|
78
|
+
"#{class_name}#{kind == :singleton ? '.' : '#'}#{method_name}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Turns one {Effects::EffectTable::Entry} into the annotation lines to render and the reason to report.
|
|
82
|
+
#
|
|
83
|
+
# @param declared — whether the method already carries an authored bound of its own
|
|
84
|
+
# ({Effects::EnvelopeIndex}); see gate 0.
|
|
85
|
+
# @return `[Array<String> annotations, Symbol|nil reason]`. An empty
|
|
86
|
+
# array with a `nil` reason is "nothing to say about this method".
|
|
87
|
+
def decide(entry, envelopes: false, declared: false)
|
|
88
|
+
return [[], :withheld_declared] if declared
|
|
89
|
+
return [[], nil] if entry.nil?
|
|
90
|
+
return [[], :withheld_non_exhaustive] unless entry.exhaustive?
|
|
91
|
+
return [[], :withheld_tolerated] unless entry.proven == entry.undischarged
|
|
92
|
+
return [[], :withheld_unclaimed_callee] if entry.unclaimed?
|
|
93
|
+
# `trivial?` is the report's own "nothing to say about this method" test, and it is the emission
|
|
94
|
+
# test too: exhaustive, nothing beyond `mutate.local` proven, and NOTHING SURVIVING in the `≤`
|
|
95
|
+
# lane. The last conjunct is what a bare `proven.subsumed_by?` misses — a callee's imported
|
|
96
|
+
# envelope lands in the declared lane with no taint and no proven label, so a method whose whole
|
|
97
|
+
# body is one `Remote.fetch` reads `[] ≤ [io.net.http]` while proving nothing at all.
|
|
98
|
+
return [["%a{pure}"], :emitted] if entry.trivial?
|
|
99
|
+
return [[], :withheld_declared] if entry.proven.subsumed_by?(Effects::Summary::TRIVIAL_BOUND)
|
|
100
|
+
return [[], nil] unless envelopes
|
|
101
|
+
# A surviving declared label is not spelled either. The envelope grammar carries ONE bound per
|
|
102
|
+
# declaration and has no way to say "proves this, claims that", so writing the proven set alone
|
|
103
|
+
# would publish a bound narrower than the claim the project already carries.
|
|
104
|
+
return [[], :withheld_declared] unless entry.rendered_declared.empty?
|
|
105
|
+
# `top?` is the unbounded reading: it names no labels, so there is no envelope to spell.
|
|
106
|
+
return [[], nil] if entry.proven.top? || entry.proven.empty?
|
|
107
|
+
|
|
108
|
+
[["%a{rigor:v1:effect #{entry.proven.to_a.join(', ')}}"], :emitted]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Per-run lookup over an {Effects::EffectTable}. Built by the CLI when the project's `effects:` opt-in
|
|
112
|
+
# is on, and `nil` otherwise — which is what makes effects-off output byte-identical to a run before
|
|
113
|
+
# this feature existed.
|
|
114
|
+
class Annotator
|
|
115
|
+
NO_ENTRIES = [].freeze
|
|
116
|
+
private_constant :NO_ENTRIES
|
|
117
|
+
|
|
118
|
+
# @param envelope_index — the run's {Effects::EnvelopeIndex}, so
|
|
119
|
+
# gate 0 can see a bound the author already wrote. `nil` skips that half of the gate.
|
|
120
|
+
# @param config_envelopes — the project's
|
|
121
|
+
# `effects.envelopes:` entries. Passed SEPARATELY from the index because the index drops every
|
|
122
|
+
# entry without a `namespace:` (it serves call-site import, and a `match:` glob is a fact about
|
|
123
|
+
# where a class is defined that a per-file collection window cannot see). Emission can see it:
|
|
124
|
+
# a sig-gen candidate carries the defining file it came from, which is exactly what
|
|
125
|
+
# {Effects::ConfigEnvelopes.selects?} matches a `match:` entry against.
|
|
126
|
+
def initialize(table:, envelopes: false, envelope_index: nil, config_envelopes: NO_ENTRIES)
|
|
127
|
+
@table = table
|
|
128
|
+
@envelopes = envelopes
|
|
129
|
+
@envelope_index = envelope_index
|
|
130
|
+
@config_envelopes = config_envelopes
|
|
131
|
+
freeze
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# @param path — the `.rb` file the def came from, for a `match:`-selected
|
|
135
|
+
# `effects.envelopes:` entry.
|
|
136
|
+
# @return `[Array<String>, Symbol|nil]` — see {EffectAnnotation.decide}.
|
|
137
|
+
def annotate(class_name:, method_name:, kind:, path: nil)
|
|
138
|
+
key = EffectAnnotation.key_for(class_name, method_name, kind)
|
|
139
|
+
return [[], nil] if key.nil?
|
|
140
|
+
|
|
141
|
+
EffectAnnotation.decide(
|
|
142
|
+
@table[key], envelopes: @envelopes,
|
|
143
|
+
declared: declared?(class_name, method_name, kind, path)
|
|
144
|
+
)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private
|
|
148
|
+
|
|
149
|
+
def declared?(class_name, method_name, kind, path)
|
|
150
|
+
annotated?(class_name, method_name, kind) || config_selected?(class_name, path)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def annotated?(class_name, method_name, kind)
|
|
154
|
+
return false if @envelope_index.nil?
|
|
155
|
+
|
|
156
|
+
@envelope_index.annotated?(class_name, kind == :singleton, method_name.to_s)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# `namespace:` entries are answered by the index too; asking here as well costs one scan over a
|
|
160
|
+
# handful of entries and keeps the `match:` half from being a second, differently-shaped answer.
|
|
161
|
+
def config_selected?(class_name, path)
|
|
162
|
+
return false if @config_envelopes.empty? || class_name.nil?
|
|
163
|
+
|
|
164
|
+
@config_envelopes.any? { |entry| Effects::ConfigEnvelopes.selects?(entry, class_name, match_paths(path)) }
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Every spelling of the candidate's file a `match:` glob might be written against.
|
|
168
|
+
#
|
|
169
|
+
# A glob is matched `Dir.pwd`-relative (`ConfigEnvelopes.for_classes` defaults `project_root:` to
|
|
170
|
+
# the same thing, so the two sides agree and both agree with `rigor check`), while a candidate's
|
|
171
|
+
# path is whatever the invocation named: relative when `paths:` supplied it, absolute when the
|
|
172
|
+
# user typed one, and neither when it arrives through a symlink — `sig-gen alias/presented.rb`
|
|
173
|
+
# where `alias` links to `lib` is a real invocation, and the glob its author wrote says `lib/`.
|
|
174
|
+
#
|
|
175
|
+
# All three are tried rather than one canonical form. Normalising to the real path alone would
|
|
176
|
+
# break a project whose own `paths:` entry is the symlink (the glob would then be written against
|
|
177
|
+
# the link), and normalising to `Dir.pwd` alone is what let the symlink through.
|
|
178
|
+
def match_paths(path)
|
|
179
|
+
[path, relative_path(path), relative_path(real_path(path))].compact.uniq
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def relative_path(path)
|
|
183
|
+
return nil if path.nil?
|
|
184
|
+
|
|
185
|
+
expanded = File.expand_path(path.to_s)
|
|
186
|
+
root = "#{File.expand_path(Dir.pwd)}#{File::SEPARATOR}"
|
|
187
|
+
expanded.start_with?(root) ? expanded.delete_prefix(root) : nil
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Resolved through every symlink on the way, or nil when the file is gone — a candidate names a
|
|
191
|
+
# file the generator just read, so a miss here is a race rather than a shape to handle.
|
|
192
|
+
def real_path(path)
|
|
193
|
+
return nil if path.nil?
|
|
194
|
+
|
|
195
|
+
File.realpath(path.to_s)
|
|
196
|
+
rescue SystemCallError
|
|
197
|
+
nil
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|