rigortype 0.4.1 → 0.4.3
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 +1 -1
- data/data/builtins/ruby_core/io.yml +1 -0
- data/data/builtins/ruby_core/proc.yml +4 -0
- data/data/builtins/ruby_core/random.yml +4 -0
- data/data/builtins/ruby_core/struct.yml +3 -0
- data/data/core_overlay/env.rbs +34 -0
- data/data/core_overlay/gc.rbs +50 -0
- data/data/core_overlay/hash.rbs +45 -0
- data/data/core_overlay/integer.rbs +31 -0
- data/data/core_overlay/io_buffer.rbs +27 -0
- data/data/core_overlay/kernel.rbs +40 -0
- data/data/core_overlay/match_data.rbs +28 -0
- data/data/core_overlay/module.rbs +51 -0
- data/data/core_overlay/object_space.rbs +29 -0
- data/data/core_overlay/proc.rbs +45 -0
- data/data/core_overlay/range.rbs +40 -0
- data/data/core_overlay/source_range.rbs +78 -0
- data/data/core_overlay/string.rbs +86 -0
- data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
- data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
- data/docs/handbook/01-getting-started.md +3 -3
- data/docs/handbook/06-classes.md +3 -1
- data/docs/handbook/11-sig-gen.md +18 -5
- data/docs/handbook/appendix-liskov.md +5 -0
- data/docs/manual/02-cli-reference.md +19 -1
- data/docs/manual/03-configuration.md +7 -5
- data/docs/manual/04-diagnostics.md +2 -1
- data/docs/manual/05-inspecting-types.md +50 -0
- data/docs/manual/06-baseline.md +2 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/12-caching.md +18 -1
- data/docs/manual/14-rails-quickstart.md +2 -2
- data/docs/manual/18-removing-dead-code.md +41 -4
- data/docs/manual/plugins/README.md +6 -1
- data/docs/manual/plugins/rigor-activerecord.md +21 -0
- data/docs/manual/plugins/rigor-alba.md +60 -0
- data/docs/manual/plugins/rigor-minitest.md +22 -4
- data/docs/manual/plugins/rigor-typelizer.md +71 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +11 -0
- data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
- data/lib/rigor/analysis/check_rules.rb +292 -59
- data/lib/rigor/analysis/dependency_recorder.rb +15 -0
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
- data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
- data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
- data/lib/rigor/analysis/erb_template_detector.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +29 -9
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +30 -5
- data/lib/rigor/analysis/incremental_run_slot.rb +6 -4
- data/lib/rigor/analysis/incremental_session.rb +103 -23
- data/lib/rigor/analysis/path_expansion.rb +62 -6
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
- data/lib/rigor/analysis/reachability/graph.rb +241 -37
- data/lib/rigor/analysis/reachability/scan.rb +490 -77
- data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
- data/lib/rigor/analysis/rule_catalog.rb +43 -0
- data/lib/rigor/analysis/run_cache_key.rb +16 -1
- data/lib/rigor/analysis/run_cache_probe.rb +1 -1
- data/lib/rigor/analysis/run_stats.rb +28 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +113 -6
- data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -19
- data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -1
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +62 -13
- data/lib/rigor/analysis/worker_session.rb +8 -5
- data/lib/rigor/cache/engine_source.rb +21 -7
- data/lib/rigor/cache/incremental_snapshot.rb +55 -5
- data/lib/rigor/cli/annotate_command.rb +5 -0
- data/lib/rigor/cli/baseline_command.rb +7 -18
- data/lib/rigor/cli/check_command.rb +140 -40
- data/lib/rigor/cli/docs_command.rb +5 -4
- data/lib/rigor/cli/lens_command.rb +144 -0
- data/lib/rigor/cli/lsp_command.rb +5 -2
- data/lib/rigor/cli/skill_command.rb +3 -3
- data/lib/rigor/cli/skill_describe.rb +1 -1
- data/lib/rigor/cli/trace_command.rb +5 -0
- data/lib/rigor/cli/type_of_command.rb +6 -3
- data/lib/rigor/cli/unused_command.rb +166 -27
- data/lib/rigor/cli.rb +39 -5
- data/lib/rigor/configuration/severity_profile.rb +6 -0
- data/lib/rigor/configuration.rb +61 -2
- data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
- data/lib/rigor/environment/gated_signature_guard.rb +43 -0
- data/lib/rigor/environment/gated_signature_plan.rb +339 -0
- data/lib/rigor/environment/installed_gem_set.rb +38 -11
- data/lib/rigor/environment/lockfile_resolver.rb +37 -6
- data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +316 -14
- data/lib/rigor/environment/required_features.rb +208 -0
- data/lib/rigor/environment.rb +33 -8
- data/lib/rigor/inference/acceptance.rb +28 -3
- data/lib/rigor/inference/builtins/cruby_definers.rb +78 -0
- data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
- data/lib/rigor/inference/captured_locals.rb +43 -3
- data/lib/rigor/inference/def_node_resolver.rb +23 -1
- data/lib/rigor/inference/expression_typer.rb +261 -29
- data/lib/rigor/inference/global_write_census.rb +7 -4
- data/lib/rigor/inference/guard_rebinding.rb +1 -1
- data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
- data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +286 -0
- data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
- data/lib/rigor/inference/in_effect_refinements.rb +256 -0
- data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
- data/lib/rigor/inference/key_presence_guard.rb +303 -0
- data/lib/rigor/inference/macro_block_self_type.rb +59 -12
- data/lib/rigor/inference/match_rebinding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
- data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
- data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
- data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
- data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +247 -28
- data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
- data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
- data/lib/rigor/inference/method_dispatcher.rb +96 -14
- data/lib/rigor/inference/module_function_state.rb +2 -1
- data/lib/rigor/inference/narrowing.rb +20 -6
- data/lib/rigor/inference/object_mixins.rb +180 -0
- data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
- data/lib/rigor/inference/project_patched_methods.rb +12 -7
- data/lib/rigor/inference/project_patched_scanner.rb +108 -26
- data/lib/rigor/inference/refine_census.rb +239 -0
- data/lib/rigor/inference/refine_self.rb +317 -0
- data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +475 -0
- data/lib/rigor/inference/refined_dispatch.rb +320 -0
- data/lib/rigor/inference/repeated_or_writes.rb +2 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
- data/lib/rigor/inference/scope_indexer.rb +827 -65
- data/lib/rigor/inference/statement_evaluator.rb +294 -51
- data/lib/rigor/inference/string_mutation.rb +9 -3
- data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
- data/lib/rigor/inference/untyped_rejoin.rb +75 -0
- data/lib/rigor/inference/version_guard.rb +19 -10
- data/lib/rigor/language_server/project_context.rb +87 -5
- data/lib/rigor/language_server/server.rb +18 -0
- data/lib/rigor/lens/anchor.rb +115 -0
- data/lib/rigor/lens/contract_reader.rb +118 -0
- data/lib/rigor/lens/document.rb +113 -0
- data/lib/rigor/lens/file_reader.rb +433 -0
- data/lib/rigor/lens/members.rb +153 -0
- data/lib/rigor/lens/parameters.rb +91 -0
- data/lib/rigor/lens/plugins.rb +40 -0
- data/lib/rigor/lens/run.rb +280 -0
- data/lib/rigor/lens/slot.rb +79 -0
- data/lib/rigor/lens.rb +40 -0
- data/lib/rigor/plugin/inflector.rb +1 -1
- data/lib/rigor/plugin/io_boundary.rb +42 -3
- data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
- data/lib/rigor/project_environment.rb +48 -6
- data/lib/rigor/protection/mutation_cache.rb +3 -2
- data/lib/rigor/protection/mutator.rb +1 -1
- data/lib/rigor/reflection/constant_ancestors.rb +127 -11
- data/lib/rigor/reflection/constant_path.rb +13 -2
- data/lib/rigor/reflection.rb +40 -8
- data/lib/rigor/scope/discovery_index.rb +40 -1
- data/lib/rigor/scope/resolution_chain.rb +159 -11
- data/lib/rigor/scope.rb +211 -21
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +100 -10
- data/lib/rigor/sig_gen/method_candidate.rb +2 -2
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
- data/lib/rigor/source/alias_names.rb +52 -0
- data/lib/rigor/source/literals.rb +3 -2
- data/lib/rigor/source/symbol_name.rb +20 -0
- data/lib/rigor/type/combinator.rb +34 -2
- data/lib/rigor/type/float_range.rb +4 -1
- data/lib/rigor/type/nominal.rb +6 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
- data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
- data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
- data/plugins/rigor-alba/sig/alba.rbs +4 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +7 -6
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
- data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
- data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
- data/sig/rigor/plugin/io_boundary.rbs +3 -0
- data/sig/rigor/scope.rbs +30 -4
- data/sig/rigor/source.rbs +4 -0
- data/sig/rigor.rbs +9 -4
- data/skills/rigor-project-init/references/02-configure.md +2 -2
- metadata +83 -1
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
module AcLibraryRb
|
|
2
|
+
# A segment tree over the monoid `(S, op, e)`.
|
|
3
|
+
class Segtree[S]
|
|
4
|
+
attr_reader d: Array[S]
|
|
5
|
+
attr_reader op: _Call2[S, S, S]
|
|
6
|
+
attr_reader n: Integer
|
|
7
|
+
attr_reader leaf_size: Integer
|
|
8
|
+
attr_reader log: Integer
|
|
9
|
+
|
|
10
|
+
def initialize: (Array[S] | Integer v, S e) { (S, S) -> S } -> void
|
|
11
|
+
| (Array[S] | Integer v, _Call2[S, S, S] op, S e) -> void
|
|
12
|
+
|
|
13
|
+
def set: (Integer pos, S x) -> void
|
|
14
|
+
alias []= set
|
|
15
|
+
|
|
16
|
+
def get: (Integer pos) -> S
|
|
17
|
+
alias [] get
|
|
18
|
+
|
|
19
|
+
def prod: (Integer l, Integer r) -> S
|
|
20
|
+
| (Range[Integer] range) -> S
|
|
21
|
+
|
|
22
|
+
def all_prod: () -> S
|
|
23
|
+
|
|
24
|
+
def max_right: (Integer l) { (S) -> boolish } -> Integer
|
|
25
|
+
|
|
26
|
+
def min_left: (Integer r) { (S) -> boolish } -> Integer
|
|
27
|
+
|
|
28
|
+
def update: (Integer k) -> S
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class SegTree = Segtree
|
|
32
|
+
|
|
33
|
+
# A lazy segment tree over the monoid `(S, op, e)` acted on by the maps `(F, composition, id)`, where
|
|
34
|
+
# `mapping.call(f, x)` applies `f` to `x` and `composition.call(f, g)` is `f` after `g`.
|
|
35
|
+
class LazySegtree[S, F]
|
|
36
|
+
attr_reader d: Array[S]
|
|
37
|
+
attr_reader lz: Array[F]
|
|
38
|
+
attr_reader e: S
|
|
39
|
+
attr_reader id: F
|
|
40
|
+
attr_accessor op: _Call2[S, S, S]
|
|
41
|
+
attr_accessor mapping: _Call2[F, S, S]?
|
|
42
|
+
attr_accessor composition: _Call2[F, F, F]?
|
|
43
|
+
|
|
44
|
+
# The first form is chosen when the second argument is a `Proc`.
|
|
45
|
+
def initialize: (Array[S] | Integer v, ^(S, S) -> S op, S e, _Call2[F, S, S] mapping,
|
|
46
|
+
_Call2[F, F, F] composition, F id) -> void
|
|
47
|
+
| (Array[S] | Integer v, S e, F id, ?_Call2[S, S, S]? op, ?_Call2[F, S, S]? mapping,
|
|
48
|
+
?_Call2[F, F, F]? composition) ?{ (S, S) -> S } -> void
|
|
49
|
+
|
|
50
|
+
def set_mapping: () { (F, S) -> S } -> Proc
|
|
51
|
+
|
|
52
|
+
def set_composition: () { (F, F) -> F } -> Proc
|
|
53
|
+
|
|
54
|
+
def set: (Integer pos, S x) -> void
|
|
55
|
+
alias []= set
|
|
56
|
+
|
|
57
|
+
def get: (Integer pos) -> S
|
|
58
|
+
alias [] get
|
|
59
|
+
|
|
60
|
+
def prod: (Integer l, Integer r) -> S
|
|
61
|
+
| (Range[Integer] range) -> S
|
|
62
|
+
|
|
63
|
+
def all_prod: () -> S
|
|
64
|
+
|
|
65
|
+
def apply: (Integer pos, F f) -> void
|
|
66
|
+
| (Integer l, Integer r, F f) -> void
|
|
67
|
+
| (Range[Integer] range, F f) -> void
|
|
68
|
+
|
|
69
|
+
def range_apply: (Integer l, Integer r, F f) -> void
|
|
70
|
+
|
|
71
|
+
def max_right: (Integer l) { (S) -> boolish } -> Integer
|
|
72
|
+
|
|
73
|
+
def min_left: (Integer r) { (S) -> boolish } -> Integer
|
|
74
|
+
|
|
75
|
+
def update: (Integer k) -> S
|
|
76
|
+
|
|
77
|
+
def all_apply: (Integer k, F f) -> void
|
|
78
|
+
|
|
79
|
+
def push: (Integer k) -> void
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
class LazySegTree = LazySegtree
|
|
83
|
+
end
|
|
@@ -309,8 +309,9 @@ module Rigor
|
|
|
309
309
|
args
|
|
310
310
|
end
|
|
311
311
|
|
|
312
|
+
# The name is interned later (`filter_name.to_sym`), so a name no Symbol can hold reads as nil.
|
|
312
313
|
def literal_symbol_or_string(node)
|
|
313
|
-
Rigor::Source::Literals.
|
|
314
|
+
Rigor::Source::Literals.symbol_or_string(node)&.to_s
|
|
314
315
|
end
|
|
315
316
|
|
|
316
317
|
def filter_lookup_violation(call_node, arg_node, filter_name, methods, spell_checker, ambiguous_filters:)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "rigor/source/node_children"
|
|
4
|
+
require "rigor/source/symbol_name"
|
|
4
5
|
|
|
5
6
|
require "prism"
|
|
6
7
|
|
|
@@ -81,11 +82,16 @@ module Rigor
|
|
|
81
82
|
# fixed point rather than recursing keeps a cyclic `class A < B; class B < A` source — which Ruby
|
|
82
83
|
# rejects but a static parse can still be handed — from recursing forever.
|
|
83
84
|
def close_over_roots(candidates)
|
|
84
|
-
|
|
85
|
+
groups = candidates.group_by(&:class_name)
|
|
86
|
+
# Files that name different superclasses for one class are a TypeError in Ruby, so no ancestry
|
|
87
|
+
# reading of that class is correct. It is declined outright, and so is everything beneath it.
|
|
88
|
+
conflicted = groups.filter_map { |name, group| name if group.map(&:superclass_name).uniq.size > 1 }.to_set
|
|
89
|
+
by_name = groups.except(*conflicted).transform_values { |group| merge_reopened(group) }
|
|
85
90
|
reached = @base_classes.to_set
|
|
86
91
|
loop do
|
|
87
92
|
grown = candidates.select do |entry|
|
|
88
|
-
!
|
|
93
|
+
!conflicted.include?(entry.class_name) && !reached.include?(entry.class_name) &&
|
|
94
|
+
reached.include?(entry.superclass_name)
|
|
89
95
|
end
|
|
90
96
|
break if grown.empty?
|
|
91
97
|
|
|
@@ -94,6 +100,21 @@ module Rigor
|
|
|
94
100
|
reached.filter_map { |name| by_name[name] }
|
|
95
101
|
end
|
|
96
102
|
|
|
103
|
+
# A class declared in several files that each name its superclass is one class to Ruby, so its names
|
|
104
|
+
# are the union of every file's. Keeping only the last file's entry let a sibling that declared nothing
|
|
105
|
+
# usable shadow the valid one. The `own_method_names` union assumes every reopening file is loaded; a
|
|
106
|
+
# file Zeitwerk shadows is not modelled. A superclass-less reopening is not a candidate (see visit_class).
|
|
107
|
+
def merge_reopened(entries)
|
|
108
|
+
return entries.first if entries.size == 1
|
|
109
|
+
|
|
110
|
+
first = entries.first
|
|
111
|
+
first.with(
|
|
112
|
+
declared_names: entries.flat_map(&:declared_names).uniq.sort.freeze,
|
|
113
|
+
object_reads: entries.flat_map(&:object_reads).uniq.sort.freeze,
|
|
114
|
+
own_method_names: entries.flat_map(&:own_method_names).uniq.sort.freeze
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
|
|
97
118
|
def read_safely(path)
|
|
98
119
|
@io_boundary.read_file(path)
|
|
99
120
|
rescue Plugin::AccessDeniedError, Errno::ENOENT
|
|
@@ -180,9 +201,14 @@ module Rigor
|
|
|
180
201
|
receiver.arguments.nil?
|
|
181
202
|
end
|
|
182
203
|
|
|
204
|
+
# A name whose bytes no Symbol can hold (`attributes :"\xff"` in a file that does not parse cleanly)
|
|
205
|
+
# is dropped here, so it never reaches the index or the `project_defines?` lookup that would raise.
|
|
206
|
+
# The file's other names are kept: a recoverable syntax error elsewhere in it does not void them.
|
|
183
207
|
def symbol_arguments(node)
|
|
184
208
|
(node.arguments&.arguments || []).filter_map do |argument|
|
|
185
|
-
|
|
209
|
+
next unless argument.is_a?(Prism::SymbolNode)
|
|
210
|
+
|
|
211
|
+
Rigor::Source::SymbolName.of(argument.unescaped)&.to_s
|
|
186
212
|
end
|
|
187
213
|
end
|
|
188
214
|
|
|
@@ -239,7 +239,11 @@ module Rigor
|
|
|
239
239
|
def project_defines?(class_name, method_name, scope)
|
|
240
240
|
return false unless scope.respond_to?(:user_def_through_ancestors)
|
|
241
241
|
|
|
242
|
-
|
|
242
|
+
# A name no Symbol can hold cannot be a method the project defines (#1831): decline, do not raise.
|
|
243
|
+
method = Rigor::Source::SymbolName.of(method_name.to_s)
|
|
244
|
+
return false if method.nil?
|
|
245
|
+
|
|
246
|
+
!scope.user_def_through_ancestors(class_name, method).first.nil?
|
|
243
247
|
end
|
|
244
248
|
|
|
245
249
|
# `REST::AccountSerializer` offers two readings — the namespaced `REST::Account` and the
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Plugin
|
|
5
|
+
class Activerecord < Rigor::Plugin::Base
|
|
6
|
+
# The model classes ActiveRecord loads BY NAME from an association (`belongs_to :user` → `User`), for
|
|
7
|
+
# `rigor unused` (ADR-102 WD3). The name is derived at runtime and never written in source, so a
|
|
8
|
+
# reference index sees a model reached only this way as unreferenced.
|
|
9
|
+
#
|
|
10
|
+
# The rows come from the {ModelIndex}'s association list, which {ModelDiscoverer} already extracts
|
|
11
|
+
# (class-body declarations and `with_options` groups only — not `class << self`, a `def` or another
|
|
12
|
+
# block). This module only resolves them, mirroring ActiveRecord:
|
|
13
|
+
#
|
|
14
|
+
# - `class_name:` (String or Symbol) wins; `"::Foo"` is rooted.
|
|
15
|
+
# - otherwise `has_many` / `has_and_belongs_to_many` → `name.singularize.camelize`, and
|
|
16
|
+
# `belongs_to` / `has_one` → `name.camelize` (`Reflection#derive_class_name`).
|
|
17
|
+
# - the name is tried as nested in the owner itself, then in each enclosing namespace innermost first,
|
|
18
|
+
# then at top level (`ActiveRecord::Inheritance#compute_type`).
|
|
19
|
+
# - a root is published only when the project declares a model of that name.
|
|
20
|
+
#
|
|
21
|
+
# Declined — no root, never a guess: a polymorphic association (no single target), a non-literal
|
|
22
|
+
# `class_name:` / `**opts`, `anonymous_class:`, a `with_options` group with non-literal options, and a
|
|
23
|
+
# `through:` association without a literal `class_name:` / `source_type:`. Without them a `through:` target is the SOURCE association's class on the through model, and
|
|
24
|
+
# that association (declared there, hence in this same index) roots it itself.
|
|
25
|
+
#
|
|
26
|
+
# A computed name equal to the owner's own demodulized name declines too (Rails 7.2+ tries `::Name`
|
|
27
|
+
# first; the target is then the owner itself or a top-level class).
|
|
28
|
+
#
|
|
29
|
+
# ActiveRecord takes the first candidate that is a defined constant. The index knows only models, so the
|
|
30
|
+
# walk stops (declines) at a candidate that is not a model but is a namespace of one: Rails would pick
|
|
31
|
+
# that module and fail, never the next candidate. A non-model class at an earlier candidate is invisible
|
|
32
|
+
# here.
|
|
33
|
+
#
|
|
34
|
+
# The roots are flat: an association on a model `rigor unused` itself reports as dead still roots its
|
|
35
|
+
# target, exactly as the other plugins' roots do.
|
|
36
|
+
module AssociationRoots
|
|
37
|
+
COLLECTION_MACROS = %i[has_many has_and_belongs_to_many].freeze
|
|
38
|
+
ROOTED_MACROS = (COLLECTION_MACROS + %i[belongs_to has_one]).freeze
|
|
39
|
+
VALID_NAME = /\A(?:::)?[A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*\z/
|
|
40
|
+
private_constant :VALID_NAME
|
|
41
|
+
|
|
42
|
+
module_function
|
|
43
|
+
|
|
44
|
+
# Sorted, unique model class names. Raises {Rigor::Plugin::Inflector::Unavailable} when a name had to
|
|
45
|
+
# be inflected and could not be.
|
|
46
|
+
def call(model_index)
|
|
47
|
+
namespaces = model_index.class_names.flat_map { |name| prefixes(name) }.to_set
|
|
48
|
+
roots = model_index.entries.each_value.flat_map do |entry|
|
|
49
|
+
entry.associations.filter_map { |row| resolve(entry.class_name, row, model_index, namespaces) }
|
|
50
|
+
end
|
|
51
|
+
roots.uniq.sort
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def prefixes(name)
|
|
55
|
+
segments = name.split("::")
|
|
56
|
+
(1...segments.length).map { |n| segments.first(n).join("::") }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def resolve(owner, row, model_index, namespaces)
|
|
60
|
+
return nil unless ROOTED_MACROS.include?(row[:macro])
|
|
61
|
+
return nil if row[:polymorphic] || row[:roots_declined]
|
|
62
|
+
|
|
63
|
+
type_name = type_name(row)
|
|
64
|
+
return nil if type_name.nil? || !VALID_NAME.match?(type_name)
|
|
65
|
+
# Rails 7.2+ (`MacroReflection#_klass`) tries `::Name` first when the computed name is the owner's
|
|
66
|
+
# own demodulized name, so the target may be the owner or a top-level class: decline.
|
|
67
|
+
return nil if type_name == owner.split("::").last
|
|
68
|
+
|
|
69
|
+
candidates(owner, type_name).each do |candidate|
|
|
70
|
+
return candidate if model_index.model?(candidate)
|
|
71
|
+
return nil if namespaces.include?(candidate)
|
|
72
|
+
end
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def type_name(row)
|
|
77
|
+
option = row[:class_name_option]
|
|
78
|
+
option = row[:source_type_option] if option.nil? && row[:through]
|
|
79
|
+
return nil if option == :dynamic
|
|
80
|
+
return option unless option.nil?
|
|
81
|
+
return nil if row[:through]
|
|
82
|
+
|
|
83
|
+
name = row[:name].to_s
|
|
84
|
+
name = Rigor::Plugin::Inflector.singularize(name) if COLLECTION_MACROS.include?(row[:macro])
|
|
85
|
+
Rigor::Plugin::Inflector.camelize(name)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# `compute_type`: a rooted name is looked up as is; otherwise the owner ITSELF first (`Post has_many
|
|
89
|
+
# :comments` tries `Post::Comment`), then its enclosing namespaces innermost first, then top level.
|
|
90
|
+
def candidates(owner, type_name)
|
|
91
|
+
return [type_name.delete_prefix("::")] if type_name.start_with?("::")
|
|
92
|
+
|
|
93
|
+
segments = owner.split("::")
|
|
94
|
+
nested = segments.length.downto(1).map { |n| "#{segments.first(n).join('::')}::#{type_name}" }
|
|
95
|
+
nested + [type_name]
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -940,7 +940,7 @@ module Rigor
|
|
|
940
940
|
node.name == :define_singleton_method
|
|
941
941
|
arg0 = node.arguments&.arguments&.first
|
|
942
942
|
name = Rigor::Source::Literals.symbol_name(arg0) || (arg0.is_a?(Prism::StringNode) ? arg0.unescaped : nil)
|
|
943
|
-
key = TABLE_NAME_DECORATOR_METHODS[name
|
|
943
|
+
key = TABLE_NAME_DECORATOR_METHODS[Rigor::Source::SymbolName.of(name)] if name
|
|
944
944
|
if key
|
|
945
945
|
readers << key
|
|
946
946
|
decorators[key] = node.block.is_a?(Prism::BlockNode) ? literal_def_value(node.block.body) : { computed: true }
|
|
@@ -1263,9 +1263,10 @@ module Rigor
|
|
|
1263
1263
|
# association / enum / validation declared inside is invisible to the discoverer, turning
|
|
1264
1264
|
# `where(<assoc>: ...)` into a false `unknown-column`. Nested `with_options` blocks recurse.
|
|
1265
1265
|
#
|
|
1266
|
-
#
|
|
1267
|
-
#
|
|
1268
|
-
#
|
|
1266
|
+
# This method returns only the nodes and does NOT merge the group's own options (e.g.
|
|
1267
|
+
# `with_options class_name: 'Account'`); {#association_calls} is the variant that does, and it is
|
|
1268
|
+
# what the association rows use. The other lookups (enums, scopes, validations, callbacks) ignore
|
|
1269
|
+
# group options.
|
|
1269
1270
|
def declaration_calls(body)
|
|
1270
1271
|
return [] if body.nil?
|
|
1271
1272
|
|
|
@@ -1291,20 +1292,42 @@ module Rigor
|
|
|
1291
1292
|
return [] if body.nil?
|
|
1292
1293
|
|
|
1293
1294
|
rows = []
|
|
1294
|
-
|
|
1295
|
+
association_calls(body, [], false).each do |node, inherited, opaque|
|
|
1295
1296
|
kind = ASSOCIATION_METHODS[node.name]
|
|
1296
1297
|
next if kind.nil?
|
|
1297
1298
|
next if node.receiver # skip `self.has_many` and similar
|
|
1298
1299
|
|
|
1299
|
-
row = build_association_row(node, kind)
|
|
1300
|
+
row = build_association_row(node, kind, inherited, opaque)
|
|
1300
1301
|
rows << row unless row.nil?
|
|
1301
1302
|
end
|
|
1302
1303
|
rows
|
|
1303
1304
|
end
|
|
1304
1305
|
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1306
|
+
# {#declaration_calls} that also carries the options of the enclosing `with_options(...)` groups, as
|
|
1307
|
+
# `[node, option_args, opaque]`. `option_args` are the keyword-hash arguments of those groups, which
|
|
1308
|
+
# ActiveRecord merges under the call's own options; `opaque` is true when a group passes anything
|
|
1309
|
+
# else (`with_options opts do`), so its options are unknown.
|
|
1310
|
+
def association_calls(body, inherited, opaque)
|
|
1311
|
+
return [] if body.nil?
|
|
1312
|
+
|
|
1313
|
+
body.compact_child_nodes.flat_map do |node|
|
|
1314
|
+
next [] unless node.is_a?(Prism::CallNode)
|
|
1315
|
+
|
|
1316
|
+
if node.name == :with_options && node.block.is_a?(Prism::BlockNode)
|
|
1317
|
+
args = node.arguments&.arguments || []
|
|
1318
|
+
hashes = args.grep(Prism::KeywordHashNode)
|
|
1319
|
+
association_calls(node.block.body, hashes + inherited, opaque || hashes.size != args.size)
|
|
1320
|
+
else
|
|
1321
|
+
[[node, inherited, opaque]]
|
|
1322
|
+
end
|
|
1323
|
+
end
|
|
1324
|
+
end
|
|
1325
|
+
|
|
1326
|
+
def build_association_row(node, kind, inherited, opaque)
|
|
1327
|
+
own = node.arguments&.arguments
|
|
1328
|
+
return nil if own.nil? || own.empty?
|
|
1329
|
+
|
|
1330
|
+
args = own + inherited
|
|
1308
1331
|
|
|
1309
1332
|
name = Rigor::Source::Literals.symbol_name(args.first)
|
|
1310
1333
|
return nil if name.nil?
|
|
@@ -1322,7 +1345,65 @@ module Rigor
|
|
|
1322
1345
|
end
|
|
1323
1346
|
|
|
1324
1347
|
{ name: name, kind: kind, target: target, polymorphic: polymorphic,
|
|
1325
|
-
nullable: association_nullable?(node.name, args)
|
|
1348
|
+
nullable: association_nullable?(node.name, args),
|
|
1349
|
+
macro: node.name, class_name_option: class_name_option(args),
|
|
1350
|
+
through: association_key?(args, "through"),
|
|
1351
|
+
source_type_option: literal_option(args, "source_type"),
|
|
1352
|
+
roots_declined: opaque || association_key?(args, "anonymous_class") || own_options_unknown?(own) }
|
|
1353
|
+
end
|
|
1354
|
+
|
|
1355
|
+
# The `class_name:` option as ActiveRecord's `compute_type` will read it: the literal String (a
|
|
1356
|
+
# Symbol is `to_s`'d by Rails too), `nil` when the option is absent, and `:dynamic` when it is
|
|
1357
|
+
# present but not a literal — the `:reachability_roots` consumer must decline then, where the
|
|
1358
|
+
# `target` above falls back to the inferred name.
|
|
1359
|
+
# A `**opts` splat may carry a `class_name:` too, so it reads as present-but-unknown.
|
|
1360
|
+
def class_name_option(args)
|
|
1361
|
+
literal_option(args, "class_name") ||
|
|
1362
|
+
(args.any? { |a| a.is_a?(Prism::KeywordHashNode) && a.elements.any?(Prism::AssocSplatNode) } ? :dynamic : nil)
|
|
1363
|
+
end
|
|
1364
|
+
|
|
1365
|
+
def literal_option(args, key)
|
|
1366
|
+
args.each do |arg|
|
|
1367
|
+
next unless arg.is_a?(Prism::KeywordHashNode)
|
|
1368
|
+
|
|
1369
|
+
arg.elements.each do |pair|
|
|
1370
|
+
next unless pair.is_a?(Prism::AssocNode) && Source::Literals.symbol_named?(pair.key, key)
|
|
1371
|
+
|
|
1372
|
+
value = pair.value
|
|
1373
|
+
return value.unescaped if value.is_a?(Prism::StringNode) || value.is_a?(Prism::SymbolNode)
|
|
1374
|
+
|
|
1375
|
+
return :dynamic
|
|
1376
|
+
end
|
|
1377
|
+
end
|
|
1378
|
+
nil
|
|
1379
|
+
end
|
|
1380
|
+
|
|
1381
|
+
ROOT_OPTION_KEYS = %w[class_name polymorphic through source_type anonymous_class].freeze
|
|
1382
|
+
LITERAL_OPTION_VALUES = [Prism::StringNode, Prism::SymbolNode, Prism::TrueNode, Prism::FalseNode].freeze
|
|
1383
|
+
private_constant :ROOT_OPTION_KEYS, :LITERAL_OPTION_VALUES
|
|
1384
|
+
|
|
1385
|
+
# Whether the association call's OWN arguments hide an option that decides its target class: a
|
|
1386
|
+
# positional options hash or variable, a `**splat`, or a non-literal value for one of the keys. A
|
|
1387
|
+
# `with_options` group's literal value must not stand in for it.
|
|
1388
|
+
def own_options_unknown?(own)
|
|
1389
|
+
own.drop(1).any? do |arg|
|
|
1390
|
+
next false if arg.is_a?(Prism::LambdaNode) # the scope argument carries no class option
|
|
1391
|
+
next true unless arg.is_a?(Prism::KeywordHashNode)
|
|
1392
|
+
|
|
1393
|
+
arg.elements.any? do |pair|
|
|
1394
|
+
next true unless pair.is_a?(Prism::AssocNode)
|
|
1395
|
+
|
|
1396
|
+
ROOT_OPTION_KEYS.any? { |key| Source::Literals.symbol_named?(pair.key, key) } &&
|
|
1397
|
+
LITERAL_OPTION_VALUES.none? { |klass| pair.value.is_a?(klass) }
|
|
1398
|
+
end
|
|
1399
|
+
end
|
|
1400
|
+
end
|
|
1401
|
+
|
|
1402
|
+
def association_key?(args, key)
|
|
1403
|
+
args.any? do |arg|
|
|
1404
|
+
arg.is_a?(Prism::KeywordHashNode) &&
|
|
1405
|
+
arg.elements.any? { |pair| pair.is_a?(Prism::AssocNode) && Source::Literals.symbol_named?(pair.key, key) }
|
|
1406
|
+
end
|
|
1326
1407
|
end
|
|
1327
1408
|
|
|
1328
1409
|
# Whether a `:singular` association's accessor can return `nil`. `has_one` genuinely can (no
|
|
@@ -1356,6 +1437,9 @@ module Rigor
|
|
|
1356
1437
|
|
|
1357
1438
|
return true if pair.value.is_a?(Prism::TrueNode)
|
|
1358
1439
|
return false if pair.value.is_a?(Prism::FalseNode)
|
|
1440
|
+
|
|
1441
|
+
# The call's own non-literal value shadows a `with_options` group's literal one.
|
|
1442
|
+
return nil
|
|
1359
1443
|
end
|
|
1360
1444
|
end
|
|
1361
1445
|
nil
|
|
@@ -7,6 +7,7 @@ require_relative "activerecord/schema_parser"
|
|
|
7
7
|
require_relative "activerecord/structure_sql_parser"
|
|
8
8
|
require_relative "activerecord/model_index"
|
|
9
9
|
require_relative "activerecord/model_discoverer"
|
|
10
|
+
require_relative "activerecord/association_roots"
|
|
10
11
|
require_relative "activerecord/analyzer"
|
|
11
12
|
require_relative "activerecord/effects"
|
|
12
13
|
|
|
@@ -64,6 +65,11 @@ module Rigor
|
|
|
64
65
|
manifest(
|
|
65
66
|
id: "activerecord",
|
|
66
67
|
target_gems: ["activerecord"],
|
|
68
|
+
# 0.14.0, 2026-10-10 (#1721) — the plugin publishes `:reachability_roots`: the models ActiveRecord loads
|
|
69
|
+
# by name from an association. The association rows gain `macro`, `class_name_option`, `through` and
|
|
70
|
+
# `source_type_option`, so a cached 0.13.0 payload lacks the fields the roots are derived from and
|
|
71
|
+
# would publish none; the bump (part of the producer cache KEY) is what stops a warm run serving it.
|
|
72
|
+
#
|
|
67
73
|
# 0.13.0, 2026-09-24 (#1321) — the class-side `Model.find` with two or more arguments returns
|
|
68
74
|
# `Array[Model]` instead of the model, and its `model-call` note says so. A model that defines its own
|
|
69
75
|
# `self.find` keeps its own answer; the note reads syntax alone and does not see that override. No
|
|
@@ -118,7 +124,7 @@ module Rigor
|
|
|
118
124
|
# a scope lambda body / class-method body now contributes `Relation[Model]` via `scope.self_type`
|
|
119
125
|
# instead of falling through to `Kernel#select` (the IO multiplexer, `Array[String]` return). Plus
|
|
120
126
|
# `:select` added to the relation-entry-point list.
|
|
121
|
-
version: "0.
|
|
127
|
+
version: "0.14.0",
|
|
122
128
|
description: "Types ActiveRecord finders against the project's db/schema.rb and AR models.",
|
|
123
129
|
config_schema: {
|
|
124
130
|
"schema_file" => { kind: :string, default: "db/schema.rb" },
|
|
@@ -128,7 +134,7 @@ module Rigor
|
|
|
128
134
|
"model_search_paths" => { kind: :array, default: ["app/models"] },
|
|
129
135
|
"model_base_classes" => { kind: :array, default: %w[ApplicationRecord ActiveRecord::Base] }
|
|
130
136
|
},
|
|
131
|
-
produces: [
|
|
137
|
+
produces: %i[model_index reachability_roots],
|
|
132
138
|
# ADR-25 — the bundled `ActiveRecord::Relation` RBS that relation-typed call sites (`has_many`
|
|
133
139
|
# accessors, `Model.where`, scopes) dispatch against.
|
|
134
140
|
signature_paths: ["sig"],
|
|
@@ -241,6 +247,9 @@ module Rigor
|
|
|
241
247
|
# this, so the disclosure is in the coordinator's table before a single worker is forked.
|
|
242
248
|
disclose_load_errors
|
|
243
249
|
return if index.nil? || index.empty?
|
|
250
|
+
|
|
251
|
+
# Roots do not read columns, so reduced mode (Redmine) publishes them too.
|
|
252
|
+
publish_association_roots(services, index)
|
|
244
253
|
# Reduced mode stays UNPUBLISHED. Every consumer reads `columns:` as authoritative and fires on a
|
|
245
254
|
# key missing from it — rigor-actionpack's `permit(:title)` check and rigor-shoulda-matchers'
|
|
246
255
|
# `have_db_column(:title)` / `validate_presence_of(:title)` matchers both do — so publishing the
|
|
@@ -256,6 +265,18 @@ module Rigor
|
|
|
256
265
|
)
|
|
257
266
|
end
|
|
258
267
|
|
|
268
|
+
# ADR-102 WD3 — the models an association loads by name; see {AssociationRoots}. An unavailable
|
|
269
|
+
# inflector (ADR-39: decline, never approximate) leaves the report as it was.
|
|
270
|
+
def publish_association_roots(services, index)
|
|
271
|
+
roots = AssociationRoots.call(index)
|
|
272
|
+
return if roots.empty?
|
|
273
|
+
|
|
274
|
+
services.fact_store.publish(plugin_id: manifest.id, name: :reachability_roots, value: roots)
|
|
275
|
+
rescue Rigor::Plugin::Inflector::Unavailable
|
|
276
|
+
nil
|
|
277
|
+
end
|
|
278
|
+
private :publish_association_roots
|
|
279
|
+
|
|
259
280
|
def diagnostics_for_file(path:, scope:, root:)
|
|
260
281
|
index = model_index
|
|
261
282
|
# The schema-load disclosure is independent of whether an index was built: reduced mode still
|
|
@@ -401,6 +422,7 @@ module Rigor
|
|
|
401
422
|
|
|
402
423
|
if call_node.receiver
|
|
403
424
|
class_call_return_type(call_node, scope, index) ||
|
|
425
|
+
relation_splat_find_return_type(call_node, scope) ||
|
|
404
426
|
relation_call_return_type(call_node, scope, index) ||
|
|
405
427
|
instance_call_return_type(call_node, scope, index)
|
|
406
428
|
else
|
|
@@ -670,6 +692,25 @@ module Rigor
|
|
|
670
692
|
relation_of(model_name)
|
|
671
693
|
end
|
|
672
694
|
|
|
695
|
+
# `relation.find(*ids)` answers by argument count as `Model.find` does (`finder_return_type`), counting a splat or
|
|
696
|
+
# a `**` hash as one argument, which is the policy the bundled `Relation#find` documents: `find(*ids)` is the
|
|
697
|
+
# element, and `find(id, *ids)` and `find(*args, **opts)` are Arrays. The engine lets a splat stand for any
|
|
698
|
+
# number of arguments (#1801), so through the RBS overloads `find(*ids)` would join `Dynamic[Array[Elem] |
|
|
699
|
+
# Elem]`, on which a following `update` or `save` no longer resolves on the model and loses its
|
|
700
|
+
# `io.db.write`. Only a block-less call with a splat is answered here; every other `find` stays with the RBS.
|
|
701
|
+
def relation_splat_find_return_type(call_node, scope)
|
|
702
|
+
return nil unless call_node.name == :find && call_node.block.nil?
|
|
703
|
+
return nil unless call_node.arguments&.arguments&.any?(Prism::SplatNode)
|
|
704
|
+
|
|
705
|
+
model_name = relation_element_class_name(scope.type_of(call_node.receiver))
|
|
706
|
+
return nil if model_name.nil?
|
|
707
|
+
|
|
708
|
+
model = Rigor::Type::Combinator.nominal_of(model_name)
|
|
709
|
+
return model if call_argument_count(call_node) < 2
|
|
710
|
+
|
|
711
|
+
Rigor::Type::Combinator.nominal_of("Array", type_args: [model])
|
|
712
|
+
end
|
|
713
|
+
|
|
673
714
|
# Whether `name` is a declared `scope` on ANY model in the index. A run-lifetime memoised Set so the
|
|
674
715
|
# per-call check in `relation_call_return_type` stays O(1).
|
|
675
716
|
def scope_name?(name, index)
|
|
@@ -292,7 +292,9 @@ module ActiveRecord
|
|
|
292
292
|
# loses its `io.db.write`. So a single argument stays `Elem`. A
|
|
293
293
|
# splat and a `**` hash each count as one argument, so `find(*ids)`
|
|
294
294
|
# is `Elem`, while `find(id, *ids)` and `find(*args, **opts)` are
|
|
295
|
-
# Arrays even when the splat or the hash turns out empty.
|
|
295
|
+
# Arrays even when the splat or the hash turns out empty. The engine
|
|
296
|
+
# reads a splat as any number of arguments (#1801), so the plugin
|
|
297
|
+
# answers a splat call by this count (`relation_splat_find_return_type`).
|
|
296
298
|
#
|
|
297
299
|
# With a block `find` is `Enumerable#find` (`return super if
|
|
298
300
|
# block_given?`), so the id overloads do not apply: no argument
|