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
|
@@ -64,6 +64,31 @@ module Rigor
|
|
|
64
64
|
manifest(
|
|
65
65
|
id: "activerecord",
|
|
66
66
|
target_gems: ["activerecord"],
|
|
67
|
+
# 0.13.0, 2026-09-24 (#1321) — the class-side `Model.find` with two or more arguments returns
|
|
68
|
+
# `Array[Model]` instead of the model, and its `model-call` note says so. A model that defines its own
|
|
69
|
+
# `self.find` keeps its own answer; the note reads syntax alone and does not see that override. No
|
|
70
|
+
# producer payload changed shape; the bump records the type change as the 0.11.0 and 0.12.0 entries do.
|
|
71
|
+
#
|
|
72
|
+
# 0.12.0, 2026-09-19 (#1089) — a written-receiver read of an enum-backed column returns the enum's
|
|
73
|
+
# KEY type (the key String constants, or `String` for an empty key list) instead of the column's
|
|
74
|
+
# SQL storage type. No producer payload changed shape, but the version is the cache key a project
|
|
75
|
+
# sees and this changes which calls the plugin claims at the written-receiver spelling — a warm
|
|
76
|
+
# run must not serve the pre-change stream (the same call the 0.11.0 bump records).
|
|
77
|
+
#
|
|
78
|
+
# 0.11.0, 2026-09-17 (#963 item 2) — an implicit-self read of a column / association / `column?`
|
|
79
|
+
# predicate inside the model's own `def` is answered `untyped` instead of reaching no path, so the
|
|
80
|
+
# engine's own-method veto sees the member and a same-named top-level `def` stops binding ahead of
|
|
81
|
+
# it. No producer payload changed shape, but the version is the cache key a project sees and this
|
|
82
|
+
# changes which calls the plugin claims — a warm run must not serve the pre-change stream.
|
|
83
|
+
#
|
|
84
|
+
# 0.10.0, 2026-09-17 (#1049) — three macro families join the model entry and the published
|
|
85
|
+
# `:model_index` fact: `delegate`, associations declared in an included concern's `included do`, and
|
|
86
|
+
# the Paperclip / Active Storage attachment macros. `ModelIndex::Entry` gains a `macro_methods`
|
|
87
|
+
# member, so a cached 0.9.0 payload is not Marshal-compatible; and the concern fold WIDENS the
|
|
88
|
+
# association list of models the 0.9.0 payload recorded without it. Both make the bump — part of the
|
|
89
|
+
# producer cache KEY — what stops a warm run serving the pre-change surface to a consumer that fails
|
|
90
|
+
# closed on a name it cannot see.
|
|
91
|
+
#
|
|
67
92
|
# 0.9.0, 2026-09-10 (#534 item 7) — the bundled signatures gained `sig/active_record/framework.rbs`:
|
|
68
93
|
# the Active Record exception hierarchy, the `ActiveModel` namespace and `Arel`. No producer payload
|
|
69
94
|
# changed shape, but the version is the cache key a project sees, and the RBS half of a plugin's
|
|
@@ -93,7 +118,7 @@ module Rigor
|
|
|
93
118
|
# a scope lambda body / class-method body now contributes `Relation[Model]` via `scope.self_type`
|
|
94
119
|
# instead of falling through to `Kernel#select` (the IO multiplexer, `Array[String]` return). Plus
|
|
95
120
|
# `:select` added to the relation-entry-point list.
|
|
96
|
-
version: "0.
|
|
121
|
+
version: "0.13.0",
|
|
97
122
|
description: "Types ActiveRecord finders against the project's db/schema.rb and AR models.",
|
|
98
123
|
config_schema: {
|
|
99
124
|
"schema_file" => { kind: :string, default: "db/schema.rb" },
|
|
@@ -211,6 +236,10 @@ module Rigor
|
|
|
211
236
|
# no-op).
|
|
212
237
|
def prepare(services)
|
|
213
238
|
index = model_index
|
|
239
|
+
# Issue #1051 — registered HERE, on the run's parent, because `#prepare` is where the schema read is
|
|
240
|
+
# forced and therefore where every load error already exists. The pool's one pre-fork session runs
|
|
241
|
+
# this, so the disclosure is in the coordinator's table before a single worker is forked.
|
|
242
|
+
disclose_load_errors
|
|
214
243
|
return if index.nil? || index.empty?
|
|
215
244
|
# Reduced mode stays UNPUBLISHED. Every consumer reads `columns:` as authoritative and fires on a
|
|
216
245
|
# key missing from it — rigor-actionpack's `permit(:title)` check and rigor-shoulda-matchers'
|
|
@@ -227,15 +256,17 @@ module Rigor
|
|
|
227
256
|
)
|
|
228
257
|
end
|
|
229
258
|
|
|
230
|
-
def diagnostics_for_file(path:, scope:, root:)
|
|
259
|
+
def diagnostics_for_file(path:, scope:, root:)
|
|
231
260
|
index = model_index
|
|
232
261
|
# The schema-load disclosure is independent of whether an index was built: reduced mode still
|
|
233
262
|
# produces one (and still analyzes), while a genuine index failure produces one and nothing else.
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
263
|
+
# It leaves through the run-scoped channel rather than this method's return, so re-registering here
|
|
264
|
+
# is free — it only matters for a load error appended after `#prepare` already ran.
|
|
265
|
+
disclose_load_errors
|
|
266
|
+
return [] if index.nil? || index.empty?
|
|
267
|
+
return [] if migration_path?(path)
|
|
237
268
|
|
|
238
|
-
|
|
269
|
+
Analyzer.new(path: path, model_index: index, scope: scope).analyze(root).diagnostics
|
|
239
270
|
end
|
|
240
271
|
|
|
241
272
|
# Rails migration files (`db/migrate/<timestamp>_*.rb`) and post-migration files
|
|
@@ -256,6 +287,61 @@ module Rigor
|
|
|
256
287
|
MIGRATION_PATH_PATTERNS.any? { |pattern| path_s.match?(pattern) }
|
|
257
288
|
end
|
|
258
289
|
|
|
290
|
+
# ADR-113 WD4 (#1082) — enumerates the members this plugin synthesizes on `class_name`, for the
|
|
291
|
+
# `rigor lens` declaration map. The rows come straight from the prepared {ModelIndex} — the same
|
|
292
|
+
# fact the `dynamic_return` gate and {#diagnostics_for_file} already read — so the hook adds no
|
|
293
|
+
# discovery of its own and stays off the `check` hot path (ADR-52): `check` never calls it. An
|
|
294
|
+
# unknown class, a failed index, and a non-model class all answer `[]`; a reduced-mode index (no
|
|
295
|
+
# schema) simply lists no columns.
|
|
296
|
+
#
|
|
297
|
+
# `type` is the most conservative of the answers the plugin gives the member at `check` — a row
|
|
298
|
+
# that claimed a type `check` never produces would misstate the member (ADR-113 treats a lens
|
|
299
|
+
# answer differing from `check` as a bug):
|
|
300
|
+
#
|
|
301
|
+
# - `:scope` — `Relation[Model]`, the class-side answer `Post.published` gets.
|
|
302
|
+
# - `:association_reader` — the same object {#association_return_type} contributes at a call
|
|
303
|
+
# site: `Relation[Target]` for a collection, `Nominal[Target]` (`| nil` for `has_one` /
|
|
304
|
+
# `optional: true`) for a singular, and nil for a polymorphic target the plugin cannot type.
|
|
305
|
+
# - `:column_reader` / `:column_predicate` — `Dynamic[top]` ON PURPOSE (#963): that is the
|
|
306
|
+
# answer the bare, receiver-less read gets inside the model's own `def`, the only answer free
|
|
307
|
+
# of receiver-type context. A written `user.name` still narrows to the column's type, but the
|
|
308
|
+
# member-level answer stays dynamic — the precise variant at member level was measured at 57
|
|
309
|
+
# false positives on mastodon.
|
|
310
|
+
# - `:enum` — the enum's attribute reader (`status`), listed ONLY when no column row already
|
|
311
|
+
# carries the name (a non-column enum attribute, or a reduced-mode index with no schema). An
|
|
312
|
+
# enum-backed column appears under `:column_reader` instead, so no member lists twice. The
|
|
313
|
+
# plugin answers nothing for the bare enum attribute at `check`, so `type` is nil — not
|
|
314
|
+
# `untyped`, which would claim an answer `check` does not give. The per-value predicates Rails
|
|
315
|
+
# generates ride in `:macro_method` with their affix-resolved names.
|
|
316
|
+
# - `:macro_method` — `delegate` names, attachment readers, enum predicates (#1049 records names
|
|
317
|
+
# only): the plugin knows the member exists and answers no type for it, so `type` is nil.
|
|
318
|
+
def declared_members(class_name)
|
|
319
|
+
index = model_index
|
|
320
|
+
return [] if index.nil?
|
|
321
|
+
|
|
322
|
+
entry = index.find(class_name)
|
|
323
|
+
return [] if entry.nil?
|
|
324
|
+
|
|
325
|
+
untyped = Rigor::Type::Combinator.untyped
|
|
326
|
+
entry.column_names.flat_map do |name|
|
|
327
|
+
[{ name: name, kind: :column_reader, type: untyped },
|
|
328
|
+
{ name: "#{name}?", kind: :column_predicate, type: untyped }]
|
|
329
|
+
end +
|
|
330
|
+
entry.associations.map do |association|
|
|
331
|
+
{ name: association[:name], kind: :association_reader,
|
|
332
|
+
type: association_return_type(entry, association[:name]) }
|
|
333
|
+
end +
|
|
334
|
+
entry.scopes.map do |name|
|
|
335
|
+
{ name: name, kind: :scope, type: relation_of(entry.class_name) }
|
|
336
|
+
end +
|
|
337
|
+
(entry.enums.keys - entry.column_names).map do |column|
|
|
338
|
+
{ name: column, kind: :enum, type: nil }
|
|
339
|
+
end +
|
|
340
|
+
entry.macro_methods.map do |name|
|
|
341
|
+
{ name: name, kind: :macro_method, type: nil }
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
259
345
|
# The class-side finder / relation entry-point names `finder_return_type` recognises. Static half of
|
|
260
346
|
# the `dynamic_return` name gate; the run-time half comes from the model index (scopes,
|
|
261
347
|
# associations, columns).
|
|
@@ -314,15 +400,16 @@ module Rigor
|
|
|
314
400
|
return nil if index.nil? || index.empty?
|
|
315
401
|
|
|
316
402
|
if call_node.receiver
|
|
317
|
-
class_call_return_type(call_node, index) ||
|
|
403
|
+
class_call_return_type(call_node, scope, index) ||
|
|
318
404
|
relation_call_return_type(call_node, scope, index) ||
|
|
319
405
|
instance_call_return_type(call_node, scope, index)
|
|
320
406
|
else
|
|
321
|
-
implicit_self_class_call_return_type(call_node, scope, index)
|
|
407
|
+
implicit_self_class_call_return_type(call_node, scope, index) ||
|
|
408
|
+
implicit_self_instance_member_type(call_node, scope, index)
|
|
322
409
|
end
|
|
323
410
|
end
|
|
324
411
|
|
|
325
|
-
def class_call_return_type(call_node, index)
|
|
412
|
+
def class_call_return_type(call_node, scope, index)
|
|
326
413
|
model_name = constant_receiver_name(call_node.receiver)
|
|
327
414
|
return nil if model_name.nil?
|
|
328
415
|
|
|
@@ -330,7 +417,7 @@ module Rigor
|
|
|
330
417
|
entry = index.find(model_name)
|
|
331
418
|
return nil if entry.nil?
|
|
332
419
|
|
|
333
|
-
finder_return_type(call_node, entry) ||
|
|
420
|
+
finder_return_type(call_node, entry, scope) ||
|
|
334
421
|
class_scope_return_type(call_node, entry)
|
|
335
422
|
end
|
|
336
423
|
|
|
@@ -356,20 +443,115 @@ module Rigor
|
|
|
356
443
|
entry = index.find(self_type.class_name)
|
|
357
444
|
return nil if entry.nil?
|
|
358
445
|
|
|
359
|
-
finder_return_type(call_node, entry) ||
|
|
446
|
+
finder_return_type(call_node, entry, scope) ||
|
|
360
447
|
class_scope_return_type(call_node, entry)
|
|
361
448
|
end
|
|
362
449
|
|
|
363
|
-
#
|
|
364
|
-
#
|
|
365
|
-
#
|
|
366
|
-
#
|
|
367
|
-
def
|
|
450
|
+
# Implicit-self INSTANCE member read — `name` / `account` written without a receiver inside the
|
|
451
|
+
# model's own `def`, where `self` is `Nominal[Model]`. Both existing instance paths key on a WRITTEN
|
|
452
|
+
# receiver, so this spelling reached no path at all; issue #963 item 2 is what that costs beyond
|
|
453
|
+
# precision. The engine's own-method veto (#618) asks the dispatcher whether a plugin answers the
|
|
454
|
+
# name before letting a same-named top-level `def` bind, and a member no tier answers is a member the
|
|
455
|
+
# top-level `def` binds ahead of — typing `name.upcase` inside `def shout` as the def's `nil` and
|
|
456
|
+
# firing `undefined method 'upcase' for nil` on working Rails code.
|
|
457
|
+
#
|
|
458
|
+
# **`untyped`, not the column's type**, and the difference is measured rather than assumed. The
|
|
459
|
+
# precise variant (the same `association_return_type` / `column_return_type` pair the written-receiver
|
|
460
|
+
# path uses) was run over the corpus and added 57 diagnostics on mastodon and 5 on redmine, all new:
|
|
461
|
+
# `flow.always-truthy-condition` where a predicate reader now folds a guard to a constant, and
|
|
462
|
+
# `call.possible-nil-receiver` where a singular association's `nil` arm survives a `.compact` the
|
|
463
|
+
# engine does not fold. Those are precision questions about the reader's TYPE, and each is a false
|
|
464
|
+
# positive on correct code. What #963 needs is only that the member EXISTS, so the contribution is the
|
|
465
|
+
# ADR-82 WD4 answer `#ruby_type_to_type`'s decline already documents next door: the plugin knows the
|
|
466
|
+
# reader is there and declines to say more here. The site keeps the `Dynamic` it already had, gains
|
|
467
|
+
# the `framework_dsl_boundary` attribution, and the veto gets its answer.
|
|
468
|
+
#
|
|
469
|
+
# The project's OWN definition wins, and is not merely a precision preference: `def name` on the
|
|
470
|
+
# model — or on an ancestor it declares — is an override Ruby dispatches to, and the plugin tier sits
|
|
471
|
+
# ABOVE the engine's body-inference tiers, so answering here would displace the override's real return
|
|
472
|
+
# type with `Dynamic`. `discovered_method_through_ancestors?` is the same table the veto's own
|
|
473
|
+
# discovery arm reads, and it fails toward declining (a budget-exhausted walk answers true).
|
|
474
|
+
def implicit_self_instance_member_type(call_node, scope, index)
|
|
475
|
+
return nil if scope.nil?
|
|
476
|
+
return nil unless call_node.arguments.nil?
|
|
477
|
+
return nil unless call_node.block.nil?
|
|
478
|
+
|
|
479
|
+
self_type = scope.self_type
|
|
480
|
+
return nil unless self_type.is_a?(Rigor::Type::Nominal)
|
|
481
|
+
|
|
482
|
+
entry = index.find(self_type.class_name)
|
|
483
|
+
return nil if entry.nil?
|
|
484
|
+
return nil unless instance_member_name?(entry, call_node.name)
|
|
485
|
+
return nil if scope.discovered_method_through_ancestors?(self_type.class_name, call_node.name, :instance)
|
|
486
|
+
return nil if rbs_answers_before_object?(self_type.class_name, call_node.name, scope)
|
|
487
|
+
|
|
488
|
+
Rigor::Type::Combinator.untyped
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
# A signature the project wrote about the model — or about anything in the model's MRO — is
|
|
492
|
+
# authorship, and this tier sits above `RbsDispatch`, so answering `untyped` for a name RBS declares
|
|
493
|
+
# would displace the declared type at the implicit-self spelling ONLY, leaving `name` and `self.name`
|
|
494
|
+
# typed differently in one method body. Declining restores both: RBS answers the call, and the
|
|
495
|
+
# engine's own veto still sees the member through its pre-`::Object` RBS arm, so a top-level `def` of
|
|
496
|
+
# the name does not bind either.
|
|
497
|
+
#
|
|
498
|
+
# The cut-off is `Inference::ExternalAncestorResolution.declared_before_object?`'s and NOT its
|
|
499
|
+
# own-class sibling's, and the difference is the whole point: RBS's definition builder resolves
|
|
500
|
+
# through ancestors, so a reader declared on a `sig/application_record.rbs` superclass or on an
|
|
501
|
+
# `include`d module comes back with THAT owner, and an own-class test would call it undeclared and
|
|
502
|
+
# displace it one ancestor up. What the cut-off keeps out is an owner at or after a top-level
|
|
503
|
+
# `def`'s own rung — `Object`, `Kernel`,
|
|
504
|
+
# `BasicObject` — whose names say nothing about whether the model answers. Instance-side only, so a
|
|
505
|
+
# sidecar declaring `def self.name` is correctly not counted. Fail-soft: an unreadable environment
|
|
506
|
+
# declines to claim a declaration exists, which leaves the member visible to the veto.
|
|
507
|
+
TOP_LEVEL_DEF_OWNERS = %w[Object Kernel BasicObject].freeze
|
|
508
|
+
private_constant :TOP_LEVEL_DEF_OWNERS
|
|
509
|
+
|
|
510
|
+
def rbs_answers_before_object?(class_name, method_name, scope)
|
|
511
|
+
definition = services.reflection.instance_method_definition(class_name, method_name, scope: scope)
|
|
512
|
+
return false if definition.nil? || !definition.respond_to?(:defined_in)
|
|
513
|
+
|
|
514
|
+
defined_in = definition.defined_in
|
|
515
|
+
return false if defined_in.nil?
|
|
516
|
+
|
|
517
|
+
!TOP_LEVEL_DEF_OWNERS.include?(defined_in.to_s.delete_prefix("::"))
|
|
518
|
+
rescue StandardError
|
|
519
|
+
false
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# Whether the model entry declares `method_name` as an instance-side member: an association accessor,
|
|
523
|
+
# a column reader, or the ActiveRecord-generated `column?` predicate. The same three families
|
|
524
|
+
# `#recognised_method_names` puts in the dispatch gate, asked of ONE entry.
|
|
525
|
+
def instance_member_name?(entry, method_name)
|
|
526
|
+
name = method_name.to_s
|
|
527
|
+
return true unless entry.association(method_name).nil?
|
|
528
|
+
|
|
529
|
+
column_name = name.end_with?("?") ? name[0..-2] : name
|
|
530
|
+
!entry.column(column_name).nil?
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
# Class-side finders + the class-side relation entry points. `find` returns the model, or an Array of
|
|
534
|
+
# them for two or more ids, or with a block the model or `nil`; `find_by!` returns the model; `find_by` adds the `nil` arm; `where` /
|
|
535
|
+
# `all` / `order` / `limit` / `none` open a relation. The relation then carries its element type
|
|
536
|
+
# through any further chained query method via the bundled `ActiveRecord::Relation` RBS.
|
|
537
|
+
def finder_return_type(call_node, entry, scope)
|
|
368
538
|
case call_node.name
|
|
369
539
|
when :find
|
|
370
|
-
return
|
|
540
|
+
return block_find_return_type(call_node, entry, scope) if call_node.block
|
|
371
541
|
|
|
372
|
-
|
|
542
|
+
arity = call_argument_count(call_node)
|
|
543
|
+
return nil if arity.zero?
|
|
544
|
+
|
|
545
|
+
# `Model.find` is `all.find`, so it answers by arity exactly as the bundled `Relation#find`
|
|
546
|
+
# overloads do, and the comment there says why one argument stays the model.
|
|
547
|
+
model = Rigor::Type::Combinator.nominal_of(entry.class_name)
|
|
548
|
+
return model if arity < 2
|
|
549
|
+
|
|
550
|
+
# A model that defines its own `self.find` (`find(owner, id)`) takes two arguments for its own
|
|
551
|
+
# reasons, so the Array reading of Rails' `find` is withheld and the project's method answers.
|
|
552
|
+
return nil if scope.discovered_method_through_ancestors?(entry.class_name, :find, :singleton)
|
|
553
|
+
|
|
554
|
+
Rigor::Type::Combinator.nominal_of("Array", type_args: [model])
|
|
373
555
|
when :find_by!
|
|
374
556
|
# The bang variant raises `RecordNotFound` instead of returning `nil`, so the result is
|
|
375
557
|
# non-nullable.
|
|
@@ -396,6 +578,29 @@ module Rigor
|
|
|
396
578
|
end
|
|
397
579
|
end
|
|
398
580
|
|
|
581
|
+
# `Model.find { |m| … }` / `Model.find(&:pred?)` never reaches the id lookup: `Core.find` passes a
|
|
582
|
+
# block to `super` (`all.find`), whose `Relation#find` does `return super if block_given?`, landing on
|
|
583
|
+
# `Enumerable#find` over the loaded records. So the answer is the element or `nil`, exactly what the
|
|
584
|
+
# bundled `Relation#find` block overload says and what the engine already answers for `Array#find`.
|
|
585
|
+
# The `nil` arm is the search miss, not a worst-case reading: the block-form `find` sites in the
|
|
586
|
+
# survey corpus, all on Arrays, guard it (`&.`, `||=`, `while (x = …)`).
|
|
587
|
+
#
|
|
588
|
+
# Declining here is not neutral: when the model is RBS-known, a decline falls through to
|
|
589
|
+
# `singleton(Model)`, which declares no `find`, and fires `call.undefined-method` on working code.
|
|
590
|
+
# An argument next to the block is `Enumerable#find`'s `ifnone` callable, whose result joins the
|
|
591
|
+
# union. That form is rare enough that it answers `untyped` rather than typing the callable, as the
|
|
592
|
+
# `Relation#find` overload does. A project `self.find` keeps its own answer, as it does for the
|
|
593
|
+
# multi-id form.
|
|
594
|
+
def block_find_return_type(call_node, entry, scope)
|
|
595
|
+
return nil if scope.discovered_method_through_ancestors?(entry.class_name, :find, :singleton)
|
|
596
|
+
return Rigor::Type::Combinator.untyped unless call_argument_count(call_node).zero?
|
|
597
|
+
|
|
598
|
+
Rigor::Type::Combinator.union(
|
|
599
|
+
Rigor::Type::Combinator.nominal_of(entry.class_name),
|
|
600
|
+
Rigor::Type::Combinator.constant_of(nil)
|
|
601
|
+
)
|
|
602
|
+
end
|
|
603
|
+
|
|
399
604
|
# `Model.table_name` is a String either way; whether it is safe to VALUE-PIN turns on how the name was
|
|
400
605
|
# derived. `Entry#table_name_exact?` holds ONLY for a name the source declared as a String literal
|
|
401
606
|
# (`self.table_name = "…"`, inherited down an STI chain, and not overridden by a computed one). An
|
|
@@ -538,12 +743,39 @@ module Rigor
|
|
|
538
743
|
return nil if column.nil?
|
|
539
744
|
return bool_type if predicate
|
|
540
745
|
|
|
746
|
+
# #1089 — an enum-backed column reads as its KEY (`"active"`), not its SQL storage type
|
|
747
|
+
# (`Integer` for a `t.integer :status` column). Rails returns the configured key as a String from
|
|
748
|
+
# the reader (`user.status # => "active"`), so typing it by `column.ruby_type` makes a correct
|
|
749
|
+
# `user.status.upcase` fire `call.undefined-method` on Integer and lets a wrong `user.status + 1`
|
|
750
|
+
# pass. The writer / `where(status:)` side is out of scope and untouched — it accepts both keys
|
|
751
|
+
# and values.
|
|
752
|
+
return enum_key_type(entry, column_name) if entry.enum?(column_name)
|
|
753
|
+
|
|
541
754
|
inner = ruby_type_to_type(column.ruby_type)
|
|
542
755
|
return Rigor::Type::Combinator.untyped if inner.nil?
|
|
543
756
|
|
|
544
757
|
column.array? ? Rigor::Type::Combinator.nominal_of("Array", type_args: [inner]) : inner
|
|
545
758
|
end
|
|
546
759
|
|
|
760
|
+
# The KEY type of an enum-backed column. {ModelIndex} only records an enum when every key is a
|
|
761
|
+
# static Symbol literal — `parse_enum_call` declines a non-literal values expression outright — so
|
|
762
|
+
# a column in `entry.enums` carries a COMPLETE key set by construction, and the union of the key
|
|
763
|
+
# String constants is exact for the declared enum rather than the wider `String`. The empty-list
|
|
764
|
+
# guard (`enum :status, []` parses) falls back to `String`.
|
|
765
|
+
#
|
|
766
|
+
# Nullability follows `#column_return_type`'s other-column convention: non-nullable, so
|
|
767
|
+
# `user.status.upcase` does not light up `possible-nil-receiver` — Rails code calls enum readers
|
|
768
|
+
# directly as a matter of course, and the same under-report-the-nil trade the parent doc makes
|
|
769
|
+
# applies.
|
|
770
|
+
def enum_key_type(entry, column_name)
|
|
771
|
+
keys = entry.enum_values(column_name)
|
|
772
|
+
return Rigor::Type::Combinator.nominal_of("String") if keys.empty?
|
|
773
|
+
|
|
774
|
+
Rigor::Type::Combinator.union(
|
|
775
|
+
*keys.map { |key| Rigor::Type::Combinator.constant_of(key.to_s) }
|
|
776
|
+
)
|
|
777
|
+
end
|
|
778
|
+
|
|
547
779
|
# Maps a `SchemaTable::Column#ruby_type` string to a Rigor type. `"Object"` (json / jsonb / a
|
|
548
780
|
# `serialize`d or `mount_uploader`ed column / an unrecognised SQL type) declines — `Nominal[Object]`
|
|
549
781
|
# would be NARROWER than the RBS-erased envelope and could surface false `call.undefined-method` on a
|
|
@@ -614,7 +846,8 @@ module Rigor
|
|
|
614
846
|
scopes: entry.scopes,
|
|
615
847
|
validations: entry.validated_attributes,
|
|
616
848
|
callbacks: entry.callbacks,
|
|
617
|
-
aliases: entry.aliases
|
|
849
|
+
aliases: entry.aliases,
|
|
850
|
+
macro_methods: entry.macro_methods
|
|
618
851
|
}.freeze
|
|
619
852
|
end.freeze
|
|
620
853
|
end
|
|
@@ -632,7 +865,9 @@ module Rigor
|
|
|
632
865
|
# model-file additions — so no priming walk is needed (it used to run the discover twice).
|
|
633
866
|
@model_index = cache_for(:model_index, params: {}).call
|
|
634
867
|
rescue StandardError => e
|
|
635
|
-
@load_errors << {
|
|
868
|
+
@load_errors << { key: "2-model-index-failed",
|
|
869
|
+
message: "model index build failed: #{e.class}: #{e.message}",
|
|
870
|
+
severity: :warning }
|
|
636
871
|
nil
|
|
637
872
|
end
|
|
638
873
|
|
|
@@ -651,7 +886,8 @@ module Rigor
|
|
|
651
886
|
# here.
|
|
652
887
|
@schema_table = cache_for(:schema_table, params: {}).call
|
|
653
888
|
rescue Plugin::AccessDeniedError => e
|
|
654
|
-
@load_errors << {
|
|
889
|
+
@load_errors << { key: "1-schema-read-refused",
|
|
890
|
+
message: "rigor-activerecord: #{e.message}#{REDUCED_MODE_SUFFIX}",
|
|
655
891
|
severity: :warning }
|
|
656
892
|
nil
|
|
657
893
|
rescue Errno::ENOENT
|
|
@@ -660,14 +896,16 @@ module Rigor
|
|
|
660
896
|
# associations and table names off the discovered models, so this is a disclosure of REDUCED
|
|
661
897
|
# capability rather than a failure — `:info`, the grade the plugins use for "here is what I
|
|
662
898
|
# recognised", not `:warning`, the grade they use for "I could not run".
|
|
663
|
-
@load_errors << {
|
|
899
|
+
@load_errors << { key: "1-schema-file-missing",
|
|
900
|
+
message: "rigor-activerecord: schema file `#{@schema_file}` (or " \
|
|
664
901
|
"`#{@structure_sql_file}`) not found#{REDUCED_MODE_SUFFIX}",
|
|
665
902
|
severity: :info }
|
|
666
903
|
nil
|
|
667
904
|
rescue StandardError => e
|
|
668
905
|
# A schema that EXISTS but does not parse is a real, actionable problem — the user meant it to be
|
|
669
906
|
# read. Reduced mode still applies, but the disclosure stays a warning.
|
|
670
|
-
@load_errors << {
|
|
907
|
+
@load_errors << { key: "1-schema-parse-failed",
|
|
908
|
+
message: "rigor-activerecord: failed to parse `#{@schema_file}`: " \
|
|
671
909
|
"#{e.class}: #{e.message}#{REDUCED_MODE_SUFFIX}",
|
|
672
910
|
severity: :warning }
|
|
673
911
|
nil
|
|
@@ -679,23 +917,31 @@ module Rigor
|
|
|
679
917
|
"(`where(col:)`, column readers) are skipped"
|
|
680
918
|
private_constant :REDUCED_MODE_SUFFIX
|
|
681
919
|
|
|
682
|
-
# Project-global disclosures,
|
|
683
|
-
#
|
|
684
|
-
#
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
920
|
+
# Project-global disclosures, handed to the engine's run-scoped channel ({Plugin::Base#disclose_once},
|
|
921
|
+
# issue #1051). They are facts about the project's INPUTS — "there is no `db/schema.rb`, so column
|
|
922
|
+
# checks are off" — so the engine positions them at `.rigor.yml:1:1` and emits each `key` once per
|
|
923
|
+
# run.
|
|
924
|
+
#
|
|
925
|
+
# This used to be an `@load_errors_emitted` flag consulted from `#diagnostics_for_file`, which put the
|
|
926
|
+
# row on whichever file this instance saw first. On a Redmine-shape project (migrations only, no
|
|
927
|
+
# `schema.rb`) the per-FILE form before that produced 346 identical rows; on a Solidus monorepo, 999.
|
|
928
|
+
# The flag fixed the count per instance, but a fork-pool worker gets its OWN instance, so `--workers
|
|
929
|
+
# N` re-multiplied it by N and each copy landed on that worker's first file — after #393 possibly an
|
|
930
|
+
# `.erb` template unit, where a schema notice reads as a claim about a view.
|
|
931
|
+
# The `key`s carry an ordinal prefix because the engine emits a plugin's disclosures in KEY order
|
|
932
|
+
# (#1051), and the three schema outcomes — mutually exclusive, since `@schema_load_attempted` memoises
|
|
933
|
+
# the attempt — must still precede a model-index failure the way `@load_errors` records them. Keys are
|
|
934
|
+
# identities, never shown to the user, so the prefix costs nothing but the ordering.
|
|
935
|
+
def disclose_load_errors
|
|
936
|
+
@load_errors.each do |error|
|
|
937
|
+
disclose_once(
|
|
938
|
+
error.fetch(:key),
|
|
694
939
|
message: error.fetch(:message),
|
|
695
940
|
severity: error.fetch(:severity),
|
|
696
941
|
rule: "load-error"
|
|
697
942
|
)
|
|
698
943
|
end
|
|
944
|
+
nil
|
|
699
945
|
end
|
|
700
946
|
end
|
|
701
947
|
|