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
|
@@ -108,6 +108,18 @@ module Rigor
|
|
|
108
108
|
# name — {#resolve_table_name_decorator} keeps walking outward past it, exactly like Rails' own
|
|
109
109
|
# `respond_to?(:table_name_prefix)` ancestor search.
|
|
110
110
|
@namespace_table_name_decorators = {}
|
|
111
|
+
# `concern module name => { scopes:, associations:, macro_methods: }` — what a module contributes
|
|
112
|
+
# to every class that includes it (#534 item 5, #1049). Unlike {@type_override_columns} these are
|
|
113
|
+
# NOT global: a declaration reaches a model only along a real `include` edge ({@include_edges}),
|
|
114
|
+
# so a same-named `scope` / association / `delegate` in a concern the model does not include
|
|
115
|
+
# contributes nothing.
|
|
116
|
+
@concern_declarations = {}
|
|
117
|
+
# `constant name => [raw included module name]` — every `include Foo` / `include Foo::Bar` a class
|
|
118
|
+
# or module body spells, as written. Resolution against the walked module set happens after the
|
|
119
|
+
# whole tree is read ({#resolve_concern_name}), because the including file may be walked first.
|
|
120
|
+
@include_edges = {}
|
|
121
|
+
# Every module declaration walked, for resolving an `include`'s relative constant spelling.
|
|
122
|
+
@module_names = Set.new
|
|
111
123
|
end
|
|
112
124
|
|
|
113
125
|
attr_reader :type_override_columns
|
|
@@ -127,7 +139,8 @@ module Rigor
|
|
|
127
139
|
# Every file is walked (and every module's `table_name_prefix` / `table_name_suffix` recorded)
|
|
128
140
|
# before any row is resolved — a model's file may sort, and so be visited, before the file that
|
|
129
141
|
# declares its enclosing module's decorator.
|
|
130
|
-
|
|
142
|
+
rows = fold_concern_declarations(resolve_models(candidates), superclass_map: superclass_map)
|
|
143
|
+
attach_table_name_decorators(rows, superclass_map: superclass_map)
|
|
131
144
|
end
|
|
132
145
|
|
|
133
146
|
private
|
|
@@ -217,7 +230,8 @@ module Rigor
|
|
|
217
230
|
scopes: (Array(base[:scopes]) + Array(addition[:scopes])).uniq,
|
|
218
231
|
validations: (Array(base[:validations]) + Array(addition[:validations])).uniq,
|
|
219
232
|
callbacks: (Array(base[:callbacks]) + Array(addition[:callbacks])).uniq,
|
|
220
|
-
aliases: (base[:aliases] || {}).merge(addition[:aliases] || {})
|
|
233
|
+
aliases: (base[:aliases] || {}).merge(addition[:aliases] || {}),
|
|
234
|
+
macro_methods: (Array(base[:macro_methods]) + Array(addition[:macro_methods])).uniq
|
|
221
235
|
)
|
|
222
236
|
end
|
|
223
237
|
|
|
@@ -423,6 +437,7 @@ module Rigor
|
|
|
423
437
|
superclass = strip_root(constant_path_name(node.superclass)) if node.superclass
|
|
424
438
|
|
|
425
439
|
collect_type_overrides(node.body)
|
|
440
|
+
record_include_edges(full_name, node.body)
|
|
426
441
|
# Rails' `full_table_name_prefix` walks `module_parents.detect { respond_to?(:table_name_prefix) }`
|
|
427
442
|
# — lexical nesting, which does not care whether the namespace container was written `module Blog`
|
|
428
443
|
# or `class Blog`. A `class` used purely as a namespace holder (or a model class that happens to
|
|
@@ -440,7 +455,8 @@ module Rigor
|
|
|
440
455
|
scopes: lookup_scopes(node.body),
|
|
441
456
|
validations: lookup_validations(node.body),
|
|
442
457
|
callbacks: lookup_callbacks(node.body),
|
|
443
|
-
aliases: lookup_aliases(node.body)
|
|
458
|
+
aliases: lookup_aliases(node.body),
|
|
459
|
+
macro_methods: lookup_macro_methods(node.body)
|
|
444
460
|
})
|
|
445
461
|
|
|
446
462
|
# Recurse into the body in case nested classes exist.
|
|
@@ -457,6 +473,9 @@ module Rigor
|
|
|
457
473
|
|
|
458
474
|
full_name = declared_constant_name(module_local_name, lexical_path)
|
|
459
475
|
record_table_name_decorators(full_name, node.body, is_class: false)
|
|
476
|
+
@module_names << full_name
|
|
477
|
+
record_concern_declarations(full_name, node.body)
|
|
478
|
+
record_include_edges(full_name, node.body)
|
|
460
479
|
|
|
461
480
|
inner_path = [full_name]
|
|
462
481
|
walk_for_classes(node.body, inner_path, &) if node.body
|
|
@@ -519,6 +538,177 @@ module Rigor
|
|
|
519
538
|
type_arg.is_a?(Prism::ConstantReadNode) || type_arg.is_a?(Prism::ConstantPathNode)
|
|
520
539
|
end
|
|
521
540
|
|
|
541
|
+
# Records what a concern module contributes to every class that includes it (#534 item 5, #1049;
|
|
542
|
+
# mastodon's `scope :without_suspended` in `Account::Suspensions`, `has_one :account_stat` and
|
|
543
|
+
# `delegate :statuses_count, …, to: :account_stat` in `Account::Counters`).
|
|
544
|
+
# ActiveSupport::Concern evaluates an `included do … end` block against the INCLUDING class, so
|
|
545
|
+
# whatever it declares is declared on every model that includes the module — and on no other model,
|
|
546
|
+
# which is why these are kept per-module here rather than globally the way {#collect_type_overrides}
|
|
547
|
+
# keeps column names.
|
|
548
|
+
#
|
|
549
|
+
# Concern-hood is recognised the way {#type_override_declaration_calls} already recognises it: by the
|
|
550
|
+
# `included do … end` block itself, not by an `extend ActiveSupport::Concern` line.
|
|
551
|
+
#
|
|
552
|
+
# The module's OWN top-level body contributes too, but only `delegate`: `delegate` is
|
|
553
|
+
# `Module#delegate`, so at a module's top level it defines ordinary INSTANCE methods on the module,
|
|
554
|
+
# which the including class then inherits through its ancestry — exactly the shape
|
|
555
|
+
# `Account::Counters` uses. A top-level `scope` / `has_one` / `has_attached_file` is by contrast a
|
|
556
|
+
# plain send to the module object (a runtime `NoMethodError`), never a class-level declaration on any
|
|
557
|
+
# model, so those are read from `included do … end` only.
|
|
558
|
+
def record_concern_declarations(module_name, body)
|
|
559
|
+
found = concern_declarations(body)
|
|
560
|
+
return if found.each_value.all?(&:empty?)
|
|
561
|
+
|
|
562
|
+
record = (@concern_declarations[module_name] ||= { scopes: [], associations: [], macro_methods: [] })
|
|
563
|
+
found.each { |key, values| record[key].concat(values) }
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def concern_declarations(body)
|
|
567
|
+
record = { scopes: [], associations: [], macro_methods: [] }
|
|
568
|
+
return record if body.nil?
|
|
569
|
+
|
|
570
|
+
body.compact_child_nodes.each do |node|
|
|
571
|
+
next unless node.is_a?(Prism::CallNode) && node.receiver.nil?
|
|
572
|
+
|
|
573
|
+
if node.name == :included && node.block.is_a?(Prism::BlockNode)
|
|
574
|
+
block_body = node.block.body
|
|
575
|
+
record[:scopes].concat(lookup_scopes(block_body))
|
|
576
|
+
record[:associations].concat(lookup_associations(block_body))
|
|
577
|
+
record[:macro_methods].concat(lookup_macro_methods(block_body))
|
|
578
|
+
elsif node.name == :delegate
|
|
579
|
+
record[:macro_methods].concat(delegate_method_names(node))
|
|
580
|
+
end
|
|
581
|
+
end
|
|
582
|
+
record
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
# Records every `include Foo` / `include Foo::Bar` a class or module body spells, AS WRITTEN — the
|
|
586
|
+
# constant is resolved only once the whole tree has been walked ({#resolve_concern_name}), because the
|
|
587
|
+
# concern's file may well sort after the file that includes it. An `include` inside a concern's own
|
|
588
|
+
# `included do … end` block counts too: that block runs against the including class, so the module
|
|
589
|
+
# lands on the model exactly as a body-level `include` does. A non-constant argument
|
|
590
|
+
# (`include build_module`) renders as nil and declines.
|
|
591
|
+
def record_include_edges(constant_name, body)
|
|
592
|
+
names = include_call_targets(body)
|
|
593
|
+
return if names.empty?
|
|
594
|
+
|
|
595
|
+
(@include_edges[constant_name] ||= []).concat(names)
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
def include_call_targets(body)
|
|
599
|
+
return [] if body.nil?
|
|
600
|
+
|
|
601
|
+
body.compact_child_nodes.flat_map do |node|
|
|
602
|
+
next [] unless node.is_a?(Prism::CallNode) && node.receiver.nil?
|
|
603
|
+
|
|
604
|
+
if node.name == :included && node.block.is_a?(Prism::BlockNode)
|
|
605
|
+
include_call_targets(node.block.body)
|
|
606
|
+
elsif node.name == :include
|
|
607
|
+
Array(node.arguments&.arguments).filter_map { |arg| constant_path_name(arg) }
|
|
608
|
+
else
|
|
609
|
+
[]
|
|
610
|
+
end
|
|
611
|
+
end
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
# Adds each model's concern-declared scopes, associations and macro methods to the ones its own body
|
|
615
|
+
# declares. Attribution is by real `include` edge: a concern nobody includes contributes nothing, and
|
|
616
|
+
# a same-named `scope` / `has_one` / `delegate` in an unrelated concern does not leak onto this model.
|
|
617
|
+
# A concern that includes another concern is followed transitively (Ruby's own semantics — the inner
|
|
618
|
+
# module ends up in the model's ancestry either way).
|
|
619
|
+
#
|
|
620
|
+
# The model's SUPERCLASS chain is walked too, because a concern is routinely included once in
|
|
621
|
+
# `ApplicationRecord` for every model to get. The chain is walked here rather than left to
|
|
622
|
+
# {ModelIndex.sti_chain}, which only links a model to another MODEL: a configured base class is not a
|
|
623
|
+
# discovered model, so `class Account < ApplicationRecord` has no `sti_parent` and the index's chain
|
|
624
|
+
# stops at `Account`. (A `scope` written directly in the base class's own body is still not
|
|
625
|
+
# propagated — that pre-existing gap is a different mechanism and is documented, not widened here.)
|
|
626
|
+
#
|
|
627
|
+
# The model's OWN association rows are kept LAST so {#dedup_named_rows}'s last-wins rule lets a class
|
|
628
|
+
# body redeclaration override the concern's, which is what Ruby does: the class's own
|
|
629
|
+
# `has_one :user, class_name: "…"` beats the one the included module installed.
|
|
630
|
+
def fold_concern_declarations(rows, superclass_map: {})
|
|
631
|
+
return rows if @concern_declarations.empty?
|
|
632
|
+
|
|
633
|
+
rows.map do |row|
|
|
634
|
+
inherited = ancestry_concern_declarations(row.fetch(:class_name), superclass_map)
|
|
635
|
+
next row if inherited.each_value.all?(&:empty?)
|
|
636
|
+
|
|
637
|
+
row.merge(
|
|
638
|
+
scopes: (Array(row[:scopes]) + inherited[:scopes]).uniq.freeze,
|
|
639
|
+
associations: dedup_named_rows(inherited[:associations] + Array(row[:associations])),
|
|
640
|
+
macro_methods: (Array(row[:macro_methods]) + inherited[:macro_methods]).uniq.freeze
|
|
641
|
+
)
|
|
642
|
+
end
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
# The concern declarations reaching `class_name` through its own includes and those of every
|
|
646
|
+
# superclass up the chain (`visited` stops a cycle a malformed source could spell).
|
|
647
|
+
def ancestry_concern_declarations(class_name, superclass_map)
|
|
648
|
+
found = { scopes: [], associations: [], macro_methods: [] }
|
|
649
|
+
curr = class_name
|
|
650
|
+
visited = Set.new
|
|
651
|
+
|
|
652
|
+
while curr && visited.add?(curr)
|
|
653
|
+
concern_declarations_for(curr).each { |key, values| found[key].concat(values) }
|
|
654
|
+
curr = superclass_map[curr]
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
found[:scopes].uniq!
|
|
658
|
+
found[:macro_methods].uniq!
|
|
659
|
+
found
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
# Breadth-first over the `include` edges reachable from `class_name`, unioning each visited concern's
|
|
663
|
+
# declarations. `visited` makes a mutually-including pair of concerns terminate rather than loop.
|
|
664
|
+
def concern_declarations_for(class_name)
|
|
665
|
+
found = { scopes: [], associations: [], macro_methods: [] }
|
|
666
|
+
queue = Array(@include_edges[class_name]).map { |raw| [raw, class_name] }
|
|
667
|
+
visited = Set.new([class_name])
|
|
668
|
+
|
|
669
|
+
until queue.empty?
|
|
670
|
+
raw, from_name = queue.shift
|
|
671
|
+
resolved = resolve_concern_name(raw, from_name)
|
|
672
|
+
next if visited.include?(resolved)
|
|
673
|
+
|
|
674
|
+
visited << resolved
|
|
675
|
+
(@concern_declarations[resolved] || {}).each { |key, values| found[key].concat(values) }
|
|
676
|
+
queue.concat(Array(@include_edges[resolved]).map { |inner| [inner, resolved] })
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
found
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
# Resolves an `include`'s constant spelling against the modules actually walked, mirroring Ruby's
|
|
683
|
+
# lexical lookup closely enough for the shapes concerns use: a rooted name (`include ::Suspendable`)
|
|
684
|
+
# names the top-level constant, a relative one is tried under the including constant and each of its
|
|
685
|
+
# enclosing namespaces first (`include Suspensions` inside `class Account` → `Account::Suspensions`,
|
|
686
|
+
# which is what Ruby resolves), and otherwise it is taken as written. A name no walked module answers
|
|
687
|
+
# to simply contributes nothing.
|
|
688
|
+
#
|
|
689
|
+
# Two known imprecisions, both cheap only to state:
|
|
690
|
+
#
|
|
691
|
+
# - The enclosing-namespace attempts are made for a COMPACT declaration too (`module A::B`'s own
|
|
692
|
+
# `include C` tries `A::B::C` then `A::C`), where Ruby's cref would only see `A::B` and the top
|
|
693
|
+
# level. Harmless: the extra candidates must match a module that was actually walked, and a
|
|
694
|
+
# project with both `A::B::C` and a top-level `C` concern would have to want the latter.
|
|
695
|
+
# - The final bare-name fall-back attributes BY SPELLING. A nearer constant declared outside
|
|
696
|
+
# `model_search_paths` — `Account::Suspensions` in `lib/`, say, while the concern roots hold a
|
|
697
|
+
# top-level `Suspensions` — is invisible to this walker, so the wrong one can win. Reading
|
|
698
|
+
# further than the model roots needs its own cache descriptor (see {#discover}'s `watch:` note),
|
|
699
|
+
# so the spelling is trusted instead.
|
|
700
|
+
def resolve_concern_name(raw, from_name)
|
|
701
|
+
name = strip_root(raw)
|
|
702
|
+
return name if raw.start_with?("::")
|
|
703
|
+
|
|
704
|
+
([from_name] + namespace_ancestors(from_name)).each do |prefix|
|
|
705
|
+
candidate = "#{prefix}::#{name}"
|
|
706
|
+
return candidate if @module_names.include?(candidate)
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
name
|
|
710
|
+
end
|
|
711
|
+
|
|
522
712
|
# Renders a constant-path node (`Admin::User`, `::ApplicationRecord`) as a String, keeping the
|
|
523
713
|
# leading `::` of a rooted path — {#declared_constant_name} reads it as "reset the lexical path"
|
|
524
714
|
# before the marker is dropped. Returns nil for shapes the discoverer chooses not to handle.
|
|
@@ -1333,6 +1523,193 @@ module Rigor
|
|
|
1333
1523
|
aliases.freeze
|
|
1334
1524
|
end
|
|
1335
1525
|
|
|
1526
|
+
# The attachment macros whose reader names are folded, mapped to the reader SUFFIXES each one
|
|
1527
|
+
# installs beside the bare accessor (#1049).
|
|
1528
|
+
#
|
|
1529
|
+
# Each key is the macro name of exactly one gem, and none of the three is a name another common
|
|
1530
|
+
# library claims — `has_attached_file` is Paperclip's, `has_one_attached` / `has_many_attached` are
|
|
1531
|
+
# Active Storage's — so the macro's presence in a model body IS the evidence that the project uses
|
|
1532
|
+
# that gem. No lockfile probe is needed (and none would be sound for a vendored or path-sourced
|
|
1533
|
+
# gem); a project that spells none of the three gets nothing folded.
|
|
1534
|
+
#
|
|
1535
|
+
# PAPERCLIP's set is deliberately minimal: `has_attached_file :file` installs `file`, `file=` and
|
|
1536
|
+
# `file?`, and those are the names #1049's serializer table needs. The `file_file_name` /
|
|
1537
|
+
# `file_content_type` / `file_file_size` / `file_updated_at` quartet is NOT listed because Paperclip
|
|
1538
|
+
# backs it with four real schema columns, which the index already carries from `db/schema.rb`;
|
|
1539
|
+
# synthesising them here would claim them on a model whose table lacks them.
|
|
1540
|
+
#
|
|
1541
|
+
# ACTIVE STORAGE's set is `avatar`, `avatar=`, `avatar_attachment`, `avatar_blob` for the singular
|
|
1542
|
+
# macro and `images`, `images=`, `images_attachments`, `images_blobs` for the collection one — the
|
|
1543
|
+
# `has_one_attached` / `has_many_attached` generated-methods surface, none of it column-backed.
|
|
1544
|
+
# This is name MEMBERSHIP only, and does not overlap `rigor-activestorage`, which contributes the
|
|
1545
|
+
# accessor's TYPE (`ActiveStorage::Attached::One`) and publishes no fact: a consumer that reads
|
|
1546
|
+
# `:model_index` to ask "does this model answer `avatar`?" gets nothing from that plugin, and gets
|
|
1547
|
+
# nothing at all when the project does not load it.
|
|
1548
|
+
ATTACHMENT_MACROS = {
|
|
1549
|
+
has_attached_file: ["", "=", "?"],
|
|
1550
|
+
has_one_attached: ["", "=", "_attachment", "_blob"],
|
|
1551
|
+
has_many_attached: ["", "=", "_attachments", "_blobs"]
|
|
1552
|
+
}.freeze
|
|
1553
|
+
Ractor.make_shareable(ATTACHMENT_MACROS)
|
|
1554
|
+
private_constant :ATTACHMENT_MACROS
|
|
1555
|
+
|
|
1556
|
+
# Instance-method names a declaration macro installs that neither the schema nor the association /
|
|
1557
|
+
# alias / scope lists already carry (#1049): `delegate`'s method names, the attachment macros'
|
|
1558
|
+
# readers, and an `enum`'s per-value predicates. Names ONLY — what a delegate target returns is a
|
|
1559
|
+
# separate question no consumer asks today.
|
|
1560
|
+
#
|
|
1561
|
+
# Every name here is spelled out by the declaration itself. A shape this walker cannot read off the
|
|
1562
|
+
# source (a non-literal `prefix:`, a `to:` that is not a Symbol when `prefix: true` needs its
|
|
1563
|
+
# spelling, a computed enum value list) contributes NOTHING rather than a guess: a consumer that
|
|
1564
|
+
# fails closed on an unanswered name loses precision when a name is missing, but is answered WRONG
|
|
1565
|
+
# when a name is invented.
|
|
1566
|
+
def lookup_macro_methods(body)
|
|
1567
|
+
return [] if body.nil?
|
|
1568
|
+
|
|
1569
|
+
names = []
|
|
1570
|
+
declaration_calls(body).each do |node|
|
|
1571
|
+
next if node.receiver
|
|
1572
|
+
|
|
1573
|
+
if node.name == :delegate
|
|
1574
|
+
names.concat(delegate_method_names(node))
|
|
1575
|
+
elsif ATTACHMENT_MACROS.key?(node.name)
|
|
1576
|
+
names.concat(attachment_method_names(node))
|
|
1577
|
+
elsif node.name == :enum
|
|
1578
|
+
names.concat(enum_predicate_names(node))
|
|
1579
|
+
end
|
|
1580
|
+
end
|
|
1581
|
+
names.uniq.freeze
|
|
1582
|
+
end
|
|
1583
|
+
|
|
1584
|
+
# `delegate :a, :b, to: :x` → `["a", "b"]`; `prefix: true` prepends the `to:` target's own spelling
|
|
1585
|
+
# (`delegate :can?, to: :user, prefix: true` → `"user_can?"`), and `prefix: :admin` prepends the
|
|
1586
|
+
# given one. ActiveSupport builds the name by plain concatenation, so a `?` / `!` / `=` suffix rides
|
|
1587
|
+
# along untouched.
|
|
1588
|
+
#
|
|
1589
|
+
# `allow_nil:` does not enter into it — it changes what the method RETURNS, never what it is called.
|
|
1590
|
+
# `prefix: true` with a `to:` this walker cannot render as a name declines the whole call.
|
|
1591
|
+
def delegate_method_names(node)
|
|
1592
|
+
args = node.arguments&.arguments
|
|
1593
|
+
return [] if args.nil? || args.empty?
|
|
1594
|
+
|
|
1595
|
+
names = args.filter_map { |arg| Rigor::Source::Literals.symbol_name(arg) }
|
|
1596
|
+
return [] if names.empty?
|
|
1597
|
+
|
|
1598
|
+
prefix = delegate_prefix(args)
|
|
1599
|
+
return [] if prefix == :unfoldable
|
|
1600
|
+
|
|
1601
|
+
names.map { |name| "#{prefix}#{name}" }
|
|
1602
|
+
end
|
|
1603
|
+
|
|
1604
|
+
# The `"user_"`-style prefix a `delegate` call's `prefix:` option establishes: `""` when the option
|
|
1605
|
+
# is absent or false, `:unfoldable` when it is present in a shape this walker cannot read.
|
|
1606
|
+
def delegate_prefix(args)
|
|
1607
|
+
value = keyword_option(args, "prefix")
|
|
1608
|
+
# `prefix: nil` is what ActiveSupport's own default is, and it reads as "no prefix" there, not as
|
|
1609
|
+
# an option the call spelled unreadably.
|
|
1610
|
+
return "" if value.nil? || value.is_a?(Prism::FalseNode) || value.is_a?(Prism::NilNode)
|
|
1611
|
+
|
|
1612
|
+
return delegate_target_prefix(args) if value.is_a?(Prism::TrueNode)
|
|
1613
|
+
|
|
1614
|
+
literal = name_literal(value)
|
|
1615
|
+
return :unfoldable if literal.nil? || literal.empty?
|
|
1616
|
+
|
|
1617
|
+
"#{literal}_"
|
|
1618
|
+
end
|
|
1619
|
+
|
|
1620
|
+
# `prefix: true` names the prefix after the `to:` target, so the target has to render as a name.
|
|
1621
|
+
# An instance-variable target (`to: :@user`, the spelling ActiveSupport accepts for delegating to an
|
|
1622
|
+
# ivar) does NOT: Rails strips the `@` when it builds the method name, and reading it as written
|
|
1623
|
+
# would invent `@user_email`.
|
|
1624
|
+
def delegate_target_prefix(args)
|
|
1625
|
+
name = name_literal(keyword_option(args, "to"))
|
|
1626
|
+
return :unfoldable if name.nil? || name.empty? || name.start_with?("@")
|
|
1627
|
+
|
|
1628
|
+
"#{name}_"
|
|
1629
|
+
end
|
|
1630
|
+
|
|
1631
|
+
# A Symbol or String literal rendered as a plain name, or nil for anything else. Rails accepts both
|
|
1632
|
+
# spellings wherever it takes a method or prefix name.
|
|
1633
|
+
def name_literal(node)
|
|
1634
|
+
Rigor::Source::Literals.symbol_name(node) || (node.is_a?(Prism::StringNode) ? node.unescaped : nil)
|
|
1635
|
+
end
|
|
1636
|
+
|
|
1637
|
+
# `has_attached_file :file` / `has_one_attached :avatar` / `has_many_attached :images` → the reader
|
|
1638
|
+
# names {ATTACHMENT_MACROS} lists for that macro. A non-Symbol-literal name declines.
|
|
1639
|
+
def attachment_method_names(node)
|
|
1640
|
+
name = Rigor::Source::Literals.symbol_name(node.arguments&.arguments&.first)
|
|
1641
|
+
return [] if name.nil? || name.empty?
|
|
1642
|
+
|
|
1643
|
+
ATTACHMENT_MACROS.fetch(node.name).map { |suffix| "#{name}#{suffix}" }
|
|
1644
|
+
end
|
|
1645
|
+
|
|
1646
|
+
# An `enum`'s per-value predicates — `enum :visibility, { … limited: 4 }, suffix: :visibility` →
|
|
1647
|
+
# `"limited_visibility?"` (mastodon's `Status::Visibility`). Rails composes the name as
|
|
1648
|
+
# `"#{prefix}#{value}#{suffix}"`, where `prefix: true` / `suffix: true` use the enum's own column
|
|
1649
|
+
# name and a Symbol / String uses that spelling; Rails 6's `_prefix:` / `_suffix:` keywords are read
|
|
1650
|
+
# as the same option.
|
|
1651
|
+
#
|
|
1652
|
+
# Only the `?` predicate is folded. The bang (`limited_visibility!`) writes to the database and the
|
|
1653
|
+
# same-named class-side scope is not an instance method at all; neither is a name a consumer of this
|
|
1654
|
+
# fact asks about, and both would be invention beyond what the table needs.
|
|
1655
|
+
#
|
|
1656
|
+
# The enum's COLUMN and value list are deliberately not folded into the row's `enums:` from here —
|
|
1657
|
+
# see {#concern_declarations}. This method contributes names, which every consumer of the fact
|
|
1658
|
+
# WIDENS a known-name set with; `enums:` drives `Analyzer#validate_enum_value`, which FIRES.
|
|
1659
|
+
#
|
|
1660
|
+
# `instance_methods: false` (Rails 7.1+) declines the whole call: that option is how a model asks
|
|
1661
|
+
# Rails NOT to define the per-value predicates, so folding them would claim methods the application
|
|
1662
|
+
# does not have — the one way this family could answer a fail-closed consumer wrongly rather than
|
|
1663
|
+
# just incompletely. A non-literal value for the option declines for the same reason.
|
|
1664
|
+
def enum_predicate_names(node)
|
|
1665
|
+
row = parse_enum_call(node)
|
|
1666
|
+
return [] if row.nil? || row[:values].empty?
|
|
1667
|
+
|
|
1668
|
+
args = node.arguments&.arguments || []
|
|
1669
|
+
instance_methods = keyword_option(args, "instance_methods")
|
|
1670
|
+
return [] unless instance_methods.nil? || instance_methods.is_a?(Prism::TrueNode)
|
|
1671
|
+
|
|
1672
|
+
prefix = enum_affix(args, %w[prefix _prefix], row[:column]) { |literal| "#{literal}_" }
|
|
1673
|
+
suffix = enum_affix(args, %w[suffix _suffix], row[:column]) { |literal| "_#{literal}" }
|
|
1674
|
+
return [] if prefix == :unfoldable || suffix == :unfoldable
|
|
1675
|
+
|
|
1676
|
+
row[:values].map { |value| "#{prefix}#{value}#{suffix}?" }
|
|
1677
|
+
end
|
|
1678
|
+
|
|
1679
|
+
# The rendered `prefix:` / `suffix:` affix of an `enum` call, `""` when the option is absent or
|
|
1680
|
+
# false and `:unfoldable` when it is present in a shape this walker cannot read.
|
|
1681
|
+
def enum_affix(args, keys, column_name)
|
|
1682
|
+
value = keys.filter_map { |key| keyword_option(args, key) }.first
|
|
1683
|
+
return "" if value.nil? || value.is_a?(Prism::FalseNode)
|
|
1684
|
+
return yield(column_name) if value.is_a?(Prism::TrueNode)
|
|
1685
|
+
|
|
1686
|
+
literal = name_literal(value)
|
|
1687
|
+
return :unfoldable if literal.nil? || literal.empty?
|
|
1688
|
+
|
|
1689
|
+
yield(literal)
|
|
1690
|
+
end
|
|
1691
|
+
|
|
1692
|
+
# The VALUE node of a keyword argument, across every keyword hash in the call's argument list, or
|
|
1693
|
+
# nil when the call does not spell that key. Unlike {#association_option} this hands back the node
|
|
1694
|
+
# rather than a Boolean, because the callers need Symbol and String values too.
|
|
1695
|
+
#
|
|
1696
|
+
# A BRACED hash argument (`enum :visibility, { limited: 4 }, suffix: :visibility`) is a
|
|
1697
|
+
# `Prism::HashNode` and is deliberately not scanned: there the braces hold the enum's own value map,
|
|
1698
|
+
# and an enum value spelled `prefix:` would otherwise read as the call's `prefix:` option. Rails 6's
|
|
1699
|
+
# `enum status: { … }, _prefix: true` puts both in ONE `KeywordHashNode`, which is scanned.
|
|
1700
|
+
def keyword_option(args, key)
|
|
1701
|
+
args.each do |arg|
|
|
1702
|
+
next unless arg.is_a?(Prism::KeywordHashNode)
|
|
1703
|
+
|
|
1704
|
+
arg.elements.each do |pair|
|
|
1705
|
+
next unless pair.is_a?(Prism::AssocNode) && Source::Literals.symbol_named?(pair.key, key)
|
|
1706
|
+
|
|
1707
|
+
return pair.value
|
|
1708
|
+
end
|
|
1709
|
+
end
|
|
1710
|
+
nil
|
|
1711
|
+
end
|
|
1712
|
+
|
|
1336
1713
|
# Collects every Symbol-literal positional argument from a CallNode. Used by both
|
|
1337
1714
|
# `lookup_validations` and `lookup_callbacks` to extract the attribute / method name list.
|
|
1338
1715
|
def symbol_args(node)
|
|
@@ -15,7 +15,7 @@ module Rigor
|
|
|
15
15
|
# Phase 2's schema is OPTIONAL. A project that ships raw migrations only — the DB-agnostic Rails
|
|
16
16
|
# pattern, where `db/schema.rb` is gitignored (Redmine) — has no schema to combine, and `schema_table:
|
|
17
17
|
# nil` then yields the REDUCED index: every {Entry} carries its resolved table name, associations,
|
|
18
|
-
# enums, scopes, validations, callbacks and
|
|
18
|
+
# enums, scopes, validations, callbacks, aliases and macro methods, and an EMPTY column set. Only the column-keyed
|
|
19
19
|
# surface stands down; the schema was never an input to any of the rest. {#columns_known?} tells the
|
|
20
20
|
# two modes apart — an empty column set means "the schema is unknown", not "this model has no
|
|
21
21
|
# columns", and consumers that would fire on an unrecognised column must not read the two the same
|
|
@@ -27,7 +27,7 @@ module Rigor
|
|
|
27
27
|
#
|
|
28
28
|
# Single-table-inheritance subclasses (`class Admin < User`) carry an `sti_parent:` pointer; their
|
|
29
29
|
# {Entry} resolves its table from the root model and inherits the chain's declared associations /
|
|
30
|
-
# enums / aliases / scopes / validations / callbacks.
|
|
30
|
+
# enums / aliases / scopes / validations / callbacks / macro methods.
|
|
31
31
|
class ModelIndex
|
|
32
32
|
# `associations` is a frozen `Array<Hash>` where each row carries `{ name:, kind:, target:,
|
|
33
33
|
# nullable: }`:
|
|
@@ -47,7 +47,7 @@ module Rigor
|
|
|
47
47
|
# exact. Only a true reading licenses pinning the name to a value; see
|
|
48
48
|
# `Activerecord#table_name_return_type` for why the schema is not admitted as corroboration.
|
|
49
49
|
Entry = Struct.new(:class_name, :table_name, :table_name_exact, :columns, :associations,
|
|
50
|
-
:enums, :scopes, :validations, :callbacks, :aliases,
|
|
50
|
+
:enums, :scopes, :validations, :callbacks, :aliases, :macro_methods,
|
|
51
51
|
keyword_init: true) do
|
|
52
52
|
def table_name_exact? = table_name_exact == true
|
|
53
53
|
|
|
@@ -90,6 +90,13 @@ module Rigor
|
|
|
90
90
|
# declarations.
|
|
91
91
|
def alias?(name) = aliases.key?(name.to_s)
|
|
92
92
|
def resolve_alias(name) = aliases[name.to_s]
|
|
93
|
+
|
|
94
|
+
# `macro_methods` is `Array<method_name>` — the instance methods a declaration macro installs that
|
|
95
|
+
# none of the sets above already carries (#1049): `delegate`'s names, the Paperclip / Active
|
|
96
|
+
# Storage attachment readers, and an `enum`'s per-value predicates. Names only, with no type: a
|
|
97
|
+
# consumer asks "does this model answer `followers_count`?", which is what a fail-closed check
|
|
98
|
+
# needs and all the declaration states.
|
|
99
|
+
def macro_method?(name) = macro_methods.include?(name.to_s)
|
|
93
100
|
end
|
|
94
101
|
|
|
95
102
|
attr_reader :entries
|
|
@@ -149,8 +156,8 @@ module Rigor
|
|
|
149
156
|
end
|
|
150
157
|
|
|
151
158
|
# STI children inherit their ancestors' declared associations / enums / aliases / scopes /
|
|
152
|
-
# validations / callbacks. Without the merge a `where(<parent-association>: ...)`
|
|
153
|
-
# child would surface as a false `unknown-column`.
|
|
159
|
+
# validations / callbacks / macro methods. Without the merge a `where(<parent-association>: ...)`
|
|
160
|
+
# on the child would surface as a false `unknown-column`.
|
|
154
161
|
acc[class_name] = Entry.new(
|
|
155
162
|
class_name: class_name,
|
|
156
163
|
table_name: table_name,
|
|
@@ -161,7 +168,8 @@ module Rigor
|
|
|
161
168
|
scopes: chain.flat_map { |r| Array(r[:scopes]) }.uniq.freeze,
|
|
162
169
|
validations: chain.flat_map { |r| Array(r[:validations]) }.uniq.freeze,
|
|
163
170
|
callbacks: chain.flat_map { |r| Array(r[:callbacks]) }.map(&:freeze).freeze,
|
|
164
|
-
aliases: merge_aliases(chain)
|
|
171
|
+
aliases: merge_aliases(chain),
|
|
172
|
+
macro_methods: chain.flat_map { |r| Array(r[:macro_methods]) }.uniq.freeze
|
|
165
173
|
).freeze
|
|
166
174
|
end
|
|
167
175
|
new(entries.freeze, columns_known: !schema_table.nil?)
|