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,263 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rigor/plugin"
|
|
4
|
+
|
|
5
|
+
require_relative "active_model_serializers/serializer_index"
|
|
6
|
+
require_relative "active_model_serializers/serializer_discoverer"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module Plugin
|
|
10
|
+
# rigor-active-model-serializers — types the implicit-self `object` reader inside a serializer as the
|
|
11
|
+
# model the serializer serializes.
|
|
12
|
+
#
|
|
13
|
+
# It emits no diagnostic. What it contributes is one return type and the gem's own framework
|
|
14
|
+
# constants, and both exist for the same measurement: the 2026-09-01 corpus opacity sweep (#534
|
|
15
|
+
# item 6) found `object` to be Mastodon's single largest unresolved implicit-self send — 751 sites in
|
|
16
|
+
# `app/serializers` alone — with no plugin owning ActiveModelSerializers at all. Every
|
|
17
|
+
# `object.account.username` chain below one of those sites was dispatching on `Dynamic[top]`.
|
|
18
|
+
#
|
|
19
|
+
# plugins:
|
|
20
|
+
# - gem: rigor-active-model-serializers
|
|
21
|
+
# config:
|
|
22
|
+
# serializer_search_paths: ["app/serializers", "app/lib"] # default; optional
|
|
23
|
+
# serializer_base_classes: ["ActiveModel::Serializer"] # default; optional
|
|
24
|
+
# model_overrides: {} # default; optional
|
|
25
|
+
#
|
|
26
|
+
# ## What `object` types to, and when it declines
|
|
27
|
+
#
|
|
28
|
+
# AMS's `object` is `ActiveModel::Serializer#object`, the resource the serializer was constructed
|
|
29
|
+
# with. Nothing in a serializer's source STATES that resource's class — AMS binds it at `new` time —
|
|
30
|
+
# so the type is only ever derived, and two independent things have to agree before it is:
|
|
31
|
+
#
|
|
32
|
+
# 1. The `<Model>Serializer` naming convention resolves to exactly one model `rigor-activerecord`
|
|
33
|
+
# discovered (`REST::AccountSerializer` → `Account`), and
|
|
34
|
+
# 2. that model ANSWERS the serializer: every name the serializer will read off its resource — the
|
|
35
|
+
# `attributes` / `has_many` / `has_one` declarations it does not define itself, plus every
|
|
36
|
+
# `object.<name>` in its body — is a column, association, enum, alias or scope of that model, or a
|
|
37
|
+
# method the project defines on it.
|
|
38
|
+
#
|
|
39
|
+
# A `model_overrides` entry short-circuits both: the project asserted the answer. Anything else
|
|
40
|
+
# DECLINES, and `object` keeps whatever the engine would have given it (`Dynamic`).
|
|
41
|
+
#
|
|
42
|
+
# ## Why the name alone is not enough
|
|
43
|
+
#
|
|
44
|
+
# The name is a guess and the model index only proves the guessed class EXISTS, never that this
|
|
45
|
+
# serializer serializes it. Mastodon has three serializers where the guess is wrong and the class is
|
|
46
|
+
# real: `REST::ConversationSerializer` serializes an `AccountConversation` (its `unread`,
|
|
47
|
+
# `participant_accounts` and `last_status` are all absent from `Conversation`), and both
|
|
48
|
+
# `REST::InstanceSerializer` and `REST::V1::InstanceSerializer` serialize an `InstancePresenter`
|
|
49
|
+
# (`object.contact`, `object.thumbnail`). A corpus diff cannot see the damage, because an Active
|
|
50
|
+
# Record model's surface is open and a wrong-but-real model absorbs every read in silence — so the
|
|
51
|
+
# check has to be a positive one, made before the answer is contributed.
|
|
52
|
+
#
|
|
53
|
+
# Requiring EVERY name to be answered, rather than most of them, is the same choice: a serializer
|
|
54
|
+
# whose resource is a decorator around the model shares most of the model's surface, and it is
|
|
55
|
+
# exactly the one or two extra names that say so.
|
|
56
|
+
#
|
|
57
|
+
# ## Scope
|
|
58
|
+
#
|
|
59
|
+
# - **SimpleForm inputs are NOT handled.** The sweep's 875-site `object` count mixes AMS serializers
|
|
60
|
+
# with `SimpleForm::Inputs::Base#object`, a different gem with a different resource story
|
|
61
|
+
# (`object` there is the form's record, named by the `simple_form_for` call site, not by the input
|
|
62
|
+
# class). That belongs in a `rigor-simple-form` plugin and is deliberately left out.
|
|
63
|
+
# - **`serializer:` / `each_serializer:` options are not read.** `has_many :emojis, serializer:
|
|
64
|
+
# REST::CustomEmojiSerializer` states which serializer renders an association, never which model a
|
|
65
|
+
# serializer serializes, so it cannot ground `object`.
|
|
66
|
+
# - **A serializer with no declarations and no `object` reads gets no answer.** There is nothing to
|
|
67
|
+
# check the name against, and the name alone is what this refuses to trust.
|
|
68
|
+
class ActiveModelSerializers < Rigor::Plugin::Base
|
|
69
|
+
manifest(
|
|
70
|
+
id: "active-model-serializers",
|
|
71
|
+
target_gems: ["active_model_serializers"],
|
|
72
|
+
version: "0.1.0",
|
|
73
|
+
description: "Types the implicit-self `object` reader inside ActiveModel::Serializer subclasses " \
|
|
74
|
+
"as the serializer's model, and declares the gem's framework constants.",
|
|
75
|
+
config_schema: {
|
|
76
|
+
# `app/lib` is in the default set because a project base serializer routinely lives outside
|
|
77
|
+
# `app/serializers` — Mastodon's `ActivityPub::Serializer`, the parent of 60 of its 153
|
|
78
|
+
# serializers, is at `app/lib/activitypub/serializer.rb`. Without it the ancestry closure stops
|
|
79
|
+
# at the base class and two fifths of the project's serializers are simply not serializers as
|
|
80
|
+
# far as this plugin is concerned. A directory the project does not have costs one probe.
|
|
81
|
+
"serializer_search_paths" => { kind: :array, default: ["app/serializers", "app/lib"] },
|
|
82
|
+
"serializer_base_classes" => { kind: :array, default: ["ActiveModel::Serializer"] },
|
|
83
|
+
# Serializer class name => model class name, for the resources the derivation cannot reach: a
|
|
84
|
+
# serializer whose resource is a presenter or a decorator, or one named for a JSON shape rather
|
|
85
|
+
# than a model. An override is the project's own assertion and is NOT re-checked — a project
|
|
86
|
+
# that names a class Rigor has no RBS for gets a lenient nominal.
|
|
87
|
+
"model_overrides" => { kind: :hash, default: {} }
|
|
88
|
+
},
|
|
89
|
+
# Optional on purpose: without `rigor-activerecord` there is no model set to check a name
|
|
90
|
+
# against, and every non-overridden serializer declines. That is the designed degradation — the
|
|
91
|
+
# plugin contributes less, never something wrong.
|
|
92
|
+
consumes: [{ plugin_id: "activerecord", name: :model_index, optional: true }],
|
|
93
|
+
# ADR-25 (#534 item 7, same admission rule as `rigor-activerecord`'s `sig/active_record/
|
|
94
|
+
# framework.rbs`) — the gem's own constants, declared so they resolve and asserting nothing else.
|
|
95
|
+
signature_paths: ["sig"],
|
|
96
|
+
# ADR-26 — every class and module the bundled signature names is open, so that declaring it buys
|
|
97
|
+
# constant resolution and asserts nothing about a member. `ActiveModel::Serializer` is the row
|
|
98
|
+
# that matters most (a project's serializers inherit from it, and its whole instance surface —
|
|
99
|
+
# `object`, `scope`, `read_attribute_for_serialization`, the `_attributes` class state — is
|
|
100
|
+
# unenumerated), and `ActiveModelSerializers` / `::Adapter` are the ones the canonical
|
|
101
|
+
# initializer dispatches on: `ActiveModelSerializers.config.adapter = :json_api` and
|
|
102
|
+
# `ActiveModelSerializers::Adapter.register(...)` are what an AMS `config/initializers` file says.
|
|
103
|
+
open_receivers: [
|
|
104
|
+
"ActiveModel::Serializer",
|
|
105
|
+
"ActiveModel::Serializer::CollectionSerializer",
|
|
106
|
+
"ActiveModelSerializers",
|
|
107
|
+
"ActiveModelSerializers::Adapter",
|
|
108
|
+
"ActiveModelSerializers::Model",
|
|
109
|
+
"ActiveModelSerializers::SerializableResource"
|
|
110
|
+
]
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
producer :serializer_index, watch: -> { [[@serializer_search_paths, "**/*.rb"]] } do |_params|
|
|
114
|
+
SerializerDiscoverer.new(
|
|
115
|
+
io_boundary: io_boundary,
|
|
116
|
+
search_paths: @serializer_search_paths,
|
|
117
|
+
base_classes: @serializer_base_classes
|
|
118
|
+
).discover
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def init(_services)
|
|
122
|
+
@serializer_search_paths = Array(config.fetch("serializer_search_paths")).map(&:to_s)
|
|
123
|
+
@serializer_base_classes = Array(config.fetch("serializer_base_classes")).map(&:to_s)
|
|
124
|
+
@model_overrides = config.fetch("model_overrides").to_h { |k, v| [k.to_s.delete_prefix("::"), v.to_s] }
|
|
125
|
+
@derivations = {}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# The `methods:` gate keeps this off every dispatch whose name is not `object`; the receiver and
|
|
129
|
+
# argument checks keep it off `foo.object` and `object(x)`, neither of which is the AMS reader.
|
|
130
|
+
dynamic_return methods: [:object] do |call_node, scope|
|
|
131
|
+
next nil unless call_node.is_a?(Prism::CallNode)
|
|
132
|
+
next nil unless call_node.receiver.nil?
|
|
133
|
+
next nil unless call_node.arguments.nil?
|
|
134
|
+
|
|
135
|
+
entry = serializer_entry(scope)
|
|
136
|
+
next nil if entry.nil?
|
|
137
|
+
# An explicit `def object`, here or on an ancestor, is the definition that runs. Answering over
|
|
138
|
+
# it would replace a type the engine derived from real source with a guess, and silence whatever
|
|
139
|
+
# that source proves.
|
|
140
|
+
next nil if entry.defines_object? || project_defines_object?(entry.class_name, scope)
|
|
141
|
+
|
|
142
|
+
model_name = model_class_name_for(entry, scope)
|
|
143
|
+
next nil if model_name.nil?
|
|
144
|
+
|
|
145
|
+
Rigor::Type::Combinator.nominal_of(model_name)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
# The discovered serializer whose body `self` is in, or nil. Membership of the index is the only
|
|
151
|
+
# gate: a `*Serializer` name is not evidence of anything, since `object` is an ordinary method name
|
|
152
|
+
# that a `Json::ConversationSerializer` or an `Oj::AccountSerializer` may define for itself.
|
|
153
|
+
def serializer_entry(scope)
|
|
154
|
+
self_type = scope&.self_type
|
|
155
|
+
return nil unless self_type.respond_to?(:class_name)
|
|
156
|
+
|
|
157
|
+
name = self_type.class_name
|
|
158
|
+
return nil if name.nil? || name.empty?
|
|
159
|
+
|
|
160
|
+
producer_value(:serializer_index)&.find(name)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def project_defines_object?(serializer_name, scope)
|
|
164
|
+
return false unless scope.respond_to?(:user_def_through_ancestors)
|
|
165
|
+
|
|
166
|
+
!scope.user_def_through_ancestors(serializer_name, :object).first.nil?
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# The model a serializer serializes, or nil where nothing corroborates it. Memoised per serializer
|
|
170
|
+
# class INCLUDING the nil answer: the ancestor walks behind `project_defines?` are run once per
|
|
171
|
+
# name per serializer, and `object` is read hundreds of times across a real `app/serializers`.
|
|
172
|
+
def model_class_name_for(entry, scope)
|
|
173
|
+
return @derivations[entry.class_name] if @derivations.key?(entry.class_name)
|
|
174
|
+
|
|
175
|
+
@derivations[entry.class_name] = derive_model_class_name(entry, scope)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def derive_model_class_name(entry, scope)
|
|
179
|
+
override = @model_overrides[entry.class_name]
|
|
180
|
+
return override unless override.nil? || override.empty?
|
|
181
|
+
|
|
182
|
+
# The published `:model_index` fact is a Hash keyed by the model's DE-ROOTED constant path
|
|
183
|
+
# (#583). `rigor-activerecord` withholds it entirely in reduced mode (no `db/schema.rb` /
|
|
184
|
+
# `db/structure.sql`), so a schema-less project declines here rather than typing `object` off a
|
|
185
|
+
# name alone.
|
|
186
|
+
index = read_fact(plugin_id: "activerecord", name: :model_index)
|
|
187
|
+
return nil if index.nil? || index.empty?
|
|
188
|
+
|
|
189
|
+
resolved = convention_candidates(entry.class_name).select { |candidate| index.key?(candidate) }
|
|
190
|
+
# Two readings that both name a real model (`Admin::AccountSerializer` where `Admin::Account` and
|
|
191
|
+
# `Account` both exist) is an ambiguity, not a first-hit: nothing here ranks one above the other.
|
|
192
|
+
return nil unless resolved.size == 1
|
|
193
|
+
|
|
194
|
+
model = resolved.first
|
|
195
|
+
answers?(entry, index.fetch(model), model, scope) ? model : nil
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Whether the model ANSWERS the serializer — see the class comment for why this is required and why
|
|
199
|
+
# it is required of every name rather than most of them. A serializer that declares nothing and
|
|
200
|
+
# reads nothing states nothing to check, and is declined for that reason rather than admitted for
|
|
201
|
+
# it.
|
|
202
|
+
def answers?(entry, row, model_name, scope)
|
|
203
|
+
declarations = entry.unhandled_declarations
|
|
204
|
+
reads = entry.resource_reads
|
|
205
|
+
return false if declarations.empty? && reads.empty?
|
|
206
|
+
|
|
207
|
+
members = model_members(row)
|
|
208
|
+
# A declaration the serializer itself renders is not a read of the resource at all. The entry
|
|
209
|
+
# subtracts the serializer's OWN methods; a base serializer's `def formatted` is only visible to
|
|
210
|
+
# the engine's ancestor walk, and a declaration it handles says nothing about the model.
|
|
211
|
+
declarations.all? do |name|
|
|
212
|
+
project_defines?(entry.class_name, name, scope) ||
|
|
213
|
+
members.include?(name) || project_defines?(model_name, name, scope)
|
|
214
|
+
end && reads.all? { |name| members.include?(name) || project_defines?(model_name, name, scope) }
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Every name the model answers that its `:model_index` row states. The `?` forms are Active
|
|
218
|
+
# Record's own per-column predicates, which a serializer reads as readily as the column.
|
|
219
|
+
#
|
|
220
|
+
# `macro_methods` (#1049) is the row's name-only set: `delegate`'s methods, the Paperclip / Active
|
|
221
|
+
# Storage attachment readers, and an `enum`'s per-value predicates, from the model body and from every
|
|
222
|
+
# concern the model includes. It is what lets `REST::AccountSerializer`'s `followers_count` / `user` /
|
|
223
|
+
# `avatar` reads resolve; an older producer that does not publish the key reads as the empty set here,
|
|
224
|
+
# which declines exactly as before rather than erroring.
|
|
225
|
+
def model_members(row)
|
|
226
|
+
columns = Array(row[:columns]).map(&:to_s)
|
|
227
|
+
associations = Array(row[:associations]).map { |a| a[:name].to_s }
|
|
228
|
+
enums = row[:enums].is_a?(Hash) ? row[:enums].keys.map(&:to_s) : []
|
|
229
|
+
aliases = row[:aliases].is_a?(Hash) ? row[:aliases].keys.map(&:to_s) : []
|
|
230
|
+
scopes = Array(row[:scopes]).map(&:to_s)
|
|
231
|
+
macros = Array(row[:macro_methods]).map(&:to_s)
|
|
232
|
+
(columns + columns.map { |c| "#{c}?" } + associations + enums + aliases + scopes + macros).to_set
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# A method the project writes in Ruby on `class_name` or an ancestor of it. On the model side that
|
|
236
|
+
# is `Account#local?` or a concern's reader — the model index cannot see these, and without them
|
|
237
|
+
# the check would decline nearly every real serializer. On the serializer side it is a base
|
|
238
|
+
# serializer's own rendering method, which is not a read of the resource at all.
|
|
239
|
+
def project_defines?(class_name, method_name, scope)
|
|
240
|
+
return false unless scope.respond_to?(:user_def_through_ancestors)
|
|
241
|
+
|
|
242
|
+
!scope.user_def_through_ancestors(class_name, method_name.to_sym).first.nil?
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# `REST::AccountSerializer` offers two readings — the namespaced `REST::Account` and the
|
|
246
|
+
# demodulized `Account` — and Rails apps use both (`Admin::AccountSerializer` for `Admin::Account`
|
|
247
|
+
# is as real as Mastodon's `REST::AccountSerializer` for `Account`). Both are offered to the model
|
|
248
|
+
# index, and exactly one of them has to land.
|
|
249
|
+
def convention_candidates(serializer_name)
|
|
250
|
+
return [] unless serializer_name.end_with?("Serializer")
|
|
251
|
+
# A class named exactly `Serializer` is a namespace's base serializer, never a model's.
|
|
252
|
+
return [] if serializer_name.split("::").last == "Serializer"
|
|
253
|
+
|
|
254
|
+
stripped = serializer_name.delete_suffix("Serializer").delete_suffix("::")
|
|
255
|
+
return [] if stripped.empty?
|
|
256
|
+
|
|
257
|
+
[stripped, stripped.split("::").last].uniq.reject(&:empty?)
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
Rigor::Plugin.register(ActiveModelSerializers)
|
|
262
|
+
end
|
|
263
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Gem entry point. Required by Rigor's plugin loader when `.rigor.yml` lists
|
|
4
|
+
# `rigor-active-model-serializers` under `plugins:`. The loader expects this `require` to side-effect a
|
|
5
|
+
# call to `Rigor::Plugin.register`, which the body of
|
|
6
|
+
# `lib/rigor/plugin/active_model_serializers.rb` performs at load time.
|
|
7
|
+
require_relative "rigor/plugin/active_model_serializers"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# The ActiveModelSerializers gem's own constants — issue #534 item 6.
|
|
2
|
+
#
|
|
3
|
+
# The 2026-09-01 corpus opacity sweep counted `ActiveModel::Serializer` 101 times on Mastodon,
|
|
4
|
+
# `ActiveModelSerializers::Model` 29 and `ActiveModelSerializers::SerializableResource` 25, every
|
|
5
|
+
# one of them resolving to `untyped` because nothing in the bundle named the constant. This file
|
|
6
|
+
# names them, and nothing more.
|
|
7
|
+
#
|
|
8
|
+
# ## The admission rule
|
|
9
|
+
#
|
|
10
|
+
# The same rule `plugins/rigor-activerecord/sig/active_record/framework.rbs` states: declaring a
|
|
11
|
+
# constant makes it RBS-KNOWN, and a known class with an incomplete signature turns every member the
|
|
12
|
+
# signature omits into a false `call.undefined-method`. Every class here is therefore ALSO listed in
|
|
13
|
+
# the manifest's `open_receivers:` (ADR-26), which is what keeps the members this file does not
|
|
14
|
+
# enumerate lenient.
|
|
15
|
+
#
|
|
16
|
+
# That pairing is load-bearing rather than defensive here, because — unlike the Active Record
|
|
17
|
+
# exception hierarchy, which is rescued and not dispatched on — `ActiveModel::Serializer` and
|
|
18
|
+
# `ActiveModelSerializers::Model` are exactly the "class a project INHERITS from" shape the Active
|
|
19
|
+
# Record file declines to declare. `class REST::AccountSerializer < ActiveModel::Serializer` is what
|
|
20
|
+
# an AMS application looks like, and `object`, `scope`, `read_attribute_for_serialization`,
|
|
21
|
+
# `attributes`, `has_many` and the rest of the AMS surface are not enumerated below. The Mastodon
|
|
22
|
+
# before/after diff recorded in `docs/notes/20260917-ams-object-recognizer.md` is the evidence that
|
|
23
|
+
# the pairing holds on a real application.
|
|
24
|
+
#
|
|
25
|
+
# `ActiveModel` is declared as a module by `rigor-activerecord` as well; RBS reopens a module across
|
|
26
|
+
# files, so the two declarations compose rather than collide, and neither plugin depends on the
|
|
27
|
+
# other being enabled.
|
|
28
|
+
|
|
29
|
+
module ActiveModel
|
|
30
|
+
class Serializer
|
|
31
|
+
class CollectionSerializer
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
module ActiveModelSerializers
|
|
37
|
+
class Model
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class SerializableResource
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
module Adapter
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -49,6 +49,7 @@ module Rigor
|
|
|
49
49
|
"shoryuken" => ["io.net"], "backburner" => ["io.net"], "sucker_punch" => [],
|
|
50
50
|
"async" => [], "inline" => [], "test" => []
|
|
51
51
|
}.freeze
|
|
52
|
+
Ractor.make_shareable(TRANSPORTS)
|
|
52
53
|
|
|
53
54
|
# The meaning half, which is adapter-independent and is what a policy actually names.
|
|
54
55
|
MEANING = ["rails.activejob.enqueue", "job.enqueue"].freeze
|
|
@@ -129,12 +129,12 @@ module Rigor
|
|
|
129
129
|
services.fact_store.publish(plugin_id: manifest.id, name: :reachability_roots, value: roots)
|
|
130
130
|
end
|
|
131
131
|
|
|
132
|
-
# File-level only: the load-error
|
|
132
|
+
# File-level only: the load-error disclosure. Per-call arity validation runs over the engine-owned
|
|
133
133
|
# walk via the node_rule below (ADR-37). The job index is lazily loaded + memoised by
|
|
134
134
|
# `producer_value`, shared by both surfaces.
|
|
135
135
|
def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
|
|
136
136
|
index = producer_value(:job_index)
|
|
137
|
-
|
|
137
|
+
disclose_load_error if index.nil? && producer_error(:job_index)
|
|
138
138
|
|
|
139
139
|
[]
|
|
140
140
|
end
|
|
@@ -148,10 +148,14 @@ module Rigor
|
|
|
148
148
|
|
|
149
149
|
private
|
|
150
150
|
|
|
151
|
-
|
|
151
|
+
# Issue #1056 — "the job index did not load" is a fact about the run's INPUTS, not about the file being
|
|
152
|
+
# analysed, so it is registered rather than returned: see {Plugin::Base#disclose_once} for the
|
|
153
|
+
# channel, the `(plugin id, key)` de-duplication and why the engine positions it at `.rigor.yml:1:1`.
|
|
154
|
+
# Returned from the per-file hook it carried no once-guard at all and repeated on every file.
|
|
155
|
+
def disclose_load_error
|
|
152
156
|
error = producer_error(:job_index)
|
|
153
|
-
|
|
154
|
-
|
|
157
|
+
disclose_once(
|
|
158
|
+
:job_index_load_failed,
|
|
155
159
|
message: "rigor-activejob: failed to discover jobs: #{error.class}: #{error.message}",
|
|
156
160
|
severity: :warning,
|
|
157
161
|
rule: "load-error"
|
|
@@ -13,13 +13,15 @@ module Rigor
|
|
|
13
13
|
# | Method | Recognised arg shape | Validation |
|
|
14
14
|
# | --- | --- | --- |
|
|
15
15
|
# | `Model.find(id)` | any positional | arity check (1+ args) |
|
|
16
|
+
# | `Model.find { … }` | a block, no args | none (`Enumerable#find`) |
|
|
16
17
|
# | `Model.find_by(col: v, ...)` | keyword args | each key must be a column |
|
|
17
18
|
# | `Model.where(col: v, ...)` | keyword args | each key must be a column |
|
|
18
19
|
# | `Model.where(string)` | String literal | parser-side; not validated |
|
|
19
20
|
#
|
|
20
21
|
# Successful matches surface as `:info` diagnostics naming the resolved table; unknown columns
|
|
21
22
|
# surface as `:error`. Calls whose receiver is not a model in the index, and calls with
|
|
22
|
-
# non-keyword arguments to `where` / `find_by`, stay silent.
|
|
23
|
+
# non-keyword arguments to `where` / `find_by`, stay silent. A model's own `self.find` owns its arity,
|
|
24
|
+
# so the arity check and the notes for the forms the return-type tier leaves to it stay silent too.
|
|
23
25
|
class Analyzer
|
|
24
26
|
# Methods that take a column → value Hash and need each key validated against the receiver's
|
|
25
27
|
# column set. The bang variants (`find_by!` raises instead of returning nil; `find_or_create_by!`
|
|
@@ -35,9 +37,13 @@ module Rigor
|
|
|
35
37
|
|
|
36
38
|
attr_reader :diagnostics
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
# `scope` is the file's entry scope, which carries the project's discovered methods; it answers
|
|
41
|
+
# whether a model defines its own `self.find`. An editor run seeds none, so there the check misses
|
|
42
|
+
# even a same-file `self.find` the typer sees (#1329).
|
|
43
|
+
def initialize(path:, model_index:, scope:)
|
|
39
44
|
@path = path
|
|
40
45
|
@model_index = model_index
|
|
46
|
+
@scope = scope
|
|
41
47
|
@diagnostics = []
|
|
42
48
|
end
|
|
43
49
|
|
|
@@ -69,16 +75,42 @@ module Rigor
|
|
|
69
75
|
end
|
|
70
76
|
end
|
|
71
77
|
|
|
78
|
+
# A block turns `find` into `Enumerable#find` over `all` (see `Activerecord#block_find_return_type`),
|
|
79
|
+
# which takes no id, so the zero-argument check applies only to the block-less id lookup.
|
|
80
|
+
#
|
|
81
|
+
# The arity check and the notes describe Rails' `find`. A model's own `self.find` has its own arity,
|
|
82
|
+
# and the return-type tier (`Activerecord#finder_return_type`) leaves the multi-id and block forms to
|
|
83
|
+
# it, so a note there would claim a type the call does not get. One id still types as the model.
|
|
72
84
|
def validate_find(node, entry)
|
|
85
|
+
return validate_block_find(node, entry) if node.block
|
|
86
|
+
|
|
73
87
|
arity = call_argument_count(node)
|
|
74
88
|
if arity.zero?
|
|
89
|
+
return if own_find?(entry)
|
|
90
|
+
|
|
75
91
|
push_error(node, "wrong-arity",
|
|
76
92
|
"`#{entry.class_name}.find` expects at least 1 argument, got 0")
|
|
77
93
|
return
|
|
78
94
|
end
|
|
95
|
+
return if arity >= 2 && own_find?(entry)
|
|
79
96
|
|
|
97
|
+
returned = arity >= 2 ? "Array[#{entry.class_name}]" : entry.class_name
|
|
80
98
|
push_info(node, "model-call",
|
|
81
|
-
"`#{entry.class_name}.find` returns #{
|
|
99
|
+
"`#{entry.class_name}.find` returns #{returned} (table: `#{entry.table_name}`)")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def validate_block_find(node, entry)
|
|
103
|
+
return unless call_argument_count(node).zero?
|
|
104
|
+
return if own_find?(entry)
|
|
105
|
+
|
|
106
|
+
push_info(node, "model-call",
|
|
107
|
+
"`#{entry.class_name}.find` returns #{entry.class_name} | nil (table: `#{entry.table_name}`)")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# The predicate `Activerecord#finder_return_type` declines on, so the note and the type agree in a
|
|
111
|
+
# CLI run. In an editor run this scope lacks the file's own methods (#1329).
|
|
112
|
+
def own_find?(entry)
|
|
113
|
+
@scope.discovered_method_through_ancestors?(entry.class_name, :find, :singleton)
|
|
82
114
|
end
|
|
83
115
|
|
|
84
116
|
def validate_column_hash_call(node, entry)
|
|
@@ -38,32 +38,55 @@ module Rigor
|
|
|
38
38
|
|
|
39
39
|
READ = ["io.db.read"].freeze
|
|
40
40
|
WRITE = ["io.db.write"].freeze
|
|
41
|
+
READ_WRITE = ["io.db.read", "io.db.write"].freeze
|
|
41
42
|
TRANSACTION = ["io.db.transaction"].freeze
|
|
42
43
|
SCHEMA_WRITE = ["io.db.write", "rails.schema.write"].freeze
|
|
43
44
|
|
|
44
45
|
# Class-side finders. Every one issues a `SELECT` the moment it is called — that is what separates
|
|
45
|
-
# them from `where`, which returns a relation and issues nothing.
|
|
46
|
+
# them from `where`, which returns a relation and issues nothing. An `async_*` calculation schedules
|
|
47
|
+
# it on the async executor, or runs it at once when there is none. `first_or_initialize` is
|
|
48
|
+
# `first || new`, and `extract_associated` loads the association it preloads.
|
|
46
49
|
SINGLETON_READS = %w[
|
|
47
50
|
find find_by find_by! first first! last last! take take! sole find_sole_by
|
|
48
|
-
second third fourth fifth forty_two
|
|
51
|
+
second second! third third! fourth fourth! fifth fifth! forty_two forty_two!
|
|
52
|
+
second_to_last second_to_last! third_to_last third_to_last!
|
|
49
53
|
exists? any? none? one? many? empty? count sum average minimum maximum calculate
|
|
50
54
|
pluck pick ids find_each find_in_batches in_batches find_by_sql count_by_sql
|
|
51
|
-
find_or_initialize_by
|
|
55
|
+
find_or_initialize_by first_or_initialize extract_associated
|
|
56
|
+
async_ids async_count async_average async_minimum async_maximum async_sum async_pluck async_pick
|
|
52
57
|
].freeze
|
|
53
58
|
|
|
54
|
-
# Class-side writers
|
|
59
|
+
# Class-side writers that issue their statement and query nothing of their own: `create` is
|
|
60
|
+
# `new(…).save`, the bulk writers compile one `INSERT`, and the counter writers are an `update_all` on
|
|
61
|
+
# `unscoped`, which cannot eager-load.
|
|
55
62
|
SINGLETON_WRITES = %w[
|
|
56
63
|
create create! insert insert! insert_all insert_all! upsert upsert_all
|
|
57
|
-
|
|
58
|
-
|
|
64
|
+
update_counters increment_counter decrement_counter
|
|
65
|
+
].freeze
|
|
66
|
+
|
|
67
|
+
# Class-side writers that query as well, checked against activerecord 8.1.3.1. Most are in
|
|
68
|
+
# `Querying::QUERYING_METHODS`, which Rails delegates to `all`, so each is the Relation method of the
|
|
69
|
+
# same name and the row carries the read that `relation.rbs` gives it: a lookup before the write, a
|
|
70
|
+
# `find_by!` after a failed insert, the records `destroy_all` loads, or the `SELECT` of distinct
|
|
71
|
+
# primary keys an eager-loading, limited scope runs before its UPDATE / DELETE. `all` adds the default
|
|
72
|
+
# scope, which can eager-load. `delete` is `where(id:).delete_all`, and `destroy` calls `find`. Three
|
|
73
|
+
# are not delegated and read on their own: `update(!)` calls `find` or walks `all.each`, and
|
|
74
|
+
# `reset_counters` counts the association before it writes the column.
|
|
75
|
+
SINGLETON_READ_WRITES = %w[
|
|
76
|
+
update update! update_all delete delete_all delete_by destroy destroy_all destroy_by touch_all
|
|
77
|
+
find_or_create_by find_or_create_by! create_or_find_by create_or_find_by!
|
|
78
|
+
first_or_create first_or_create! reset_counters
|
|
59
79
|
].freeze
|
|
60
80
|
|
|
61
81
|
# Instance-side readers. `reload` re-issues the `SELECT` and replaces the record's attributes,
|
|
62
82
|
# which is a receiver mutation as well as a read.
|
|
63
83
|
INSTANCE_READS = %w[valid? invalid? reload].freeze
|
|
64
84
|
|
|
65
|
-
# Instance-side writers. Each is a statement issued now
|
|
66
|
-
# `
|
|
85
|
+
# Instance-side writers. Each is a statement issued now, and none queries on its own: `update` is
|
|
86
|
+
# `assign_attributes` plus `save`, and `increment!` / `update_columns` compile one UPDATE. What `save`
|
|
87
|
+
# or `destroy` reads beyond that comes from the model's callbacks and validators. The callback edge
|
|
88
|
+
# carries the ones it reads: symbol-argument callback macros and a uniqueness validator. Nothing
|
|
89
|
+
# carries the reads a required `belongs_to`, a `touch:` option or a `dependent:` option registers.
|
|
67
90
|
INSTANCE_WRITES = %w[
|
|
68
91
|
save save! update update! update_attribute update_attributes update_attributes!
|
|
69
92
|
update_column update_columns destroy destroy! delete touch increment! decrement!
|
|
@@ -80,6 +103,26 @@ module Rigor
|
|
|
80
103
|
all? any? none? one? include? member? first count sum
|
|
81
104
|
].freeze
|
|
82
105
|
|
|
106
|
+
# The writers an association's `CollectionProxy` defines, plus its `delete` / `destroy`, which
|
|
107
|
+
# override a plain Relation's own `delete(id_or_array)` / `destroy(id)`. Those are
|
|
108
|
+
# `where(id:).delete_all` and `find(id).destroy`, both inside this row's labels. Each adds records
|
|
109
|
+
# to the association's in-memory target or removes them from it. Depending on whether the owner is
|
|
110
|
+
# saved and on the association's `dependent:` option, it may also read, write, and open a
|
|
111
|
+
# transaction, so the row names all three rather than the parent `io.db`, which
|
|
112
|
+
# `--label io.db.write` would not match.
|
|
113
|
+
#
|
|
114
|
+
# The mutation is spelt from the call site, as every row here is, and bare `mutate` is the label
|
|
115
|
+
# for a receiver that is not the caller's `self`: the proxy is an object the caller holds, like the
|
|
116
|
+
# session in rigor-actionpack's `SESSION_WRITE`. The RBS envelopes on `build` / `reset` are written
|
|
117
|
+
# from the callee's side instead, and say `mutate.self`.
|
|
118
|
+
#
|
|
119
|
+
# A saved owner's `<<` saves the record, which runs the model's `before_save` / `after_commit`
|
|
120
|
+
# callbacks. No edge carries those to the caller, which is the same gap `Relation#create` has: the
|
|
121
|
+
# call site's receiver is `Relation[Post]`, and the edge that would reach `Post#save` needs the type
|
|
122
|
+
# argument, which the collector does not record (#1313).
|
|
123
|
+
PROXY_WRITERS = %w[<< push append concat replace delete destroy clear].freeze
|
|
124
|
+
PROXY_WRITE = ["io.db.read", "io.db.write", "io.db.transaction", "mutate"].freeze
|
|
125
|
+
|
|
83
126
|
# Raw SQL, narrowed by the statement's own leading verb ({Rigor::Effects::Narrowing} `sql_verb`).
|
|
84
127
|
ADAPTER_SQL = %w[execute exec_query exec_insert exec_update exec_delete select_all select_one
|
|
85
128
|
select_value select_values select_rows query query_value query_values].freeze
|
|
@@ -103,6 +146,12 @@ module Rigor
|
|
|
103
146
|
# Ambient AR calls that are neither a read nor a write of rows.
|
|
104
147
|
TRANSACTIONAL = %w[transaction with_lock lock!].freeze
|
|
105
148
|
|
|
149
|
+
# The instance-side locks re-read the row: `lock!` is `reload(lock:)`, a `SELECT … FOR UPDATE`, and
|
|
150
|
+
# `with_lock` runs it inside a transaction. `lock!` opens no transaction of its own, so on it the
|
|
151
|
+
# transaction label names the one whose end releases the lock, and over-states a `lock!` outside one.
|
|
152
|
+
INSTANCE_LOCKS = %w[with_lock lock!].freeze
|
|
153
|
+
LOCK = ["io.db.read", "io.db.transaction"].freeze
|
|
154
|
+
|
|
106
155
|
module_function
|
|
107
156
|
|
|
108
157
|
def attributions
|
|
@@ -114,7 +163,11 @@ module Rigor
|
|
|
114
163
|
why: "issues the SELECT at the call — this is the materializing " \
|
|
115
164
|
"half of the builder/materializer split") +
|
|
116
165
|
rows(BASE, SINGLETON_WRITES, WRITE, singleton: true,
|
|
117
|
-
why: "issues an INSERT
|
|
166
|
+
why: "issues an INSERT at the call and queries nothing first") +
|
|
167
|
+
rows(BASE, SINGLETON_READ_WRITES, READ_WRITE,
|
|
168
|
+
singleton: true,
|
|
169
|
+
why: "issues an UPDATE / DELETE / INSERT at the call, and a SELECT before it or after " \
|
|
170
|
+
"a failed insert") +
|
|
118
171
|
rows(BASE, TRANSACTIONAL, TRANSACTION, singleton: true,
|
|
119
172
|
why: "opens a transaction; the block's own origins join " \
|
|
120
173
|
"by containment, so the row states only the BEGIN")
|
|
@@ -125,14 +178,21 @@ module Rigor
|
|
|
125
178
|
why: "re-reads the row (`reload`) or runs the validators, whose uniqueness checks query") +
|
|
126
179
|
rows(BASE, INSTANCE_WRITES, WRITE,
|
|
127
180
|
why: "persists the record — the write a `db: none` envelope is written to catch") +
|
|
128
|
-
rows(BASE, TRANSACTIONAL, TRANSACTION,
|
|
129
|
-
|
|
181
|
+
rows(BASE, TRANSACTIONAL - INSTANCE_LOCKS, TRANSACTION, why: "opens a transaction around the block") +
|
|
182
|
+
rows(BASE, INSTANCE_LOCKS, LOCK,
|
|
183
|
+
why: "re-reads the row with `SELECT … FOR UPDATE`; `with_lock` does it inside a transaction")
|
|
130
184
|
end
|
|
131
185
|
|
|
132
186
|
def relation_rows
|
|
133
187
|
rows(RELATION, RELATION_MATERIALIZERS, READ,
|
|
134
188
|
why: "an Enumerable delegation on a Relation: it calls `each`, which runs the query. Not " \
|
|
135
|
-
"declared in the bundled RBS because declaring it would change how it types")
|
|
189
|
+
"declared in the bundled RBS because declaring it would change how it types") +
|
|
190
|
+
rows(RELATION, PROXY_WRITERS, PROXY_WRITE,
|
|
191
|
+
why: "a CollectionProxy writer: it changes the association's target and, for a saved " \
|
|
192
|
+
"owner, the rows behind it. Keyed on Relation because that is the type the plugin " \
|
|
193
|
+
"gives an association reader. A plain Relation either has no such method or, through " \
|
|
194
|
+
"its own `delete` / `destroy`, deletes by id or finds and destroys, which stays inside " \
|
|
195
|
+
"the bound. The model's save callbacks are not edged")
|
|
136
196
|
end
|
|
137
197
|
|
|
138
198
|
# Two rows per selector, because raw SQL is written two ways and only one of them names a type.
|