rigortype 0.3.9 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/data/core_overlay/enumerable.rbs +51 -0
- data/data/core_overlay/enumerator.rbs +84 -0
- data/data/core_overlay/hash_rbs3.rbs +41 -0
- data/data/core_overlay/process.rbs +40 -0
- data/data/core_overlay/string_io.rbs +33 -0
- data/data/effects/core.yml +3 -3
- data/data/gem_overlay/activesupport/core_ext.rbs +127 -4
- data/docs/handbook/03-narrowing.md +95 -10
- data/docs/handbook/04-tuples-and-shapes.md +8 -6
- data/docs/handbook/07-rbs-and-extended.md +16 -11
- data/docs/handbook/10-sorbet.md +9 -10
- data/docs/handbook/11-sig-gen.md +454 -13
- data/docs/manual/02-cli-reference.md +63 -2
- data/docs/manual/03-configuration.md +7 -0
- data/docs/manual/04-diagnostics.md +4 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/10-mcp-server.md +3 -2
- data/docs/manual/16-rbs-extended-annotations.md +40 -7
- data/docs/manual/19-effect-labels.md +10 -0
- data/docs/manual/plugins/README.md +7 -0
- data/docs/manual/plugins/rigor-actioncable.md +8 -1
- data/docs/manual/plugins/rigor-actionmailer.md +7 -0
- data/docs/manual/plugins/rigor-actionpack.md +243 -0
- data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
- data/docs/manual/plugins/rigor-activejob.md +7 -0
- data/docs/manual/plugins/rigor-activerecord.md +110 -4
- data/docs/manual/plugins/rigor-activestorage.md +8 -1
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
- data/docs/manual/plugins/rigor-grape.md +106 -0
- data/docs/manual/plugins/rigor-graphql.md +23 -2
- data/docs/manual/plugins/rigor-pundit.md +8 -1
- data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
- data/docs/manual/plugins/rigor-rails-routes.md +7 -0
- data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
- data/docs/manual/plugins/rigor-sidekiq.md +8 -1
- data/docs/manual/plugins/rigor-sorbet.md +21 -5
- data/exe/rigor +19 -4
- data/lib/rigor/analysis/baseline.rb +1 -1
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
- data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
- data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
- data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
- data/lib/rigor/analysis/check_rules.rb +520 -49
- data/lib/rigor/analysis/dependency_recorder.rb +23 -0
- data/lib/rigor/analysis/fact_store.rb +9 -0
- data/lib/rigor/analysis/incremental.rb +26 -0
- data/lib/rigor/analysis/incremental_session.rb +36 -4
- data/lib/rigor/analysis/project_scan.rb +11 -1
- data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
- data/lib/rigor/analysis/reachability/scan_cache.rb +1 -0
- data/lib/rigor/analysis/rule_catalog.rb +127 -0
- data/lib/rigor/analysis/run_cache_key.rb +20 -11
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +167 -11
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
- data/lib/rigor/analysis/runner/pool_coordinator.rb +107 -15
- data/lib/rigor/analysis/runner/project_pre_passes.rb +26 -7
- data/lib/rigor/analysis/runner.rb +278 -20
- data/lib/rigor/analysis/template_unit_collector.rb +303 -0
- data/lib/rigor/analysis/template_unit_paths.rb +91 -0
- data/lib/rigor/analysis/template_unit_positions.rb +293 -0
- data/lib/rigor/analysis/template_units.rb +399 -0
- data/lib/rigor/analysis/worker_session.rb +53 -10
- data/lib/rigor/bleeding_edge.rb +0 -2
- data/lib/rigor/builtins/hkt_builtins.rb +1 -0
- data/lib/rigor/builtins/imported_refinements.rb +4 -0
- data/lib/rigor/builtins/regex_refinement.rb +17 -9
- data/lib/rigor/builtins/static_return_refinements.rb +2 -0
- data/lib/rigor/cache/descriptor.rb +6 -1
- data/lib/rigor/cache/engine_source.rb +29 -1
- data/lib/rigor/cache/incremental_snapshot.rb +33 -1
- data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
- data/lib/rigor/cache/rbs_descriptor.rb +37 -7
- data/lib/rigor/cache/store.rb +0 -1
- data/lib/rigor/ci_detector.rb +1 -0
- data/lib/rigor/cli/doc_links.rb +1 -1
- data/lib/rigor/cli/docs_command.rb +4 -4
- data/lib/rigor/cli/plugin_command.rb +3 -3
- data/lib/rigor/cli/prism_colorizer.rb +0 -1
- data/lib/rigor/cli/sig_gen_command.rb +210 -29
- data/lib/rigor/cli/skill_command.rb +1 -1
- data/lib/rigor/cli/skill_describe.rb +0 -1
- data/lib/rigor/cli/type_of_command.rb +19 -5
- data/lib/rigor/cli/type_of_renderer.rb +20 -9
- data/lib/rigor/cli/type_of_template_probe.rb +189 -0
- data/lib/rigor/cli.rb +21 -3
- data/lib/rigor/configuration/severity_profile.rb +19 -3
- data/lib/rigor/configuration.rb +66 -3
- data/lib/rigor/effects/ancestry_recorder.rb +191 -0
- data/lib/rigor/effects/attribution.rb +11 -2
- data/lib/rigor/effects/callee_rule.rb +368 -0
- data/lib/rigor/effects/catalog.rb +7 -4
- data/lib/rigor/effects/collector.rb +11 -5
- data/lib/rigor/effects/config_envelopes.rb +9 -2
- data/lib/rigor/effects/definition_context.rb +179 -0
- data/lib/rigor/effects/effect_table.rb +11 -3
- data/lib/rigor/effects/envelope_check.rb +1 -1
- data/lib/rigor/effects/envelope_index.rb +15 -0
- data/lib/rigor/effects/file_collection.rb +59 -5
- data/lib/rigor/effects/framework_units.rb +1 -1
- data/lib/rigor/effects/identity.rb +16 -0
- data/lib/rigor/effects/local_ownership.rb +38 -12
- data/lib/rigor/effects/method_key.rb +21 -0
- data/lib/rigor/effects/mutation_classifier.rb +23 -12
- data/lib/rigor/effects/plugin_facts.rb +43 -31
- data/lib/rigor/effects/propagator.rb +295 -16
- data/lib/rigor/effects/registry.rb +1 -1
- data/lib/rigor/effects/scanner.rb +121 -72
- data/lib/rigor/effects/signature_sources.rb +1 -1
- data/lib/rigor/effects/snapshot.rb +2 -1
- data/lib/rigor/effects/summary.rb +27 -4
- data/lib/rigor/effects/unit_scan.rb +385 -30
- data/lib/rigor/effects/visibility.rb +101 -0
- data/lib/rigor/environment/lockfile_resolver.rb +17 -0
- data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
- data/lib/rigor/environment/member_consistency.rb +298 -0
- data/lib/rigor/environment/rbs_loader.rb +399 -133
- data/lib/rigor/environment.rb +103 -38
- data/lib/rigor/hashing/xxh3.rb +264 -0
- data/lib/rigor/inference/acceptance.rb +139 -9
- data/lib/rigor/inference/block_auto_splat.rb +216 -0
- data/lib/rigor/inference/block_call_timing.rb +338 -0
- data/lib/rigor/inference/block_parameter_binder.rb +73 -27
- data/lib/rigor/inference/block_repetition.rb +71 -0
- data/lib/rigor/inference/body_fixpoint.rb +2 -1
- data/lib/rigor/inference/budget_trace.rb +2 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
- data/lib/rigor/inference/captured_locals.rb +387 -15
- data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
- data/lib/rigor/inference/content_join.rb +200 -27
- data/lib/rigor/inference/def_return_typer.rb +11 -7
- data/lib/rigor/inference/define_method_block_self.rb +64 -0
- data/lib/rigor/inference/element_read_widening.rb +22 -10
- data/lib/rigor/inference/error_info.rb +196 -0
- data/lib/rigor/inference/expression_typer.rb +1532 -496
- data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
- data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
- data/lib/rigor/inference/global_write_census.rb +239 -0
- data/lib/rigor/inference/guard_rebinding.rb +447 -0
- data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
- data/lib/rigor/inference/index_write_widening.rb +16 -3
- data/lib/rigor/inference/indexed_narrowing.rb +61 -9
- data/lib/rigor/inference/jump_targets.rb +82 -0
- data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
- data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
- data/lib/rigor/inference/last_line.rb +340 -0
- data/lib/rigor/inference/last_status.rb +144 -0
- data/lib/rigor/inference/macro_block_self_type.rb +167 -17
- data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
- data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
- data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
- data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
- data/lib/rigor/inference/match_rebinding.rb +392 -0
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
- data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
- data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
- data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
- data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +881 -38
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher.rb +214 -13
- data/lib/rigor/inference/method_parameter_binder.rb +8 -3
- data/lib/rigor/inference/multi_target_binder.rb +340 -52
- data/lib/rigor/inference/mutation_rejoin.rb +4 -1
- data/lib/rigor/inference/mutation_widening.rb +70 -48
- data/lib/rigor/inference/narrowing.rb +540 -120
- data/lib/rigor/inference/operand_effects.rb +167 -0
- data/lib/rigor/inference/operand_walk.rb +88 -0
- data/lib/rigor/inference/optimistic_origin.rb +152 -9
- data/lib/rigor/inference/parameter_inference_collector.rb +3 -2
- data/lib/rigor/inference/project_method_ownership.rb +136 -0
- data/lib/rigor/inference/project_patched_methods.rb +7 -2
- data/lib/rigor/inference/project_patched_scanner.rb +7 -3
- data/lib/rigor/inference/receiver_alias.rb +90 -1
- data/lib/rigor/inference/receiver_blind_block.rb +219 -0
- data/lib/rigor/inference/refinement_mutation.rb +15 -11
- data/lib/rigor/inference/repeated_or_writes.rb +463 -0
- data/lib/rigor/inference/return_barrier.rb +54 -0
- data/lib/rigor/inference/rewrite_mutation.rb +120 -0
- data/lib/rigor/inference/scope_indexer.rb +4615 -551
- data/lib/rigor/inference/statement_evaluator.rb +3176 -490
- data/lib/rigor/inference/stored_block_call.rb +54 -0
- data/lib/rigor/inference/string_mutation.rb +44 -7
- data/lib/rigor/inference/unknown_store_widening.rb +200 -0
- data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
- data/lib/rigor/language_server/debouncer.rb +0 -1
- data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
- data/lib/rigor/language_server/hover_renderer.rb +3 -3
- data/lib/rigor/language_server/project_context.rb +5 -3
- data/lib/rigor/mcp/server.rb +2 -1
- data/lib/rigor/plugin/base.rb +168 -5
- data/lib/rigor/plugin/box_probe.rb +91 -0
- data/lib/rigor/plugin/bundled_catalog.rb +1 -1
- data/lib/rigor/plugin/effect_attribution.rb +58 -4
- data/lib/rigor/plugin/loader.rb +2 -1
- data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
- data/lib/rigor/plugin/manifest.rb +71 -10
- data/lib/rigor/plugin/registry.rb +35 -1
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
- data/lib/rigor/plugin/template_unit.rb +196 -0
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/protection/discovery_seed.rb +3 -1
- data/lib/rigor/protection/kill_signature.rb +0 -1
- data/lib/rigor/protection/mutation_cache.rb +1 -2
- data/lib/rigor/rbs_extended.rb +27 -0
- data/lib/rigor/reflection/constant_ancestors.rb +97 -0
- data/lib/rigor/reflection/constant_path.rb +19 -6
- data/lib/rigor/reflection.rb +72 -105
- data/lib/rigor/scope/discovery_index.rb +135 -2
- data/lib/rigor/scope.rb +859 -49
- data/lib/rigor/sig_gen/alias_index.rb +289 -0
- data/lib/rigor/sig_gen/classification.rb +22 -5
- data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
- data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
- data/lib/rigor/sig_gen/generator.rb +558 -31
- data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
- data/lib/rigor/sig_gen/method_candidate.rb +47 -3
- data/lib/rigor/sig_gen/observation_collector.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +124 -9
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
- data/lib/rigor/sig_gen/write_result.rb +19 -3
- data/lib/rigor/sig_gen/writer.rb +166 -35
- data/lib/rigor/sig_gen.rb +3 -0
- data/lib/rigor/signature_path_audit.rb +1 -1
- data/lib/rigor/source/node_walker.rb +0 -3
- data/lib/rigor/source/parameter_envelope.rb +72 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/combinator.rb +88 -12
- data/lib/rigor/type/difference.rb +1 -0
- data/lib/rigor/type/hash_shape.rb +1 -1
- data/lib/rigor/type/refined.rb +1 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
- data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
- data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
- data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
- data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
- data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
- data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
- data/plugins/rigor-grape/sig/grape.rbs +263 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
- data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
- data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
- data/sig/rigor/analysis/baseline.rbs +69 -7
- data/sig/rigor/analysis/fact_store.rbs +1 -1
- data/sig/rigor/analysis/project_scan.rbs +74 -0
- data/sig/rigor/effects/config_envelopes.rbs +100 -0
- data/sig/rigor/effects/effect_table.rbs +60 -0
- data/sig/rigor/effects/envelope.rbs +97 -0
- data/sig/rigor/effects/envelope_index.rbs +33 -0
- data/sig/rigor/effects/file_collection.rbs +81 -0
- data/sig/rigor/effects/label.rbs +26 -0
- data/sig/rigor/effects/label_set.rbs +46 -0
- data/sig/rigor/effects/method_key.rbs +24 -0
- data/sig/rigor/effects/origin.rbs +50 -0
- data/sig/rigor/effects/plugin_facts.rbs +141 -0
- data/sig/rigor/effects/registry.rbs +68 -0
- data/sig/rigor/effects/summary.rbs +50 -0
- data/sig/rigor/effects/taint_cause.rbs +12 -0
- data/sig/rigor/environment.rbs +12 -10
- data/sig/rigor/inference/optimistic_origin.rbs +10 -0
- data/sig/rigor/inference.rbs +6 -4
- data/sig/rigor/plugin/additional_initializer.rbs +36 -0
- data/sig/rigor/plugin/base.rbs +30 -7
- data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
- data/sig/rigor/plugin/effect_attribution.rbs +66 -0
- data/sig/rigor/plugin/effect_edge.rbs +33 -0
- data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
- data/sig/rigor/plugin/io_boundary.rbs +1 -1
- data/sig/rigor/plugin/loader.rbs +3 -3
- data/sig/rigor/plugin/manifest.rbs +50 -11
- data/sig/rigor/plugin/protocol_contract.rbs +68 -0
- data/sig/rigor/plugin/registry.rbs +62 -1
- data/sig/rigor/plugin.rbs +1 -1
- data/sig/rigor/rbs_extended.rbs +1 -1
- data/sig/rigor/reflection.rbs +7 -6
- data/sig/rigor/scope.rbs +135 -20
- data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
- data/sig/rigor/source.rbs +4 -4
- data/sig/rigor/testing.rbs +10 -4
- data/sig/rigor/type.rbs +6 -0
- data/sig/rigor.rbs +38 -20
- data/skills/rigor-ask/SKILL.md +8 -5
- data/skills/rigor-baseline-reduce/SKILL.md +4 -6
- data/skills/rigor-ci-setup/SKILL.md +17 -21
- data/skills/rigor-doctor/SKILL.md +24 -22
- data/skills/rigor-doctor/references/01-checks.md +97 -33
- data/skills/rigor-editor-setup/SKILL.md +6 -4
- data/skills/rigor-mcp-setup/SKILL.md +5 -4
- data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
- data/skills/rigor-next-steps/SKILL.md +4 -2
- data/skills/rigor-plugin-author/SKILL.md +19 -23
- data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
- data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
- data/skills/rigor-plugin-review/SKILL.md +6 -4
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
- data/skills/rigor-plugin-tune/SKILL.md +4 -2
- data/skills/rigor-project-init/SKILL.md +9 -7
- data/skills/rigor-project-init/references/01-detect.md +13 -9
- data/skills/rigor-project-init/references/02-configure.md +33 -8
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
- data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
- data/skills/rigor-protection-uplift/SKILL.md +4 -6
- data/skills/rigor-rbs-setup/SKILL.md +4 -2
- data/skills/rigor-type-oracle/SKILL.md +4 -6
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
- data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
- data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
- data/skills/rigor-upgrade/SKILL.md +13 -8
- metadata +108 -1
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
require_relative "../cache/descriptor"
|
|
6
|
+
require_relative "template_unit_collector"
|
|
7
|
+
require_relative "template_unit_paths"
|
|
8
|
+
require_relative "../plugin/template_unit"
|
|
9
|
+
require_relative "../type/combinator"
|
|
10
|
+
require_relative "diagnostic"
|
|
11
|
+
|
|
12
|
+
module Rigor
|
|
13
|
+
module Analysis
|
|
14
|
+
# The run's **template units** — the engine side of the ADR-16 Tier-D seam #392 revived (design note
|
|
15
|
+
# `docs/design/20260816-effect-labels.md` § 11.3).
|
|
16
|
+
#
|
|
17
|
+
# Built ONCE, on the parent, before analysis: the loaded plugins' `template_globs:` are expanded, each
|
|
18
|
+
# match's bytes are handed to {Plugin::Base#template_units_for_file}, and what comes back is frozen into
|
|
19
|
+
# this index. Everything after that point is data — `Marshal`-clean Strings and Hashes — so a fork-pool
|
|
20
|
+
# worker analyses a unit from exactly the bytes the parent synthesised and the pooled and sequential
|
|
21
|
+
# paths cannot disagree about what the template said.
|
|
22
|
+
#
|
|
23
|
+
# ## Why the unit is analysed as a file rather than as a synthesised method
|
|
24
|
+
#
|
|
25
|
+
# Wrapping `ruby_source` in a `class … def … end` would shift every line, so the engine would have to
|
|
26
|
+
# compose the plugin's line map with an offset of its own, and a change to the wrapper would silently
|
|
27
|
+
# move every diagnostic. Instead the unit's Ruby is parsed **as-is**, under a per-file {Scope} whose
|
|
28
|
+
# `self_type`, locals and ivars the index seeds directly — the same seeding
|
|
29
|
+
# `StatementEvaluator#build_method_entry_scope` performs at a method boundary, applied at the file
|
|
30
|
+
# boundary. Line 12 of `ruby_source` is line 12 of the parse, and the ONLY mapping in play is the
|
|
31
|
+
# plugin's own.
|
|
32
|
+
#
|
|
33
|
+
# ## What a unit does not participate in
|
|
34
|
+
#
|
|
35
|
+
# `--incremental` dependents. A template unit is synthesised, not read, so nothing in the ADR-46
|
|
36
|
+
# dependency graph names it and an edit to a `.rb` file a unit calls into has no edge back. The
|
|
37
|
+
# conservative reading is taken and stated: **a run always re-analyses every template unit**, which
|
|
38
|
+
# costs a parse per template on the warm incremental path and can never serve a stale answer. Making
|
|
39
|
+
# units first-class dependents is a later slice's work, and needs a decision the design note does not
|
|
40
|
+
# settle.
|
|
41
|
+
#
|
|
42
|
+
# ## Carrying an index across runs (#1038)
|
|
43
|
+
#
|
|
44
|
+
# A long-lived {LanguageServer::ProjectContext} holds one warm {ProjectScan}, which carries the index it
|
|
45
|
+
# built, and every per-buffer publish hands it back through `collect(previous:)`. A path whose template
|
|
46
|
+
# has not moved on disk REUSES the unit already compiled for it, so a keystroke runs no plugin code —
|
|
47
|
+
# for ERB (#393) that is an Erubi compile of every view in the project, per keystroke, which is what
|
|
48
|
+
# this exists to remove. What is re-done per run is cheap and is what notices a change: the globs are
|
|
49
|
+
# re-expanded (only a glob notices a template APPEARING or vanishing) and each surviving path is
|
|
50
|
+
# revalidated through the ADR-87 stat-then-digest choke point ({Cache::FileDigest.stat_fresh?}), whose
|
|
51
|
+
# authority is the content digest, not the stat tuple. Three things rebuild the index outright, because
|
|
52
|
+
# each can change what a transform produces for bytes that never moved: a different root, a different
|
|
53
|
+
# set of claimed globs, and a different set of glob-claiming plugins.
|
|
54
|
+
#
|
|
55
|
+
# A fourth rebuilds one PLUGIN'S claim outright (#1047): any of that plugin's templates edited, added or
|
|
56
|
+
# deleted (except the deletion of a template that had produced no unit, which the carried rows cannot
|
|
57
|
+
# attribute to a plugin and which contributed nothing a sibling could read). A transform may read the
|
|
58
|
+
# plugin's other templates — rigor-actionpack seeds a partial's locals from the views that render it —
|
|
59
|
+
# so the freshness of `_card` alone cannot vouch for `_card`'s unit. The editor's buffer does not count
|
|
60
|
+
# as an edit, so this never costs a keystroke, and it bites only on an on-disk edit the owner has not
|
|
61
|
+
# invalidated for — a save already rebuilds cold. A template read with no unit (declined, or raised) is
|
|
62
|
+
# carried as a bare stat pack for exactly this decision, so an unchanged declined file is not mistaken
|
|
63
|
+
# for an edit.
|
|
64
|
+
#
|
|
65
|
+
# Separately, and whatever the rest of the index does, the editor's own buffer is never carried: a
|
|
66
|
+
# path the buffer is bound to is recompiled from the buffer's bytes on every publish, and the compiled
|
|
67
|
+
# result stays in THAT run's index — the warm index on the ProjectScan only ever holds units compiled
|
|
68
|
+
# from files on disk. A template the plugin DECLINES, or whose transform raised, likewise produces no
|
|
69
|
+
# unit to carry and is re-offered every run.
|
|
70
|
+
#
|
|
71
|
+
# A sequential CLI run passes no `previous:` and so builds the index from scratch exactly as before: it
|
|
72
|
+
# has no warm index to carry, its process ends with the run, and adding a cross-process memo would be a
|
|
73
|
+
# second cache to prove sound for no measurable gain (the index is built once per `rigor check`).
|
|
74
|
+
class TemplateUnits
|
|
75
|
+
# What a declared type name binds to when nothing resolves it. `Dynamic[top]` — the analyzer's own
|
|
76
|
+
# "a value is here and I cannot see its class".
|
|
77
|
+
DYNAMIC = Type::Combinator.dynamic(Type::Combinator.top)
|
|
78
|
+
private_constant :DYNAMIC
|
|
79
|
+
|
|
80
|
+
# One synthesised unit, flattened off the plugin's {Plugin::TemplateUnit} with its identity resolved.
|
|
81
|
+
#
|
|
82
|
+
# `path` is both the file the user wrote AND the logical path the engine analyses under: a diagnostic
|
|
83
|
+
# the parse produces already carries it, so nothing has to rewrite a path — only a LINE.
|
|
84
|
+
Entry = Data.define(:logical_name, :path, :source, :line_map, :self_type, :locals, :ivar_seeds,
|
|
85
|
+
:digest, :unit_key, :plugin_id, :suppressed_rules) do
|
|
86
|
+
# #393 — the per-unit rule posture. True when the plugin declared that this family of finding is
|
|
87
|
+
# not meaningful in its compiled output.
|
|
88
|
+
def suppresses?(rule)
|
|
89
|
+
return false if rule.nil? || suppressed_rules.empty?
|
|
90
|
+
|
|
91
|
+
suppressed_rules.any? { |prefix| rule.to_s.start_with?(prefix) }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# See {Plugin::TemplateUnit#template_line}. Re-implemented on the flattened entry so a worker never
|
|
95
|
+
# needs the plugin value object (or the plugin) to position a diagnostic.
|
|
96
|
+
def template_line(ruby_line)
|
|
97
|
+
return ruby_line if line_map.empty?
|
|
98
|
+
return line_map[ruby_line] if line_map.key?(ruby_line)
|
|
99
|
+
|
|
100
|
+
before = line_map.keys.select { |line| line < ruby_line }
|
|
101
|
+
before.empty? ? 1 : line_map[before.max]
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# The index a run with no template-claiming plugin analyses under. Eagerly built on the main Ractor
|
|
106
|
+
# at load time (bottom of the class body) rather than memoised on first use: `@empty ||= new({})` is
|
|
107
|
+
# a class-ivar WRITE, which a non-main Ractor may not perform. {WorkerSession#initialize} reaches it
|
|
108
|
+
# whenever `template_units:` is not supplied, which is exactly how the Ractor backend constructs its
|
|
109
|
+
# workers — so the lazy memo killed every worker in its constructor and degraded every run (#1055).
|
|
110
|
+
class << self
|
|
111
|
+
attr_reader :empty
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Expands every loaded plugin's `template_globs:` and runs its transform. The work lives in
|
|
115
|
+
# {TemplateUnitCollector}, which owns the glob expansion, the failure isolation, the editor-buffer
|
|
116
|
+
# substitution and the #1038 carry; this class is what the rest of the run READS.
|
|
117
|
+
#
|
|
118
|
+
# @param previous — a warm index from an earlier run of the same project (#1038). Every unit in it
|
|
119
|
+
# whose template is unchanged on disk is carried over instead of recompiled; see the class note.
|
|
120
|
+
def self.collect(registry:, root: Dir.pwd, buffer: nil, previous: nil)
|
|
121
|
+
TemplateUnitCollector.collect(registry: registry, root: root, buffer: buffer, previous: previous)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# One template file a plugin claimed and did not deliver a usable unit for — a transform that raised,
|
|
125
|
+
# a file that could not be read, or a unit naming the wrong path. Reported as a `plugin_loader`
|
|
126
|
+
# `runtime-error` row by {Runner#template_unit_failure_diagnostics}, which is the isolation envelope
|
|
127
|
+
# every other plugin hook reports through (ADR-2 § "Plugin Trust and I/O Policy").
|
|
128
|
+
Failure = Data.define(:plugin_id, :path, :message)
|
|
129
|
+
|
|
130
|
+
def initialize(entries, failures: [], claimed_globs: [], root: Dir.pwd, stats: {},
|
|
131
|
+
plugin_signature: [])
|
|
132
|
+
@entries = entries.freeze
|
|
133
|
+
@failures = failures.freeze
|
|
134
|
+
# The globs the loaded plugins claimed, whether or not anything matched. They are the CACHE's
|
|
135
|
+
# business, not the analysis's: see {#glob_entries}.
|
|
136
|
+
@claimed_globs = claimed_globs.freeze
|
|
137
|
+
@root = root
|
|
138
|
+
# #1038 — the freshness token per template this index READ successfully: the ADR-87
|
|
139
|
+
# `(digest, size, mtime_ns, ctime_ns, inode)` pack over the bytes the transform was handed. Not part
|
|
140
|
+
# of the index's own identity ({#digest}) — a stat is not an answer, only the question of whether a
|
|
141
|
+
# carried answer still holds.
|
|
142
|
+
@stats = stats.freeze
|
|
143
|
+
@plugin_signature = plugin_signature.freeze
|
|
144
|
+
@declined_unit_keys = build_declined_unit_keys
|
|
145
|
+
freeze
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
attr_reader :failures
|
|
149
|
+
|
|
150
|
+
# #1038 — the `{ path => [entry, stat_pack] }` a later run may reuse, or `{}` when the whole index has
|
|
151
|
+
# to be rebuilt. The three whole-index refusals live here rather than per path because each of them
|
|
152
|
+
# can change what a transform PRODUCES for a template whose bytes never moved: a different root is a
|
|
153
|
+
# different project, a different claim set can hand a path to a different plugin, and a different
|
|
154
|
+
# plugin set is different code.
|
|
155
|
+
def carry_over(root:, claimed_globs:, plugin_signature:)
|
|
156
|
+
return {} unless @root.to_s == root.to_s
|
|
157
|
+
return {} unless @claimed_globs == claimed_globs
|
|
158
|
+
return {} unless @plugin_signature == plugin_signature
|
|
159
|
+
|
|
160
|
+
# A template that was READ but produced no unit (declined, or its transform raised) is carried as
|
|
161
|
+
# `[nil, pack]`: there is no unit to reuse, but its freshness still answers whether it CHANGED, which
|
|
162
|
+
# is what the collector's whole-claim decision needs (#1047) — without it one declined template
|
|
163
|
+
# would read as an edit on every run and cost the whole claim its carry.
|
|
164
|
+
@stats.each_with_object({}) do |(path, packed), carried|
|
|
165
|
+
carried[path] = [@entries[path], packed] if packed
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def empty?
|
|
170
|
+
@entries.empty?
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# #1065 — the `view:` keys of the templates this run READ and produced no unit for: an ERB whose
|
|
174
|
+
# compiled Ruby does not parse, and a handler the claiming plugin declines outright (a `.haml` view
|
|
175
|
+
# under a decline-only claim). They are what stops the effect edge's format fallback from resolving
|
|
176
|
+
# PAST a template that exists: Action View runs `_watchers.js.haml`, so joining `_watchers.html`'s
|
|
177
|
+
# effects there would be a label no execution produces. An unclaimed handler is invisible here and
|
|
178
|
+
# the fallback still fires for it, which is why a plugin claims the handlers it declines.
|
|
179
|
+
#
|
|
180
|
+
# The key is derived from the paths the plugin ITSELF keyed: a produced unit gives the root prefix
|
|
181
|
+
# its logical name was taken relative to (`app/views/` for `app/views/users/_card.html.erb` →
|
|
182
|
+
# `users/_card.html`), and a declined path under that prefix is keyed the same way, minus its
|
|
183
|
+
# handler. A run whose claim produced no unit at all learns no prefix and blocks nothing, which
|
|
184
|
+
# leaves the fallback exactly as it was rather than guessing at a layout the engine does not own.
|
|
185
|
+
attr_reader :declined_unit_keys
|
|
186
|
+
|
|
187
|
+
# The logical paths the run analyses on top of its `.rb` expansion, sorted.
|
|
188
|
+
def paths
|
|
189
|
+
@entries.keys.sort
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def [](path)
|
|
193
|
+
@entries[normalize(path)]
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def key?(path)
|
|
197
|
+
@entries.key?(normalize(path))
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# `{ path => ruby_source }` — what {Runner#parse_source} and {WorkerSession#parse_source} read
|
|
201
|
+
# instead of the bytes on disk.
|
|
202
|
+
def sources
|
|
203
|
+
@entries.transform_values(&:source)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# The index's cache identity: every unit's digest (source bytes + transform id + synthesis version)
|
|
207
|
+
# keyed by path, plus every FAILURE keyed by path, hashed once. nil only when the run's plugins
|
|
208
|
+
# claimed no globs at all, so a project with no template-unit plugin perturbs no cache key.
|
|
209
|
+
#
|
|
210
|
+
# The failures are in it because a run that produced only failures still produced an ANSWER — one
|
|
211
|
+
# `plugin_loader` row per file — and without them that run's key equalled the no-templates key. A
|
|
212
|
+
# later run whose template now compiles, or whose template is gone, reconstructed the same key, and
|
|
213
|
+
# the ADR-87 boot-slim probe (which loads no plugin, so its slot is always absent) served the stale
|
|
214
|
+
# rows. The slot now exists whenever any plugin claimed a glob, which is exactly the condition under
|
|
215
|
+
# which the probe's key is knowingly unreconstructable.
|
|
216
|
+
def build_declined_unit_keys
|
|
217
|
+
declined = @stats.keys - @entries.keys
|
|
218
|
+
return NO_DECLINED_KEYS if declined.empty?
|
|
219
|
+
|
|
220
|
+
prefixes = @entries.filter_map { |path, entry| root_prefix(path, entry.logical_name) }.uniq
|
|
221
|
+
return NO_DECLINED_KEYS if prefixes.empty?
|
|
222
|
+
|
|
223
|
+
declined.filter_map { |path| declined_unit_key(path, prefixes) }.uniq.sort.freeze
|
|
224
|
+
end
|
|
225
|
+
private :build_declined_unit_keys
|
|
226
|
+
|
|
227
|
+
NO_DECLINED_KEYS = [].freeze
|
|
228
|
+
private_constant :NO_DECLINED_KEYS
|
|
229
|
+
|
|
230
|
+
# The directory a plugin took a logical name relative to, or nil when the path and the name do not
|
|
231
|
+
# agree — a plugin free to key a unit any way it likes is one this derivation must decline for.
|
|
232
|
+
def root_prefix(path, logical_name)
|
|
233
|
+
stem = strip_handler(path)
|
|
234
|
+
stem.end_with?(logical_name) ? stem.delete_suffix(logical_name) : nil
|
|
235
|
+
end
|
|
236
|
+
private :root_prefix
|
|
237
|
+
|
|
238
|
+
def declined_unit_key(path, prefixes)
|
|
239
|
+
stem = strip_handler(path)
|
|
240
|
+
prefix = prefixes.find { |candidate| stem.start_with?(candidate) && stem.length > candidate.length }
|
|
241
|
+
prefix.nil? ? nil : "#{Plugin::TemplateUnit::KEY_PREFIX}#{stem.delete_prefix(prefix)}"
|
|
242
|
+
end
|
|
243
|
+
private :declined_unit_key
|
|
244
|
+
|
|
245
|
+
# `app/views/users/_card.html.erb` → `app/views/users/_card.html`. A template's logical name never
|
|
246
|
+
# carries its handler, which is the one naming rule the seam itself states.
|
|
247
|
+
def strip_handler(path)
|
|
248
|
+
path.sub(%r{\.[^./]+\z}, "")
|
|
249
|
+
end
|
|
250
|
+
private :strip_handler
|
|
251
|
+
|
|
252
|
+
def digest
|
|
253
|
+
return nil if @claimed_globs.empty?
|
|
254
|
+
|
|
255
|
+
rows = @entries.keys.sort.map { |path| "unit\x00#{path}\x00#{@entries[path].digest}" } +
|
|
256
|
+
@failures.map { |failure| "fail\x00#{failure.path}\x00#{failure.message}" }.sort
|
|
257
|
+
Digest::SHA256.hexdigest((["globs\x00#{@claimed_globs.sort.join("\x00")}"] + rows).join("\n"))
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# The ADR-60 WD3 / #979 `:names` glob rows the run's dependency descriptor records: one per claimed
|
|
261
|
+
# pattern, whether or not it matched.
|
|
262
|
+
#
|
|
263
|
+
# Without them a template APPEARING under a claimed glob moved nothing a warm run could see — the
|
|
264
|
+
# descriptor listed only the files that already existed — so a project whose first template was added
|
|
265
|
+
# between two runs kept replaying the answer computed before it existed. `:names` rather than `:stat`
|
|
266
|
+
# for the same reason the signature roots use it (`cache.md` § the run-descriptor row inventory): the
|
|
267
|
+
# question a glob row adds is which files MATCH, and edits to those files are carried by their own
|
|
268
|
+
# file rows.
|
|
269
|
+
def glob_entries
|
|
270
|
+
@claimed_globs.map do |pattern|
|
|
271
|
+
Cache::Descriptor::GlobEntry.compute(root: @root.to_s, pattern: pattern, mode: :names)
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Every template file this run read — the ones that produced a unit AND the ones that failed. A
|
|
276
|
+
# failure's file is as much an input to the run's answer as a success's: the `plugin_loader` row it
|
|
277
|
+
# produced must not outlive the edit that fixes the template.
|
|
278
|
+
def source_paths
|
|
279
|
+
(@entries.keys + @failures.map(&:path)).uniq.sort
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# The Prism `scopes:` argument for a unit's parse, or nil for a path that is not one.
|
|
283
|
+
#
|
|
284
|
+
# Without it the seeded locals are inert: Prism parses a bare identifier with no assignment in sight
|
|
285
|
+
# as a **method call**, so `size` in a template was a `CallNode` and `Scope#local(:size)` was never
|
|
286
|
+
# consulted. Declaring the render site's locals as an enclosing scope is exactly what Rails does when
|
|
287
|
+
# it compiles a partial's locals into the method's parameters, and it is what makes `locals:` mean
|
|
288
|
+
# anything.
|
|
289
|
+
def parse_scopes(path)
|
|
290
|
+
entry = @entries[normalize(path)]
|
|
291
|
+
return nil if entry.nil? || entry.locals.empty?
|
|
292
|
+
|
|
293
|
+
[entry.locals.keys.map(&:to_sym)]
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# The `view:<logical_name>` effect-unit key for a path, or nil.
|
|
297
|
+
# The file whose bytes a unit's path was READ from: the editor's buffer when one is bound to it, the
|
|
298
|
+
# project file otherwise. Read by the run's dependency descriptor, so it digests what the run read.
|
|
299
|
+
def physical_path(path, buffer)
|
|
300
|
+
TemplateUnitCollector.physical_path(path, @root, buffer)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def unit_key_for(path)
|
|
304
|
+
@entries[normalize(path)]&.unit_key
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Binds the declared `self`, locals and ivar seeds onto the per-file scope, so the unit's body types
|
|
308
|
+
# as the render site would run it.
|
|
309
|
+
#
|
|
310
|
+
# A type name that resolves to nothing is bound `Dynamic` rather than guessed OR left unbound, and
|
|
311
|
+
# the difference matters most for `self_type:`. Leaving it unbound is not "no claim": it is the claim
|
|
312
|
+
# that the body runs at top level, so every helper call in the template reports
|
|
313
|
+
# `call.unresolved-toplevel` — a finding per line, caused by the plugin naming a class whose RBS the
|
|
314
|
+
# project does not ship (`ActionView::Base`, on the very first Rails app to try this). `Dynamic` is
|
|
315
|
+
# the honest reading of "a receiver is declared and the analyzer cannot see it" (ADR-5), and it is
|
|
316
|
+
# silent.
|
|
317
|
+
def seed(scope, path)
|
|
318
|
+
entry = @entries[normalize(path)]
|
|
319
|
+
return scope if entry.nil?
|
|
320
|
+
|
|
321
|
+
scope = bind_self(scope, entry)
|
|
322
|
+
entry.locals.each do |name, type_name|
|
|
323
|
+
scope = scope.with_local(name.to_sym, resolve(scope, type_name))
|
|
324
|
+
end
|
|
325
|
+
entry.ivar_seeds.each do |name, type_name|
|
|
326
|
+
scope = scope.with_ivar(name.to_sym, resolve(scope, type_name))
|
|
327
|
+
end
|
|
328
|
+
scope
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Re-points a diagnostic produced inside a unit at the template's own line, and drops the ones the
|
|
332
|
+
# unit's plugin declared it does not report (#393). Every other diagnostic passes through untouched,
|
|
333
|
+
# so it is safe to stamp a whole run's stream through this.
|
|
334
|
+
#
|
|
335
|
+
# The suppression is per unit rather than global because it is a claim about ONE compiler's output:
|
|
336
|
+
# a plugin whose synthesised receivers are still coarse suppresses `call.` so the view layer can be
|
|
337
|
+
# analysed for its EFFECTS — which need no receiver precision — without the typing half costing the
|
|
338
|
+
# project a diagnostic per template line. It is deliberately not a `disable:` entry: `disable:` would
|
|
339
|
+
# silence the rule in the project's `.rb` files too, which is the opposite of what is wanted.
|
|
340
|
+
def remap(diagnostics)
|
|
341
|
+
return diagnostics if @entries.empty?
|
|
342
|
+
|
|
343
|
+
diagnostics.filter_map do |diagnostic|
|
|
344
|
+
entry = @entries[normalize(diagnostic.path)]
|
|
345
|
+
next diagnostic if entry.nil?
|
|
346
|
+
next nil if entry.suppresses?(diagnostic.rule)
|
|
347
|
+
next diagnostic if entry.line_map.empty?
|
|
348
|
+
|
|
349
|
+
relocate(diagnostic, entry.template_line(diagnostic.line))
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# The template's lines and the compiled Ruby's columns do not correspond — a compiler preserves lines
|
|
354
|
+
# and rewrites the text of each (Erubi's documented property, and the reason a Rails backtrace can
|
|
355
|
+
# name `show.html.erb:12`). So a diagnostic from a unit keeps its line and drops to column 1 rather
|
|
356
|
+
# than pointing at a column of a file whose bytes are not what was analysed.
|
|
357
|
+
#
|
|
358
|
+
# Applied whenever the unit carries a map, NOT only when the line moves: a compiler that happens to
|
|
359
|
+
# leave a line where it was still rewrote that line's text, and ERB is exactly that case — the map is
|
|
360
|
+
# near-identity and the columns are meaningless anyway.
|
|
361
|
+
def relocate(diagnostic, line)
|
|
362
|
+
Diagnostic.new(
|
|
363
|
+
path: diagnostic.path, line: line, column: 1, message: diagnostic.message,
|
|
364
|
+
severity: diagnostic.severity, rule: diagnostic.rule, source_family: diagnostic.source_family,
|
|
365
|
+
receiver_type: diagnostic.receiver_type, method_name: diagnostic.method_name,
|
|
366
|
+
project_definition_site: diagnostic.project_definition_site
|
|
367
|
+
)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
private
|
|
371
|
+
|
|
372
|
+
def normalize(path)
|
|
373
|
+
TemplateUnitPaths.relative(path, @root)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def bind_self(scope, entry)
|
|
377
|
+
return scope if entry.self_type.nil?
|
|
378
|
+
|
|
379
|
+
scope.with_self_type(resolve(scope, entry.self_type))
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def resolve(scope, type_name)
|
|
383
|
+
environment = scope.environment
|
|
384
|
+
resolved = environment&.nominal_for_name(type_name)
|
|
385
|
+
return resolved if resolved
|
|
386
|
+
return DYNAMIC unless scope.discovered_classes.key?(type_name)
|
|
387
|
+
|
|
388
|
+
Type::Combinator.nominal_of(type_name)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# Populates the `@empty` singleton on the main Ractor at load time. `Ractor.make_shareable` rather
|
|
392
|
+
# than `freeze` so a worker's READ of the class ivar is legal too: a non-main Ractor may read a
|
|
393
|
+
# class/module ivar only when the value is deeply shareable, and `#initialize` freezes the index
|
|
394
|
+
# itself but leaves `@root` — a fresh `Dir.pwd` String — unfrozen. The baked root is inert here: an
|
|
395
|
+
# index with no entries and no claimed globs answers nothing and carries nothing over.
|
|
396
|
+
@empty = Ractor.make_shareable(new({}))
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
end
|
|
@@ -22,6 +22,7 @@ require_relative "dependency_recorder"
|
|
|
22
22
|
require_relative "dependency_source_inference"
|
|
23
23
|
require_relative "diagnostic"
|
|
24
24
|
require_relative "erb_template_detector"
|
|
25
|
+
require_relative "template_units"
|
|
25
26
|
|
|
26
27
|
module Rigor
|
|
27
28
|
module Analysis
|
|
@@ -83,7 +84,8 @@ module Rigor
|
|
|
83
84
|
def initialize(configuration:, cache_store: nil, # rubocop:disable Metrics/MethodLength,Metrics/ParameterLists
|
|
84
85
|
plugin_blueprints: [], explain: false, buffer: nil,
|
|
85
86
|
synthetic_method_index: nil, project_patched_methods: nil,
|
|
86
|
-
project_scope_seed: {}, source_files: [], record_dependencies: false
|
|
87
|
+
project_scope_seed: {}, source_files: [], record_dependencies: false,
|
|
88
|
+
template_units: nil, locked_gems: nil)
|
|
87
89
|
@configuration = configuration
|
|
88
90
|
@cache_store = cache_store
|
|
89
91
|
@explain = explain
|
|
@@ -108,6 +110,15 @@ module Rigor
|
|
|
108
110
|
# ADR-32 WD4 — full project file list (frozen Array<String>) for env-build-time invocation of any
|
|
109
111
|
# loaded plugin's `source_rbs_synthesizer` callable.
|
|
110
112
|
@source_files = source_files
|
|
113
|
+
# #392 — the run's template units, synthesised on the PARENT before the fork and inherited here as
|
|
114
|
+
# frozen data. A worker analyses a unit from the same bytes the parent compiled, which is what makes
|
|
115
|
+
# pooled and sequential agree about a file that does not exist on disk in the form being parsed.
|
|
116
|
+
@template_units = template_units || TemplateUnits.empty
|
|
117
|
+
# #1064 — the project's `Gemfile.lock` map, resolved by the PARENT. Bundler's lockfile parser reads
|
|
118
|
+
# RubyGems module state a non-main Ractor may not touch, so a Ractor worker that resolved it here got
|
|
119
|
+
# `Ractor::IsolationError` and an empty map — no gem `sig/` directories, no ADR-72 overlays. nil (the
|
|
120
|
+
# fork backend and every other caller) resolves inside `Environment.for_project` exactly as before.
|
|
121
|
+
@locked_gems = locked_gems
|
|
111
122
|
|
|
112
123
|
# NOTE: `Inference::MethodDispatcher::FileFolding.fold_platform_specific_paths` is process-global
|
|
113
124
|
# state. Writing it from a non-main Ractor would raise `Ractor::IsolationError`, so the session does
|
|
@@ -140,7 +151,8 @@ module Rigor
|
|
|
140
151
|
**ProjectEnvironment.dependency_discovery_options(configuration),
|
|
141
152
|
synthetic_method_index: @synthetic_method_index,
|
|
142
153
|
project_patched_methods: @project_patched_methods,
|
|
143
|
-
source_files: @source_files
|
|
154
|
+
source_files: @source_files,
|
|
155
|
+
locked_gems: @locked_gems
|
|
144
156
|
)
|
|
145
157
|
@prepare_diagnostics = run_plugin_prepare.freeze
|
|
146
158
|
end
|
|
@@ -164,10 +176,12 @@ module Rigor
|
|
|
164
176
|
def analyze_with_effects(path)
|
|
165
177
|
return analyze_body(path) unless @record_effects
|
|
166
178
|
|
|
179
|
+
entry = @template_units[path]
|
|
167
180
|
diagnostics = nil
|
|
168
181
|
collection = Effects::Collector.collect_for(
|
|
169
182
|
path, attribution: @effect_attribution, envelopes: effect_envelope_index,
|
|
170
|
-
plugin_facts: effect_plugin_facts
|
|
183
|
+
plugin_facts: effect_plugin_facts, unit_key: entry&.unit_key,
|
|
184
|
+
unit_owner: entry&.self_type
|
|
171
185
|
) do
|
|
172
186
|
diagnostics = analyze_body(path)
|
|
173
187
|
end
|
|
@@ -206,21 +220,24 @@ module Rigor
|
|
|
206
220
|
return parse_diagnostics(path, parse_result)
|
|
207
221
|
end
|
|
208
222
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
223
|
+
root = parse_result.value
|
|
224
|
+
Effects::Collector.record_root(root)
|
|
225
|
+
scope = @template_units.seed(
|
|
226
|
+
seed_project_scope(Scope.empty(environment: @environment, source_path: path)), path
|
|
227
|
+
)
|
|
228
|
+
index = Inference::ScopeIndexer.index(root, default_scope: scope)
|
|
212
229
|
# ADR-53 B4 — built-in collectors + plugin node rules share one walk.
|
|
213
|
-
node_collectors = CheckRules.build_node_collectors(path, index)
|
|
214
|
-
node_results = node_rule_results_by_plugin(path,
|
|
230
|
+
node_collectors = CheckRules.build_node_collectors(path, index, root)
|
|
231
|
+
node_results = node_rule_results_by_plugin(path, root, scope, node_collectors, index)
|
|
215
232
|
diagnostics = CheckRules.diagnose(
|
|
216
233
|
path: path,
|
|
217
|
-
root:
|
|
234
|
+
root: root,
|
|
218
235
|
scope_index: index,
|
|
219
236
|
comments: parse_result.comments,
|
|
220
237
|
disabled_rules: @configuration.disabled_rules,
|
|
221
238
|
node_collectors: node_collectors
|
|
222
239
|
)
|
|
223
|
-
diagnostics += plugin_emitted_diagnostics(path,
|
|
240
|
+
diagnostics += plugin_emitted_diagnostics(path, root, scope, node_results)
|
|
224
241
|
diagnostics + explain_diagnostics(path, parse_result.value, scope)
|
|
225
242
|
rescue Errno::ENOENT => e
|
|
226
243
|
[analyzer_error(path, e.message)]
|
|
@@ -295,6 +312,26 @@ module Rigor
|
|
|
295
312
|
@file_effects
|
|
296
313
|
end
|
|
297
314
|
|
|
315
|
+
# Issue #1051 — the run-scoped disclosures ({Plugin::Base#disclose_once}) this session's plugin
|
|
316
|
+
# instances registered, each tagged with the plugin id the coordinator de-duplicates by — including the
|
|
317
|
+
# positioned batches of {Plugin::Base#emit_once} (#1060), which ride the same records. Marshal-clean
|
|
318
|
+
# (frozen Hashes of Strings and Symbols, and frozen `Diagnostic` copies for a batch) so the fork pool
|
|
319
|
+
# ships them back like the three drains above;
|
|
320
|
+
# a registration made on the PARENT during `#prepare` is already visible to the coordinator through
|
|
321
|
+
# the pre-fork session, and one a worker makes while analysing its slice arrives through the payload.
|
|
322
|
+
# Entries are not cleared — a drain is a read, and the parent session is drained again on the degrade
|
|
323
|
+
# path.
|
|
324
|
+
def drain_run_disclosures
|
|
325
|
+
return [] if @plugin_registry.empty?
|
|
326
|
+
|
|
327
|
+
@plugin_registry.plugins.flat_map do |plugin|
|
|
328
|
+
plugin_id = safe_plugin_id(plugin)
|
|
329
|
+
Array(plugin.run_disclosure_records).map { |record| record.merge(plugin_id: plugin_id) }
|
|
330
|
+
rescue StandardError
|
|
331
|
+
[]
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
298
335
|
private
|
|
299
336
|
|
|
300
337
|
# Mirrors {Runner#seed_project_scope}: applies the cross-file pre-pass discovery tables the
|
|
@@ -310,6 +347,12 @@ module Rigor
|
|
|
310
347
|
# physical bytes but stamp the parse buffer's `filepath:` as the LOGICAL path so downstream
|
|
311
348
|
# diagnostics carry the logical path.
|
|
312
349
|
def parse_source(path)
|
|
350
|
+
entry = @template_units[path]
|
|
351
|
+
if entry
|
|
352
|
+
return Prism.parse(entry.source, filepath: path, version: @configuration.target_ruby,
|
|
353
|
+
scopes: @template_units.parse_scopes(path))
|
|
354
|
+
end
|
|
355
|
+
|
|
313
356
|
physical = @buffer ? @buffer.resolve(path) : path
|
|
314
357
|
return Prism.parse_file(physical, version: @configuration.target_ruby) if physical == path
|
|
315
358
|
|
data/lib/rigor/bleeding_edge.rb
CHANGED
|
@@ -241,6 +241,7 @@ module Rigor
|
|
|
241
241
|
["CSV", :readlines, :singleton] => CSV_PARSED_SPEC,
|
|
242
242
|
["CSV", :parse_line, :singleton] => CSV_ROW_SPEC
|
|
243
243
|
}.freeze
|
|
244
|
+
Ractor.make_shareable(METHOD_RETURN_OVERRIDES)
|
|
244
245
|
|
|
245
246
|
# @return the reduced HKT type for
|
|
246
247
|
# the given (class_name, method_name, kind) triple,
|
|
@@ -64,6 +64,7 @@ module Rigor
|
|
|
64
64
|
"non-nan-float" => -> { Type::Combinator.non_nan_float },
|
|
65
65
|
"finite-float" => -> { Type::Combinator.finite_float }
|
|
66
66
|
}.freeze
|
|
67
|
+
Ractor.make_shareable(REGISTRY)
|
|
67
68
|
private_constant :REGISTRY
|
|
68
69
|
|
|
69
70
|
# `name[T]` / `name[K, V]` — type-arg parameterised refinements. Each builder takes an
|
|
@@ -141,6 +142,7 @@ module Rigor
|
|
|
141
142
|
Type::Combinator.readonly_of(args.first)
|
|
142
143
|
}
|
|
143
144
|
}.freeze
|
|
145
|
+
Ractor.make_shareable(PARAMETERISED_TYPE_BUILDERS)
|
|
144
146
|
private_constant :PARAMETERISED_TYPE_BUILDERS
|
|
145
147
|
|
|
146
148
|
# `Class[range]` — a numeric class head bounded by a Ruby range literal (ADR-109). The
|
|
@@ -176,6 +178,7 @@ module Rigor
|
|
|
176
178
|
Type::Combinator.float_range(lo, hi, exclude_end: exclusive)
|
|
177
179
|
}
|
|
178
180
|
}.freeze
|
|
181
|
+
Ractor.make_shareable(RANGE_HEAD_BUILDERS)
|
|
179
182
|
private_constant :RANGE_HEAD_BUILDERS
|
|
180
183
|
|
|
181
184
|
# `name<min, max>` — the PHPStan-style integer-bound form ADR-109 deprecates: still parsed so
|
|
@@ -193,6 +196,7 @@ module Rigor
|
|
|
193
196
|
Type::Combinator.integer_range(bounds[0], bounds[1])
|
|
194
197
|
}
|
|
195
198
|
}.freeze
|
|
199
|
+
Ractor.make_shareable(PARAMETERISED_INT_BUILDERS)
|
|
196
200
|
private_constant :PARAMETERISED_INT_BUILDERS
|
|
197
201
|
|
|
198
202
|
module_function
|
|
@@ -20,14 +20,22 @@ module Rigor
|
|
|
20
20
|
# `{n,m}` forms with `n >= 1`):
|
|
21
21
|
#
|
|
22
22
|
# - `\d`, `[0-9]` -> decimal-int-string
|
|
23
|
-
# - `\h` ->
|
|
24
|
-
# - `[0-9a-fA-F]` ->
|
|
25
|
-
# - `[0-9a-f]`, `[0-9A-F]` ->
|
|
26
|
-
# - `[0-7]` ->
|
|
23
|
+
# - `\h` -> non-empty-string
|
|
24
|
+
# - `[0-9a-fA-F]` -> non-empty-string
|
|
25
|
+
# - `[0-9a-f]`, `[0-9A-F]` -> non-empty-string
|
|
26
|
+
# - `[0-7]` -> non-empty-string
|
|
27
27
|
# - `[a-z]` -> lowercase-string
|
|
28
28
|
# - `[A-Z]` -> uppercase-string
|
|
29
29
|
# - `[[:digit:]]` -> numeric-string
|
|
30
30
|
#
|
|
31
|
+
# #1004 — the hex/octal digit classes do NOT map to `hex-int-string` / `octal-int-string`.
|
|
32
|
+
# Those refinements' predicates (`Type::Refined` PREDICATES, in `type/refined.rb`) REQUIRE the
|
|
33
|
+
# conventional `0x` / `0o` prefix, but a bare digit class like `[0-9a-fA-F]+` matches "ff" —
|
|
34
|
+
# a string with no prefix at all. Mapping a matched digit run to a refinement its own
|
|
35
|
+
# predicate rejects is unsound (worse than the plain `String` it replaces), so these rows
|
|
36
|
+
# drop to `non-empty-string`: the `+` / `{n,}` (n >= 1) quantifier already guarantees the
|
|
37
|
+
# captured run is never empty, and that much is true regardless of prefix.
|
|
38
|
+
#
|
|
31
39
|
# Anything outside the table returns `nil` so the calling narrowing site falls back to
|
|
32
40
|
# its previous behaviour (plain `String`). Arbitrary regex semantic equivalence is
|
|
33
41
|
# undecidable, so the table is intentionally a small audited set of canonical shapes
|
|
@@ -46,11 +54,11 @@ module Rigor
|
|
|
46
54
|
RULES = Ractor.make_shareable([
|
|
47
55
|
[/\A\\d#{QUANTIFIER_SOURCE}\z/, :decimal_int_string],
|
|
48
56
|
[/\A\[0-9\]#{QUANTIFIER_SOURCE}\z/, :decimal_int_string],
|
|
49
|
-
[/\A\\h#{QUANTIFIER_SOURCE}\z/, :
|
|
50
|
-
[/\A\[0-9a-fA-F\]#{QUANTIFIER_SOURCE}\z/, :
|
|
51
|
-
[/\A\[0-9a-f\]#{QUANTIFIER_SOURCE}\z/, :
|
|
52
|
-
[/\A\[0-9A-F\]#{QUANTIFIER_SOURCE}\z/, :
|
|
53
|
-
[/\A\[0-7\]#{QUANTIFIER_SOURCE}\z/, :
|
|
57
|
+
[/\A\\h#{QUANTIFIER_SOURCE}\z/, :non_empty_string],
|
|
58
|
+
[/\A\[0-9a-fA-F\]#{QUANTIFIER_SOURCE}\z/, :non_empty_string],
|
|
59
|
+
[/\A\[0-9a-f\]#{QUANTIFIER_SOURCE}\z/, :non_empty_string],
|
|
60
|
+
[/\A\[0-9A-F\]#{QUANTIFIER_SOURCE}\z/, :non_empty_string],
|
|
61
|
+
[/\A\[0-7\]#{QUANTIFIER_SOURCE}\z/, :non_empty_string],
|
|
54
62
|
[/\A\[a-z\]#{QUANTIFIER_SOURCE}\z/, :lowercase_string],
|
|
55
63
|
[/\A\[A-Z\]#{QUANTIFIER_SOURCE}\z/, :uppercase_string],
|
|
56
64
|
[/\A\[\[:digit:\]\]#{QUANTIFIER_SOURCE}\z/, :numeric_string]
|
|
@@ -93,6 +93,7 @@ module Rigor
|
|
|
93
93
|
["IO", :read, :singleton] => IO_READ_NO_LENGTH,
|
|
94
94
|
["File", :read, :singleton] => IO_READ_NO_LENGTH
|
|
95
95
|
}.freeze
|
|
96
|
+
Ractor.make_shareable(OVERRIDES)
|
|
96
97
|
private_constant :OVERRIDES
|
|
97
98
|
|
|
98
99
|
# Looks up a refined return type for the given call.
|
|
@@ -124,6 +125,7 @@ module Rigor
|
|
|
124
125
|
acc[mname] ||= []
|
|
125
126
|
acc[mname] << owner unless acc[mname].include?(owner)
|
|
126
127
|
end.freeze
|
|
128
|
+
Ractor.make_shareable(OWNERS_BY_METHOD)
|
|
127
129
|
private_constant :OWNERS_BY_METHOD
|
|
128
130
|
|
|
129
131
|
# @return the candidate owner class names for a bare method-name
|
|
@@ -53,7 +53,12 @@ module Rigor
|
|
|
53
53
|
# interface a `conforms-to` names) invalidates the warm entry. A pre-9 entry carries no signature-root
|
|
54
54
|
# row and would validate fresh across exactly that edit — the #577 reasoning, one slot over — so cached
|
|
55
55
|
# entries must read as misses once and rebuild carrying the rows.
|
|
56
|
-
|
|
56
|
+
# v10: #1123 — the def-index seed bundles gain the `:prepends` table, the instance-ancestor-ORDER half
|
|
57
|
+
# of the `include` table (`prepend M`, and `Recv.prepend(M)`), which `Scope#user_def_through_ancestors`
|
|
58
|
+
# searches ahead of a class's own `def`s. A pre-10 bundle would silently contribute no prepends for an
|
|
59
|
+
# unchanged file, so that file's consumers would keep answering with the pre-fix MRO — cached bundles
|
|
60
|
+
# must read as misses once and rebuild carrying the slot.
|
|
61
|
+
SCHEMA_VERSION = 10
|
|
57
62
|
|
|
58
63
|
# Per-slot entry value objects. Constructors validate enums / required fields and freeze the resulting
|
|
59
64
|
# struct so no caller can mutate after the entry is in a Descriptor.
|