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
|
@@ -22,12 +22,52 @@ module Rigor
|
|
|
22
22
|
# It is a STACK because captures nest (a memoised callee body evaluating another memo-miss callee): an
|
|
23
23
|
# outer capture must also see the inner window's reads, or its read-set would be non-transitive and a
|
|
24
24
|
# later replay would drop the nested callee's edges.
|
|
25
|
+
#
|
|
26
|
+
# Every event reaches every capture open at the time, so an inner capture's sets are always a subset of
|
|
27
|
+
# each outer one's: the inner capture was pushed after the outer, and everything added since reached both.
|
|
28
|
+
# {withhold} swaps in a fresh stack and restores this one untouched, so it keeps the property too.
|
|
29
|
+
# {tee_read} and {tee_missing} rely on it to stop at the first capture that already holds an event.
|
|
25
30
|
CAPTURE_KEY = :__rigor_dependency_capture_stack__
|
|
26
31
|
private_constant :CAPTURE_KEY
|
|
27
32
|
|
|
33
|
+
# The read sets an {Accumulator} or {Capture} already holds in full, by identity: replayed into it, or
|
|
34
|
+
# captured by a window it saw every event of ({capture}). Both only ever grow and a {ReadSet} is frozen,
|
|
35
|
+
# so replaying such a set into the same target again adds nothing. Recording replays the same stored
|
|
36
|
+
# read set many times over: on Rigor's own `lib`, 71% of the replays into a consumer repeated one
|
|
37
|
+
# already made into it, carrying 65% of the replayed events.
|
|
38
|
+
module ReplayMemory
|
|
39
|
+
def holds?(read_set)
|
|
40
|
+
!@held.nil? && @held.key?(read_set)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Called only once every event of `read_set` is in, so a replay that raises part-way leaves the
|
|
44
|
+
# target unmarked and the next replay adds the rest.
|
|
45
|
+
def hold!(read_set)
|
|
46
|
+
(@held ||= {}.compare_by_identity)[read_set] = true
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# #1590 — the resolution-chain filings ({file_chain_once}) an {Accumulator} or {Capture} already holds in
|
|
51
|
+
# full: per chain (by identity; the table holds the chain, so its identity cannot be reused while the target
|
|
52
|
+
# lives), the starts filed from it and the discovery index each was filed against. A filing is a pure
|
|
53
|
+
# function of those three, so filing it again into the same target adds nothing.
|
|
54
|
+
module ChainMemory
|
|
55
|
+
def chain_filed?(chain, start, discovery)
|
|
56
|
+
table = @chains&.[](chain)
|
|
57
|
+
!table.nil? && table.key?(start) && table[start].equal?(discovery)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def chain_filed!(chain, start, discovery)
|
|
61
|
+
((@chains ||= {}.compare_by_identity)[chain] ||= {})[start] = discovery
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
28
65
|
# Mutable per-consumer accumulator. Frozen into a {Record} snapshot when `record_for` returns.
|
|
29
66
|
class Accumulator
|
|
30
|
-
|
|
67
|
+
include ReplayMemory
|
|
68
|
+
include ChainMemory
|
|
69
|
+
|
|
70
|
+
attr_reader :consumer, :sources, :missing, :symbol_sources, :ancestry_sources, :suspensions
|
|
31
71
|
|
|
32
72
|
def initialize(consumer)
|
|
33
73
|
@consumer = consumer
|
|
@@ -39,6 +79,15 @@ module Rigor
|
|
|
39
79
|
# file-granularity by nature (a superclass edge touches the whole class).
|
|
40
80
|
@symbol_sources = Hash.new { |h, k| h[k] = Set.new }
|
|
41
81
|
@ancestry_sources = Set.new
|
|
82
|
+
@held = nil
|
|
83
|
+
@chains = nil
|
|
84
|
+
@suspensions = 0
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Counts the {record_for} windows of another consumer opened while this one was recording, which is
|
|
88
|
+
# when a capture window stops seeing only this consumer's events.
|
|
89
|
+
def suspend!
|
|
90
|
+
@suspensions += 1
|
|
42
91
|
end
|
|
43
92
|
|
|
44
93
|
def snapshot
|
|
@@ -65,16 +114,28 @@ module Rigor
|
|
|
65
114
|
# self-read filter is applied at {replay} time, mirroring {read_site}.
|
|
66
115
|
ReadSet = Data.define(:reads, :missing)
|
|
67
116
|
|
|
117
|
+
# What every window that saw nothing closes into: most windows {RbsDispatch}'s recording-mode memos
|
|
118
|
+
# open see nothing, and they need neither a copy of two empty Sets nor a place in a target's
|
|
119
|
+
# {ReplayMemory}.
|
|
120
|
+
EMPTY_READ_SET = Ractor.make_shareable(ReadSet.new(reads: Set.new, missing: Set.new))
|
|
121
|
+
|
|
68
122
|
# Mutable capture accumulator; snapshot into a frozen {ReadSet} when the capture window closes.
|
|
69
123
|
class Capture
|
|
124
|
+
include ReplayMemory
|
|
125
|
+
include ChainMemory
|
|
126
|
+
|
|
70
127
|
attr_reader :reads, :missing
|
|
71
128
|
|
|
72
129
|
def initialize
|
|
73
130
|
@reads = Set.new
|
|
74
131
|
@missing = Set.new
|
|
132
|
+
@held = nil
|
|
133
|
+
@chains = nil
|
|
75
134
|
end
|
|
76
135
|
|
|
77
136
|
def snapshot
|
|
137
|
+
return EMPTY_READ_SET if reads.empty? && missing.empty?
|
|
138
|
+
|
|
78
139
|
ReadSet.new(reads: reads.dup.freeze, missing: missing.dup.freeze)
|
|
79
140
|
end
|
|
80
141
|
end
|
|
@@ -94,6 +155,7 @@ module Rigor
|
|
|
94
155
|
# the previous recorder on exit.
|
|
95
156
|
def record_for(consumer)
|
|
96
157
|
previous = Thread.current[KEY]
|
|
158
|
+
previous&.suspend!
|
|
97
159
|
accumulator = Accumulator.new(consumer.to_s)
|
|
98
160
|
Thread.current[KEY] = accumulator
|
|
99
161
|
@mutex.synchronize { @active_count += 1 }
|
|
@@ -109,6 +171,13 @@ module Rigor
|
|
|
109
171
|
@active_count.positive?
|
|
110
172
|
end
|
|
111
173
|
|
|
174
|
+
# Whether THIS thread's reads reach an accumulator: inside {record_for}, including a {withhold}
|
|
175
|
+
# window. {active?} answers for the process and is the cheap gate; this is the exact one, for a caller
|
|
176
|
+
# that must know whether an empty {capture} means "nothing to record" or "nothing was watching".
|
|
177
|
+
def recording?
|
|
178
|
+
!Thread.current[KEY].nil?
|
|
179
|
+
end
|
|
180
|
+
|
|
112
181
|
# ADR-84 WD2 — runs the block under an observe-and-forward capture window and returns
|
|
113
182
|
# `[block_result, read_set]`. Reads recorded during the window reach the current consumer's accumulator
|
|
114
183
|
# unchanged (forwarding) and are additionally collected into the returned frozen {ReadSet} (observing),
|
|
@@ -116,6 +185,8 @@ module Rigor
|
|
|
116
185
|
# {active?}; opening a capture with no accumulator active would collect reads no run is recording.
|
|
117
186
|
def capture
|
|
118
187
|
captures = (Thread.current[CAPTURE_KEY] ||= [])
|
|
188
|
+
accumulator = Thread.current[KEY]
|
|
189
|
+
suspensions = accumulator&.suspensions
|
|
119
190
|
capture = Capture.new
|
|
120
191
|
captures.push(capture)
|
|
121
192
|
begin
|
|
@@ -123,6 +194,48 @@ module Rigor
|
|
|
123
194
|
ensure
|
|
124
195
|
captures.pop
|
|
125
196
|
end
|
|
197
|
+
read_set = capture.snapshot
|
|
198
|
+
note_captured(read_set, captures, accumulator, suspensions)
|
|
199
|
+
[result, read_set]
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Every event a window saw also reached the window outside it, and was filed for the consumer that was
|
|
203
|
+
# recording at the time, except the consumer's own self-reads, which a replay into that consumer drops
|
|
204
|
+
# anyway. So the enclosing window holds the closed window's read set in full, and so does the consumer's
|
|
205
|
+
# accumulator when no other consumer recorded in between. Noting that lets a later replay of the set
|
|
206
|
+
# there skip a pass that could add nothing: on Rigor's own `lib`, two thirds of the first replays into a
|
|
207
|
+
# consumer were of a set captured while that consumer was recording.
|
|
208
|
+
def note_captured(read_set, captures, accumulator, suspensions)
|
|
209
|
+
return if read_set.equal?(EMPTY_READ_SET)
|
|
210
|
+
|
|
211
|
+
captures.last&.hold!(read_set)
|
|
212
|
+
return unless accumulator && Thread.current[KEY].equal?(accumulator)
|
|
213
|
+
return unless accumulator.suspensions == suspensions
|
|
214
|
+
|
|
215
|
+
accumulator.hold!(read_set)
|
|
216
|
+
end
|
|
217
|
+
private_class_method :note_captured
|
|
218
|
+
|
|
219
|
+
# Issue #992 — runs the block with its reads DETACHED from the current consumer (and from any enclosing
|
|
220
|
+
# capture) and returns `[block_result, read_set]`, so a caller that learns only afterwards whether its
|
|
221
|
+
# answer depended on those reads can {replay} them or drop them. `call.wrong-arity` reads a class's whole
|
|
222
|
+
# envelope bucket to decide a call that turns out to fit, and recording that as a file-level ancestry
|
|
223
|
+
# edge would make every correct call on a project class re-check on any edit to the class's files.
|
|
224
|
+
# Returns a nil read set when no recording is active.
|
|
225
|
+
def withhold
|
|
226
|
+
previous = Thread.current[KEY]
|
|
227
|
+
return [yield, nil] if previous.nil?
|
|
228
|
+
|
|
229
|
+
previous_captures = Thread.current[CAPTURE_KEY]
|
|
230
|
+
capture = Capture.new
|
|
231
|
+
Thread.current[KEY] = Accumulator.new(previous.consumer)
|
|
232
|
+
Thread.current[CAPTURE_KEY] = [capture]
|
|
233
|
+
begin
|
|
234
|
+
result = yield
|
|
235
|
+
ensure
|
|
236
|
+
Thread.current[KEY] = previous
|
|
237
|
+
Thread.current[CAPTURE_KEY] = previous_captures
|
|
238
|
+
end
|
|
126
239
|
[result, capture.snapshot]
|
|
127
240
|
end
|
|
128
241
|
|
|
@@ -131,22 +244,75 @@ module Rigor
|
|
|
131
244
|
# capture so an enclosing capture window stays transitive when a memo hit substitutes for a body walk.
|
|
132
245
|
def replay(read_set)
|
|
133
246
|
accumulator = Thread.current[KEY]
|
|
134
|
-
return if accumulator.nil? || read_set.nil?
|
|
247
|
+
return if accumulator.nil? || read_set.nil? || read_set.equal?(EMPTY_READ_SET)
|
|
135
248
|
|
|
136
249
|
captures = Thread.current[CAPTURE_KEY]
|
|
250
|
+
tee_read_set(captures, read_set) if captures && !captures.empty?
|
|
251
|
+
# Each set keeps its own insertion order whichever target is filled first, and a skipped replay is one
|
|
252
|
+
# that could only have re-added what the target already holds.
|
|
253
|
+
return if accumulator.holds?(read_set)
|
|
254
|
+
|
|
255
|
+
consumer = accumulator.consumer
|
|
137
256
|
read_set.reads.each do |pair|
|
|
138
|
-
captures&.each { |c| c.reads.add(pair) }
|
|
139
257
|
path, symbol = pair
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
accumulate_read(accumulator, path, symbol)
|
|
258
|
+
accumulate_read(accumulator, path, symbol) unless path == consumer
|
|
143
259
|
end
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
260
|
+
missing = accumulator.missing
|
|
261
|
+
read_set.missing.each { |entry| missing << entry }
|
|
262
|
+
accumulator.hold!(read_set)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# #1590 — runs the block, which files a resolution chain's edges from entry `start` (nil: the root and the
|
|
266
|
+
# whole chain) against `discovery`, unless the current consumer's accumulator AND the innermost open capture
|
|
267
|
+
# already hold that filing; the block files the same edges every time, so skipping it there changes nothing.
|
|
268
|
+
# Every event reaches every capture open at the time (see CAPTURE_KEY), so the innermost capture holding the
|
|
269
|
+
# filing means every open capture does. A filing the accumulator holds but a capture opened since does not is
|
|
270
|
+
# filed again, so that capture's read set, replayed elsewhere, still carries it; and {withhold} starts a fresh
|
|
271
|
+
# accumulator and capture, which hold nothing. The marks are made only once the block has run, as {hold!}'s are.
|
|
272
|
+
def file_chain_once(chain, start, discovery)
|
|
273
|
+
accumulator = Thread.current[KEY]
|
|
274
|
+
return if accumulator.nil?
|
|
275
|
+
|
|
276
|
+
captures = Thread.current[CAPTURE_KEY]
|
|
277
|
+
open = captures && !captures.empty?
|
|
278
|
+
if accumulator.chain_filed?(chain, start, discovery) &&
|
|
279
|
+
(!open || captures.last.chain_filed?(chain, start, discovery))
|
|
280
|
+
return
|
|
147
281
|
end
|
|
282
|
+
|
|
283
|
+
yield
|
|
284
|
+
accumulator.chain_filed!(chain, start, discovery)
|
|
285
|
+
captures.each { |capture| capture.chain_filed!(chain, start, discovery) } if open
|
|
148
286
|
end
|
|
149
287
|
|
|
288
|
+
# {replay}'s share for the open captures. The innermost capture having seen `read_set` already means every
|
|
289
|
+
# open capture holds all of it (see CAPTURE_KEY).
|
|
290
|
+
def tee_read_set(captures, read_set)
|
|
291
|
+
innermost = captures.last
|
|
292
|
+
return if innermost.holds?(read_set)
|
|
293
|
+
|
|
294
|
+
read_set.reads.each { |pair| tee_read(captures, pair) }
|
|
295
|
+
read_set.missing.each { |entry| tee_missing(captures, entry) }
|
|
296
|
+
innermost.hold!(read_set)
|
|
297
|
+
end
|
|
298
|
+
private_class_method :tee_read_set
|
|
299
|
+
|
|
300
|
+
# Adds a read pair to the open captures, innermost first, and stops at the first capture that already holds
|
|
301
|
+
# it: every capture outside that one holds it too (see CAPTURE_KEY). The capture sets end up exactly as
|
|
302
|
+
# adding it to every capture would leave them, in the same insertion order.
|
|
303
|
+
def tee_read(captures, pair)
|
|
304
|
+
index = captures.size - 1
|
|
305
|
+
index -= 1 while index >= 0 && captures[index].reads.add?(pair)
|
|
306
|
+
end
|
|
307
|
+
private_class_method :tee_read
|
|
308
|
+
|
|
309
|
+
# {tee_read} for a name-keyed `missing` entry.
|
|
310
|
+
def tee_missing(captures, entry)
|
|
311
|
+
index = captures.size - 1
|
|
312
|
+
index -= 1 while index >= 0 && captures[index].missing.add?(entry)
|
|
313
|
+
end
|
|
314
|
+
private_class_method :tee_missing
|
|
315
|
+
|
|
150
316
|
# Records that the current consumer read a declaration / body whose definition site is `path_line` (a
|
|
151
317
|
# `"path:line"` String, or nil). When `symbol` is given (a `"ClassName#method"` String), the read is a
|
|
152
318
|
# method-call edge and is recorded at symbol granularity in `symbol_sources` in addition to the coarse
|
|
@@ -158,16 +324,26 @@ module Rigor
|
|
|
158
324
|
accumulator = Thread.current[KEY]
|
|
159
325
|
return if accumulator.nil? || path_line.nil?
|
|
160
326
|
|
|
161
|
-
path = path_line
|
|
327
|
+
path = site_path(path_line)
|
|
162
328
|
return unless path
|
|
163
329
|
|
|
164
330
|
captures = Thread.current[CAPTURE_KEY]
|
|
165
|
-
captures
|
|
331
|
+
tee_read(captures, [path, symbol].freeze) if captures && !captures.empty?
|
|
166
332
|
return if path == accumulator.consumer
|
|
167
333
|
|
|
168
334
|
accumulate_read(accumulator, path, symbol)
|
|
169
335
|
end
|
|
170
336
|
|
|
337
|
+
# The path part of a `"path:line"` site: what `path_line.split(":", 2).first` answers, without the Array
|
|
338
|
+
# and the discarded line String.
|
|
339
|
+
def site_path(path_line)
|
|
340
|
+
colon = path_line.byteindex(":")
|
|
341
|
+
return path_line.byteslice(0, colon) if colon
|
|
342
|
+
|
|
343
|
+
path_line.empty? ? nil : path_line.dup
|
|
344
|
+
end
|
|
345
|
+
private_class_method :site_path
|
|
346
|
+
|
|
171
347
|
# Records a NAME-KEYED cross-file dependency: this consumer's answer depends on what the project makes
|
|
172
348
|
# of `name` under `kind`, so a recheck must re-analyze it when that changes. The recorded key is
|
|
173
349
|
# inverted into `negative_dependents` and matched against the producer for its kind.
|
|
@@ -183,10 +359,38 @@ module Rigor
|
|
|
183
359
|
return if accumulator.nil?
|
|
184
360
|
|
|
185
361
|
entry = "#{kind}:#{name}"
|
|
186
|
-
Thread.current[CAPTURE_KEY]
|
|
362
|
+
captures = Thread.current[CAPTURE_KEY]
|
|
363
|
+
tee_missing(captures, entry) if captures && !captures.empty?
|
|
187
364
|
accumulator.missing.add(entry)
|
|
188
365
|
end
|
|
189
366
|
|
|
367
|
+
# {read_name} keyed on the LAST SEGMENT of a constant path, `name.delete_prefix("::").split("::").last`,
|
|
368
|
+
# which is the key the `class:` and `constant:` name edges use. The segment is found by scanning for the
|
|
369
|
+
# last separator rather than by building every segment: on Rigor's own `lib` that split cost about seven
|
|
370
|
+
# objects a call over half a million calls. A name the scan could read differently from `split` (one
|
|
371
|
+
# that is not ASCII, or carries a run of colons or a trailing one) takes `split`'s answer.
|
|
372
|
+
def read_last_segment(kind, name)
|
|
373
|
+
return if Thread.current[KEY].nil?
|
|
374
|
+
|
|
375
|
+
segment = last_segment(name)
|
|
376
|
+
read_name(kind, segment) if segment
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def last_segment(name)
|
|
380
|
+
return name.delete_prefix("::").split("::").last unless plain_constant_path?(name)
|
|
381
|
+
|
|
382
|
+
start = name.start_with?("::") ? 2 : 0
|
|
383
|
+
separator = name.rindex("::")
|
|
384
|
+
cut = separator && separator >= start ? separator + 2 : start
|
|
385
|
+
cut == name.bytesize ? nil : name.byteslice(cut, name.bytesize - cut)
|
|
386
|
+
end
|
|
387
|
+
private_class_method :last_segment
|
|
388
|
+
|
|
389
|
+
def plain_constant_path?(name)
|
|
390
|
+
name.ascii_only? && !name.end_with?(":") && !name.include?(":::")
|
|
391
|
+
end
|
|
392
|
+
private_class_method :plain_constant_path?
|
|
393
|
+
|
|
190
394
|
# The miss-only spelling of {read_name}: a cross-file lookup of `name` (kind `:method` / `:class` /
|
|
191
395
|
# `:toplevel` / …) that resolved to nothing. Kept as the name the internal spec's negative-edge
|
|
192
396
|
# contract uses.
|
|
@@ -194,13 +398,14 @@ module Rigor
|
|
|
194
398
|
read_name(kind, name)
|
|
195
399
|
end
|
|
196
400
|
|
|
197
|
-
# The positive-edge aggregation shared by {read_site} and {replay}.
|
|
401
|
+
# The positive-edge aggregation shared by {read_site} and {replay}. `sources` is the union of the other
|
|
402
|
+
# two tables' paths, and this is the only writer of all three, so a read the finer table already holds is
|
|
403
|
+
# already in `sources` and costs one Set probe instead of two.
|
|
198
404
|
def accumulate_read(accumulator, path, symbol)
|
|
199
|
-
accumulator.sources << path
|
|
200
405
|
if symbol
|
|
201
|
-
accumulator.symbol_sources[path]
|
|
202
|
-
|
|
203
|
-
accumulator.
|
|
406
|
+
accumulator.sources << path if accumulator.symbol_sources[path].add?(symbol)
|
|
407
|
+
elsif accumulator.ancestry_sources.add?(path)
|
|
408
|
+
accumulator.sources << path
|
|
204
409
|
end
|
|
205
410
|
end
|
|
206
411
|
private_class_method :accumulate_read
|
|
@@ -21,8 +21,17 @@ module Rigor
|
|
|
21
21
|
# #775 — a target is an immutable value keyed by its name, and the local target is built on every
|
|
22
22
|
# binding (`Scope#with_local` invalidates through it) and every local-fact read: ~350k times on
|
|
23
23
|
# the lib self-check for a few thousand distinct names. One shared instance per name.
|
|
24
|
+
#
|
|
25
|
+
# #1055 — the table is a class ivar holding an unshareable Hash, and a non-main Ractor may neither
|
|
26
|
+
# write one nor read one. A pool worker therefore pays the allocation instead of sharing the
|
|
27
|
+
# instance: the interning is a per-process ALLOCATION saving, never an identity contract (`Target`
|
|
28
|
+
# is a `Data` value and compares by value), so declining it changes nothing a caller can observe.
|
|
29
|
+
# The guard is a C-level predicate on the hot path, which is what keeps the sequential run's
|
|
30
|
+
# saving intact.
|
|
24
31
|
def self.local(name)
|
|
25
32
|
name = name.to_sym
|
|
33
|
+
return new(kind: :local, name: name) unless Ractor.main?
|
|
34
|
+
|
|
26
35
|
@local_targets[name] ||= new(kind: :local, name: name)
|
|
27
36
|
end
|
|
28
37
|
@local_targets = {}
|
|
@@ -120,6 +120,32 @@ module Rigor
|
|
|
120
120
|
appeared.freeze
|
|
121
121
|
end
|
|
122
122
|
|
|
123
|
+
# Issue #1120 — the method names whose refinements moved in this edit: a `[refined class, method, refining
|
|
124
|
+
# module]` entry that appeared in, or vanished from, any file in `paths`. A consumer whose
|
|
125
|
+
# `call.undefined-method` answer consulted the refinement table recorded a `refinement:<method>` edge
|
|
126
|
+
# whichever way it answered, because the answer is a function of the whole project's refinements of that
|
|
127
|
+
# name. `paths` MUST include removed files, whose whole before-table vanished. `before` / `after` map a
|
|
128
|
+
# path to that file's `{refined class => {method => [modules]}}` table (nil when it refines nothing).
|
|
129
|
+
def changed_refinement_names(paths, before, after)
|
|
130
|
+
moved = Set.new
|
|
131
|
+
paths.each do |path|
|
|
132
|
+
entries_before = refinement_entries(before[path])
|
|
133
|
+
entries_after = refinement_entries(after[path])
|
|
134
|
+
(entries_before ^ entries_after).each { |(_refined, method_name, _module)| moved << method_name }
|
|
135
|
+
end
|
|
136
|
+
moved.freeze
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def refinement_entries(table)
|
|
140
|
+
entries = Set.new
|
|
141
|
+
table&.each do |refined, methods|
|
|
142
|
+
methods.each do |method_name, modules|
|
|
143
|
+
modules.each { |refining| entries << [refined, method_name, refining] }
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
entries
|
|
147
|
+
end
|
|
148
|
+
|
|
123
149
|
# Issue #639 — the SYMMETRIC reading of {appeared_classes}: the names whose declaration in a file
|
|
124
150
|
# appeared OR VANISHED. Appearance alone answered the miss side, which was the only side recording a
|
|
125
151
|
# `class:` edge; a bare reference that RESOLVED now records one too, and what invalidates it is the
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
require_relative "../incremental_run_slot"
|
|
7
|
+
require_relative "../../cache/file_digest"
|
|
8
|
+
require_relative "../../cache/incremental_snapshot"
|
|
9
|
+
|
|
10
|
+
module Rigor
|
|
11
|
+
module Analysis
|
|
12
|
+
module IncrementalRunSlot
|
|
13
|
+
# ADR-45 WD2 (#1507) — whether a run may record its answer at all. Some of a slot's rows are built when the
|
|
14
|
+
# run ENDS rather than when it reads, and each of those must still describe the tree the run analysed: a
|
|
15
|
+
# save that lands while the run reads — an editor's, a `bundle install`'s, a `mkdir` — would otherwise leave
|
|
16
|
+
# the slot vouching for a tree the analysis never saw, and the probe would serve the pre-save answer against
|
|
17
|
+
# the post-save tree.
|
|
18
|
+
#
|
|
19
|
+
# So the session takes a mark before the run reads anything ({.start}) and asks {#admits?} about those rows
|
|
20
|
+
# before it writes. A row recorded as the run read (a plugin's read, a listing it took) needs no guard: a
|
|
21
|
+
# later save leaves it stale, and validation says so.
|
|
22
|
+
#
|
|
23
|
+
# The mark is the filesystem's own clock, read off the change time of a file written for the purpose: a
|
|
24
|
+
# timestamp compared across clocks misses an edit that lands inside a coarse filesystem's tick, and change
|
|
25
|
+
# times (unlike modification times) cannot be set back by `cp -p` or `touch -d`. One clock means one
|
|
26
|
+
# filesystem, so the mark is taken only when the store is on the project's filesystem, and a row the key
|
|
27
|
+
# does not pin refuses the write when it is on any other. Declining to write is always safe; it leaves the
|
|
28
|
+
# next null run on the full path.
|
|
29
|
+
#
|
|
30
|
+
# A filesystem's change times tick coarsely (a millisecond or more on ext4 and tmpfs), so a file saved just
|
|
31
|
+
# before the stamp can carry the stamp's own change time. {#admits?} refuses a change time at or after the
|
|
32
|
+
# mark, since a save after the stamp in the same tick reads the same; taking the first stamp as the mark
|
|
33
|
+
# would therefore refuse every save that landed shortly before the run, and no slot would be written. So the
|
|
34
|
+
# mark is taken on a tick boundary: stamps are written until one carries a later change time than the first,
|
|
35
|
+
# and that later change time is the mark. Everything saved before the run started carries a change time no
|
|
36
|
+
# later than the first stamp, hence before the mark; everything saved after the mark carries at least the
|
|
37
|
+
# mark. The wait is bounded ({TICK_WAIT_LIMIT}); a filesystem that does not tick within it takes no mark,
|
|
38
|
+
# which is the safe answer. A filesystem whose first stamp lands on a whole second (HFS+, FAT, ext3) ticks too
|
|
39
|
+
# coarsely to wait out and takes no mark at once.
|
|
40
|
+
#
|
|
41
|
+
# Where the change time is not the time of the write, the mark says nothing, and the guard takes none or
|
|
42
|
+
# cannot tell: native Windows, where a change time is the creation time, takes no mark; a network or FUSE
|
|
43
|
+
# filesystem (NFS with its attribute cache, virtiofs, sshfs) may report a change time from before a save,
|
|
44
|
+
# which nothing here detects. A clock that steps backwards (an NTP correction) inside the run would date a
|
|
45
|
+
# save before the mark, so {#admits?} takes one more stamp and refuses when it reads earlier than the mark.
|
|
46
|
+
# A step undone before the run ends is not seen.
|
|
47
|
+
class WriteGuard
|
|
48
|
+
STAMP_DIR = "incremental"
|
|
49
|
+
private_constant :STAMP_DIR
|
|
50
|
+
|
|
51
|
+
# How long to wait, in seconds, for the filesystem's change time to tick past the first stamp.
|
|
52
|
+
TICK_WAIT_LIMIT = 0.05
|
|
53
|
+
# The pause between stamps.
|
|
54
|
+
TICK_RETRY_DELAY = 0.0005
|
|
55
|
+
private_constant :TICK_RETRY_DELAY
|
|
56
|
+
|
|
57
|
+
# Takes the mark, or nil when the slot must not be written: no stamp can be written, it is not on the
|
|
58
|
+
# project's filesystem, or the snapshot fingerprint the run was given no longer describes the tree.
|
|
59
|
+
#
|
|
60
|
+
# The fingerprint is recomputed AFTER the stamp: the caller computed it earlier, and a lockfile or a
|
|
61
|
+
# signature file under the configured or auto-detected `sig/` roots, or a `pre_eval:` file, changed in
|
|
62
|
+
# between would leave the snapshot the run restores keyed by one tree
|
|
63
|
+
# and the run reading another. From the stamp on, the lockfiles are the guard's to watch ({#admits?}), which
|
|
64
|
+
# is also what lets the slot's key, whose only file inputs they are, be computed when the run ends.
|
|
65
|
+
#
|
|
66
|
+
# The mark also records which of the paths an existence row can name are present: the lockfiles, the
|
|
67
|
+
# analysis roots, the `pre_eval:` entries and the signature roots.
|
|
68
|
+
def self.start(configuration:, roots:, cache_root:, fingerprint:)
|
|
69
|
+
return nil if Gem.win_platform?
|
|
70
|
+
|
|
71
|
+
started_ns, device = stamp(cache_root)
|
|
72
|
+
return nil if started_ns.nil? || device_of(Dir.pwd) != device
|
|
73
|
+
return nil unless Cache::IncrementalSnapshot.fingerprint(configuration: configuration, roots: roots) ==
|
|
74
|
+
fingerprint
|
|
75
|
+
|
|
76
|
+
lockfiles = lockfile_paths(configuration)
|
|
77
|
+
named = Array(roots) + configuration.pre_eval + (configuration.signature_paths || ["sig"])
|
|
78
|
+
presence = (lockfiles + named.map { |path| File.absolute_path(path.to_s) }).to_h do |path|
|
|
79
|
+
[path, File.exist?(path)]
|
|
80
|
+
end
|
|
81
|
+
new(started_ns: started_ns, device: device, lockfiles: lockfiles, presence: presence,
|
|
82
|
+
stamp_dir: stamp_dir(cache_root))
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# The mark: the change time and device of the first stamp whose change time is later than that of a
|
|
86
|
+
# stamp taken before it, on the same device; nil when none is written within {TICK_WAIT_LIMIT}.
|
|
87
|
+
def self.stamp(cache_root)
|
|
88
|
+
dir = stamp_dir(cache_root)
|
|
89
|
+
FileUtils.mkdir_p(dir)
|
|
90
|
+
first_ns, device = write_stamp(dir)
|
|
91
|
+
# A change time on a whole second is a filesystem that ticks by the second: waiting for it would burn the
|
|
92
|
+
# whole bound. One that ticks finer lands there once in a billion stamps, and declining is safe.
|
|
93
|
+
return nil if first_ns.nil? || (first_ns % 1_000_000_000).zero?
|
|
94
|
+
|
|
95
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + TICK_WAIT_LIMIT
|
|
96
|
+
loop do
|
|
97
|
+
ns, dev = write_stamp(dir)
|
|
98
|
+
return nil if ns.nil? || dev != device
|
|
99
|
+
return [ns, device] if ns > first_ns
|
|
100
|
+
return nil if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
101
|
+
|
|
102
|
+
sleep(TICK_RETRY_DELAY)
|
|
103
|
+
end
|
|
104
|
+
rescue SystemCallError, IOError
|
|
105
|
+
nil
|
|
106
|
+
end
|
|
107
|
+
private_class_method :stamp
|
|
108
|
+
|
|
109
|
+
def self.stamp_dir(cache_root)
|
|
110
|
+
File.join(cache_root.to_s, STAMP_DIR)
|
|
111
|
+
end
|
|
112
|
+
private_class_method :stamp_dir
|
|
113
|
+
|
|
114
|
+
# One stamp's change time and device; the file is removed again.
|
|
115
|
+
def self.write_stamp(dir)
|
|
116
|
+
path = File.join(dir, "run-#{Process.pid}-#{SecureRandom.hex(4)}.stamp")
|
|
117
|
+
File.write(path, "")
|
|
118
|
+
stat = File.stat(path)
|
|
119
|
+
[Cache::FileDigest.ns_of(stat.ctime), stat.dev]
|
|
120
|
+
rescue SystemCallError, IOError
|
|
121
|
+
nil
|
|
122
|
+
ensure
|
|
123
|
+
FileUtils.rm_f(path) if path
|
|
124
|
+
end
|
|
125
|
+
private_class_method :write_stamp
|
|
126
|
+
|
|
127
|
+
def self.device_of(path)
|
|
128
|
+
File.stat(path).dev
|
|
129
|
+
rescue SystemCallError
|
|
130
|
+
nil
|
|
131
|
+
end
|
|
132
|
+
private_class_method :device_of
|
|
133
|
+
|
|
134
|
+
# Every lockfile the key or the snapshot fingerprint may digest, present or not: a lockfile that appears
|
|
135
|
+
# while the run reads moves the key as surely as one rewritten, and one rewritten and restored digests as it
|
|
136
|
+
# did while the run may have read the other bytes.
|
|
137
|
+
def self.lockfile_paths(configuration)
|
|
138
|
+
configured = [
|
|
139
|
+
[configuration.bundler_lockfile, Environment::LockfileResolver],
|
|
140
|
+
[configuration.rbs_collection_lockfile, Environment::RbsCollectionDiscovery]
|
|
141
|
+
].filter_map { |path, resolver| resolver.configured_lockfile_path(path) if path }
|
|
142
|
+
(configured + %w[Gemfile.lock rbs_collection.lock.yaml].map { |path| File.absolute_path(path) }).uniq
|
|
143
|
+
end
|
|
144
|
+
private_class_method :lockfile_paths
|
|
145
|
+
|
|
146
|
+
def initialize(started_ns:, device:, lockfiles:, presence: {}, stamp_dir: nil)
|
|
147
|
+
@stamp_dir = stamp_dir
|
|
148
|
+
@started_ns = started_ns
|
|
149
|
+
@device = device
|
|
150
|
+
@lockfiles = lockfiles
|
|
151
|
+
@presence = presence
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# True when nothing the guard watches moved since the mark:
|
|
155
|
+
#
|
|
156
|
+
# - no lockfile the key or the fingerprint digests was written, created or removed;
|
|
157
|
+
# - no file `rows` or `pinned` names changed. A content row's file changed if its change time moved. An
|
|
158
|
+
# existence row asks only whether its path is there, so a path the mark recorded changed if it came or
|
|
159
|
+
# went, and an editor's lock file created and removed beside it, which moves its directory's change
|
|
160
|
+
# time, does not count. That needs no clock, so it holds on any filesystem; it cannot see a path that
|
|
161
|
+
# comes and goes again within the run. Any other existence row falls back to change times: its own when
|
|
162
|
+
# present, its nearest existing ancestor's when absent;
|
|
163
|
+
# - no directory a glob row lists gained or lost an entry, nor, for a stat-mode glob, did any file it
|
|
164
|
+
# matches change.
|
|
165
|
+
#
|
|
166
|
+
# A path on another filesystem than the mark refuses the write when it is one of `rows`, and is passed
|
|
167
|
+
# over when it is one of `pinned`: the rows the key pins (the engine's own signature files, a gem's
|
|
168
|
+
# signatures, which the engine slot and the lockfiles identify) live wherever the engine and the gems are
|
|
169
|
+
# installed — a container image's layer, a Nix store — and refusing them would turn the fast path off for
|
|
170
|
+
# every such installation. On the mark's filesystem they are checked like any other row.
|
|
171
|
+
#
|
|
172
|
+
# A content row whose file is gone passes: the row is stale already, and validation says so.
|
|
173
|
+
#
|
|
174
|
+
# @param rows — a {Cache::Descriptor} of the rows the key does not pin
|
|
175
|
+
# @param pinned — a {Cache::Descriptor} of the rows it does
|
|
176
|
+
def admits?(rows, pinned: Cache::Descriptor.new)
|
|
177
|
+
!clock_stepped_back? && @lockfiles.none? { |path| lockfile_changed?(path) } &&
|
|
178
|
+
files_unchanged?(rows.files, strict: true) && globs_unchanged?(rows.globs, strict: true) &&
|
|
179
|
+
files_unchanged?(pinned.files, strict: false) && globs_unchanged?(pinned.globs, strict: false)
|
|
180
|
+
rescue StandardError
|
|
181
|
+
false
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# `descriptor` with every `:stat` row's recording instant no later than the mark. A row recorded as the run
|
|
185
|
+
# read carries the instant the run began, and validation trusts a stat tuple whose mtime is before it. On a
|
|
186
|
+
# coarse clock a save that lands after the read, in the tick the file's mtime already has and with the same
|
|
187
|
+
# size, leaves the tuple unmoved, so it would be trusted. With the instant at the mark, every row whose mtime is
|
|
188
|
+
# at or after the mark is racy, and validation re-hashes it.
|
|
189
|
+
def mark_racy(descriptor)
|
|
190
|
+
Cache::Descriptor.new(
|
|
191
|
+
files: descriptor.files.map { |entry| entry.with_recording_instant_at_most(@started_ns) },
|
|
192
|
+
gems: descriptor.gems, plugins: descriptor.plugins, configs: descriptor.configs,
|
|
193
|
+
dependencies: descriptor.dependencies, globs: descriptor.globs
|
|
194
|
+
)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
private
|
|
198
|
+
|
|
199
|
+
# One more stamp: a change time earlier than the mark means the clock stepped back since it was taken, so
|
|
200
|
+
# a save since then may carry a change time the guard reads as before it. A stamp that cannot be written,
|
|
201
|
+
# or lands on another device, cannot vouch for the clock either.
|
|
202
|
+
def clock_stepped_back?
|
|
203
|
+
return false if @stamp_dir.nil?
|
|
204
|
+
|
|
205
|
+
ns, device = self.class.__send__(:write_stamp, @stamp_dir)
|
|
206
|
+
ns.nil? || device != @device || ns < @started_ns
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# An existence row and a content row for one path are different questions (the first asks only whether it
|
|
210
|
+
# is there, the second whether it changed), so the row that comes first must not hide the other.
|
|
211
|
+
def files_unchanged?(entries, strict:)
|
|
212
|
+
seen = Set.new
|
|
213
|
+
entries.none? do |entry|
|
|
214
|
+
next false unless seen.add?([entry.path, entry.comparator == :exists])
|
|
215
|
+
|
|
216
|
+
if entry.comparator == :exists
|
|
217
|
+
existence_changed?(entry.path, strict: strict)
|
|
218
|
+
else
|
|
219
|
+
changed?(entry.path, strict: strict)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def globs_unchanged?(entries, strict:)
|
|
225
|
+
entries.none? do |entry|
|
|
226
|
+
listed = Dir.glob(File.join(entry.root, File.dirname(entry.pattern), ""))
|
|
227
|
+
matched = entry.mode == :stat ? Dir.glob(File.join(entry.root, entry.pattern)) : []
|
|
228
|
+
(listed + matched).any? { |path| changed?(path, strict: strict) }
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# A lockfile is read for its bytes: one absent at the mark must still be absent, one present must still be
|
|
233
|
+
# there with its change time before the mark.
|
|
234
|
+
def lockfile_changed?(path)
|
|
235
|
+
return File.exist?(path) unless @presence[path]
|
|
236
|
+
|
|
237
|
+
changed?(path, strict: true, missing: true)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def existence_changed?(path, strict:)
|
|
241
|
+
absolute = File.absolute_path(path)
|
|
242
|
+
return File.exist?(absolute) != @presence[absolute] if @presence.key?(absolute)
|
|
243
|
+
|
|
244
|
+
nearest = absolute
|
|
245
|
+
nearest = File.dirname(nearest) until File.exist?(nearest) || File.dirname(nearest) == nearest
|
|
246
|
+
moved?(File.stat(nearest), strict: strict)
|
|
247
|
+
rescue SystemCallError
|
|
248
|
+
true
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def changed?(path, strict:, missing: false)
|
|
252
|
+
moved?(File.stat(path), strict: strict)
|
|
253
|
+
rescue SystemCallError
|
|
254
|
+
missing
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# A change time is comparable with the mark only on the stamp's filesystem.
|
|
258
|
+
def moved?(stat, strict:)
|
|
259
|
+
return strict if stat.dev != @device
|
|
260
|
+
|
|
261
|
+
Cache::FileDigest.ns_of(stat.ctime) >= @started_ns
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|