rigortype 0.3.9 → 0.4.1
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 +5 -4
- 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/08-understanding-errors.md +3 -2
- data/docs/handbook/10-sorbet.md +9 -10
- data/docs/handbook/11-sig-gen.md +454 -13
- data/docs/install.md +87 -15
- data/docs/manual/02-cli-reference.md +65 -4
- data/docs/manual/03-configuration.md +12 -0
- data/docs/manual/04-diagnostics.md +4 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/08-skills.md +4 -3
- data/docs/manual/10-mcp-server.md +3 -2
- data/docs/manual/12-caching.md +43 -4
- data/docs/manual/14-rails-quickstart.md +19 -11
- 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 +427 -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 +638 -136
- data/lib/rigor/analysis/dependency_recorder.rb +222 -17
- data/lib/rigor/analysis/fact_store.rb +9 -0
- data/lib/rigor/analysis/incremental.rb +26 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +266 -0
- data/lib/rigor/analysis/incremental_run_slot.rb +171 -0
- data/lib/rigor/analysis/incremental_session.rb +309 -53
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
- 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 +3 -1
- data/lib/rigor/analysis/rule_catalog.rb +127 -0
- data/lib/rigor/analysis/run_cache_key.rb +46 -18
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +168 -12
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
- data/lib/rigor/analysis/runner/pool_coordinator.rb +109 -17
- data/lib/rigor/analysis/runner/project_pre_passes.rb +44 -17
- data/lib/rigor/analysis/runner.rb +603 -54
- data/lib/rigor/analysis/shadow_harness.rb +27 -0
- data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
- data/lib/rigor/analysis/template_unit_collector.rb +303 -0
- data/lib/rigor/analysis/template_unit_paths.rb +93 -0
- data/lib/rigor/analysis/template_unit_positions.rb +293 -0
- data/lib/rigor/analysis/template_units.rb +409 -0
- data/lib/rigor/analysis/worker_session.rb +57 -13
- 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 +45 -1
- data/lib/rigor/cache/engine_source.rb +29 -1
- data/lib/rigor/cache/file_digest.rb +28 -0
- data/lib/rigor/cache/incremental_snapshot.rb +102 -8
- 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 +63 -4
- data/lib/rigor/cache/value_digest.rb +325 -0
- data/lib/rigor/ci_detector.rb +1 -0
- data/lib/rigor/cli/check_command.rb +83 -13
- data/lib/rigor/cli/doc_links.rb +1 -1
- data/lib/rigor/cli/docs_command.rb +4 -4
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/effects_report.rb +2 -2
- 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/unused_command.rb +3 -3
- data/lib/rigor/cli.rb +21 -3
- data/lib/rigor/config_audit.rb +3 -2
- data/lib/rigor/config_path.rb +29 -0
- data/lib/rigor/configuration/severity_profile.rb +24 -3
- data/lib/rigor/configuration.rb +71 -7
- 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 +49 -37
- 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/bundle_sig_discovery.rb +5 -3
- data/lib/rigor/environment/lockfile_resolver.rb +27 -1
- data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
- data/lib/rigor/environment/member_consistency.rb +298 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
- data/lib/rigor/environment/rbs_loader.rb +400 -134
- data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
- data/lib/rigor/environment.rb +104 -96
- 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 +88 -26
- 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 +393 -15
- data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
- data/lib/rigor/inference/content_join.rb +200 -27
- data/lib/rigor/inference/declaration_walk/context.rb +195 -0
- data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
- data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
- data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
- data/lib/rigor/inference/declaration_walk.rb +7 -0
- data/lib/rigor/inference/def_node_resolver.rb +3 -2
- 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/definer_resolution.rb +266 -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 +1665 -538
- data/lib/rigor/inference/external_ancestor_resolution.rb +264 -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 +488 -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 +179 -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 +901 -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/module_function_state.rb +213 -0
- data/lib/rigor/inference/multi_target_binder.rb +345 -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 +674 -124
- 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 +13 -11
- 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/certainty.rb +176 -0
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
- data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
- data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
- data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +130 -0
- data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
- data/lib/rigor/inference/scope_indexer.rb +5913 -701
- data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
- data/lib/rigor/inference/statement_evaluator.rb +3249 -497
- 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 +196 -13
- 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/io_boundary.rb +82 -4
- data/lib/rigor/plugin/isolation.rb +1 -1
- 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/trust_policy.rb +3 -3
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/protection/discovery_seed.rb +20 -9
- data/lib/rigor/protection/kill_signature.rb +0 -1
- data/lib/rigor/protection/mutation_cache.rb +4 -5
- data/lib/rigor/rbs_extended.rb +27 -0
- data/lib/rigor/reflection/constant_ancestors.rb +146 -0
- data/lib/rigor/reflection/constant_path.rb +41 -13
- data/lib/rigor/reflection.rb +73 -107
- data/lib/rigor/scope/discovery_index.rb +351 -2
- data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
- data/lib/rigor/scope/resolution_chain.rb +884 -0
- data/lib/rigor/scope.rb +981 -198
- 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 +580 -47
- 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 +112 -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-ffi/lib/rigor/plugin/ffi.rb +10 -0
- 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 +14 -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 +2 -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 +177 -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 +46 -18
- data/skills/rigor-project-init/references/01-detect.md +13 -9
- data/skills/rigor-project-init/references/02-configure.md +40 -10
- 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 +132 -1
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Plugin
|
|
7
|
+
# One **template unit** — a non-Ruby source file a plugin compiled into Ruby, handed to the engine to
|
|
8
|
+
# be parsed and typed like a file (ADR-16 Tier D, revived by #392 as this seam; design note
|
|
9
|
+
# `docs/design/20260816-effect-labels.md` § 11.3).
|
|
10
|
+
#
|
|
11
|
+
# ADR-16's Tier D declared `external_files:` — "files evaluated as if their body were pasted at a
|
|
12
|
+
# declared call site, with `self` typed as a declared class" — and ADR-60 WD1 removed it for want of a
|
|
13
|
+
# consumer. What Tier D lacked, and what this carries, is a **source transform with a line map** ahead
|
|
14
|
+
# of parsing: the file on disk is not Ruby, so the engine analyses `ruby_source` and reports at the
|
|
15
|
+
# template's own lines.
|
|
16
|
+
#
|
|
17
|
+
# A unit is pure data. The plugin's transform runs ONCE, on the parent, before any analysis; nothing
|
|
18
|
+
# here is a callable, so the unit survives `Marshal` into a fork-pool worker exactly as the macro
|
|
19
|
+
# substrate's value objects do.
|
|
20
|
+
#
|
|
21
|
+
# - `logical_name` — the name the unit is keyed by, independent of the handler: `users/show.html`, not
|
|
22
|
+
# `users/show.html.erb`. Becomes the `view:<logical_name>` effect-unit key, so an ERB → Haml rewrite
|
|
23
|
+
# is not a rename.
|
|
24
|
+
# - `path` — the template file as the user wrote it. Every diagnostic and every source trace names it.
|
|
25
|
+
# - `ruby_source` — the compiled Ruby the engine parses.
|
|
26
|
+
# - `line_map` — `{ ruby_source line => template line }`, 1-based. A line the map does not mention
|
|
27
|
+
# anchors at the nearest mapped line before it, and at line 1 when there is none. **An EMPTY map is
|
|
28
|
+
# the identity, not "no positions"**: every compiled line reports at its own number and the columns
|
|
29
|
+
# pass through untouched. That is correct only for a transform that emits nothing the template did
|
|
30
|
+
# not contain — a transform that prepends so much as one preamble line and omits the map reports
|
|
31
|
+
# every finding off by that many lines. Omit it only for a byte-preserving transform.
|
|
32
|
+
# - `self_type` — the fully-qualified class the body's `self` is typed as, or nil for a bare body.
|
|
33
|
+
# - `locals` — `{ name => type name }`, the parameters the render site passes.
|
|
34
|
+
# - `ivar_seeds` — `{ "@name" => type name }`, the assigns the rendering action set.
|
|
35
|
+
# - `transform_id` — the compiler's identity (`"erubi-1.13"`). Joins the unit digest, so re-compiling
|
|
36
|
+
# with a different compiler re-analyses. Defaults to the declaring plugin's `id@version`.
|
|
37
|
+
#
|
|
38
|
+
# - `suppressed_rules` — rule-id PREFIXES the engine drops for this unit's diagnostics (#393). The
|
|
39
|
+
# per-unit rule posture: a compiled template is not code the user wrote, and a plugin that knows its
|
|
40
|
+
# compiler's idioms is the only party that can say which families of finding are meaningful there.
|
|
41
|
+
# `["call."]` is what an ERB plugin declares while the synthesised receivers are still coarse — the
|
|
42
|
+
# unit still contributes its EFFECTS, which is what the view layer is analysed for first, without
|
|
43
|
+
# the typing half costing the project a diagnostic per line. Empty means "report everything", which
|
|
44
|
+
# is what a plugin whose synthesis it trusts should declare.
|
|
45
|
+
#
|
|
46
|
+
# A type name that resolves to nothing is not an error here: the engine falls back to `Dynamic`, which
|
|
47
|
+
# taints honestly rather than fabricating a type the plugin could not justify (ADR-5).
|
|
48
|
+
class TemplateUnit
|
|
49
|
+
# Bumped whenever the engine changes what it synthesises from a unit — the seeding rules, the
|
|
50
|
+
# scope binding, the effect-unit key spelling. It rides the unit digest, so a change here
|
|
51
|
+
# invalidates every cached run that analysed a unit, exactly as a changed transform does.
|
|
52
|
+
SYNTHESIS_VERSION = 2
|
|
53
|
+
|
|
54
|
+
# The prefix an effect-unit key carries for a template unit. Deliberately not a {Effects::MethodKey}
|
|
55
|
+
# shape: a view has no owner class and no selector, and spelling one would put a method in the
|
|
56
|
+
# snapshot that no call site can name.
|
|
57
|
+
KEY_PREFIX = "view:"
|
|
58
|
+
|
|
59
|
+
IVAR_NAME = /\A@[A-Za-z_][A-Za-z0-9_]*\z/
|
|
60
|
+
private_constant :IVAR_NAME
|
|
61
|
+
|
|
62
|
+
LOCAL_NAME = /\A[a-z_][A-Za-z0-9_]*\z/
|
|
63
|
+
private_constant :LOCAL_NAME
|
|
64
|
+
|
|
65
|
+
# A rule prefix is a dotted family (`call.`) or a whole rule id (`call.undefined-method`); the match
|
|
66
|
+
# is `String#start_with?`, so the former covers the latter. A bare `""` would suppress the unit
|
|
67
|
+
# entirely and is refused.
|
|
68
|
+
RULE_PREFIX = /\A[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*\.?\z/
|
|
69
|
+
private_constant :RULE_PREFIX
|
|
70
|
+
|
|
71
|
+
attr_reader :logical_name, :path, :ruby_source, :line_map, :self_type, :locals, :ivar_seeds,
|
|
72
|
+
:transform_id, :suppressed_rules
|
|
73
|
+
|
|
74
|
+
# Every field is one declared property of the unit; grouping them behind a context object would only
|
|
75
|
+
# move the same list into a second value class.
|
|
76
|
+
def initialize(logical_name:, path:, ruby_source:, line_map: {}, self_type: nil, locals: {}, # rubocop:disable Metrics/ParameterLists
|
|
77
|
+
ivar_seeds: {}, transform_id: nil, suppressed_rules: [])
|
|
78
|
+
@logical_name = validate_string!("logical_name", logical_name)
|
|
79
|
+
@path = validate_string!("path", path)
|
|
80
|
+
@ruby_source = validate_source!(ruby_source)
|
|
81
|
+
@line_map = validate_line_map!(line_map)
|
|
82
|
+
@self_type = self_type.nil? ? nil : validate_string!("self_type", self_type)
|
|
83
|
+
@locals = validate_bindings!("locals", locals, LOCAL_NAME)
|
|
84
|
+
@ivar_seeds = validate_bindings!("ivar_seeds", ivar_seeds, IVAR_NAME)
|
|
85
|
+
@transform_id = transform_id.nil? ? nil : validate_string!("transform_id", transform_id)
|
|
86
|
+
@suppressed_rules = validate_rule_prefixes!(suppressed_rules)
|
|
87
|
+
freeze
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The effect-unit key and the snapshot key: `view:users/show.html`.
|
|
91
|
+
def unit_key
|
|
92
|
+
"#{KEY_PREFIX}#{@logical_name}"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# The unit's cache identity — **source bytes + transform id + synthesis version**, the triple
|
|
96
|
+
# `docs/internal-spec/cache.md` § computed-value keys asks a synthesised input to carry. The
|
|
97
|
+
# template's own bytes are deliberately NOT in it: the engine analyses `ruby_source`, and two
|
|
98
|
+
# templates that compile to the same Ruby have the same answer.
|
|
99
|
+
#
|
|
100
|
+
# @param fallback_transform_id — the declaring plugin's `id@version`, used when the unit
|
|
101
|
+
# named no transform of its own.
|
|
102
|
+
def digest(fallback_transform_id = nil)
|
|
103
|
+
Digest::SHA256.hexdigest(
|
|
104
|
+
[
|
|
105
|
+
"source:#{@ruby_source}",
|
|
106
|
+
"transform:#{@transform_id || fallback_transform_id || 'unknown'}",
|
|
107
|
+
"suppressed:#{@suppressed_rules.join(',')}",
|
|
108
|
+
"synthesis:#{SYNTHESIS_VERSION}"
|
|
109
|
+
].join("\x00")
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# The template line a `ruby_source` line reports at. Falls back to the nearest mapped line before it
|
|
114
|
+
# — a synthesised prologue or a buffer-append the compiler emitted between two template lines still
|
|
115
|
+
# points into the template rather than past its end — and to line 1 when the map says nothing at all.
|
|
116
|
+
def template_line(ruby_line)
|
|
117
|
+
return ruby_line if @line_map.empty?
|
|
118
|
+
return @line_map[ruby_line] if @line_map.key?(ruby_line)
|
|
119
|
+
|
|
120
|
+
before = @line_map.keys.select { |line| line < ruby_line }
|
|
121
|
+
before.empty? ? 1 : @line_map[before.max]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def to_h
|
|
125
|
+
{
|
|
126
|
+
"logical_name" => @logical_name, "path" => @path, "ruby_source" => @ruby_source,
|
|
127
|
+
"line_map" => @line_map, "self_type" => @self_type, "locals" => @locals,
|
|
128
|
+
"ivar_seeds" => @ivar_seeds, "transform_id" => @transform_id,
|
|
129
|
+
"suppressed_rules" => @suppressed_rules
|
|
130
|
+
}
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def ==(other)
|
|
134
|
+
other.is_a?(TemplateUnit) && other.to_h == to_h
|
|
135
|
+
end
|
|
136
|
+
alias eql? ==
|
|
137
|
+
|
|
138
|
+
def hash
|
|
139
|
+
[self.class, to_h].hash
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
private
|
|
143
|
+
|
|
144
|
+
def validate_string!(field, value)
|
|
145
|
+
text = value.to_s
|
|
146
|
+
raise ArgumentError, "TemplateUnit #{field} must be a non-empty String" if text.empty?
|
|
147
|
+
|
|
148
|
+
text.dup.freeze
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def validate_source!(value)
|
|
152
|
+
raise ArgumentError, "TemplateUnit ruby_source must be a String" unless value.is_a?(String)
|
|
153
|
+
|
|
154
|
+
value.dup.freeze
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# 1-based on both sides. A zero or negative line is a compiler bug the engine would turn into a
|
|
158
|
+
# diagnostic positioned off the top of the file, so it is refused at construction.
|
|
159
|
+
def validate_line_map!(value)
|
|
160
|
+
raise ArgumentError, "TemplateUnit line_map must be a Hash" unless value.is_a?(Hash)
|
|
161
|
+
|
|
162
|
+
value.to_h do |from, to|
|
|
163
|
+
unless from.is_a?(Integer) && to.is_a?(Integer) && from.positive? && to.positive?
|
|
164
|
+
raise ArgumentError, "TemplateUnit line_map must map positive Integer to positive Integer"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
[from, to]
|
|
168
|
+
end.freeze
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def validate_rule_prefixes!(value)
|
|
172
|
+
raise ArgumentError, "TemplateUnit suppressed_rules must be an Array" unless value.is_a?(Array)
|
|
173
|
+
|
|
174
|
+
value.map do |prefix|
|
|
175
|
+
text = prefix.to_s
|
|
176
|
+
unless text.match?(RULE_PREFIX)
|
|
177
|
+
raise ArgumentError, "TemplateUnit suppressed_rules entry #{text.inspect} is malformed"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
text.dup.freeze
|
|
181
|
+
end.uniq.freeze
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def validate_bindings!(field, value, pattern)
|
|
185
|
+
raise ArgumentError, "TemplateUnit #{field} must be a Hash" unless value.is_a?(Hash)
|
|
186
|
+
|
|
187
|
+
value.to_h do |name, type_name|
|
|
188
|
+
key = name.to_s
|
|
189
|
+
raise ArgumentError, "TemplateUnit #{field} key #{key.inspect} is malformed" unless key.match?(pattern)
|
|
190
|
+
|
|
191
|
+
[key.dup.freeze, validate_string!("#{field} value", type_name)]
|
|
192
|
+
end.freeze
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
@@ -36,7 +36,7 @@ module Rigor
|
|
|
36
36
|
|
|
37
37
|
@trusted_gems = trusted_gems.map { |g| g.to_s.dup.freeze }.uniq.sort.freeze
|
|
38
38
|
@allowed_read_roots = allowed_read_roots
|
|
39
|
-
.map { |path| File.
|
|
39
|
+
.map { |path| File.absolute_path(path).freeze }
|
|
40
40
|
.uniq
|
|
41
41
|
.sort
|
|
42
42
|
.freeze
|
|
@@ -46,10 +46,10 @@ module Rigor
|
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
# @return true when the absolute path falls inside any allowed read root. Symlinks are
|
|
49
|
-
# resolved through `File.
|
|
49
|
+
# resolved through `File.absolute_path` only (no `realpath`); plugins with adversarial intent are out of
|
|
50
50
|
# scope per ADR-2.
|
|
51
51
|
def allow_read?(path)
|
|
52
|
-
absolute = File.
|
|
52
|
+
absolute = File.absolute_path(path.to_s)
|
|
53
53
|
@allowed_read_roots.any? { |root| inside?(absolute, root) }
|
|
54
54
|
end
|
|
55
55
|
|
data/lib/rigor/plugin.rb
CHANGED
|
@@ -17,8 +17,8 @@ module Rigor
|
|
|
17
17
|
#
|
|
18
18
|
# The answer (issue #260's recorded decision) is one table set, built once, threaded to both sides: the
|
|
19
19
|
# {Mutator} base scope and {DiagnosticOracle}'s runner seam. This module builds that table set — the FULL
|
|
20
|
-
# discovery bundle (`discovered_def_nodes`, `discovered_methods`, superclasses, includes, …), not just
|
|
21
|
-
# class-identity slice site selection needed, because resolving `find` on `singleton(Account)` is what makes
|
|
20
|
+
# discovery bundle (`discovered_def_nodes`, `discovered_methods`, superclasses, includes, extends, …), not just
|
|
21
|
+
# the class-identity slice site selection needed, because resolving `find` on `singleton(Account)` is what makes
|
|
22
22
|
# the site killable.
|
|
23
23
|
#
|
|
24
24
|
# The shape is a plain Hash of {Scope::DiscoveryIndex} slot names, which is exactly what both consumers take:
|
|
@@ -84,9 +84,10 @@ module Rigor
|
|
|
84
84
|
# collectors driven over the same tree) that {Analysis::Runner::ProjectPrePasses#discover} uses. Empty
|
|
85
85
|
# tables are dropped so an empty seed stays `{}` and every consumer's "seed nothing" branch keeps working.
|
|
86
86
|
#
|
|
87
|
-
# `discovered_class_sources` is
|
|
88
|
-
#
|
|
89
|
-
#
|
|
87
|
+
# `discovered_class_sources` is carried as the runner carries it on every run: Tier 2 never records edges,
|
|
88
|
+
# but an ancestor-order reader asks whether a class is declared in more than one file. The mixin tables
|
|
89
|
+
# (`discovered_includes`, `discovered_prepends`, `discovered_extends`) ride the same fold, so a singleton
|
|
90
|
+
# `extend` reads the same from a per-file scope under coverage and protection as it does under `check`.
|
|
90
91
|
def discovery_tables(paths)
|
|
91
92
|
seed_tables(Inference::ScopeIndexer.discovered_project_index_for_paths(paths))
|
|
92
93
|
end
|
|
@@ -99,17 +100,27 @@ module Rigor
|
|
|
99
100
|
tables = { discovered_classes: index.fetch(:classes) }
|
|
100
101
|
%i[
|
|
101
102
|
def_nodes def_nestings singleton_def_nodes def_sources singleton_def_sources superclasses includes
|
|
102
|
-
|
|
103
|
+
extends prepends unpositioned_mixins class_sources
|
|
104
|
+
method_visibilities methods parameter_envelopes data_member_layouts struct_member_layouts
|
|
105
|
+
deferred_ranges refinements global_write_census
|
|
103
106
|
].each do |slot|
|
|
104
107
|
tables[seed_key(slot)] = def_index.fetch(slot)
|
|
105
108
|
end
|
|
106
|
-
|
|
109
|
+
# ADR-119 WD1 — the pairs travel as one: an unpaired empty table is dropped here, a pair only when both
|
|
110
|
+
# halves are empty (`compact_pairs`), so a non-empty sibling is never stranded by an empty member.
|
|
111
|
+
tables.merge!(def_index.fetch(:siblings))
|
|
112
|
+
Scope::DiscoveryIndex.compact_pairs(
|
|
113
|
+
tables.reject { |name, table| !Scope::DiscoveryIndex.paired?(name) && (table.nil? || table.empty?) }
|
|
114
|
+
)
|
|
107
115
|
end
|
|
108
116
|
|
|
117
|
+
UNPREFIXED_SLOTS = %i[data_member_layouts struct_member_layouts unpositioned_mixins].freeze
|
|
118
|
+
private_constant :UNPREFIXED_SLOTS
|
|
119
|
+
|
|
109
120
|
# The def-index slot names match the {Scope::DiscoveryIndex} ones under a `discovered_` prefix, except the
|
|
110
|
-
# two member-layout tables, which carry the same name on both sides.
|
|
121
|
+
# two member-layout tables and the unpositioned-mixin table, which carry the same name on both sides.
|
|
111
122
|
def seed_key(slot)
|
|
112
|
-
|
|
123
|
+
UNPREFIXED_SLOTS.include?(slot) ? slot : :"discovered_#{slot}"
|
|
113
124
|
end
|
|
114
125
|
end
|
|
115
126
|
end
|
|
@@ -140,7 +140,7 @@ module Rigor
|
|
|
140
140
|
end
|
|
141
141
|
|
|
142
142
|
def snapshot_root_candidates(roots, configured)
|
|
143
|
-
[roots, Array(roots).map { |path| File.
|
|
143
|
+
[roots, Array(roots).map { |path| File.absolute_path(path) },
|
|
144
144
|
configured, Array(configured).map { |path| relative_to_pwd(path) }]
|
|
145
145
|
.compact.reject(&:empty?).uniq
|
|
146
146
|
end
|
|
@@ -148,8 +148,8 @@ module Rigor
|
|
|
148
148
|
# `path` rendered relative to the working directory when it lies underneath it; unchanged otherwise.
|
|
149
149
|
# Shared by the root-candidate list and the per-file snapshot lookup, which face the same skew.
|
|
150
150
|
def relative_to_pwd(path)
|
|
151
|
-
absolute = File.
|
|
152
|
-
prefix = "#{File.
|
|
151
|
+
absolute = File.absolute_path(path)
|
|
152
|
+
prefix = "#{File.absolute_path(Dir.pwd)}#{File::SEPARATOR}"
|
|
153
153
|
absolute.start_with?(prefix) ? absolute.delete_prefix(prefix) : path.to_s
|
|
154
154
|
end
|
|
155
155
|
|
|
@@ -182,8 +182,7 @@ module Rigor
|
|
|
182
182
|
# `Unavailable` rescue above and disables the cache, which is the only sound reading of a key slot
|
|
183
183
|
# that could not be computed.
|
|
184
184
|
def engine_source_entries
|
|
185
|
-
|
|
186
|
-
identity.nil? ? [] : [config_entry("engine-source", identity)]
|
|
185
|
+
Cache::EngineSource.key_config_entries
|
|
187
186
|
end
|
|
188
187
|
|
|
189
188
|
# Probed BEFORE the snapshot load, for the reason string rather than for soundness. Since #289
|
data/lib/rigor/rbs_extended.rb
CHANGED
|
@@ -106,6 +106,12 @@ module Rigor
|
|
|
106
106
|
# `docs/type-specification/rbs-extended.md`.
|
|
107
107
|
INFERRED_RETURN_DIRECTIVE = "rigor:v1:inferred-return"
|
|
108
108
|
|
|
109
|
+
# `%a{rigor:v1:inferred-signature}` — every type slot on the member, parameters and return alike,
|
|
110
|
+
# is a placeholder: the author's comment block asserted nothing about this member's own contract.
|
|
111
|
+
# Strictly narrower than {INFERRED_RETURN_DIRECTIVE}, which also covers a member with an authored
|
|
112
|
+
# parameter and no authored return. Normative in `docs/type-specification/rbs-extended.md`.
|
|
113
|
+
INFERRED_SIGNATURE_DIRECTIVE = "rigor:v1:inferred-signature"
|
|
114
|
+
|
|
109
115
|
module_function
|
|
110
116
|
|
|
111
117
|
# Whether `method_def` declares its return type inferred rather than contracted (issue #823).
|
|
@@ -131,6 +137,27 @@ module Rigor
|
|
|
131
137
|
annotations.any? { |annotation| annotation.string.to_s.strip == INFERRED_RETURN_DIRECTIVE }
|
|
132
138
|
end
|
|
133
139
|
|
|
140
|
+
# Whether `method_def` declares its ENTIRE signature inferred — every parameter and the return alike
|
|
141
|
+
# — rather than only its return (issue #991).
|
|
142
|
+
#
|
|
143
|
+
# `inferred_return?` alone cannot separate a fully bare `def` from one whose parameters ARE declared
|
|
144
|
+
# (`# @rbs num: Float`) and only its return defaulted: both trip the same return-only provenance,
|
|
145
|
+
# because no equivalent per-parameter survivor exists to tell them apart. This reads the plugin's
|
|
146
|
+
# narrower, member-level fact instead: true only when nothing at all was asserted about the member.
|
|
147
|
+
# `call.wrong-arity` declines on this rather than on {inferred_return?}, since a parameter the author
|
|
148
|
+
# DID annotate is an assertion about the parameter list, and the parameter list is exactly what arity
|
|
149
|
+
# is a question about (see the rule's own comment).
|
|
150
|
+
#
|
|
151
|
+
# @param method_def — an `RBS::Definition::Method`, or anything else answering `#annotations`.
|
|
152
|
+
def inferred_signature?(method_def)
|
|
153
|
+
return false if method_def.nil?
|
|
154
|
+
|
|
155
|
+
annotations = method_def.annotations
|
|
156
|
+
return false if annotations.nil? || annotations.empty?
|
|
157
|
+
|
|
158
|
+
annotations.any? { |annotation| annotation.string.to_s.strip == INFERRED_SIGNATURE_DIRECTIVE }
|
|
159
|
+
end
|
|
160
|
+
|
|
134
161
|
# Reads RBS::Extended predicate effects off `RBS::Definition::Method#annotations`. Returns the effects in
|
|
135
162
|
# source order; duplicates and unrecognised `rigor:v1:` directives are dropped. Returns an empty array (NEVER
|
|
136
163
|
# `nil`) for a method with no recognised annotations so callers can iterate unconditionally.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
# {Reflection}'s constant-ancestor walk ([#354](https://github.com/rigortype/rigor/issues/354)), split out for
|
|
5
|
+
# the reader rather than for the loader: the whole-spelling ladder's step 2 (`reflection.rb`) and the
|
|
6
|
+
# segment-wise path walk (`reflection/constant_path.rb`) both ask which project classes and modules a
|
|
7
|
+
# name's owner inherits constants from, and this file answers that once for both. Reopening the module
|
|
8
|
+
# keeps the walk behind the facade its callers already read.
|
|
9
|
+
module Reflection
|
|
10
|
+
# #354 — thread-local slot for the per-run ancestor-scope memo. See {.ancestor_constant_scopes}.
|
|
11
|
+
ANCESTOR_SCOPES_KEY = :__rigor_ancestor_constant_scopes__
|
|
12
|
+
private_constant :ANCESTOR_SCOPES_KEY
|
|
13
|
+
|
|
14
|
+
# The recording run's slot. See {.recorded_ancestor_constant_scopes}.
|
|
15
|
+
RECORDED_ANCESTOR_SCOPES_KEY = :__rigor_recorded_ancestor_constant_scopes__
|
|
16
|
+
private_constant :RECORDED_ANCESTOR_SCOPES_KEY
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# #354 — the project classes and modules whose own constants `class_name` inherits, in Ruby's
|
|
21
|
+
# ancestor order: the entries of `class_name`'s `Scope::ResolutionChain` — prepended modules, then
|
|
22
|
+
# included ones, then the superclass's chain likewise — each once. `class_name` itself is excluded:
|
|
23
|
+
# step 1 already covered it.
|
|
24
|
+
#
|
|
25
|
+
# Issue #1571 — this was a breadth-first walk of its own, which is not Ruby's order: `class C < Base;
|
|
26
|
+
# include A` with `A` including `M` reached `Base::X` (depth 1) before `M::X` (depth 2), while Ruby's
|
|
27
|
+
# ancestors are `[C, A, M, Base]`. It reads the chain's `:constants` flavor, whose predicate for a
|
|
28
|
+
# project class also admits a module that holds only constants.
|
|
29
|
+
#
|
|
30
|
+
# Only PROJECT ancestors appear: an as-written name that resolves to no discovered class or module
|
|
31
|
+
# is an external entry on the chain and contributes nothing here, so a `class Foo <
|
|
32
|
+
# ActiveRecord::Base` adds no candidate and a constant owned by an RBS-known ancestor still resolves
|
|
33
|
+
# only if the bare name reaches it at step 3. That gap is deliberate for this slice: widening to the
|
|
34
|
+
# RBS ancestor graph is a separate question with its own FP surface.
|
|
35
|
+
#
|
|
36
|
+
# Memoised per run because step 2 runs on every constant reference whose lexical candidates all
|
|
37
|
+
# miss — which is the common case for a core-class reference (`String` inside `class Foo`). The
|
|
38
|
+
# bucket keys on the identity of the runner-seeded run-generation token (ADR-84 WD2), falling
|
|
39
|
+
# back to the per-file discovery table for runner-less scopes, so a re-run in one process (LSP,
|
|
40
|
+
# ADR-62 warm loop) cannot hit stale entries.
|
|
41
|
+
def ancestor_constant_scopes(class_name, scope)
|
|
42
|
+
ancestor_constant_worlds(class_name, scope).first
|
|
43
|
+
end
|
|
44
|
+
private_class_method :ancestor_constant_scopes
|
|
45
|
+
|
|
46
|
+
# ADR-24 / #1570 — the first ancestor that owns a constant, asked as {.ancestor_constant_scopes} orders
|
|
47
|
+
# them and settled by `Scope::ResolutionChain#settle`: the block's first truthy answer where the chain
|
|
48
|
+
# stands, and otherwise the answer of the breadth-first order this rung used before the chain
|
|
49
|
+
# (`Scope::ResolutionChain::MasterOrder`) — where a skipped `include` would put a different owner first,
|
|
50
|
+
# which constant Ruby reads depends on the order the bodies ran.
|
|
51
|
+
def agreed_ancestor_hit(class_name, scope, &)
|
|
52
|
+
scopes, chain = ancestor_constant_worlds(class_name, scope)
|
|
53
|
+
owner, hit = first_ancestor_hit(scopes, &)
|
|
54
|
+
verdict = chain.settle(scope, owner, owner: owner) do |retro|
|
|
55
|
+
first_ancestor_hit(constant_scopes_of(retro, class_name), &)&.first
|
|
56
|
+
end
|
|
57
|
+
return hit if verdict == :chain
|
|
58
|
+
|
|
59
|
+
first_ancestor_hit(master_constant_scopes(class_name, scope), &)&.last
|
|
60
|
+
end
|
|
61
|
+
private_class_method :agreed_ancestor_hit
|
|
62
|
+
|
|
63
|
+
def master_constant_scopes(class_name, scope)
|
|
64
|
+
Scope::ResolutionChain::MasterOrder.breadth_first(scope, class_name.to_s, :constants)
|
|
65
|
+
end
|
|
66
|
+
private_class_method :master_constant_scopes
|
|
67
|
+
|
|
68
|
+
def first_ancestor_hit(entries)
|
|
69
|
+
entries.each do |entry|
|
|
70
|
+
hit = yield entry
|
|
71
|
+
return [entry, hit] if hit
|
|
72
|
+
end
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
private_class_method :first_ancestor_hit
|
|
76
|
+
|
|
77
|
+
# `[scopes, chain]` — {.ancestor_constant_scopes} and the chain they were read from, which settles the
|
|
78
|
+
# answer.
|
|
79
|
+
def ancestor_constant_worlds(class_name, scope)
|
|
80
|
+
# ADR-46: the answer depends on every class on the chain, and the memo is run-scoped rather than
|
|
81
|
+
# file-scoped — a hit would skip the recording and under-record the edge for every later file. A
|
|
82
|
+
# recording run reads the chain, which is memoised per discovery index, and files its edges on
|
|
83
|
+
# every call.
|
|
84
|
+
return recorded_ancestor_constant_scopes(class_name, scope) if Analysis::DependencyRecorder.active?
|
|
85
|
+
|
|
86
|
+
generation = scope.run_generation || scope.discovered_superclasses
|
|
87
|
+
slot = Thread.current[ANCESTOR_SCOPES_KEY]
|
|
88
|
+
unless slot && slot[0].equal?(generation)
|
|
89
|
+
slot = [generation, {}]
|
|
90
|
+
Thread.current[ANCESTOR_SCOPES_KEY] = slot
|
|
91
|
+
end
|
|
92
|
+
bucket = slot[1]
|
|
93
|
+
bucket.fetch(class_name) { bucket[class_name] = compute_ancestor_constant_scopes(class_name, scope) }
|
|
94
|
+
end
|
|
95
|
+
private_class_method :ancestor_constant_worlds
|
|
96
|
+
|
|
97
|
+
# The recording run's memo. The answer is every project entry of the chain, so its edges are the
|
|
98
|
+
# root's and every entry's — what `Scope::ResolutionChain#record` files, which a computation does and a
|
|
99
|
+
# hit replays, in the same order. One slot keyed on the discovery index: the answer and the edges are
|
|
100
|
+
# both read from it, so the key is exact, and the slot never pins more than the index it serves.
|
|
101
|
+
def recorded_ancestor_constant_scopes(class_name, scope)
|
|
102
|
+
discovery = scope.discovery
|
|
103
|
+
slot = Thread.current[RECORDED_ANCESTOR_SCOPES_KEY]
|
|
104
|
+
unless slot && slot[0].equal?(discovery)
|
|
105
|
+
slot = [discovery, {}]
|
|
106
|
+
Thread.current[RECORDED_ANCESTOR_SCOPES_KEY] = slot
|
|
107
|
+
end
|
|
108
|
+
bucket = slot[1]
|
|
109
|
+
worlds = bucket[class_name]
|
|
110
|
+
return bucket[class_name] = compute_ancestor_constant_scopes(class_name, scope) if worlds.nil?
|
|
111
|
+
|
|
112
|
+
Scope::ResolutionChain.for(scope, class_name.to_s, :instance, :constants).record(scope)
|
|
113
|
+
worlds
|
|
114
|
+
end
|
|
115
|
+
private_class_method :recorded_ancestor_constant_scopes
|
|
116
|
+
|
|
117
|
+
# Reads the chain whole, so a recording run files every class on it (a no-op otherwise).
|
|
118
|
+
def compute_ancestor_constant_scopes(class_name, scope)
|
|
119
|
+
chain = Scope::ResolutionChain.for(scope, class_name.to_s, :instance, :constants)
|
|
120
|
+
chain.record(scope)
|
|
121
|
+
scopes = constant_scopes_of(chain, class_name)
|
|
122
|
+
[scopes, chain].freeze
|
|
123
|
+
end
|
|
124
|
+
private_class_method :compute_ancestor_constant_scopes
|
|
125
|
+
|
|
126
|
+
# The chain's project entries in order, each once, without `class_name` itself.
|
|
127
|
+
def constant_scopes_of(chain, class_name)
|
|
128
|
+
out = []
|
|
129
|
+
chain.entries.each do |entry|
|
|
130
|
+
name = entry.name
|
|
131
|
+
out << name unless name.nil? || name == class_name || out.include?(name)
|
|
132
|
+
end
|
|
133
|
+
out.freeze
|
|
134
|
+
end
|
|
135
|
+
private_class_method :constant_scopes_of
|
|
136
|
+
|
|
137
|
+
# Whether `name` is a namespace the project declares — the predicate the chain's `:constants` flavor
|
|
138
|
+
# resolves ancestor names with, and the path walk's (`reflection/constant_path.rb`) test for a head.
|
|
139
|
+
def known_project_namespace?(name, scope)
|
|
140
|
+
scope.discovered_superclasses.key?(name) ||
|
|
141
|
+
scope.discovered_includes.key?(name) ||
|
|
142
|
+
scope.discovered_classes.key?(name)
|
|
143
|
+
end
|
|
144
|
+
private_class_method :known_project_namespace?
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -35,10 +35,17 @@ module Rigor
|
|
|
35
35
|
# silent wrong answer for a silent absent one at no gain (the same reasoning as
|
|
36
36
|
# {.toplevel_first_constant_type}'s caller-derived rungs).
|
|
37
37
|
def resolve_constant_path_name(name, scope, &known)
|
|
38
|
+
walk_constant_path(name, scope, known, caller_derived: true)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# {.resolve_constant_path_name}'s walk. `caller_derived: false` is {.resolve_constant_type}'s flag: the
|
|
42
|
+
# head skips the enclosing class's ancestors — the one step a recorded top-level body takes from its
|
|
43
|
+
# caller — while every later segment still walks its owner's ancestors, as Ruby does.
|
|
44
|
+
def walk_constant_path(name, scope, known, caller_derived:)
|
|
38
45
|
segments = name.to_s.split("::")
|
|
39
46
|
return nil unless segments.size > 1
|
|
40
47
|
|
|
41
|
-
owner = path_head_owner(segments.first, scope)
|
|
48
|
+
owner = path_head_owner(segments.first, scope, caller_derived)
|
|
42
49
|
last = segments.size - 1
|
|
43
50
|
segments.each_with_index do |segment, index|
|
|
44
51
|
next if index.zero?
|
|
@@ -50,25 +57,34 @@ module Rigor
|
|
|
50
57
|
end
|
|
51
58
|
owner
|
|
52
59
|
end
|
|
60
|
+
private_class_method :walk_constant_path
|
|
53
61
|
|
|
54
62
|
# Step 2.5's rung as a type: the segment-wise name, then that name looked up like any other
|
|
55
63
|
# candidate so the answer comes from the same source-precedence order as every other rung.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
64
|
+
#
|
|
65
|
+
# Issue #1290 — the final segment's candidates are looked up with the reference's `shadows` names
|
|
66
|
+
# ({.constant_type_at}), so an owner's own `B` another file writes stops the walk before the owner's
|
|
67
|
+
# superclass answers with its published `B`.
|
|
68
|
+
def constant_path_type(name, scope, shadows:, caller_derived: true)
|
|
69
|
+
known = ->(candidate) { constant_type_at(candidate, scope, shadows) }
|
|
70
|
+
qualified = walk_constant_path(name, scope, known, caller_derived: caller_derived)
|
|
71
|
+
qualified && constant_type_at(qualified, scope, shadows)
|
|
59
72
|
end
|
|
60
73
|
private_class_method :constant_path_type
|
|
61
74
|
|
|
62
75
|
# The namespace a path's FIRST segment names — the bare-name ladder, restricted to answers that can
|
|
63
76
|
# OWN the segments that follow. A value constant is not one of them, so it declines here rather
|
|
64
77
|
# than producing a namespace prefix no source knows.
|
|
65
|
-
def path_head_owner(head, scope)
|
|
78
|
+
def path_head_owner(head, scope, caller_derived)
|
|
66
79
|
hit = first_namespace_hit(lexical_nesting_chain(scope), head, scope)
|
|
67
80
|
return hit if hit
|
|
68
81
|
|
|
69
|
-
prefix = enclosing_class_path(scope)
|
|
82
|
+
prefix = caller_derived ? enclosing_class_path(scope) : nil
|
|
70
83
|
unless prefix.nil? || prefix.empty?
|
|
71
|
-
hit =
|
|
84
|
+
hit = bounded_agreed_hit(prefix, scope) do |entry|
|
|
85
|
+
candidate = "#{entry}::#{head}"
|
|
86
|
+
candidate if namespace_known?(candidate, scope)
|
|
87
|
+
end
|
|
72
88
|
return hit if hit
|
|
73
89
|
end
|
|
74
90
|
|
|
@@ -93,28 +109,40 @@ module Rigor
|
|
|
93
109
|
candidate = "#{owner}::#{segment}"
|
|
94
110
|
return candidate if yield(candidate)
|
|
95
111
|
|
|
96
|
-
|
|
112
|
+
bounded_agreed_hit(owner, scope) do |ancestor|
|
|
97
113
|
inherited = "#{ancestor}::#{segment}"
|
|
98
|
-
|
|
114
|
+
inherited if yield(inherited)
|
|
99
115
|
end
|
|
100
|
-
nil
|
|
101
116
|
end
|
|
102
117
|
private_class_method :constant_in_namespace
|
|
103
118
|
|
|
119
|
+
# {.agreed_ancestor_hit} over the budget-bounded scope lists: the first answer where
|
|
120
|
+
# `Scope::ResolutionChain#settle` lets the chain stand, and otherwise the breadth-first order's.
|
|
121
|
+
def bounded_agreed_hit(class_name, scope, &)
|
|
122
|
+
scopes, chain = ancestor_constant_worlds(class_name, scope)
|
|
123
|
+
owner, hit = first_ancestor_hit(bounded(scopes), &)
|
|
124
|
+
verdict = chain.settle(scope, owner, owner: owner) do |retro|
|
|
125
|
+
first_ancestor_hit(bounded(constant_scopes_of(retro, class_name)), &)&.first
|
|
126
|
+
end
|
|
127
|
+
return hit if verdict == :chain
|
|
128
|
+
|
|
129
|
+
first_ancestor_hit(bounded(master_constant_scopes(class_name, scope)), &)&.last
|
|
130
|
+
end
|
|
131
|
+
private_class_method :bounded_agreed_hit
|
|
132
|
+
|
|
104
133
|
# How many of {.ancestor_constant_scopes}' entries one segment may consult, under
|
|
105
134
|
# `Scope::ANCESTOR_WALK_LIMIT` — the budget `Scope`'s method lookup already spends on this same
|
|
106
135
|
# graph, reused rather than re-chosen so one pathological ancestry costs one bound. The list
|
|
107
136
|
# itself is the memoised one step 2 builds, so this bounds the per-segment consultation the walk
|
|
108
137
|
# adds and not the graph. Reported through the shared counter, so a truncated walk is visible in a
|
|
109
138
|
# budget trace instead of looking like a plain miss.
|
|
110
|
-
def
|
|
111
|
-
scopes = ancestor_constant_scopes(class_name, scope)
|
|
139
|
+
def bounded(scopes)
|
|
112
140
|
return scopes if scopes.size <= Scope::ANCESTOR_WALK_LIMIT
|
|
113
141
|
|
|
114
142
|
Inference::BudgetTrace.hit(Inference::BudgetTrace::ANCESTOR_WALK_LIMIT)
|
|
115
143
|
scopes.first(Scope::ANCESTOR_WALK_LIMIT)
|
|
116
144
|
end
|
|
117
|
-
private_class_method :
|
|
145
|
+
private_class_method :bounded
|
|
118
146
|
|
|
119
147
|
# Whether `name` can OWN a constant. `class_known?` covers the RBS and registry classes and modules;
|
|
120
148
|
# the project tables cover a namespace the project declares with no RBS for it.
|