rigortype 0.3.8 → 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/capability_roles/capability_roles.rbs +36 -0
- 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 +236 -17
- data/docs/handbook/02-everyday-types.md +17 -14
- data/docs/handbook/03-narrowing.md +132 -14
- data/docs/handbook/04-tuples-and-shapes.md +8 -6
- data/docs/handbook/05-methods-and-blocks.md +1 -1
- data/docs/handbook/06-classes.md +2 -2
- data/docs/handbook/07-rbs-and-extended.md +19 -1
- data/docs/handbook/10-sorbet.md +9 -10
- data/docs/handbook/11-sig-gen.md +525 -13
- data/docs/handbook/README.md +1 -1
- data/docs/handbook/appendix-elixir.md +2 -2
- data/docs/handbook/appendix-go.md +2 -2
- data/docs/handbook/appendix-java-csharp.md +2 -2
- data/docs/handbook/appendix-mypy.md +2 -2
- data/docs/handbook/appendix-phpstan.md +1 -1
- data/docs/handbook/appendix-rust.md +1 -1
- data/docs/handbook/appendix-type-theory.md +4 -4
- data/docs/handbook/appendix-typescript.md +1 -1
- data/docs/llms.txt +2 -0
- data/docs/manual/02-cli-reference.md +95 -6
- data/docs/manual/03-configuration.md +18 -0
- data/docs/manual/04-diagnostics.md +15 -0
- data/docs/manual/07-plugins.md +15 -5
- data/docs/manual/08-skills.md +23 -2
- data/docs/manual/10-mcp-server.md +3 -2
- data/docs/manual/11-ci.md +9 -0
- data/docs/manual/15-type-protection-coverage.md +8 -0
- data/docs/manual/16-rbs-extended-annotations.md +46 -8
- data/docs/manual/18-removing-dead-code.md +10 -8
- 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 +261 -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 +138 -4
- data/docs/manual/plugins/rigor-activestorage.md +8 -1
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +43 -7
- 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 +229 -1
- 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 +3 -3
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +1 -5
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +10 -2
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +6 -4
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +15 -6
- data/lib/rigor/analysis/check_rules/rule_ids.rb +21 -6
- data/lib/rigor/analysis/check_rules/rule_walk.rb +41 -5
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +1 -1
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +1 -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 +38 -4
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +0 -1
- data/lib/rigor/analysis/check_rules.rb +718 -79
- data/lib/rigor/analysis/crash_signature.rb +8 -24
- data/lib/rigor/analysis/dependency_recorder.rb +23 -0
- data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +1 -1
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +0 -2
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +0 -2
- data/lib/rigor/analysis/dependency_source_inference/index.rb +5 -5
- data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +1 -2
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
- data/lib/rigor/analysis/effects_cache_probe.rb +3 -4
- data/lib/rigor/analysis/erb_template_detector.rb +1 -2
- data/lib/rigor/analysis/fact_store.rb +15 -1
- data/lib/rigor/analysis/incremental.rb +42 -0
- data/lib/rigor/analysis/incremental_session.rb +117 -26
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +0 -1
- data/lib/rigor/analysis/project_scan.rb +11 -1
- data/lib/rigor/analysis/reachability/graph.rb +3 -5
- data/lib/rigor/analysis/reachability/plugin_roots.rb +5 -5
- data/lib/rigor/analysis/reachability/project_files.rb +2 -2
- data/lib/rigor/analysis/reachability/scan.rb +5 -5
- data/lib/rigor/analysis/reachability/scan_cache.rb +3 -2
- data/lib/rigor/analysis/reachability/signature_scan.rb +2 -2
- data/lib/rigor/analysis/result.rb +1 -3
- data/lib/rigor/analysis/rule_catalog.rb +131 -1
- data/lib/rigor/analysis/run_cache_key.rb +21 -12
- data/lib/rigor/analysis/run_cache_probe.rb +3 -5
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -8
- data/lib/rigor/analysis/runner/declaration_position.rb +1 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +340 -33
- data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +2 -3
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +18 -14
- data/lib/rigor/analysis/runner/pool_coordinator.rb +357 -97
- data/lib/rigor/analysis/runner/project_pre_passes.rb +38 -13
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +407 -51
- 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 +62 -20
- data/lib/rigor/bleeding_edge.rb +10 -24
- data/lib/rigor/builtins/hkt_builtins.rb +3 -2
- data/lib/rigor/builtins/imported_refinements.rb +159 -21
- data/lib/rigor/builtins/predefined_constant_refinements.rb +5 -7
- data/lib/rigor/builtins/regex_refinement.rb +21 -13
- data/lib/rigor/builtins/static_return_refinements.rb +7 -6
- data/lib/rigor/cache/annotation_location.rb +2 -4
- data/lib/rigor/cache/descriptor.rb +63 -20
- data/lib/rigor/cache/engine_source.rb +33 -5
- data/lib/rigor/cache/file_digest.rb +9 -1
- data/lib/rigor/cache/incremental_snapshot.rb +72 -6
- data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +0 -3
- data/lib/rigor/cache/rbs_class_type_param_names.rb +0 -3
- data/lib/rigor/cache/rbs_constant_table.rb +0 -3
- data/lib/rigor/cache/rbs_descriptor.rb +112 -14
- data/lib/rigor/cache/rbs_environment.rb +8 -4
- data/lib/rigor/cache/rbs_known_class_names.rb +0 -3
- data/lib/rigor/cache/store.rb +76 -32
- data/lib/rigor/ci_detector.rb +1 -0
- data/lib/rigor/cli/annotate_command.rb +5 -6
- data/lib/rigor/cli/check_command.rb +57 -10
- data/lib/rigor/cli/check_invocation.rb +6 -11
- data/lib/rigor/cli/check_runner_factory.rb +1 -5
- data/lib/rigor/cli/coverage_command.rb +1 -1
- data/lib/rigor/cli/coverage_mutation.rb +12 -3
- data/lib/rigor/cli/coverage_scan.rb +1 -5
- data/lib/rigor/cli/diff_command.rb +1 -1
- data/lib/rigor/cli/doc_links.rb +4 -4
- data/lib/rigor/cli/docs_command.rb +6 -6
- data/lib/rigor/cli/doctor_command.rb +85 -38
- data/lib/rigor/cli/effects_command.rb +2 -2
- data/lib/rigor/cli/effects_diff_renderer.rb +3 -3
- data/lib/rigor/cli/effects_snapshot_command.rb +1 -1
- data/lib/rigor/cli/explain_command.rb +34 -1
- data/lib/rigor/cli/fused_protection_report.rb +7 -1
- data/lib/rigor/cli/lsp_command.rb +1 -1
- data/lib/rigor/cli/mcp_command.rb +1 -1
- data/lib/rigor/cli/measurement_integrity_warning.rb +4 -5
- data/lib/rigor/cli/mutation_fork_scan.rb +6 -6
- data/lib/rigor/cli/mutation_protection_report.rb +7 -1
- data/lib/rigor/cli/plugin_command.rb +4 -4
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/prism_colorizer.rb +1 -2
- data/lib/rigor/cli/protection_fork_scan.rb +6 -6
- data/lib/rigor/cli/show_bleedingedge_command.rb +1 -1
- data/lib/rigor/cli/sig_gen_command.rb +212 -31
- data/lib/rigor/cli/skill_command.rb +2 -2
- data/lib/rigor/cli/skill_deep_probe.rb +4 -4
- data/lib/rigor/cli/skill_describe.rb +35 -35
- data/lib/rigor/cli/trace_command.rb +4 -4
- data/lib/rigor/cli/trace_renderer.rb +5 -6
- data/lib/rigor/cli/triage_command.rb +1 -1
- data/lib/rigor/cli/type_of_command.rb +29 -11
- 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/type_scan_command.rb +4 -4
- data/lib/rigor/cli/unused_command.rb +11 -3
- data/lib/rigor/cli/upgrade_command.rb +1 -1
- data/lib/rigor/cli.rb +89 -9
- data/lib/rigor/config_audit.rb +30 -5
- data/lib/rigor/configuration/severity_profile.rb +25 -9
- data/lib/rigor/configuration.rb +109 -7
- data/lib/rigor/effects/ancestry_recorder.rb +191 -0
- data/lib/rigor/effects/attribution.rb +12 -5
- 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 +15 -10
- data/lib/rigor/effects/definition_context.rb +179 -0
- data/lib/rigor/effects/definition_lines.rb +25 -6
- data/lib/rigor/effects/effect_table.rb +11 -4
- data/lib/rigor/effects/entry_points.rb +0 -2
- data/lib/rigor/effects/envelope_check.rb +9 -9
- data/lib/rigor/effects/envelope_index.rb +20 -8
- data/lib/rigor/effects/file_collection.rb +59 -5
- data/lib/rigor/effects/framework_units.rb +5 -7
- data/lib/rigor/effects/identity.rb +20 -6
- data/lib/rigor/effects/inline_anchor.rb +7 -7
- data/lib/rigor/effects/label_intent.rb +3 -4
- data/lib/rigor/effects/liskov_check.rb +8 -8
- data/lib/rigor/effects/local_ownership.rb +38 -12
- data/lib/rigor/effects/method_key.rb +22 -1
- data/lib/rigor/effects/mutation_classifier.rb +23 -12
- data/lib/rigor/effects/plugin_facts.rb +47 -37
- data/lib/rigor/effects/propagator.rb +297 -19
- data/lib/rigor/effects/registry.rb +1 -1
- data/lib/rigor/effects/scanner.rb +122 -73
- data/lib/rigor/effects/signature_sources.rb +4 -6
- data/lib/rigor/effects/snapshot.rb +12 -11
- data/lib/rigor/effects/snapshot_diff.rb +1 -1
- data/lib/rigor/effects/summary.rb +27 -4
- data/lib/rigor/effects/unit_scan.rb +393 -38
- data/lib/rigor/effects/unknown_label_check.rb +3 -8
- data/lib/rigor/effects/unknown_label_report.rb +3 -4
- data/lib/rigor/effects/visibility.rb +101 -0
- data/lib/rigor/environment/bundle_sig_discovery.rb +6 -6
- data/lib/rigor/environment/class_registry.rb +3 -1
- data/lib/rigor/environment/failure_slot.rb +2 -2
- data/lib/rigor/environment/installed_gem_set.rb +85 -0
- data/lib/rigor/environment/lockfile_resolver.rb +68 -4
- data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
- data/lib/rigor/environment/member_consistency.rb +298 -0
- data/lib/rigor/environment/missing_gem_constant_index.rb +4 -4
- data/lib/rigor/environment/rbs_collection_discovery.rb +5 -5
- data/lib/rigor/environment/rbs_coverage_report.rb +5 -5
- data/lib/rigor/environment/rbs_hierarchy.rb +3 -1
- data/lib/rigor/environment/rbs_loader.rb +674 -79
- data/lib/rigor/environment.rb +174 -24
- data/lib/rigor/flow_contribution/merger.rb +0 -2
- data/lib/rigor/flow_contribution.rb +11 -13
- data/lib/rigor/hashing/xxh3.rb +264 -0
- data/lib/rigor/inference/acceptance.rb +292 -17
- 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 +75 -30
- data/lib/rigor/inference/block_repetition.rb +71 -0
- data/lib/rigor/inference/body_fixpoint.rb +7 -6
- data/lib/rigor/inference/budget_trace.rb +3 -6
- 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 +389 -18
- data/lib/rigor/inference/closure_escape_analyzer.rb +159 -17
- data/lib/rigor/inference/content_join.rb +200 -27
- data/lib/rigor/inference/coverage_scanner.rb +4 -6
- 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/dynamic_origin.rb +1 -1
- data/lib/rigor/inference/element_read_widening.rb +180 -0
- data/lib/rigor/inference/error_info.rb +196 -0
- data/lib/rigor/inference/expression_typer.rb +2124 -468
- data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
- data/lib/rigor/inference/fork_map.rb +5 -7
- 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/hkt_reducer.rb +2 -3
- data/lib/rigor/inference/hkt_registry.rb +4 -7
- data/lib/rigor/inference/index_write_widening.rb +16 -7
- 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 +168 -21
- 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 +87 -33
- data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +305 -19
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/json_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -1
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
- data/lib/rigor/inference/method_dispatcher/math_folding.rb +41 -3
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -5
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +165 -111
- 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/random_folding.rb +82 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +1046 -69
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +25 -4
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +2 -8
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +129 -54
- data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +1 -2
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -6
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +85 -4
- data/lib/rigor/inference/method_dispatcher/time_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +1 -2
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher.rb +225 -25
- data/lib/rigor/inference/method_parameter_binder.rb +12 -9
- data/lib/rigor/inference/multi_target_binder.rb +341 -55
- data/lib/rigor/inference/mutation_rejoin.rb +171 -0
- data/lib/rigor/inference/mutation_widening.rb +156 -85
- data/lib/rigor/inference/narrowing.rb +835 -162
- 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 +153 -16
- data/lib/rigor/inference/origin_lookup.rb +2 -3
- data/lib/rigor/inference/parameter_inference_collector.rb +7 -7
- data/lib/rigor/inference/pre_eval_constants.rb +6 -5
- data/lib/rigor/inference/precision_scanner.rb +3 -4
- data/lib/rigor/inference/project_method_ownership.rb +136 -0
- data/lib/rigor/inference/project_patched_methods.rb +9 -4
- data/lib/rigor/inference/project_patched_scanner.rb +10 -6
- data/lib/rigor/inference/protection_scanner.rb +1 -2
- data/lib/rigor/inference/range_constant.rb +57 -0
- data/lib/rigor/inference/rbs_type_translator.rb +49 -11
- data/lib/rigor/inference/receiver_alias.rb +93 -4
- data/lib/rigor/inference/receiver_blind_block.rb +219 -0
- data/lib/rigor/inference/refinement_mutation.rb +76 -0
- 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 +5228 -627
- data/lib/rigor/inference/statement_evaluator.rb +3452 -595
- data/lib/rigor/inference/stored_block_call.rb +54 -0
- data/lib/rigor/inference/string_mutation.rb +97 -0
- data/lib/rigor/inference/struct_fold_safety.rb +6 -7
- data/lib/rigor/inference/synthetic_method_index.rb +1 -2
- data/lib/rigor/inference/synthetic_method_scanner.rb +4 -6
- data/lib/rigor/inference/unknown_store_widening.rb +200 -0
- data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
- data/lib/rigor/inference/version_guard.rb +28 -21
- data/lib/rigor/inference/void_origin.rb +3 -3
- data/lib/rigor/inference/void_tail_summary.rb +2 -4
- data/lib/rigor/language_server/buffer_table.rb +4 -4
- data/lib/rigor/language_server/completion_provider.rb +1 -1
- data/lib/rigor/language_server/debouncer.rb +0 -1
- data/lib/rigor/language_server/diagnostic_publisher.rb +7 -6
- data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
- data/lib/rigor/language_server/folding_range_provider.rb +1 -1
- data/lib/rigor/language_server/hover_provider.rb +1 -1
- data/lib/rigor/language_server/hover_renderer.rb +4 -4
- data/lib/rigor/language_server/incremental_sync.rb +6 -6
- data/lib/rigor/language_server/project_context.rb +7 -8
- data/lib/rigor/language_server/publish_batcher.rb +2 -2
- data/lib/rigor/language_server/selection_range_provider.rb +2 -2
- data/lib/rigor/language_server/server.rb +9 -9
- data/lib/rigor/language_server/signature_help_provider.rb +1 -1
- data/lib/rigor/language_server/uri.rb +1 -1
- data/lib/rigor/mcp/server.rb +2 -1
- data/lib/rigor/plugin/base.rb +258 -26
- data/lib/rigor/plugin/box_probe.rb +91 -0
- data/lib/rigor/plugin/bundled_catalog.rb +167 -0
- data/lib/rigor/plugin/effect_attribution.rb +65 -11
- data/lib/rigor/plugin/effect_entry_points.rb +3 -3
- data/lib/rigor/plugin/fact_store.rb +6 -6
- data/lib/rigor/plugin/io_boundary.rb +93 -8
- data/lib/rigor/plugin/isolation.rb +29 -6
- data/lib/rigor/plugin/loader.rb +33 -13
- data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
- data/lib/rigor/plugin/macro/heredoc_template.rb +1 -1
- data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
- data/lib/rigor/plugin/manifest.rb +94 -11
- data/lib/rigor/plugin/registry.rb +76 -12
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +21 -4
- data/lib/rigor/plugin/template_unit.rb +196 -0
- data/lib/rigor/plugin/trust_policy.rb +2 -4
- data/lib/rigor/plugin/type_node_resolver.rb +3 -3
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/plugin_gap_advisory.rb +96 -0
- data/lib/rigor/project_environment.rb +138 -0
- data/lib/rigor/protection/analysis_guard.rb +93 -14
- data/lib/rigor/protection/closure_kill_oracle.rb +67 -23
- data/lib/rigor/protection/dependency_closure.rb +5 -8
- data/lib/rigor/protection/diagnostic_oracle.rb +2 -2
- data/lib/rigor/protection/discovery_seed.rb +14 -12
- data/lib/rigor/protection/kill_signature.rb +1 -4
- data/lib/rigor/protection/measurement_integrity.rb +1 -3
- data/lib/rigor/protection/mutation_cache.rb +7 -11
- data/lib/rigor/protection/mutation_scanner.rb +12 -15
- data/lib/rigor/protection/mutator.rb +2 -1
- data/lib/rigor/protection/test_suite_oracle.rb +5 -5
- data/lib/rigor/rbs_extended/conformance_checker.rb +4 -3
- data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
- data/lib/rigor/rbs_extended/reporter.rb +36 -10
- data/lib/rigor/rbs_extended.rb +88 -21
- data/lib/rigor/reflection/constant_ancestors.rb +97 -0
- data/lib/rigor/reflection/constant_path.rb +139 -0
- data/lib/rigor/reflection.rb +93 -110
- data/lib/rigor/runtime/jit.rb +6 -8
- data/lib/rigor/scope/discovery_index.rb +165 -2
- data/lib/rigor/scope.rb +1041 -32
- 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 +673 -53
- data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
- data/lib/rigor/sig_gen/layout_index.rb +3 -4
- data/lib/rigor/sig_gen/meta_class_shape.rb +3 -5
- data/lib/rigor/sig_gen/method_candidate.rb +47 -3
- data/lib/rigor/sig_gen/observation_collector.rb +18 -16
- data/lib/rigor/sig_gen/path_mapper.rb +4 -6
- data/lib/rigor/sig_gen/rbs_validity.rb +4 -4
- data/lib/rigor/sig_gen/renderer.rb +128 -14
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +154 -0
- data/lib/rigor/sig_gen/superclass_spelling.rb +27 -0
- data/lib/rigor/sig_gen/type_elaborator.rb +1 -3
- data/lib/rigor/sig_gen/write_result.rb +19 -3
- data/lib/rigor/sig_gen/writer.rb +189 -41
- data/lib/rigor/sig_gen.rb +3 -0
- data/lib/rigor/signature_path_audit.rb +153 -6
- data/lib/rigor/source/literals.rb +0 -23
- data/lib/rigor/source/node_children.rb +0 -2
- data/lib/rigor/source/node_locator.rb +5 -11
- data/lib/rigor/source/node_walker.rb +0 -6
- data/lib/rigor/source/parameter_envelope.rb +72 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/triage/catalogue.rb +1 -3
- data/lib/rigor/triage.rb +3 -5
- data/lib/rigor/type/accepts_result.rb +24 -6
- data/lib/rigor/type/combinator.rb +144 -5
- data/lib/rigor/type/data_class.rb +2 -2
- data/lib/rigor/type/data_instance.rb +4 -4
- data/lib/rigor/type/difference.rb +1 -0
- data/lib/rigor/type/float_range.rb +128 -0
- data/lib/rigor/type/hash_shape.rb +6 -6
- data/lib/rigor/type/integer_range.rb +13 -8
- data/lib/rigor/type/nominal.rb +8 -2
- data/lib/rigor/type/refined.rb +4 -3
- data/lib/rigor/type/struct_class.rb +3 -3
- data/lib/rigor/type/struct_instance.rb +4 -4
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_node/generic.rb +1 -1
- data/lib/rigor/type_node/range_literal.rb +25 -0
- data/lib/rigor/type_node/resolver_chain.rb +1 -1
- data/lib/rigor/type_node.rb +1 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +0 -4
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +0 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +0 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +10 -5
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +0 -4
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +3 -5
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +1 -4
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +14 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +32 -30
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +0 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -3
- 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 +264 -3
- data/plugins/rigor-actionpack/sig/action_controller.rbs +71 -0
- 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/analyzer.rb +0 -4
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +2 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +0 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +0 -2
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +10 -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 +383 -7
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +1 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +1 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +322 -41
- data/plugins/rigor-activerecord/sig/active_record/framework.rbs +123 -0
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +26 -14
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -2
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +290 -19
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +1 -0
- data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +3 -0
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +9 -7
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +7 -3
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +1 -0
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +8 -6
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +11 -7
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -11
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +9 -4
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +2 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +0 -5
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +0 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +0 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +8 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +36 -4
- data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +1 -0
- data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
- data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
- data/plugins/rigor-grape/sig/grape.rbs +263 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +8 -6
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +78 -5
- data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +1 -0
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -1
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -3
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +1 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +0 -5
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -2
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +0 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +0 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +12 -7
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +0 -12
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +1 -3
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +0 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +36 -37
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +5 -5
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -10
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +5 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +3 -2
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +2 -2
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +2 -2
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +9 -10
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +2 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +10 -17
- data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +1 -0
- data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +13 -1
- 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 +324 -18
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +0 -3
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +0 -3
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +2 -3
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +1 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +0 -3
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +1 -0
- data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +1 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +1 -9
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +1 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +0 -4
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +1 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +2 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +0 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +0 -2
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +10 -5
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +2 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +1 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +3 -9
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +4 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +1 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +3 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +7 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +51 -6
- data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +1 -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/analysis/reachability/scan_cache.rbs +12 -0
- data/sig/rigor/cache.rbs +2 -3
- 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 +17 -10
- data/sig/rigor/inference/optimistic_origin.rbs +10 -0
- data/sig/rigor/inference.rbs +9 -14
- 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 +4 -0
- data/sig/rigor/plugin/loader.rbs +3 -3
- data/sig/rigor/plugin/manifest.rbs +51 -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 +9 -6
- data/sig/rigor/scope.rbs +148 -12
- data/sig/rigor/sig_gen/skip_reason_catalog.rbs +21 -0
- data/sig/rigor/source.rbs +4 -4
- data/sig/rigor/testing.rbs +10 -4
- data/sig/rigor/type.rbs +33 -1
- data/sig/rigor.rbs +39 -20
- data/skills/rigor-ask/SKILL.md +16 -11
- 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 +25 -27
- 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 +15 -10
- data/skills/rigor-project-init/references/01-detect.md +13 -9
- data/skills/rigor-project-init/references/02-configure.md +43 -14
- 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-project-init/references/06-agent-contract.md +67 -0
- 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 +226 -0
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +265 -0
- data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +52 -0
- data/skills/rigor-type-oracle/references/03-gap-protocol.md +125 -0
- data/skills/rigor-unused-adjudicate/SKILL.md +12 -7
- data/skills/rigor-upgrade/SKILL.md +13 -8
- metadata +133 -3
- data/lib/rigor/cli/probe_environment.rb +0 -85
- data/sig/rigor/inference/builtins/numeric_catalog.rbs +0 -3
|
@@ -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.
|
|
@@ -77,9 +77,8 @@ module Rigor
|
|
|
77
77
|
# `ApplicationRecord`) setting `self.table_name_prefix` for every model under it — both already
|
|
78
78
|
# guess wrong identically on unpatched code.
|
|
79
79
|
class ModelDiscoverer
|
|
80
|
-
# @param
|
|
81
|
-
# @param
|
|
82
|
-
# @param base_classes [Array<String>] superclass names that identify a class as an AR model.
|
|
80
|
+
# @param search_paths — absolute or project-relative paths.
|
|
81
|
+
# @param base_classes — superclass names that identify a class as an AR model.
|
|
83
82
|
# Declaration macros whose column's runtime value is a rich object, not the SQL scalar. Their column
|
|
84
83
|
# must NOT be narrowed to the schema type (see {ModelIndex.build}'s type-override remap).
|
|
85
84
|
TYPE_OVERRIDE_METHODS = %i[serialize mount_uploader mount_uploaders].freeze
|
|
@@ -109,11 +108,23 @@ module Rigor
|
|
|
109
108
|
# name — {#resolve_table_name_decorator} keeps walking outward past it, exactly like Rails' own
|
|
110
109
|
# `respond_to?(:table_name_prefix)` ancestor search.
|
|
111
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
|
|
112
123
|
end
|
|
113
124
|
|
|
114
125
|
attr_reader :type_override_columns
|
|
115
126
|
|
|
116
|
-
# @return
|
|
127
|
+
# @return rows of { class_name:, table_name_override:, sti_parent:, ... }
|
|
117
128
|
def discover
|
|
118
129
|
candidates = []
|
|
119
130
|
ruby_files_under(@search_paths).each do |path|
|
|
@@ -128,7 +139,8 @@ module Rigor
|
|
|
128
139
|
# Every file is walked (and every module's `table_name_prefix` / `table_name_suffix` recorded)
|
|
129
140
|
# before any row is resolved — a model's file may sort, and so be visited, before the file that
|
|
130
141
|
# declares its enclosing module's decorator.
|
|
131
|
-
|
|
142
|
+
rows = fold_concern_declarations(resolve_models(candidates), superclass_map: superclass_map)
|
|
143
|
+
attach_table_name_decorators(rows, superclass_map: superclass_map)
|
|
132
144
|
end
|
|
133
145
|
|
|
134
146
|
private
|
|
@@ -218,7 +230,8 @@ module Rigor
|
|
|
218
230
|
scopes: (Array(base[:scopes]) + Array(addition[:scopes])).uniq,
|
|
219
231
|
validations: (Array(base[:validations]) + Array(addition[:validations])).uniq,
|
|
220
232
|
callbacks: (Array(base[:callbacks]) + Array(addition[:callbacks])).uniq,
|
|
221
|
-
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
|
|
222
235
|
)
|
|
223
236
|
end
|
|
224
237
|
|
|
@@ -424,6 +437,7 @@ module Rigor
|
|
|
424
437
|
superclass = strip_root(constant_path_name(node.superclass)) if node.superclass
|
|
425
438
|
|
|
426
439
|
collect_type_overrides(node.body)
|
|
440
|
+
record_include_edges(full_name, node.body)
|
|
427
441
|
# Rails' `full_table_name_prefix` walks `module_parents.detect { respond_to?(:table_name_prefix) }`
|
|
428
442
|
# — lexical nesting, which does not care whether the namespace container was written `module Blog`
|
|
429
443
|
# or `class Blog`. A `class` used purely as a namespace holder (or a model class that happens to
|
|
@@ -441,7 +455,8 @@ module Rigor
|
|
|
441
455
|
scopes: lookup_scopes(node.body),
|
|
442
456
|
validations: lookup_validations(node.body),
|
|
443
457
|
callbacks: lookup_callbacks(node.body),
|
|
444
|
-
aliases: lookup_aliases(node.body)
|
|
458
|
+
aliases: lookup_aliases(node.body),
|
|
459
|
+
macro_methods: lookup_macro_methods(node.body)
|
|
445
460
|
})
|
|
446
461
|
|
|
447
462
|
# Recurse into the body in case nested classes exist.
|
|
@@ -458,6 +473,9 @@ module Rigor
|
|
|
458
473
|
|
|
459
474
|
full_name = declared_constant_name(module_local_name, lexical_path)
|
|
460
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)
|
|
461
479
|
|
|
462
480
|
inner_path = [full_name]
|
|
463
481
|
walk_for_classes(node.body, inner_path, &) if node.body
|
|
@@ -520,6 +538,177 @@ module Rigor
|
|
|
520
538
|
type_arg.is_a?(Prism::ConstantReadNode) || type_arg.is_a?(Prism::ConstantPathNode)
|
|
521
539
|
end
|
|
522
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
|
+
|
|
523
712
|
# Renders a constant-path node (`Admin::User`, `::ApplicationRecord`) as a String, keeping the
|
|
524
713
|
# leading `::` of a rooted path — {#declared_constant_name} reads it as "reset the lexical path"
|
|
525
714
|
# before the marker is dropped. Returns nil for shapes the discoverer chooses not to handle.
|
|
@@ -1334,6 +1523,193 @@ module Rigor
|
|
|
1334
1523
|
aliases.freeze
|
|
1335
1524
|
end
|
|
1336
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
|
+
|
|
1337
1713
|
# Collects every Symbol-literal positional argument from a CallNode. Used by both
|
|
1338
1714
|
# `lookup_validations` and `lookup_callbacks` to extract the attribute / method name list.
|
|
1339
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?)
|