rigortype 0.4.7 → 0.4.8
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 +3 -1
- data/data/core_overlay/bigdecimal.rbs +33 -0
- data/data/core_overlay/resolv.rbs +30 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +29 -0
- data/data/gem_overlay/addressable/addressable.rbs +40 -0
- data/docs/handbook/01-getting-started.md +1 -1
- data/docs/handbook/11-sig-gen.md +35 -0
- data/docs/install.md +20 -0
- data/docs/manual/01-installation.md +67 -26
- data/docs/manual/02-cli-reference.md +307 -91
- data/docs/manual/03-configuration.md +41 -3
- data/docs/manual/04-diagnostics.md +5 -4
- data/docs/manual/08-skills.md +11 -0
- data/docs/manual/09-editor-integration.md +8 -0
- data/docs/manual/14-rails-quickstart.md +1 -1
- data/docs/manual/19-effect-labels.md +3 -4
- data/docs/manual/plugins/rigor-actionpack.md +15 -6
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +3 -0
- data/docs/manual/plugins/rigor-sorbet.md +17 -3
- data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
- data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +8 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +13 -2
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -0
- data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +40 -11
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
- data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +8 -1
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules.rb +598 -99
- data/lib/rigor/analysis/crash_signature.rb +43 -1
- data/lib/rigor/analysis/dependency_recorder.rb +233 -76
- data/lib/rigor/analysis/incremental.rb +11 -1
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -1
- data/lib/rigor/analysis/incremental_session.rb +279 -37
- data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
- data/lib/rigor/analysis/rule_catalog.rb +37 -1
- data/lib/rigor/analysis/run_cache_key.rb +27 -3
- data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +4 -0
- data/lib/rigor/analysis/runner/declaration_position.rb +7 -7
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +72 -20
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +3 -8
- data/lib/rigor/analysis/runner/pool_coordinator.rb +26 -5
- data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
- data/lib/rigor/analysis/runner.rb +222 -39
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +61 -22
- data/lib/rigor/analysis/worker_session.rb +42 -41
- data/lib/rigor/bleeding_edge.rb +19 -4
- data/lib/rigor/cache/file_digest.rb +39 -31
- data/lib/rigor/cache/incremental_snapshot.rb +66 -7
- data/lib/rigor/cache/rbs_descriptor.rb +37 -2
- data/lib/rigor/cache/rbs_environment.rb +5 -1
- data/lib/rigor/cli/baseline_command.rb +15 -2
- data/lib/rigor/cli/check_command.rb +4 -2
- data/lib/rigor/cli/effects_snapshot_command.rb +8 -1
- data/lib/rigor/cli/lens_command.rb +17 -2
- data/lib/rigor/cli/sig_gen_command.rb +8 -0
- data/lib/rigor/cli/skill_describe.rb +1 -0
- data/lib/rigor/cli.rb +5 -2
- data/lib/rigor/config_audit.rb +32 -18
- data/lib/rigor/configuration/severity_profile.rb +9 -0
- data/lib/rigor/configuration.rb +32 -5
- data/lib/rigor/effects/collector.rb +53 -21
- data/lib/rigor/environment/bundle_sig_discovery.rb +63 -28
- data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
- data/lib/rigor/environment/default_libraries.rb +17 -0
- data/lib/rigor/environment/deferred_signature_plan.rb +104 -6
- data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
- data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
- data/lib/rigor/environment/rbs_hierarchy.rb +6 -0
- data/lib/rigor/environment/rbs_loader.rb +312 -63
- data/lib/rigor/environment.rb +131 -35
- data/lib/rigor/inference/acceptance.rb +50 -15
- data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
- data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
- data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
- data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
- data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
- data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
- data/lib/rigor/inference/block_auto_splat.rb +1 -1
- data/lib/rigor/inference/block_run_count.rb +9 -2
- data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
- data/lib/rigor/inference/callee_mutation_chain.rb +1 -1
- data/lib/rigor/inference/captured_locals.rb +2 -0
- data/lib/rigor/inference/census_foreign_block.rb +354 -0
- data/lib/rigor/inference/clause_widening.rb +1 -1
- data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
- data/lib/rigor/inference/construction_escape.rb +56 -15
- data/lib/rigor/inference/declaration_walk.rb +4 -0
- data/lib/rigor/inference/def_node_resolver.rb +32 -14
- data/lib/rigor/inference/def_return_typer.rb +1 -1
- data/lib/rigor/inference/definer_resolution.rb +15 -7
- data/lib/rigor/inference/definers_index.rb +72 -16
- data/lib/rigor/inference/error_info.rb +16 -0
- data/lib/rigor/inference/expression_typer.rb +1459 -287
- data/lib/rigor/inference/flow_tracer.rb +31 -14
- data/lib/rigor/inference/fork_map.rb +3 -0
- data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
- data/lib/rigor/inference/global_write_census.rb +8 -6
- data/lib/rigor/inference/in_effect_refinements.rb +7 -0
- data/lib/rigor/inference/indexed_narrowing.rb +57 -8
- data/lib/rigor/inference/ivar_writer_census.rb +106 -11
- data/lib/rigor/inference/key_presence_guard.rb +51 -25
- data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
- data/lib/rigor/inference/last_line.rb +5 -2
- data/lib/rigor/inference/loop_value_record.rb +63 -15
- data/lib/rigor/inference/match_rebinding/frame.rb +11 -0
- data/lib/rigor/inference/match_rebinding/yielding_match.rb +74 -18
- data/lib/rigor/inference/match_rebinding.rb +21 -8
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +43 -29
- data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
- data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
- data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
- data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +12 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +409 -67
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +384 -64
- data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
- data/lib/rigor/inference/method_dispatcher.rb +66 -18
- data/lib/rigor/inference/method_parameter_binder.rb +136 -56
- data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
- data/lib/rigor/inference/module_function_state.rb +13 -2
- data/lib/rigor/inference/multi_target_binder.rb +1 -1
- data/lib/rigor/inference/mutation_widening.rb +44 -4
- data/lib/rigor/inference/narrowing.rb +201 -27
- data/lib/rigor/inference/operand_effects.rb +43 -2
- data/lib/rigor/inference/optimistic_origin.rb +133 -8
- data/lib/rigor/inference/origin_lookup.rb +1 -1
- data/lib/rigor/inference/precision_scanner.rb +1 -1
- data/lib/rigor/inference/project_method_ownership.rb +28 -10
- data/lib/rigor/inference/project_patched_methods.rb +65 -13
- data/lib/rigor/inference/project_patched_scanner.rb +120 -25
- data/lib/rigor/inference/project_patched_visibility.rb +39 -3
- data/lib/rigor/inference/rbs_type_translator.rb +68 -15
- data/lib/rigor/inference/receiver_writer_call.rb +67 -0
- data/lib/rigor/inference/recursion_rebinding.rb +157 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +56 -18
- data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -17
- data/lib/rigor/inference/refined_dispatch.rb +13 -0
- data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
- data/lib/rigor/inference/remover_holder_scan.rb +76 -22
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +12 -11
- data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +188 -28
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
- data/lib/rigor/inference/scope_indexer.rb +2079 -183
- data/lib/rigor/inference/self_call_ivar_writes.rb +26 -7
- data/lib/rigor/inference/self_preserving_block.rb +87 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +41 -6
- data/lib/rigor/inference/singleton_object_constant.rb +32 -2
- data/lib/rigor/inference/statement_evaluator.rb +970 -250
- data/lib/rigor/inference/string_mutation.rb +14 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +35 -11
- data/lib/rigor/inference/toplevel_def_census.rb +14 -6
- data/lib/rigor/inference/unknown_store_widening.rb +1 -1
- data/lib/rigor/inference/untyped_rejoin.rb +1 -1
- data/lib/rigor/inference/void_tail_summary.rb +29 -10
- data/lib/rigor/lens/claims/comparison.rb +78 -0
- data/lib/rigor/lens/claims/request.rb +131 -0
- data/lib/rigor/lens/claims/type_reader.rb +216 -0
- data/lib/rigor/lens/claims.rb +208 -0
- data/lib/rigor/lens/contract_reader.rb +5 -0
- data/lib/rigor/lens/document.rb +1 -1
- data/lib/rigor/lens/file_reader.rb +21 -8
- data/lib/rigor/lens/run.rb +8 -0
- data/lib/rigor/lens.rb +1 -1
- data/lib/rigor/library_probe.rb +68 -0
- data/lib/rigor/memo_audit.rb +282 -0
- data/lib/rigor/plugin/base.rb +15 -2
- data/lib/rigor/plugin/io_boundary.rb +25 -3
- data/lib/rigor/plugin/manifest.rb +7 -3
- data/lib/rigor/plugin/registry.rb +13 -2
- data/lib/rigor/plugin/trust_policy.rb +46 -0
- data/lib/rigor/protection/mutation_cache.rb +4 -1
- data/lib/rigor/reflection/constant_ancestors.rb +79 -22
- data/lib/rigor/reflection/declaration_collectors.rb +55 -0
- data/lib/rigor/reflection/declarations.rb +58 -0
- data/lib/rigor/reflection/project_method.rb +177 -0
- data/lib/rigor/reflection.rb +2 -0
- data/lib/rigor/rooted_glob.rb +18 -1
- data/lib/rigor/run_scoped_slots.rb +20 -20
- data/lib/rigor/scope/discovery_index.rb +149 -2
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
- data/lib/rigor/scope/resolution_chain.rb +900 -55
- data/lib/rigor/scope.rb +307 -34
- data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
- data/lib/rigor/sig_gen/classification.rb +14 -1
- data/lib/rigor/sig_gen/generator.rb +222 -13
- data/lib/rigor/sig_gen/method_candidate.rb +14 -0
- data/lib/rigor/sig_gen/observation_collector.rb +11 -2
- data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +47 -0
- data/lib/rigor/sig_gen/writer.rb +21 -0
- data/lib/rigor/sig_gen.rb +1 -0
- data/lib/rigor/source/forwardable_delegators.rb +122 -0
- data/lib/rigor/source/node_children.rb +13 -0
- data/lib/rigor/type/combinator.rb +164 -8
- data/lib/rigor/type/interface.rb +60 -0
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_of_memo/serving.rb +99 -0
- data/lib/rigor/type_of_memo/stats.rb +43 -0
- data/lib/rigor/type_of_memo.rb +337 -0
- data/lib/rigor/typing_state.rb +1094 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +2 -2
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +57 -25
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +130 -30
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -5
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +49 -13
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +4 -2
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +13 -5
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +29 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -2
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +6 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +199 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +3 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +4 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +209 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +80 -3
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +1 -6
- data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
- data/sig/rigor/analysis/fact_store.rbs +2 -3
- data/sig/rigor/cache/descriptor.rbs +14 -0
- data/sig/rigor/environment.rbs +4 -1
- data/sig/rigor/plugin/base.rbs +53 -19
- data/sig/rigor/plugin/interfaces.rbs +51 -0
- data/sig/rigor/plugin/io_boundary.rbs +2 -3
- data/sig/rigor/plugin/node_context.rbs +21 -0
- data/sig/rigor/reflection.rbs +37 -0
- data/sig/rigor/scope.rbs +22 -3
- data/sig/rigor/trinary.rbs +1 -0
- data/sig/rigor/type.rbs +9 -3
- data/sig/rigor.rbs +3 -2
- data/skills/rigor-ask/SKILL.md +1 -0
- data/skills/rigor-project-init/references/01-detect.md +1 -0
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +5 -1
- data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
- data/skills/rigor-rbs-contract/SKILL.md +147 -0
- data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
- data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
- data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
- data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
- data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
- data/skills/rigor-rbs-setup/SKILL.md +3 -1
- data/skills/rigor-type-oracle/SKILL.md +26 -1
- data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
- metadata +48 -2
- data/lib/rigor/inference/memo_audit.rb +0 -55
data/lib/rigor/version.rb
CHANGED
|
@@ -22,7 +22,7 @@ module Rigor
|
|
|
22
22
|
# `last!`, `new`, `create`, `create!`. A seeded type is a claim the engine acts on, and a wrong one
|
|
23
23
|
# is worse than none: seeding `@user` from `find_by` (which returns nil at runtime) would hand the
|
|
24
24
|
# flow rules a non-nil nominal and license folds that draw diagnostics on correct templates — the
|
|
25
|
-
# trap `Actionpack::
|
|
25
|
+
# trap `Actionpack::PARAMS_ONLY_CHAIN_METHODS` documents at length for `Parameters#[]`. Anything
|
|
26
26
|
# else assigns nothing, the ivar stays unseeded, and the template reads it as `Dynamic`, which
|
|
27
27
|
# taints honestly (ADR-5). A call on that list that returns, or may return, SEVERAL records —
|
|
28
28
|
# `find(a, b)`, `find([1, 2])`, `create([{…}, {…}])` — is excluded by the same rule; see
|
|
@@ -228,7 +228,7 @@ module Rigor
|
|
|
228
228
|
# `case`, a `rescue`, a loop, or a block that may not run — does not contribute, because a seed
|
|
229
229
|
# is a claim about what the template will FIND, and `@user = User.find(1) if params[:pick]`
|
|
230
230
|
# leaves `@user` nil on the other path. A non-nil nominal standing in for a runtime nil is the
|
|
231
|
-
# `Parameters#[]` trap (`Actionpack::
|
|
231
|
+
# `Parameters#[]` trap (`Actionpack::PARAMS_ONLY_CHAIN_METHODS`): the flow rules act on it and
|
|
232
232
|
# report live branches, which is exactly what a reader of a template would see with
|
|
233
233
|
# `view_type_checks:` on. The unseeded ivar reads `Dynamic` instead and taints honestly (ADR-5).
|
|
234
234
|
#
|
|
@@ -473,21 +473,21 @@ module Rigor
|
|
|
473
473
|
# `ActionController::Parameters` (above), but the chained `params.require(:user).permit(:name)` /
|
|
474
474
|
# `params.permit(...)` calls returned `Dynamic` at the first hop (Parameters ships no bundled RBS,
|
|
475
475
|
# so a call on it resolves lenient-to-Dynamic), leaking every downstream site (`.permit`, `.to_h`,
|
|
476
|
-
# `.each`) to unprotected.
|
|
477
|
-
# `Parameters` receiver and re-type the result as the same
|
|
478
|
-
# concrete
|
|
479
|
-
#
|
|
480
|
-
#
|
|
481
|
-
#
|
|
482
|
-
#
|
|
483
|
-
#
|
|
484
|
-
#
|
|
485
|
-
#
|
|
486
|
-
#
|
|
487
|
-
#
|
|
488
|
-
#
|
|
489
|
-
#
|
|
490
|
-
# ## The admission rule for
|
|
476
|
+
# `.each`) to unprotected. The methods that return another `Parameters` (`PARAMS_ONLY_CHAIN_METHODS`)
|
|
477
|
+
# gate on a `Parameters` receiver and re-type the result as the same
|
|
478
|
+
# lenient nominal — the sites after them stay concrete receivers and `coverage --protection` counts
|
|
479
|
+
# them as protected. `require` / `expect` are the exception below: the receiver of the call right
|
|
480
|
+
# after one is the carrier, which is not a concrete receiver.
|
|
481
|
+
#
|
|
482
|
+
# `require` and `expect` may return a scalar at runtime (`params.require(:id) → String`), so the
|
|
483
|
+
# earlier "RBS-less nominal is FP-safe" reading of them (2026-07, #534) is withdrawn by #2947: it held
|
|
484
|
+
# only while Parameters shipped no RBS. Once the collection's declaration is loaded, a nominal answer
|
|
485
|
+
# reports a scalar's method as `call.undefined-method`. They answer the gradual carrier
|
|
486
|
+
# `Dynamic[Parameters]` (`SCALAR_CAPABLE_CHAIN_METHODS`), which keeps the chain resolvable and is quiet
|
|
487
|
+
# with or without the RBS. The GitLab strong-params survey (108 leaked `.permit` sites) is the demand.
|
|
488
|
+
# Issue #534 — `expect` and `slice` joined 2026-09-01 (63 and 38 sites on mastodon).
|
|
489
|
+
#
|
|
490
|
+
# ## The admission rule for the nominal table (`PARAMS_ONLY_CHAIN_METHODS`)
|
|
491
491
|
#
|
|
492
492
|
# A method belongs here **iff its Rails implementation returns an `ActionController::Parameters`
|
|
493
493
|
# on every path** — either a fresh `new_instance_with_inherited_permitted_status(...)` or `self`.
|
|
@@ -564,27 +564,56 @@ module Rigor
|
|
|
564
564
|
# hazard ever returns — and they now carry a positive assertion that `params[:k]` types
|
|
565
565
|
# `Dynamic[ActionController::Parameters?]` (#2430), so a silent regression to bare `Dynamic[top]`
|
|
566
566
|
# or to the non-nil nominal fails them too.
|
|
567
|
-
|
|
568
|
-
|
|
567
|
+
# The links that return an `ActionController::Parameters` on every path (checked against Rails 8.x
|
|
568
|
+
# `strong_parameters.rb`). Only these may be typed as the nominal.
|
|
569
|
+
PARAMS_ONLY_CHAIN_METHODS = %i[
|
|
570
|
+
permit permit! slice
|
|
569
571
|
except without extract! slice!
|
|
570
572
|
merge merge! reverse_merge reverse_merge! with_defaults with_defaults!
|
|
571
573
|
compact compact_blank deep_dup
|
|
572
574
|
].freeze
|
|
573
575
|
|
|
574
|
-
|
|
576
|
+
# Issue #2947 — `require` and `expect` are NOT in the nominal table above. Rails returns the value itself:
|
|
577
|
+
# `require` is `self[key]` (a scalar, or an Array for an Array of keys) and `expect` is `values.first`
|
|
578
|
+
# for one key, else an Array. The "RBS-less nominal is lenient" argument holds only while Parameters
|
|
579
|
+
# ships no RBS; with the collection's declaration loaded, a nominal answer reports `.to_sym` on a
|
|
580
|
+
# scalar as `call.undefined-method`. They answer the gradual carrier `Dynamic[Parameters]` instead:
|
|
581
|
+
# quiet at every boundary, and the chain keeps resolving through the carrier-gated rule.
|
|
582
|
+
SCALAR_CAPABLE_CHAIN_METHODS = %i[require expect].freeze
|
|
583
|
+
|
|
584
|
+
STRONG_PARAMS_CHAIN_METHODS = (PARAMS_ONLY_CHAIN_METHODS + SCALAR_CAPABLE_CHAIN_METHODS).freeze
|
|
585
|
+
|
|
586
|
+
dynamic_return receivers: [REQUEST_CONTEXT_READER_TYPES[:params]], methods: PARAMS_ONLY_CHAIN_METHODS do |call_node, _scope|
|
|
575
587
|
next nil unless call_node.is_a?(Prism::CallNode)
|
|
576
588
|
|
|
577
589
|
Rigor::Type::Combinator.nominal_of(REQUEST_CONTEXT_READER_TYPES[:params])
|
|
578
590
|
end
|
|
579
591
|
|
|
580
|
-
|
|
581
|
-
|
|
592
|
+
dynamic_return receivers: [REQUEST_CONTEXT_READER_TYPES[:params]], methods: SCALAR_CAPABLE_CHAIN_METHODS do |call_node, _scope|
|
|
593
|
+
next nil unless call_node.is_a?(Prism::CallNode)
|
|
594
|
+
|
|
595
|
+
Rigor::Type::Combinator.dynamic(Rigor::Type::Combinator.nominal_of(REQUEST_CONTEXT_READER_TYPES[:params]))
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
# Issue #2430 / #2947 — the chain on a carrier receiver (`params[:user].permit(:name)`,
|
|
599
|
+
# `params.require(:user).permit(:name)`). A carrier often holds an Array or a String at runtime, and
|
|
600
|
+
# those define many of the chain names with other returns (`compact`, `slice`, `without`, `deep_dup`,
|
|
601
|
+
# `extract!`, ...), so only `permit` / `permit!` — which no core or ActiveSupport class defines — can
|
|
602
|
+
# answer the plain nominal there: such a call raises NoMethodError or returns a real Parameters, and
|
|
603
|
+
# the plain nominal keeps the sites after it concrete receivers for `coverage --protection`. Every
|
|
604
|
+
# other chain method stays the gradual carrier.
|
|
605
|
+
CARRIER_NOMINAL_CHAIN_METHODS = %i[permit permit!].freeze
|
|
606
|
+
|
|
582
607
|
dynamic_return methods: STRONG_PARAMS_CHAIN_METHODS do |call_node, scope|
|
|
583
|
-
next nil unless call_node.is_a?(Prism::CallNode)
|
|
608
|
+
next nil unless call_node.is_a?(Prism::CallNode)
|
|
609
|
+
|
|
610
|
+
receiver = call_node.receiver
|
|
611
|
+
next nil unless receiver
|
|
584
612
|
next nil unless controller_scope?(scope)
|
|
585
|
-
next nil unless params_carrier_facet?(scope.type_of(
|
|
613
|
+
next nil unless params_carrier_facet?(scope.type_of(receiver))
|
|
586
614
|
|
|
587
|
-
Rigor::Type::Combinator.
|
|
615
|
+
nominal = Rigor::Type::Combinator.nominal_of(REQUEST_CONTEXT_READER_TYPES[:params])
|
|
616
|
+
CARRIER_NOMINAL_CHAIN_METHODS.include?(call_node.name) ? nominal : Rigor::Type::Combinator.dynamic(nominal)
|
|
588
617
|
end
|
|
589
618
|
|
|
590
619
|
# Issue #534 item 1, reopened by #574. `#[]` is the single largest named-receiver pair on both survey
|
|
@@ -620,9 +649,12 @@ module Rigor
|
|
|
620
649
|
# receiver. These two rules gate on the carrier's facet instead, and only inside a controller: a
|
|
621
650
|
# carrier is minted by the `params` reader, which is controller-only, so no other scope can hold one.
|
|
622
651
|
dynamic_return methods: %i[[]] do |call_node, scope|
|
|
623
|
-
next nil unless call_node.is_a?(Prism::CallNode)
|
|
652
|
+
next nil unless call_node.is_a?(Prism::CallNode)
|
|
653
|
+
|
|
654
|
+
receiver = call_node.receiver
|
|
655
|
+
next nil unless receiver
|
|
624
656
|
next nil unless controller_scope?(scope)
|
|
625
|
-
next nil unless params_carrier_facet?(scope.type_of(
|
|
657
|
+
next nil unless params_carrier_facet?(scope.type_of(receiver))
|
|
626
658
|
|
|
627
659
|
params_read_type
|
|
628
660
|
end
|
|
@@ -161,9 +161,8 @@ module Rigor
|
|
|
161
161
|
end
|
|
162
162
|
|
|
163
163
|
def project_defines_object?(serializer_name, scope)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
!scope.user_def_through_ancestors(serializer_name, :object).first.nil?
|
|
164
|
+
Rigor::Reflection.project_method_defined(serializer_name, :object, kind: :instance, source: :def,
|
|
165
|
+
scope: scope).yes?
|
|
167
166
|
end
|
|
168
167
|
|
|
169
168
|
# The model a serializer serializes, or nil where nothing corroborates it. Memoised per serializer
|
|
@@ -237,13 +236,11 @@ module Rigor
|
|
|
237
236
|
# the check would decline nearly every real serializer. On the serializer side it is a base
|
|
238
237
|
# serializer's own rendering method, which is not a read of the resource at all.
|
|
239
238
|
def project_defines?(class_name, method_name, scope)
|
|
240
|
-
return false unless scope.respond_to?(:user_def_through_ancestors)
|
|
241
|
-
|
|
242
239
|
# A name no Symbol can hold cannot be a method the project defines (#1831): decline, do not raise.
|
|
243
240
|
method = Rigor::Source::SymbolName.of(method_name.to_s)
|
|
244
241
|
return false if method.nil?
|
|
245
242
|
|
|
246
|
-
|
|
243
|
+
Rigor::Reflection.project_method_defined(class_name, method, kind: :instance, source: :def, scope: scope).yes?
|
|
247
244
|
end
|
|
248
245
|
|
|
249
246
|
# `REST::AccountSerializer` offers two readings — the namespaced `REST::Account` and the
|
|
@@ -55,8 +55,24 @@ module Rigor
|
|
|
55
55
|
)
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
+
# Ruby binds a trailing `k: v` / `**opts` group to keyword parameters, never to positional ones, so
|
|
59
|
+
# when `#perform` takes keywords the group is not counted (the message's "got N" is then the
|
|
60
|
+
# positional count too). A job without keyword parameters keeps counting the group as one positional
|
|
61
|
+
# argument — `NoArgsJob.perform_later(name: 1)` raises at runtime. A `Hash` literal / local stays
|
|
62
|
+
# positional either way (Ruby 3 does not convert it).
|
|
63
|
+
#
|
|
64
|
+
# The call is declined (no report) when its shape is unknown: a `*splat` or `...` argument may supply
|
|
65
|
+
# any number of positionals (or a trailing keyword group), and a keyword group made only of `**opts`
|
|
66
|
+
# arguments sent to a job without keywords may be empty — declined only when the verdict differs
|
|
67
|
+
# between "empty" and "one positional hash" (`NoArgsJob.perform_later(**opts)`).
|
|
58
68
|
def arity_violation(call_node, entry)
|
|
59
|
-
|
|
69
|
+
args = call_node.arguments&.arguments || []
|
|
70
|
+
return nil if args.any? { |arg| arg.is_a?(Prism::SplatNode) || arg.is_a?(Prism::ForwardingArgumentsNode) }
|
|
71
|
+
|
|
72
|
+
keyword_group = args.last.is_a?(Prism::KeywordHashNode) ? args.last : nil
|
|
73
|
+
return nil if keyword_group && !entry.accepts_keywords && empty_group_decides?(args, keyword_group, entry)
|
|
74
|
+
|
|
75
|
+
actual = entry.accepts_keywords && keyword_group ? args.size - 1 : args.size
|
|
60
76
|
return nil if entry.accepts?(actual)
|
|
61
77
|
|
|
62
78
|
Violation.new(
|
|
@@ -66,6 +82,14 @@ module Rigor
|
|
|
66
82
|
)
|
|
67
83
|
end
|
|
68
84
|
|
|
85
|
+
# True when the group is only `**splat` arguments (so it may be empty) AND the entry's verdict changes
|
|
86
|
+
# between counting it and not counting it.
|
|
87
|
+
def empty_group_decides?(args, keyword_group, entry)
|
|
88
|
+
return false unless keyword_group.elements.all?(Prism::AssocSplatNode)
|
|
89
|
+
|
|
90
|
+
entry.accepts?(args.size - 1) != entry.accepts?(args.size)
|
|
91
|
+
end
|
|
92
|
+
|
|
69
93
|
# Renders a constant-path receiver as a String. Mirrors the helpers in rigor-activerecord /
|
|
70
94
|
# rigor-rails-routes for parity.
|
|
71
95
|
def constant_receiver_name(node)
|
|
@@ -76,7 +76,8 @@ module Rigor
|
|
|
76
76
|
base.with(
|
|
77
77
|
min_arity: [base.min_arity, addition.min_arity].min,
|
|
78
78
|
max_arity: [base.max_arity, addition.max_arity].max,
|
|
79
|
-
keyword_required: base.keyword_required & addition.keyword_required
|
|
79
|
+
keyword_required: base.keyword_required & addition.keyword_required,
|
|
80
|
+
accepts_keywords: base.accepts_keywords || addition.accepts_keywords
|
|
80
81
|
)
|
|
81
82
|
end
|
|
82
83
|
|
|
@@ -187,7 +188,7 @@ module Rigor
|
|
|
187
188
|
if perform_def.nil?
|
|
188
189
|
return JobIndex::Entry.new(
|
|
189
190
|
class_name: class_name, min_arity: 0,
|
|
190
|
-
max_arity: Float::INFINITY, keyword_required: []
|
|
191
|
+
max_arity: Float::INFINITY, keyword_required: [], accepts_keywords: true
|
|
191
192
|
)
|
|
192
193
|
end
|
|
193
194
|
|
|
@@ -195,22 +196,31 @@ module Rigor
|
|
|
195
196
|
if parameters.nil?
|
|
196
197
|
return JobIndex::Entry.new(
|
|
197
198
|
class_name: class_name, min_arity: 0,
|
|
198
|
-
max_arity: 0, keyword_required: []
|
|
199
|
+
max_arity: 0, keyword_required: [], accepts_keywords: false
|
|
199
200
|
)
|
|
200
201
|
end
|
|
201
202
|
|
|
202
|
-
|
|
203
|
+
# Parameters after an optional or rest one (`def perform(a, b = 1, c)`) are required too.
|
|
204
|
+
required_count = (parameters.requireds || []).size + (parameters.posts || []).size
|
|
203
205
|
optional_count = (parameters.optionals || []).size
|
|
204
206
|
rest_present = !parameters.rest.nil?
|
|
205
207
|
keyword_required = (parameters.keywords || []).filter_map do |kw|
|
|
206
208
|
kw.name if kw.is_a?(Prism::RequiredKeywordParameterNode)
|
|
207
209
|
end
|
|
208
210
|
|
|
211
|
+
keyword_rest = parameters.keyword_rest
|
|
212
|
+
# `...` lives in `keyword_rest` and forwards every argument, so it widens the envelope; `**nil`
|
|
213
|
+
# also lives there but declares that NO keywords are taken (ActiveJob then hands them over as a
|
|
214
|
+
# positional hash, exactly like a method with no keyword parameters).
|
|
215
|
+
forwards_all = keyword_rest.is_a?(Prism::ForwardingParameterNode)
|
|
216
|
+
takes_keywords = !(parameters.keywords || []).empty? || keyword_rest.is_a?(Prism::KeywordRestParameterNode)
|
|
217
|
+
|
|
209
218
|
JobIndex::Entry.new(
|
|
210
219
|
class_name: class_name,
|
|
211
220
|
min_arity: required_count,
|
|
212
|
-
max_arity: rest_present ? Float::INFINITY : required_count + optional_count,
|
|
213
|
-
keyword_required: keyword_required.map(&:to_sym)
|
|
221
|
+
max_arity: rest_present || forwards_all ? Float::INFINITY : required_count + optional_count,
|
|
222
|
+
keyword_required: keyword_required.map(&:to_sym),
|
|
223
|
+
accepts_keywords: takes_keywords || forwards_all
|
|
214
224
|
)
|
|
215
225
|
end
|
|
216
226
|
end
|
|
@@ -10,9 +10,11 @@ module Rigor
|
|
|
10
10
|
# `min_arity` / `max_arity` form a closed range (`Float::INFINITY` for the upper bound when
|
|
11
11
|
# `*args` is present). `keyword_required` lists any required keyword arguments — Active Job
|
|
12
12
|
# supports keyword args but they're rare in user code, so the analyzer validates positional arity
|
|
13
|
-
# only (keyword
|
|
13
|
+
# only (keyword name validation is deferred). `accepts_keywords` records whether `#perform` binds
|
|
14
|
+
# keywords at all (`k:`, `k: 1` or `**opts`), so the analyzer can keep a call's keyword arguments out
|
|
15
|
+
# of the positional count.
|
|
14
16
|
class JobIndex
|
|
15
|
-
Entry = Data.define(:class_name, :min_arity, :max_arity, :keyword_required) do
|
|
17
|
+
Entry = Data.define(:class_name, :min_arity, :max_arity, :keyword_required, :accepts_keywords) do
|
|
16
18
|
# Flexible-friendly textual form of the arity for error messages: `1`, `1..2`, `2+`.
|
|
17
19
|
def arity_label
|
|
18
20
|
return "#{min_arity}+" if max_arity == Float::INFINITY
|
|
@@ -110,7 +110,8 @@ module Rigor
|
|
|
110
110
|
# The predicate `Activerecord#finder_return_type` declines on, so the note and the type agree in a
|
|
111
111
|
# CLI run. In an editor run this scope lacks the file's own methods (#1329).
|
|
112
112
|
def own_find?(entry)
|
|
113
|
-
|
|
113
|
+
!Rigor::Reflection.project_method_defined(entry.class_name, :find, kind: :singleton, source: :discovered,
|
|
114
|
+
scope: @scope).no?
|
|
114
115
|
end
|
|
115
116
|
|
|
116
117
|
def validate_column_hash_call(node, entry)
|
|
@@ -31,10 +31,17 @@ module Rigor
|
|
|
31
31
|
# `Object` per `SchemaTable.ruby_type_for` rather than being dropped. Only the structural calls in
|
|
32
32
|
# {NON_COLUMN_METHODS} (indexes, constraints, foreign keys) are skipped.
|
|
33
33
|
#
|
|
34
|
+
# `create_table "x", primary_key: "name"` adds that column, typed from the `id:` option when the call is
|
|
35
|
+
# read completely and untyped otherwise. `record.id` reads the primary key column's value, so the `id`
|
|
36
|
+
# column takes the same type; a composite or unreadable `primary_key:` leaves it untyped. `id: false` or
|
|
37
|
+
# `id: nil` removes it and adds neither.
|
|
38
|
+
#
|
|
34
39
|
# Designed for the Prism interpretation pattern from rigor-lisp-eval — recursive descent on the AST,
|
|
35
40
|
# no eval.
|
|
36
41
|
class SchemaParser
|
|
37
42
|
TIMESTAMPS_COLUMNS = %w[created_at updated_at].freeze
|
|
43
|
+
DEFAULT_PRIMARY_KEY_TYPE = :bigint
|
|
44
|
+
UNKNOWN_PRIMARY_KEY_TYPE = :unknown
|
|
38
45
|
|
|
39
46
|
# Structural `t.<method>` calls inside a `create_table` block that declare indexes / constraints
|
|
40
47
|
# rather than columns. Everything NOT in this set that carries a literal column name is treated as
|
|
@@ -75,8 +82,10 @@ module Rigor
|
|
|
75
82
|
return [nil, nil] if table_name.nil?
|
|
76
83
|
|
|
77
84
|
block_node = call_node.block
|
|
78
|
-
columns = { "id" =>
|
|
85
|
+
columns = { "id" => implicit_id_column(call_node) }
|
|
79
86
|
columns.delete("id") if id_disabled?(call_node)
|
|
87
|
+
primary_key = named_primary_key_column(call_node)
|
|
88
|
+
columns[primary_key.name] = primary_key if primary_key
|
|
80
89
|
|
|
81
90
|
if block_node.is_a?(Prism::BlockNode) && block_node.body
|
|
82
91
|
collect_column_calls(block_node.body) do |column_call|
|
|
@@ -92,20 +101,129 @@ module Rigor
|
|
|
92
101
|
[table_name, columns.freeze]
|
|
93
102
|
end
|
|
94
103
|
|
|
104
|
+
# `record.id` reads the primary key column's value, so the `id` reader takes the key's type — the
|
|
105
|
+
# same shape the `id:` option gives the column `primary_key:` names or the implicit `id` column.
|
|
106
|
+
# A composite `primary_key:` makes `id` an Array this parser does not type, and a `primary_key:`
|
|
107
|
+
# it cannot read leaves `id` untyped as well.
|
|
108
|
+
def implicit_id_column(call_node)
|
|
109
|
+
unless single_primary_key?(call_node)
|
|
110
|
+
return SchemaTable::Column.new(name: "id", type: UNKNOWN_PRIMARY_KEY_TYPE, ruby_type: "Object")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
SchemaTable::Column.new(name: "id", **primary_key_shape(call_node))
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# `primary_key:` names one key column when it is a String or Symbol literal; `nil` or `false` falls
|
|
117
|
+
# back to the default `id` column the way `primary_key || ...` does in Rails.
|
|
118
|
+
def single_primary_key?(call_node)
|
|
119
|
+
primary_key = keyword_value(call_node, :primary_key)
|
|
120
|
+
primary_key.nil? || primary_key.is_a?(Prism::NilNode) || primary_key.is_a?(Prism::FalseNode) ||
|
|
121
|
+
!literal_name(primary_key).nil?
|
|
122
|
+
end
|
|
123
|
+
|
|
95
124
|
def id_disabled?(call_node)
|
|
96
|
-
|
|
125
|
+
id = keyword_value(call_node, :id)
|
|
126
|
+
(id.is_a?(Prism::FalseNode) || id.is_a?(Prism::NilNode)) && keywords_read?(call_keywords(call_node))
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# `create_table "settings", primary_key: "name", id: { type: :string }` creates the column `name`
|
|
130
|
+
# without the block declaring it, so the option is the only place the schema names it.
|
|
131
|
+
def named_primary_key_column(call_node)
|
|
132
|
+
return nil if id_disabled?(call_node)
|
|
133
|
+
|
|
134
|
+
name = literal_name(keyword_value(call_node, :primary_key))
|
|
135
|
+
return nil if name.nil?
|
|
136
|
+
|
|
137
|
+
SchemaTable::Column.new(name: name, **primary_key_shape(call_node))
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# The `id:` option describes the primary key column's type whether the key column is `id` itself or
|
|
141
|
+
# the column `primary_key:` names. An unreadable call still yields a shape — the column stays
|
|
142
|
+
# queryable, just untyped.
|
|
143
|
+
def primary_key_shape(call_node)
|
|
144
|
+
id = keyword_value(call_node, :id)
|
|
145
|
+
options = id.is_a?(Prism::HashNode) ? id.elements : []
|
|
146
|
+
array = keyword_in(options, :array).is_a?(Prism::TrueNode)
|
|
147
|
+
unless primary_key_type_stated?(call_node, options)
|
|
148
|
+
return { type: UNKNOWN_PRIMARY_KEY_TYPE, ruby_type: "Object", array: array }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
type = primary_key_type(id, options)
|
|
152
|
+
{ type: type, ruby_type: primary_key_ruby_type(type), array: array }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# `id: :string` or `id: { type: :string, ... }`. `id: true` asks for the default key, as does an
|
|
156
|
+
# `id:` hash that names no `type:`. A type this parser cannot read as a literal is left unknown
|
|
157
|
+
# rather than assumed to be bigint.
|
|
158
|
+
def primary_key_type(id, options)
|
|
159
|
+
return DEFAULT_PRIMARY_KEY_TYPE if id.is_a?(Prism::TrueNode)
|
|
160
|
+
|
|
161
|
+
declared = id.is_a?(Prism::HashNode) ? keyword_in(options, :type) : id
|
|
162
|
+
return DEFAULT_PRIMARY_KEY_TYPE if declared.nil?
|
|
163
|
+
|
|
164
|
+
literal_name(declared)&.to_sym || UNKNOWN_PRIMARY_KEY_TYPE
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# The key is typed only from a call this parser reads completely. `as:` is excluded because the query
|
|
168
|
+
# may define the column, and a non-literal `array:` because it decides the reader's shape.
|
|
169
|
+
def primary_key_type_stated?(call_node, options)
|
|
170
|
+
array = keyword_in(options, :array)
|
|
171
|
+
keywords_read?(call_keywords(call_node)) && keywords_read?(options) &&
|
|
172
|
+
keyword_value(call_node, :as).nil? &&
|
|
173
|
+
(array.nil? || array.is_a?(Prism::TrueNode) || array.is_a?(Prism::FalseNode))
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def call_keywords(call_node)
|
|
177
|
+
call_node.arguments.arguments.grep(Prism::KeywordHashNode).flat_map(&:elements)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Every element has a static Symbol key and no key repeats, so `keyword_in` sees what Ruby would pass.
|
|
181
|
+
def keywords_read?(elements)
|
|
182
|
+
keys = elements.map { |element| symbol_key(element.key) if element.is_a?(Prism::AssocNode) }
|
|
183
|
+
keys.none?(&:nil?) && keys.uniq.size == keys.size
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# A decimal key stays untyped, because its class depends on the scale: `Type::DecimalWithoutScale`
|
|
187
|
+
# at scale 0, `Type::Decimal` otherwise.
|
|
188
|
+
def primary_key_ruby_type(type)
|
|
189
|
+
type == :decimal ? "Object" : SchemaTable.ruby_type_for(type)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# A `decimal` column's class follows its literal `scale:`. Any other type takes its SchemaTable mapping.
|
|
193
|
+
# A call this parser does not read completely may still hold a different scale, so it reads untyped.
|
|
194
|
+
def column_ruby_type(type, call_node)
|
|
195
|
+
return SchemaTable.ruby_type_for(type) unless type == :decimal
|
|
196
|
+
return "Object" unless keywords_read?(call_keywords(call_node))
|
|
197
|
+
|
|
198
|
+
scale = keyword_value(call_node, :scale)
|
|
199
|
+
SchemaTable.decimal_ruby_type(scale.is_a?(Prism::IntegerNode) ? scale.value : nil)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def literal_name(node)
|
|
203
|
+
case node
|
|
204
|
+
when Prism::StringNode, Prism::SymbolNode then node.unescaped
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def keyword_value(call_node, name)
|
|
209
|
+
return nil if call_node.arguments.nil?
|
|
97
210
|
|
|
98
211
|
call_node.arguments.arguments.each do |arg|
|
|
99
212
|
next unless arg.is_a?(Prism::KeywordHashNode)
|
|
100
213
|
|
|
101
|
-
arg.elements
|
|
102
|
-
|
|
214
|
+
value = keyword_in(arg.elements, name)
|
|
215
|
+
return value if value
|
|
216
|
+
end
|
|
217
|
+
nil
|
|
218
|
+
end
|
|
103
219
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
220
|
+
def keyword_in(elements, name)
|
|
221
|
+
elements.each do |pair|
|
|
222
|
+
next unless pair.is_a?(Prism::AssocNode)
|
|
223
|
+
|
|
224
|
+
return pair.value if symbol_key(pair.key) == name
|
|
107
225
|
end
|
|
108
|
-
|
|
226
|
+
nil
|
|
109
227
|
end
|
|
110
228
|
|
|
111
229
|
# Walks the block body collecting `t.<method>(...)` calls. It descends through nested constructs
|
|
@@ -147,7 +265,7 @@ module Rigor
|
|
|
147
265
|
SchemaTable::Column.new(
|
|
148
266
|
name: name,
|
|
149
267
|
type: type,
|
|
150
|
-
ruby_type:
|
|
268
|
+
ruby_type: column_ruby_type(type, call_node),
|
|
151
269
|
array: keyword_true?(call_node, :array)
|
|
152
270
|
)
|
|
153
271
|
end
|
|
@@ -176,19 +294,7 @@ module Rigor
|
|
|
176
294
|
# modifiers like `t.bigint "status_ids", array: true` (Postgres array column) and `t.references
|
|
177
295
|
# "x", polymorphic: true`.
|
|
178
296
|
def keyword_true?(call_node, name)
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
call_node.arguments.arguments.each do |arg|
|
|
182
|
-
next unless arg.is_a?(Prism::KeywordHashNode)
|
|
183
|
-
|
|
184
|
-
arg.elements.each do |pair|
|
|
185
|
-
next unless pair.is_a?(Prism::AssocNode)
|
|
186
|
-
next unless symbol_key(pair.key) == name
|
|
187
|
-
|
|
188
|
-
return pair.value.is_a?(Prism::TrueNode)
|
|
189
|
-
end
|
|
190
|
-
end
|
|
191
|
-
false
|
|
297
|
+
keyword_value(call_node, name).is_a?(Prism::TrueNode)
|
|
192
298
|
end
|
|
193
299
|
|
|
194
300
|
# `t.column "name", "type"` / `t.column "name", :type` — the explicit generic column form. The
|
|
@@ -202,7 +308,7 @@ module Rigor
|
|
|
202
308
|
SchemaTable::Column.new(
|
|
203
309
|
name: name,
|
|
204
310
|
type: type_sym,
|
|
205
|
-
ruby_type:
|
|
311
|
+
ruby_type: column_ruby_type(type_sym, call_node),
|
|
206
312
|
array: keyword_true?(call_node, :array)
|
|
207
313
|
)
|
|
208
314
|
end
|
|
@@ -216,13 +322,7 @@ module Rigor
|
|
|
216
322
|
def string_argument(call_node, index)
|
|
217
323
|
return nil if call_node.arguments.nil?
|
|
218
324
|
|
|
219
|
-
|
|
220
|
-
return nil if arg.nil?
|
|
221
|
-
|
|
222
|
-
case arg
|
|
223
|
-
when Prism::StringNode then arg.unescaped
|
|
224
|
-
when Prism::SymbolNode then arg.unescaped
|
|
225
|
-
end
|
|
325
|
+
literal_name(call_node.arguments.arguments[index])
|
|
226
326
|
end
|
|
227
327
|
|
|
228
328
|
def symbol_key(node)
|
|
@@ -9,9 +9,10 @@ module Rigor
|
|
|
9
9
|
#
|
|
10
10
|
# The mapping from Rails column types to Ruby class names is deliberately conservative —
|
|
11
11
|
# `:string`/`:text` → `String`, `:integer`/`:bigint` → `Integer`, `:boolean` → `bool`,
|
|
12
|
-
# `:datetime`/`:timestamp` → `Time`, `:date` → `Date`, `:
|
|
13
|
-
#
|
|
14
|
-
#
|
|
12
|
+
# `:datetime`/`:timestamp` → `Time`, `:date` → `Date`, `:float` → `Float`. A `:decimal` column's class
|
|
13
|
+
# follows its scale, so it is typed by {.decimal_ruby_type} from the parser that reads the scale, and
|
|
14
|
+
# reads `Object` here. Exotic types (json, jsonb, ltree, hstore, custom) fall back to `Object` so the
|
|
15
|
+
# plugin stays silent rather than guessing.
|
|
15
16
|
class SchemaTable
|
|
16
17
|
Column = Struct.new(:name, :type, :ruby_type, :array, keyword_init: true) do
|
|
17
18
|
def to_h = { name: name, type: type, ruby_type: ruby_type, array: array }
|
|
@@ -26,8 +27,11 @@ module Rigor
|
|
|
26
27
|
text: "String",
|
|
27
28
|
integer: "Integer",
|
|
28
29
|
bigint: "Integer",
|
|
30
|
+
# PostgreSQL's dumper writes `id: :serial` / `t.serial` for a sequence-backed integer column.
|
|
31
|
+
serial: "Integer",
|
|
32
|
+
bigserial: "Integer",
|
|
29
33
|
float: "Float",
|
|
30
|
-
decimal: "
|
|
34
|
+
decimal: "Object",
|
|
31
35
|
boolean: "bool",
|
|
32
36
|
datetime: "Time",
|
|
33
37
|
timestamp: "Time",
|
|
@@ -45,7 +49,7 @@ module Rigor
|
|
|
45
49
|
}.freeze
|
|
46
50
|
|
|
47
51
|
# Implicit columns that every Rails table has unless the schema explicitly opts out. The plugin
|
|
48
|
-
# assumes these exist; users who run `create_table id: false` get no implicit `id` column from
|
|
52
|
+
# assumes these exist; users who run `create_table id: false` or `id: nil` get no implicit `id` column from
|
|
49
53
|
# the parser, but most apps never disable it.
|
|
50
54
|
IMPLICIT_COLUMNS = [
|
|
51
55
|
Column.new(name: "id", type: :integer, ruby_type: "Integer").freeze
|
|
@@ -83,6 +87,17 @@ module Rigor
|
|
|
83
87
|
def self.ruby_type_for(column_type)
|
|
84
88
|
RUBY_TYPE_MAPPING.fetch(column_type.to_sym, "Object")
|
|
85
89
|
end
|
|
90
|
+
|
|
91
|
+
# Maps a `decimal` column's scale to its Ruby class. Rails registers `Type::DecimalWithoutScale`
|
|
92
|
+
# (an Integer) at scale 0 and `Type::Decimal` (a BigDecimal) at any other scale. An absent or
|
|
93
|
+
# non-literal scale (`nil`) is left to the adapter, which decides it, so it reads as `Object`.
|
|
94
|
+
def self.decimal_ruby_type(scale)
|
|
95
|
+
case scale
|
|
96
|
+
when 0 then "Integer"
|
|
97
|
+
when Integer then "BigDecimal"
|
|
98
|
+
else "Object"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
86
101
|
end
|
|
87
102
|
end
|
|
88
103
|
end
|
|
@@ -138,21 +138,23 @@ module Rigor
|
|
|
138
138
|
end
|
|
139
139
|
|
|
140
140
|
def build_column(name, rest)
|
|
141
|
-
type_str, array = extract_type(rest)
|
|
141
|
+
type_str, array, scale = extract_type(rest)
|
|
142
142
|
return nil if type_str.empty?
|
|
143
143
|
|
|
144
144
|
rails_type = PG_TYPE_TO_RAILS.fetch(type_str, type_str.to_sym)
|
|
145
|
+
ruby_type = rails_type == :decimal ? SchemaTable.decimal_ruby_type(scale) : SchemaTable.ruby_type_for(rails_type)
|
|
145
146
|
SchemaTable::Column.new(
|
|
146
147
|
name: name,
|
|
147
148
|
type: rails_type,
|
|
148
|
-
ruby_type:
|
|
149
|
+
ruby_type: ruby_type,
|
|
149
150
|
array: array
|
|
150
151
|
)
|
|
151
152
|
end
|
|
152
153
|
|
|
153
154
|
# The type is every token before the first uppercase-initial token (a column modifier such as
|
|
154
155
|
# `NOT`, `NULL`, `DEFAULT`, `GENERATED`, `COLLATE`). A trailing `[]` marks a Postgres array column;
|
|
155
|
-
# a `(length[,scale])` specifier is dropped. Returns `[normalized_type, array
|
|
156
|
+
# a `(length[,scale])` specifier is dropped. Returns `[normalized_type, array?, scale]`, where `scale` is
|
|
157
|
+
# the literal second number of a `(precision, scale)` specifier, or nil when there is none.
|
|
156
158
|
def extract_type(rest)
|
|
157
159
|
type_tokens = []
|
|
158
160
|
rest.split(/\s+/).each do |token|
|
|
@@ -162,8 +164,9 @@ module Rigor
|
|
|
162
164
|
end
|
|
163
165
|
type_str = type_tokens.join(" ")
|
|
164
166
|
array = type_str.include?("[]")
|
|
167
|
+
scale = type_str[/\(\s*\d+\s*,\s*(\d+)\s*\)/, 1]&.to_i
|
|
165
168
|
type_str = type_str.gsub("[]", "").gsub(/\([0-9,\s]*\)/, "").strip.downcase
|
|
166
|
-
[type_str, array]
|
|
169
|
+
[type_str, array, scale]
|
|
167
170
|
end
|
|
168
171
|
end
|
|
169
172
|
end
|