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
|
@@ -12,12 +12,14 @@ module Rigor
|
|
|
12
12
|
# Per-session cache of the project-wide analyzer state the LSP reads on every request — chiefly the
|
|
13
13
|
# `Environment` (with its ~100-300ms RBS env build), a read-only `Cache::Store` that lets the runner hit
|
|
14
14
|
# the on-disk RBS cache without writing back, and (since the pre-pass cache slice) a frozen
|
|
15
|
-
# {Rigor::Analysis::ProjectScan} snapshot covering the plugin registry, dependency-source index,
|
|
16
|
-
# pre-pass scanner outputs.
|
|
15
|
+
# {Rigor::Analysis::ProjectScan} snapshot covering the plugin registry, dependency-source index,
|
|
16
|
+
# pre-pass scanner outputs and (since #1038) the compiled template-unit index.
|
|
17
17
|
#
|
|
18
18
|
# The pre-pass scan lets `DiagnosticPublisher#run_analysis` build a `Runner` with `prebuilt:` so per-buffer
|
|
19
19
|
# publishes skip plugin `#prepare`, the synthetic-method scanner, the project-patched scanner, and the
|
|
20
|
-
# dependency-source walker
|
|
20
|
+
# dependency-source walker — and, since #1038, the template-unit transform: a publish recompiles only a
|
|
21
|
+
# template that moved on disk since the scan was built (plus the buffer's own, always), rather than every
|
|
22
|
+
# view in the project. For projects with substrate plugins / opt-in dependency source / sizeable
|
|
21
23
|
# `pre_eval:` configuration this cuts publish wall time substantially — for the trivial case the savings
|
|
22
24
|
# are small (the per-publish path is already ≈2ms once Environment is warm).
|
|
23
25
|
#
|
data/lib/rigor/mcp/server.rb
CHANGED
|
@@ -94,7 +94,7 @@ module Rigor
|
|
|
94
94
|
type: "string",
|
|
95
95
|
enum: %w[untyped observed],
|
|
96
96
|
description: "Parameter policy: untyped (default) or observed " \
|
|
97
|
-
"(harvests call-site argument types from
|
|
97
|
+
"(harvests call-site argument types from the configured test_paths)"
|
|
98
98
|
},
|
|
99
99
|
config: { type: "string", description: "Path to .rigor.yml (optional)" }
|
|
100
100
|
}
|
|
@@ -134,6 +134,7 @@ module Rigor
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
].freeze
|
|
137
|
+
Ractor.make_shareable(TOOLS)
|
|
137
138
|
|
|
138
139
|
def initialize(config_path: nil, err: $stderr)
|
|
139
140
|
@config_path = config_path
|
data/lib/rigor/plugin/base.rb
CHANGED
|
@@ -8,6 +8,7 @@ require "prism"
|
|
|
8
8
|
require_relative "manifest"
|
|
9
9
|
require_relative "node_context"
|
|
10
10
|
require_relative "../analysis/diagnostic"
|
|
11
|
+
require_relative "../cache/engine_source"
|
|
11
12
|
# `producer generation_cap:` defaults to (and validates against) `Cache::Store::UNBOUNDED_GENERATIONS`, and a
|
|
12
13
|
# plugin class body can be evaluated before anything else pulled the cache layer in.
|
|
13
14
|
require_relative "../cache/store"
|
|
@@ -492,6 +493,10 @@ module Rigor
|
|
|
492
493
|
@fact_cache = {}
|
|
493
494
|
@producer_value_cache = {}
|
|
494
495
|
@producer_errors = {}
|
|
496
|
+
# Issue #1051 — the run-scoped disclosure table ({#disclose_once}, and since #1060 {#emit_once}).
|
|
497
|
+
# Allocated here for the same reason as the three memos above: a registration is a Hash-content
|
|
498
|
+
# mutation, so it stays sound on a plugin that freezes itself in `initialize`.
|
|
499
|
+
@run_disclosures = {}
|
|
495
500
|
end
|
|
496
501
|
|
|
497
502
|
# Override in subclasses to wire any state the plugin needs from the injected service container.
|
|
@@ -545,6 +550,38 @@ module Rigor
|
|
|
545
550
|
[]
|
|
546
551
|
end
|
|
547
552
|
|
|
553
|
+
# #392 — the **template-unit** transform: compile one non-Ruby project file into the Ruby the engine
|
|
554
|
+
# analyses (ADR-16 Tier D, revived; design note § 11.3). The engine globs the manifest's
|
|
555
|
+
# `template_globs:`, reads each match's bytes, and calls this once per match on the PARENT, before any
|
|
556
|
+
# analysis — so the transform runs exactly once per file per run and its result marshals into every
|
|
557
|
+
# fork-pool worker.
|
|
558
|
+
#
|
|
559
|
+
# Returns an `Array<Rigor::Plugin::TemplateUnit>`; an empty array declines the file, which is the
|
|
560
|
+
# right answer for a match the plugin's glob caught but its compiler cannot read. The default returns
|
|
561
|
+
# `[]`, so a plugin that declares no `template_globs:` is never asked and one that does may still
|
|
562
|
+
# decline per file.
|
|
563
|
+
#
|
|
564
|
+
# A raise isolates exactly like `#diagnostics_for_file`: the file contributes no unit and the run
|
|
565
|
+
# continues. The hook is the one place a template-unit plugin runs code — everything downstream is
|
|
566
|
+
# the frozen value object — which is what keeps the engine's analysis of a unit identical to its
|
|
567
|
+
# analysis of a `.rb` file.
|
|
568
|
+
def template_units_for_file(path:, source:) # rubocop:disable Lint/UnusedMethodArgument
|
|
569
|
+
[]
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
# #1047 — called once at the start of every template-unit COLLECTION PASS over this plugin's claim,
|
|
573
|
+
# before any `#template_units_for_file` of that pass and whether or not the pass ends up calling it
|
|
574
|
+
# (a warm pass may carry every unit and compile none). A plugin whose transform reads state gathered
|
|
575
|
+
# ACROSS its claimed files — rigor-actionpack seeds a partial's locals from the render sites in every
|
|
576
|
+
# view — memoises that state on its instance, and a long-lived owner keeps the instance across
|
|
577
|
+
# passes; this is where such a plugin learns that the memo may now be stale. The collector's
|
|
578
|
+
# per-call order is not a substitute: a warm pass offers only the editor's buffer, so no sequence of
|
|
579
|
+
# paths can tell one pass from the next.
|
|
580
|
+
#
|
|
581
|
+
# The default does nothing. A raise is swallowed: the pass continues, and the plugin's own
|
|
582
|
+
# revalidation simply does not run for it.
|
|
583
|
+
def template_units_pass_started; end
|
|
584
|
+
|
|
548
585
|
# ADR-37 slice 1 — runs the plugin's declared {.node_rule}s over one file and returns their
|
|
549
586
|
# diagnostics. The engine owns the single AST walk here so plugin authors never hand-roll a traversal:
|
|
550
587
|
# every node reachable from `root` is offered to each rule whose `node_type` it satisfies
|
|
@@ -619,6 +656,27 @@ module Rigor
|
|
|
619
656
|
[]
|
|
620
657
|
end
|
|
621
658
|
|
|
659
|
+
# ADR-113 WD4 (#1082) — enumerates the members this plugin synthesizes on `class_name`, for the
|
|
660
|
+
# `rigor lens` declaration map. A `dynamic_return` rule answers a TYPE for one call site at a time;
|
|
661
|
+
# nothing on that path can say "`User` has `email`, `name`, …", which is the fact grep can never
|
|
662
|
+
# find on a DSL-generated member. This hook is that per-class enumeration.
|
|
663
|
+
#
|
|
664
|
+
# Returns an `Array` of `{name:, kind:, type:}` Hashes. `name` is the member name as a caller spells
|
|
665
|
+
# it (String); `kind` is a Symbol grouping same-shaped members (`:column_reader`) — the lens
|
|
666
|
+
# collapses a group into one line; `type` is the `Rigor::Type` the plugin commits to for the member
|
|
667
|
+
# itself, or nil where it does not commit to one. It is the member-level answer, not the best a
|
|
668
|
+
# typed call site can do: a plugin that narrows `user.name` to `String` on a written receiver but
|
|
669
|
+
# declines to type the bare `name` read reports `Dynamic[top]` here. A member the plugin claims
|
|
670
|
+
# but deliberately leaves dynamic carries `Rigor::Type::Combinator.untyped` — the explicit
|
|
671
|
+
# `Dynamic[top]` answer, distinct from nil's "no answer".
|
|
672
|
+
#
|
|
673
|
+
# The hook is off the `check` hot path (ADR-52): `rigor lens` invokes it, `check` never does. It may
|
|
674
|
+
# read state `#prepare` built. The default returns `[]` — a plugin that synthesizes no enumerable
|
|
675
|
+
# members has nothing to declare.
|
|
676
|
+
def declared_members(class_name) # rubocop:disable Lint/UnusedMethodArgument
|
|
677
|
+
[]
|
|
678
|
+
end
|
|
679
|
+
|
|
622
680
|
# Builds a `Rigor::Analysis::Diagnostic` positioned at a Prism `node` for return from
|
|
623
681
|
# `#diagnostics_for_file`. Internalises the 1-based `line` / `start_column + 1` convention every plugin
|
|
624
682
|
# otherwise re-derives by hand, so authors pass the node and the message/severity/rule rather than
|
|
@@ -686,12 +744,108 @@ module Rigor
|
|
|
686
744
|
end
|
|
687
745
|
|
|
688
746
|
# ADR-60 WD4 — the `StandardError` a prior {#producer_value} call rescued for `id`, or nil when it
|
|
689
|
-
# succeeded or was never called. Plugins surface it as a load-error
|
|
690
|
-
#
|
|
747
|
+
# succeeded or was never called. Plugins surface it as a run-scoped `load-error` disclosure through
|
|
748
|
+
# {#disclose_once} (#1056) — a failed index is a fact about the run's inputs, not about a file.
|
|
691
749
|
def producer_error(id)
|
|
692
750
|
@producer_errors[id.to_sym]
|
|
693
751
|
end
|
|
694
752
|
|
|
753
|
+
# Issue #1051 — records a PROJECT-GLOBAL disclosure: a notice about the run's inputs ("the schema file
|
|
754
|
+
# is not there, so column checks are off") rather than about a line of a file. Registering the same
|
|
755
|
+
# `key` again — on this instance or on any other instance of this plugin in the same run — is a no-op,
|
|
756
|
+
# so the run emits the row exactly once however many workers, files or call sites reached it.
|
|
757
|
+
#
|
|
758
|
+
# disclose_once(:missing_schema,
|
|
759
|
+
# message: "rigor-activerecord: schema file `db/schema.rb` not found; …",
|
|
760
|
+
# severity: :info, rule: "load-error")
|
|
761
|
+
#
|
|
762
|
+
# Callable from any hook — `#prepare` (the usual place: the disclosure is a fact about the project,
|
|
763
|
+
# known before a single file is read), `#diagnostics_for_file`, or a `node_rule` block. It returns
|
|
764
|
+
# `nil` and emits NOTHING itself: the engine harvests the table after analysis, de-duplicates it by
|
|
765
|
+
# `(plugin id, key)` across the coordinator and every pool worker, and emits one row per surviving
|
|
766
|
+
# pair at `.rigor.yml:1:1` — the position every other run-level plugin row already uses (plugin load
|
|
767
|
+
# errors, `#prepare` raises, `plugin_trust.read-refused`).
|
|
768
|
+
#
|
|
769
|
+
# That position is the point of the facility. The pre-#1051 idiom was an `@emitted` flag consulted from
|
|
770
|
+
# `#diagnostics_for_file`, which positions the row at whichever file the plugin instance happened to
|
|
771
|
+
# see first — per WORKER under `--workers N`, so the row was duplicated, and after #393 the file it
|
|
772
|
+
# landed on could be a template unit's `.erb` path, reading as a claim about a view. A disclosure has
|
|
773
|
+
# no source position to be right about; `.rigor.yml` is where the user configured the input it is
|
|
774
|
+
# about.
|
|
775
|
+
#
|
|
776
|
+
# `severity:` defaults to `:info`, the grade plugins use for "here is what I recognised"; pass
|
|
777
|
+
# `:warning` for "I could not run". `key` is any object with a stable `#to_s` (a Symbol reads best) and
|
|
778
|
+
# is never shown to the user — it is the de-duplication identity, so it must not interpolate anything
|
|
779
|
+
# that differs between workers.
|
|
780
|
+
def disclose_once(key, message:, severity: :info, rule: "load-error")
|
|
781
|
+
id = key.to_s
|
|
782
|
+
return nil if run_registration_taken?(id, batch: false)
|
|
783
|
+
|
|
784
|
+
@run_disclosures[id] = {
|
|
785
|
+
key: id, message: message.to_s, severity: severity.to_sym, rule: rule.to_s
|
|
786
|
+
}.freeze
|
|
787
|
+
nil
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
# Issue #1060 — the POSITIONED sibling of {#disclose_once}: registers a whole batch of fully built
|
|
791
|
+
# diagnostics under `key`, emitted once per run however many plugin instances (fork-pool workers)
|
|
792
|
+
# register it. Each row keeps its own `path` / `line` / `column` — use this when a project-wide scan
|
|
793
|
+
# produces rows that are right about a position (a view template's line) but no single analysed file
|
|
794
|
+
# owns them, so returning them from `#diagnostics_for_file` would repeat them once per worker.
|
|
795
|
+
#
|
|
796
|
+
# emit_once(:view_diagnostics, scan_view_files(index))
|
|
797
|
+
#
|
|
798
|
+
# The question that chooses between the two channels is whether the row has a position it could be
|
|
799
|
+
# right about: a notice about the run's inputs has none and goes through {#disclose_once}
|
|
800
|
+
# (`.rigor.yml:1:1`); a row naming a real file and line comes here and keeps it.
|
|
801
|
+
#
|
|
802
|
+
# De-duplication is by `key` alone and the first registration wins: a later batch under the same key
|
|
803
|
+
# is dropped whole, on this instance or on any other instance in the run, never merged row by row.
|
|
804
|
+
# Every worker scans the same project, so a row-wise union could only add a worker's partial or
|
|
805
|
+
# divergent view. `key` shares one namespace per plugin with {#disclose_once} — the two channels are
|
|
806
|
+
# one registration table — but a key keeps the kind it was first registered as: repeating a key on the
|
|
807
|
+
# same channel is a no-op, and reusing it on the OTHER channel (either order) raises `ArgumentError`
|
|
808
|
+
# rather than silently dropping a genuine batch or disclosure.
|
|
809
|
+
#
|
|
810
|
+
# Callable from `#prepare`, `#diagnostics_for_file`, or a `node_rule` block; returns `nil` and emits
|
|
811
|
+
# nothing itself. The engine stamps `source_family: "plugin.<id>"` exactly as it does for a per-file
|
|
812
|
+
# row, so the qualified rule and the position a baseline keys on are the ones `#diagnostics_for_file`
|
|
813
|
+
# would have produced. Each row is copied and frozen on registration so the batch rides the fork
|
|
814
|
+
# payload and the Ractor `:done` message unchanged; a non-{Rigor::Analysis::Diagnostic} element
|
|
815
|
+
# raises `ArgumentError`, which the engine reports as this plugin's `runtime-error`.
|
|
816
|
+
def emit_once(key, diagnostics)
|
|
817
|
+
id = key.to_s
|
|
818
|
+
return nil if run_registration_taken?(id, batch: true)
|
|
819
|
+
|
|
820
|
+
batch = Array(diagnostics).map do |row|
|
|
821
|
+
next row.dup.freeze if row.is_a?(Rigor::Analysis::Diagnostic)
|
|
822
|
+
|
|
823
|
+
raise ArgumentError, "emit_once expects Rigor::Analysis::Diagnostic rows, got #{row.class}"
|
|
824
|
+
end
|
|
825
|
+
@run_disclosures[id] = { key: id, diagnostics: batch.freeze }.freeze
|
|
826
|
+
nil
|
|
827
|
+
end
|
|
828
|
+
|
|
829
|
+
# Engine-facing reader for {#disclose_once} and {#emit_once}: this instance's registrations in
|
|
830
|
+
# registration order; a positioned batch is the record carrying `:diagnostics`. Plugin authors never
|
|
831
|
+
# call it — {Rigor::Analysis::Runner} and {Rigor::Analysis::WorkerSession} drain it, and the
|
|
832
|
+
# de-duplication across instances happens there.
|
|
833
|
+
def run_disclosure_records
|
|
834
|
+
@run_disclosures.values
|
|
835
|
+
end
|
|
836
|
+
|
|
837
|
+
# #1060 — true when `id` is already registered on the same channel (the repeat is a no-op); raises when
|
|
838
|
+
# it is registered on the other one, because either answer there would drop a genuine registration.
|
|
839
|
+
def run_registration_taken?(id, batch:)
|
|
840
|
+
existing = @run_disclosures[id]
|
|
841
|
+
return false if existing.nil?
|
|
842
|
+
return true if existing.key?(:diagnostics) == batch
|
|
843
|
+
|
|
844
|
+
taken = batch ? "a #disclose_once disclosure" : "an #emit_once batch"
|
|
845
|
+
raise ArgumentError, "run-scoped key #{id.inspect} is already registered as #{taken}; use a distinct key"
|
|
846
|
+
end
|
|
847
|
+
private :run_registration_taken?
|
|
848
|
+
|
|
695
849
|
# Boilerplate-reduction helper (review §1.3): the "did you mean …?" suggestion every
|
|
696
850
|
# diagnostic-emitting plugin otherwise hand-rolls. Returns the closest of `candidates` to `name` via
|
|
697
851
|
# `DidYouMean::SpellChecker` (the same engine Ruby's own `NoMethodError` hints use), or `nil` when
|
|
@@ -705,7 +859,6 @@ module Rigor
|
|
|
705
859
|
DidYouMean::SpellChecker.new(dictionary: dictionary).correct(name.to_s).first
|
|
706
860
|
end
|
|
707
861
|
|
|
708
|
-
# Convenience accessor — `manifest` on the instance returns the class-level manifest declaration.
|
|
709
862
|
def manifest
|
|
710
863
|
self.class.manifest
|
|
711
864
|
end
|
|
@@ -808,7 +961,12 @@ module Rigor
|
|
|
808
961
|
return compute unless store
|
|
809
962
|
|
|
810
963
|
prefixed_id = "plugin.#{manifest.id}.#{producer_id}"
|
|
811
|
-
|
|
964
|
+
begin
|
|
965
|
+
key_descriptor = compose_key_descriptor(descriptor)
|
|
966
|
+
rescue Cache::EngineSource::Unavailable
|
|
967
|
+
# An unidentifiable engine runs the producer uncached rather than keyed without it (issue #1009).
|
|
968
|
+
return compute
|
|
969
|
+
end
|
|
812
970
|
lambda do
|
|
813
971
|
store.fetch_or_validate(
|
|
814
972
|
producer_id: prefixed_id,
|
|
@@ -1026,8 +1184,13 @@ module Rigor
|
|
|
1026
1184
|
# plugin-author-supplied extension carrying IDENTITY inputs (gem-version pins, `ConfigEntry` rows,
|
|
1027
1185
|
# configuration-file digests). The IoBoundary read history deliberately does NOT enter the key — it is
|
|
1028
1186
|
# recorded post-compute into the dependency descriptor instead (see {#producer_dependency_descriptor}).
|
|
1187
|
+
#
|
|
1188
|
+
# Issue #1009 — the engine's source identity rides in the auto-built half. The manifest version is the
|
|
1189
|
+
# plugin author's promise, and a checkout editing a bundled plugin (or the engine API a producer computes
|
|
1190
|
+
# through) keeps it, so without the identity the next session served the previous build's value into a
|
|
1191
|
+
# run the run-result key had correctly re-analysed.
|
|
1029
1192
|
def compose_key_descriptor(extra)
|
|
1030
|
-
auto_built = Cache::Descriptor.new(plugins: [plugin_entry])
|
|
1193
|
+
auto_built = Cache::Descriptor.new(plugins: [plugin_entry], configs: Cache::EngineSource.key_config_entries)
|
|
1031
1194
|
return auto_built if extra.nil?
|
|
1032
1195
|
|
|
1033
1196
|
Cache::Descriptor.compose(auto_built, extra)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Plugin
|
|
7
|
+
# Decides, before `exe/rigor` re-execs itself under `RUBY_BOX=1`, whether the running Ruby can host
|
|
8
|
+
# Rigor inside `Ruby::Box` at all. A released Ruby up to and including 4.0.7 cannot: `env_copy()` drops
|
|
9
|
+
# the box a class/module-body proc carries when `Ractor.make_shareable` isolates it, so the first
|
|
10
|
+
# method call inside such a proc dereferences a NULL box and the VM segfaults (Ruby Bug #22260, fixed on
|
|
11
|
+
# CRuby master by `a4ad8e461a`). Rigor defines Ractor-shareable lambdas at module scope as a standing
|
|
12
|
+
# pattern, so on an affected Ruby the `ruby_box` strategy crashes the whole run rather than declining.
|
|
13
|
+
#
|
|
14
|
+
# The check is behavioural, not a version comparison: a child Ruby runs the bug's minimal reproducer
|
|
15
|
+
# under `RUBY_BOX=1` and must print the right answer. That admits a 4.1.0dev build with the fix and a
|
|
16
|
+
# future 4.0.x that backports it, and refuses a development build cut before the fix — none of which a
|
|
17
|
+
# `RUBY_VERSION` bound gets right. The probe costs one short-lived `ruby --disable-gems` process, paid
|
|
18
|
+
# only when a run opts into `ruby_box`.
|
|
19
|
+
#
|
|
20
|
+
# Loaded by `exe/rigor` before anything else, so this file requires nothing from Rigor.
|
|
21
|
+
module BoxProbe
|
|
22
|
+
# A class-body lambda isolated by `Ractor.make_shareable`, then a method dispatch inside it — the
|
|
23
|
+
# four ingredients the crash needs. Exits 2 when `Ruby::Box` is not active (a Ruby without the
|
|
24
|
+
# feature ignores `RUBY_BOX=1`), so a missing feature is told apart from a crash.
|
|
25
|
+
SCRIPT = <<~PROBE
|
|
26
|
+
exit 2 unless defined?(Ruby::Box) && Ruby::Box.respond_to?(:enabled?) && Ruby::Box.enabled?
|
|
27
|
+
module RigorBoxProbe
|
|
28
|
+
RATIONAL = Ractor.make_shareable(lambda { |*args| Rational(*args) })
|
|
29
|
+
end
|
|
30
|
+
print RigorBoxProbe::RATIONAL.call(3, 4).inspect
|
|
31
|
+
PROBE
|
|
32
|
+
|
|
33
|
+
EXPECTED_OUTPUT = "(3/4)"
|
|
34
|
+
|
|
35
|
+
# The reproducer answers in well under a second; a child still running after this is killed and
|
|
36
|
+
# counted as a failure, so a wedged probe cannot hang the launcher (or `rigor lsp` behind it).
|
|
37
|
+
DEADLINE_SECONDS = 10
|
|
38
|
+
|
|
39
|
+
module_function
|
|
40
|
+
|
|
41
|
+
# nil when `ruby` can run Rigor under `RUBY_BOX=1`; otherwise a short reason for the launcher's
|
|
42
|
+
# warning. Only a child killed by a signal is blamed on the bug; any other failure to answer is
|
|
43
|
+
# reported as the probe failing, never as a pass.
|
|
44
|
+
def unsupported_reason(ruby = RbConfig.ruby)
|
|
45
|
+
output, status = run(ruby)
|
|
46
|
+
return "the Ruby::Box probe could not run #{ruby}" if status.nil?
|
|
47
|
+
return nil if status.success? && output == EXPECTED_OUTPUT
|
|
48
|
+
return "Ruby::Box is not available in Ruby #{RUBY_VERSION}" if status.exitstatus == 2
|
|
49
|
+
return "the Ruby::Box probe failed (#{status})" unless status.signaled?
|
|
50
|
+
|
|
51
|
+
"Ruby #{RUBY_VERSION} crashes running Ractor-shareable procs inside Ruby::Box " \
|
|
52
|
+
"(Ruby Bug #22260; a Ruby carrying its fix is needed)"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# `RUBYOPT` is cleared so a `-r` of the caller's (bootsnap, a debugger, a coverage hook) cannot fail
|
|
56
|
+
# to load under `--disable-gems` and be mistaken for the crash; the probe needs nothing but core.
|
|
57
|
+
def run(ruby)
|
|
58
|
+
reader, writer = IO.pipe
|
|
59
|
+
pid = ::Process.spawn({ "RUBY_BOX" => "1", "RUBYOPT" => nil }, ruby, "--disable-gems", "-e", SCRIPT,
|
|
60
|
+
in: File::NULL, out: writer, err: File::NULL)
|
|
61
|
+
writer.close
|
|
62
|
+
output = read_until_eof(reader)
|
|
63
|
+
return [output, ::Process.wait2(pid).last] if output
|
|
64
|
+
|
|
65
|
+
::Process.kill(:KILL, pid)
|
|
66
|
+
::Process.wait(pid)
|
|
67
|
+
[nil, nil]
|
|
68
|
+
rescue ::SystemCallError
|
|
69
|
+
[nil, nil]
|
|
70
|
+
ensure
|
|
71
|
+
writer&.close unless writer&.closed?
|
|
72
|
+
reader&.close
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# The child's whole stdout, or nil when it has not closed it by the deadline.
|
|
76
|
+
def read_until_eof(reader)
|
|
77
|
+
deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + DEADLINE_SECONDS
|
|
78
|
+
output = +""
|
|
79
|
+
loop do
|
|
80
|
+
remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
81
|
+
return nil unless remaining.positive? && reader.wait_readable(remaining)
|
|
82
|
+
|
|
83
|
+
chunk = reader.read_nonblock(4096, exception: false)
|
|
84
|
+
return output if chunk.nil?
|
|
85
|
+
|
|
86
|
+
output << chunk if chunk.is_a?(String)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -24,7 +24,7 @@ module Rigor
|
|
|
24
24
|
# One bundled plugin: the gem name a `plugins:` entry spells, and the gems it models.
|
|
25
25
|
Entry = Data.define(:gem_name, :plugin_id, :target_gems)
|
|
26
26
|
|
|
27
|
-
BUNDLED_PLUGINS_ROOT = File.join(Loader::ENGINE_ROOT, "plugins")
|
|
27
|
+
BUNDLED_PLUGINS_ROOT = File.join(Loader::ENGINE_ROOT, "plugins").freeze
|
|
28
28
|
|
|
29
29
|
@entries = nil
|
|
30
30
|
@load_failures = {}
|
|
@@ -69,7 +69,7 @@ module Rigor
|
|
|
69
69
|
SELF_HEAD = "self"
|
|
70
70
|
|
|
71
71
|
attr_reader :receiver, :method, :singleton, :labels, :narrow, :discharge, :within, :on_result,
|
|
72
|
-
:taint, :why
|
|
72
|
+
:taint, :callee, :callee_fallbacks, :responds, :why
|
|
73
73
|
|
|
74
74
|
# The taint causes a plugin row may name. A closed subset of
|
|
75
75
|
# {Rigor::Effects::TaintCause::ALL}: a plugin may say "and there is more here I cannot see", but
|
|
@@ -85,13 +85,31 @@ module Rigor
|
|
|
85
85
|
# @param narrow — a {Rigor::Effects::Narrowing} handler name, when the call's own
|
|
86
86
|
# argument literals settle a question the row cannot (`connection.execute("SELECT …")`)
|
|
87
87
|
# @param discharge — see above; honoured only for a first-party bundled plugin
|
|
88
|
+
# @param callee — the name of a {Rigor::Effects::CalleeRule} rule, when a call to this
|
|
89
|
+
# method is also a call **graph edge** the syntax does not contain: `render :show` runs
|
|
90
|
+
# `app/views/users/show.html.erb`, which is an effect unit of its own (#393 / #1048). The rule
|
|
91
|
+
# reads the call's own argument literals and names the callee key; the engine owns the strategy,
|
|
92
|
+
# exactly as it does for `narrow:`.
|
|
93
|
+
# @param callee_fallbacks — the framework's lookup ORDER for a callee the rule named, as data (#1065):
|
|
94
|
+
# `{ "js" => ["html"] }` says a callee selector of `js` that no unit answers is retried as `html`
|
|
95
|
+
# before the row's `taint:` is seeded. Keys and values are selectors; each list is tried in order
|
|
96
|
+
# and the first that resolves wins. Meaningless — and refused — without `callee:`. Which rule
|
|
97
|
+
# consults it, and for which targets, is the rule's call rather than the row's: a table a plugin
|
|
98
|
+
# writes cannot know whether a format was inherited or written by the author.
|
|
99
|
+
# @param responds — whether a call to this method supplies the unit's answer, so a
|
|
100
|
+
# {Rigor::Effects::CalleeRule::UNIT_RULES} rule on the same receiver must not also apply. `render`,
|
|
101
|
+
# `redirect_to` and `head` each set it: an action that called one of them did not take Rails'
|
|
102
|
+
# implicit render, and edging it to the conventional template would attribute a view the action
|
|
103
|
+
# never rendered.
|
|
88
104
|
# @param why — the audit justification, required exactly as `data/effects/core.yml` requires
|
|
89
105
|
# one of every row: a label with no stated reason is a claim nobody can review.
|
|
90
106
|
def initialize(receiver:, method:, labels:, why:, singleton: false, narrow: nil, discharge: false, # rubocop:disable Metrics/ParameterLists
|
|
91
|
-
within: nil, on_result: false, taint: nil)
|
|
107
|
+
within: nil, on_result: false, taint: nil, callee: nil, callee_fallbacks: nil, responds: false)
|
|
92
108
|
@receiver = validate_receiver!(receiver)
|
|
93
109
|
@method = method.to_sym
|
|
94
110
|
@singleton = singleton ? true : false
|
|
111
|
+
@callee = callee.nil? ? nil : callee.to_s.dup.freeze
|
|
112
|
+
@responds = responds ? true : false
|
|
95
113
|
@labels = normalize_labels(labels)
|
|
96
114
|
@narrow = narrow.nil? ? nil : narrow.to_s.dup.freeze
|
|
97
115
|
@discharge = discharge ? true : false
|
|
@@ -99,6 +117,7 @@ module Rigor
|
|
|
99
117
|
@on_result = on_result ? true : false
|
|
100
118
|
validate_on_result!
|
|
101
119
|
@taint = validate_taint!(taint)
|
|
120
|
+
@callee_fallbacks = validate_callee_fallbacks!(callee_fallbacks)
|
|
102
121
|
@why = validate_why!(why)
|
|
103
122
|
freeze
|
|
104
123
|
end
|
|
@@ -125,7 +144,8 @@ module Rigor
|
|
|
125
144
|
{
|
|
126
145
|
"receiver" => @receiver, "method" => @method.to_s, "singleton" => @singleton,
|
|
127
146
|
"labels" => @labels, "narrow" => @narrow, "discharge" => @discharge, "within" => @within,
|
|
128
|
-
"on_result" => @on_result, "taint" => @taint
|
|
147
|
+
"on_result" => @on_result, "taint" => @taint, "callee" => @callee,
|
|
148
|
+
"callee_fallbacks" => @callee_fallbacks, "responds" => @responds
|
|
129
149
|
}
|
|
130
150
|
end
|
|
131
151
|
|
|
@@ -166,9 +186,16 @@ module Rigor
|
|
|
166
186
|
value.dup.freeze
|
|
167
187
|
end
|
|
168
188
|
|
|
189
|
+
# A row normally has to say something. The one exception is a row whose whole contribution is a
|
|
190
|
+
# `callee:` EDGE — Rails' implicit render is a fact about an action that made no call at all, so
|
|
191
|
+
# there is no site to colour and no label to state; what the row buys is the edge to the template.
|
|
192
|
+
# Reading the edge and inventing a label beside it would put `rails.response.write` on every private
|
|
193
|
+
# helper a controller happens to define.
|
|
169
194
|
def normalize_labels(labels)
|
|
170
195
|
list = Array(labels).map { |label| label.to_s.dup.freeze }
|
|
171
|
-
|
|
196
|
+
if list.empty? && @callee.nil?
|
|
197
|
+
raise ArgumentError, "effect attribution for #{@receiver} must declare at least one label"
|
|
198
|
+
end
|
|
172
199
|
|
|
173
200
|
list.uniq.sort.freeze
|
|
174
201
|
end
|
|
@@ -197,6 +224,33 @@ module Rigor
|
|
|
197
224
|
"got #{taint.inspect}"
|
|
198
225
|
end
|
|
199
226
|
|
|
227
|
+
# A selector is one dot-free, space-free segment — the half of a callee key after the receiver
|
|
228
|
+
# (`html` in `view:users/show.html`). Anything else could never be spliced into a key a unit answers,
|
|
229
|
+
# and a fallback that silently resolved nothing would read to its author as the feature being broken.
|
|
230
|
+
FALLBACK_SELECTOR = /\A[a-z0-9_]+\z/
|
|
231
|
+
private_constant :FALLBACK_SELECTOR
|
|
232
|
+
|
|
233
|
+
def validate_callee_fallbacks!(fallbacks)
|
|
234
|
+
return nil if fallbacks.nil?
|
|
235
|
+
raise ArgumentError, "effect attribution #{key} names `callee_fallbacks:` without a `callee:`" if @callee.nil?
|
|
236
|
+
raise ArgumentError, "effect attribution #{key} `callee_fallbacks:` must be a Hash" unless fallbacks.is_a?(Hash)
|
|
237
|
+
|
|
238
|
+
fallbacks.to_h do |from, to|
|
|
239
|
+
list = Array(to).map { |selector| fallback_selector!(selector) }.uniq
|
|
240
|
+
selector = fallback_selector!(from)
|
|
241
|
+
list.delete(selector)
|
|
242
|
+
[selector, list.freeze]
|
|
243
|
+
end.sort.to_h.freeze
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def fallback_selector!(selector)
|
|
247
|
+
value = selector.to_s
|
|
248
|
+
return value.dup.freeze if FALLBACK_SELECTOR.match?(value)
|
|
249
|
+
|
|
250
|
+
raise ArgumentError, "effect attribution #{key} `callee_fallbacks:` selector must be one lowercase " \
|
|
251
|
+
"segment, got #{selector.inspect}"
|
|
252
|
+
end
|
|
253
|
+
|
|
200
254
|
def validate_why!(why)
|
|
201
255
|
value = why.to_s
|
|
202
256
|
raise ArgumentError, "effect attribution #{key} needs a `why:` justification" if value.empty?
|
data/lib/rigor/plugin/loader.rb
CHANGED
|
@@ -35,13 +35,14 @@ module Rigor
|
|
|
35
35
|
suffix = "/#{gem_name}.rb"
|
|
36
36
|
$LOADED_FEATURES.rfind { |feature| feature.end_with?(suffix) }
|
|
37
37
|
end
|
|
38
|
+
Ractor.make_shareable(FEATURE_RESOLVER)
|
|
38
39
|
|
|
39
40
|
# #194 slice 2 (ADR-93 WD5) — the engine's own root, anchored from THIS file's location: the loader
|
|
40
41
|
# lives at `<root>/lib/rigor/plugin/loader.rb`, so three levels up is the engine root. It resolves
|
|
41
42
|
# identically in a git checkout and inside an installed `rigortype` gem, because the gem packages the
|
|
42
43
|
# `plugins/` tree at the same relative path — which is exactly what makes it a trustworthy anchor for
|
|
43
44
|
# the engine's own bundled plugin copies.
|
|
44
|
-
ENGINE_ROOT = File.expand_path("../../..", __dir__)
|
|
45
|
+
ENGINE_ROOT = File.expand_path("../../..", __dir__).freeze
|
|
45
46
|
|
|
46
47
|
# @param requirer — takes a gem name OR an absolute file path (#194 slice 2 — a bundled plugin is
|
|
47
48
|
# required by its {.bundled_plugin_path}) and returns truthy on successful require. Defaulted to
|
|
@@ -36,9 +36,14 @@ module Rigor
|
|
|
36
36
|
# - `method_names` — Array of Symbol method names. A call shape `<receiver_subclass>.get('/path') { ... }`
|
|
37
37
|
# matches when `:get` is in this list. (Named `verbs:` before ADR-60 WD2 normalised the macro
|
|
38
38
|
# value-object vocabulary.)
|
|
39
|
-
# - `self_type` —
|
|
40
|
-
#
|
|
41
|
-
#
|
|
39
|
+
# - `self_type` — the `self`-binding the substrate applies inside the block. `:receiver_instance`
|
|
40
|
+
# (the default) types `self` as an instance of the receiver class — the Sinatra contract. A String
|
|
41
|
+
# binds `self` to a *named* class instead of the receiver: `"Grape::Endpoint"` binds
|
|
42
|
+
# `Nominal[Grape::Endpoint]` (the block is `instance_eval`'d on an instance of that class, e.g. a
|
|
43
|
+
# route body on `Grape::Endpoint`), and `"singleton(Grape::API::Instance)"` binds
|
|
44
|
+
# `Singleton[Grape::API::Instance]` (the block is `instance_eval`'d on that *class object*, e.g.
|
|
45
|
+
# a `namespace` body on `Grape::API::Instance`). Reserved Symbol names (`:receiver_singleton`,
|
|
46
|
+
# `:dsl_recorder`) remain unaccepted.
|
|
42
47
|
#
|
|
43
48
|
# ## Ractor-shareability
|
|
44
49
|
#
|
|
@@ -48,6 +53,13 @@ module Rigor
|
|
|
48
53
|
SELF_TYPE_RECEIVER_INSTANCE = :receiver_instance
|
|
49
54
|
VALID_SELF_TYPES = [SELF_TYPE_RECEIVER_INSTANCE].freeze
|
|
50
55
|
|
|
56
|
+
CLASS_NAME_PATTERN = /[A-Z]\w*(?:::[A-Z]\w*)*/
|
|
57
|
+
# `self_type: "Foo::Bar"` — the block is `instance_eval`'d on an instance of `Foo::Bar`.
|
|
58
|
+
NAMED_SELF_TYPE_PATTERN = /\A#{CLASS_NAME_PATTERN}\z/
|
|
59
|
+
# `self_type: "singleton(Foo::Bar)"` — the block is `instance_eval`'d on the `Foo::Bar` class
|
|
60
|
+
# object itself (Grape's `namespace`/`route_param` bodies run on `Grape::API::Instance`).
|
|
61
|
+
SINGLETON_SELF_TYPE_PATTERN = /\Asingleton\((#{CLASS_NAME_PATTERN})\)\z/
|
|
62
|
+
|
|
51
63
|
attr_reader :receiver_constraint, :method_names, :self_type
|
|
52
64
|
|
|
53
65
|
def initialize(receiver_constraint:, method_names:, self_type: SELF_TYPE_RECEIVER_INSTANCE)
|
|
@@ -57,7 +69,7 @@ module Rigor
|
|
|
57
69
|
|
|
58
70
|
@receiver_constraint = receiver_constraint.dup.freeze
|
|
59
71
|
@method_names = method_names.map(&:to_sym).freeze
|
|
60
|
-
@self_type = self_type
|
|
72
|
+
@self_type = self_type.is_a?(String) ? self_type.dup.freeze : self_type
|
|
61
73
|
freeze
|
|
62
74
|
end
|
|
63
75
|
|
|
@@ -81,6 +93,31 @@ module Rigor
|
|
|
81
93
|
[receiver_constraint, method_names, self_type].hash
|
|
82
94
|
end
|
|
83
95
|
|
|
96
|
+
# The class name a String `self_type` binds `self` to — `"Foo::Bar"` for both the plain and the
|
|
97
|
+
# `singleton(Foo::Bar)` form — or `nil` for `:receiver_instance`.
|
|
98
|
+
def self_type_name
|
|
99
|
+
return nil unless self_type.is_a?(String)
|
|
100
|
+
|
|
101
|
+
self_type[SINGLETON_SELF_TYPE_PATTERN, 1] || self_type
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Whether a String `self_type` binds the *class object* (`"singleton(Foo::Bar)"`) rather than an
|
|
105
|
+
# instance (`"Foo::Bar"`). Nominal-receiver call sites only match instance-binding entries —
|
|
106
|
+
# a `singleton(...)` binding exists solely for `instance_eval`-on-class contracts, which always
|
|
107
|
+
# surface as `Singleton[X]` receivers.
|
|
108
|
+
def singleton_binding?
|
|
109
|
+
self_type.is_a?(String) && self_type.match?(SINGLETON_SELF_TYPE_PATTERN)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Whether `self_type` names a class whose *instance* the block is `instance_eval`'d on
|
|
113
|
+
# (`"Foo::Bar"`). These entries are the only ones that also match `Nominal[Y]` receivers: inside
|
|
114
|
+
# an already-narrowed `instance_eval` body (`self : Nominal[ParamsScope]`), a nested call on
|
|
115
|
+
# `self` re-enters the same context. `:receiver_instance` keeps its original Singleton-only
|
|
116
|
+
# contract — a nominal receiver calling a class-level verb is a different (unmodelled) shape.
|
|
117
|
+
def named_instance_binding?
|
|
118
|
+
self_type.is_a?(String) && !singleton_binding?
|
|
119
|
+
end
|
|
120
|
+
|
|
84
121
|
private
|
|
85
122
|
|
|
86
123
|
def validate_receiver_constraint!(value)
|
|
@@ -108,10 +145,12 @@ module Rigor
|
|
|
108
145
|
|
|
109
146
|
def validate_self_type!(self_type)
|
|
110
147
|
return if VALID_SELF_TYPES.include?(self_type)
|
|
148
|
+
return if self_type.is_a?(String) &&
|
|
149
|
+
(self_type.match?(NAMED_SELF_TYPE_PATTERN) || self_type.match?(SINGLETON_SELF_TYPE_PATTERN))
|
|
111
150
|
|
|
112
151
|
raise ArgumentError,
|
|
113
|
-
"Plugin::Macro::BlockAsMethod#self_type must be one of #{VALID_SELF_TYPES.inspect}
|
|
114
|
-
"got #{self_type.inspect}"
|
|
152
|
+
"Plugin::Macro::BlockAsMethod#self_type must be one of #{VALID_SELF_TYPES.inspect} " \
|
|
153
|
+
"or a class-name String ('Foo::Bar' / 'singleton(Foo::Bar)'), got #{self_type.inspect}"
|
|
115
154
|
end
|
|
116
155
|
end
|
|
117
156
|
end
|