rigortype 0.4.1 → 0.4.2
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 +6 -0
- data/docs/handbook/appendix-liskov.md +5 -0
- data/docs/manual/03-configuration.md +1 -1
- data/docs/manual/04-diagnostics.md +2 -1
- data/docs/manual/06-baseline.md +2 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/18-removing-dead-code.md +32 -2
- 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 +4 -0
- data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
- data/lib/rigor/analysis/check_rules.rb +289 -57
- 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/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 +3 -1
- data/lib/rigor/analysis/incremental_session.rb +14 -1
- data/lib/rigor/analysis/reachability/graph.rb +123 -23
- data/lib/rigor/analysis/reachability/scan.rb +290 -36
- data/lib/rigor/analysis/rule_catalog.rb +43 -0
- data/lib/rigor/analysis/run_cache_key.rb +16 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +87 -5
- data/lib/rigor/analysis/runner/pool_coordinator.rb +50 -11
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +34 -3
- data/lib/rigor/analysis/worker_session.rb +7 -4
- data/lib/rigor/cache/incremental_snapshot.rb +22 -0
- 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 +2 -1
- data/lib/rigor/cli/trace_command.rb +5 -0
- data/lib/rigor/cli/type_of_command.rb +5 -3
- data/lib/rigor/cli/unused_command.rb +13 -7
- data/lib/rigor/cli.rb +10 -2
- data/lib/rigor/configuration/severity_profile.rb +6 -0
- data/lib/rigor/configuration.rb +46 -2
- data/lib/rigor/environment/gated_signature_guard.rb +43 -0
- data/lib/rigor/environment/gated_signature_plan.rb +339 -0
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +283 -14
- data/lib/rigor/environment/required_features.rb +206 -0
- data/lib/rigor/environment.rb +21 -3
- data/lib/rigor/inference/acceptance.rb +28 -3
- data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
- data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
- data/lib/rigor/inference/def_node_resolver.rb +23 -1
- data/lib/rigor/inference/expression_typer.rb +215 -16
- 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 +282 -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/method_dispatcher/alias_strict_nominals.rb +36 -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/keyword_arguments.rb +267 -0
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +259 -28
- data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
- data/lib/rigor/inference/method_dispatcher.rb +44 -11
- 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_methods.rb +10 -5
- data/lib/rigor/inference/project_patched_scanner.rb +71 -3
- data/lib/rigor/inference/refine_census.rb +239 -0
- data/lib/rigor/inference/refine_self.rb +317 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
- data/lib/rigor/inference/refined_dispatch.rb +272 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
- data/lib/rigor/inference/scope_indexer.rb +687 -60
- data/lib/rigor/inference/statement_evaluator.rb +71 -20
- data/lib/rigor/inference/string_mutation.rb +9 -3
- data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
- data/lib/rigor/inference/version_guard.rb +19 -10
- data/lib/rigor/language_server/project_context.rb +31 -0
- data/lib/rigor/language_server/server.rb +18 -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 +29 -6
- data/lib/rigor/protection/mutation_cache.rb +1 -1
- 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 +189 -10
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +55 -5
- 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-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 +27 -4
- data/sig/rigor/source.rbs +4 -0
- data/sig/rigor.rbs +3 -1
- metadata +66 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# ac-library-rb's math and string algorithms, instance methods of `AcLibraryRb` that a caller reaches by
|
|
2
|
+
# including the module.
|
|
3
|
+
module AcLibraryRb
|
|
4
|
+
# A callable taking two arguments: a `Proc`, a lambda or a `Method`.
|
|
5
|
+
interface _Call2[A, B, R]
|
|
6
|
+
def call: (A, B) -> R
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# An object `Deque#replace` can read elements from.
|
|
10
|
+
interface _ToA[T]
|
|
11
|
+
def to_a: () -> Array[T]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def convolution: (Array[Integer] a, Array[Integer] b, ?mod: Integer, ?k: Integer, ?z: Integer) -> Array[Integer]
|
|
15
|
+
|
|
16
|
+
# `[remainder, modulus]`, or `[0, 0]` when there is no solution.
|
|
17
|
+
def crt: (Array[Integer] r, Array[Integer] m) -> [Integer, Integer]
|
|
18
|
+
|
|
19
|
+
def inv_gcd: (Integer a, Integer b) -> [Integer, Integer]
|
|
20
|
+
|
|
21
|
+
def floor_sum: (Integer n, Integer m, Integer a, Integer b) -> Integer
|
|
22
|
+
|
|
23
|
+
def floor_sum_unsigned: (Integer n, Integer m, Integer a, Integer b) -> Integer
|
|
24
|
+
|
|
25
|
+
def inv_mod: (Integer x, Integer m) -> Integer
|
|
26
|
+
|
|
27
|
+
def _inv_gcd: (Integer a, Integer b) -> [Integer, Integer]
|
|
28
|
+
|
|
29
|
+
def pow_mod: (Integer x, Integer n, Integer m) -> Integer
|
|
30
|
+
|
|
31
|
+
def suffix_array: (String s) -> Array[Integer]
|
|
32
|
+
| (Array[untyped] a, ?Integer? upper) -> Array[Integer]
|
|
33
|
+
|
|
34
|
+
def sa_is: (Array[Integer] s, Integer upper) -> Array[Integer]
|
|
35
|
+
|
|
36
|
+
def sa_is_induce: (Array[Integer] s, Array[bool] ls, Array[Integer] sum_l, Array[Integer] sum_s,
|
|
37
|
+
Array[Integer] lms) -> Array[Integer]
|
|
38
|
+
|
|
39
|
+
def lcp_array: (String | Array[untyped] s, Array[Integer] sa) -> Array[Integer]
|
|
40
|
+
|
|
41
|
+
def z_algorithm: (String | Array[untyped] s) -> Array[Integer]
|
|
42
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
module AcLibraryRb
|
|
2
|
+
# An integer modulo the global modulus set by `ModInt.set_mod`.
|
|
3
|
+
class ModInt < ::Numeric
|
|
4
|
+
def self.set_mod: (Integer mod) -> bool
|
|
5
|
+
|
|
6
|
+
def self.mod=: (Integer mod) -> bool
|
|
7
|
+
|
|
8
|
+
def self.mod: () -> Integer
|
|
9
|
+
|
|
10
|
+
def self.raw: (_ToI val) -> ModInt
|
|
11
|
+
|
|
12
|
+
def self.prime?: (Integer n) -> bool
|
|
13
|
+
|
|
14
|
+
def self.inv_gcd: (Integer a, Integer b) -> [Integer, Integer]
|
|
15
|
+
|
|
16
|
+
attr_accessor val: Integer
|
|
17
|
+
alias to_i val
|
|
18
|
+
|
|
19
|
+
def initialize: (?_ToI val) -> void
|
|
20
|
+
|
|
21
|
+
def inc!: () -> self
|
|
22
|
+
|
|
23
|
+
def dec!: () -> self
|
|
24
|
+
|
|
25
|
+
def add!: (_ToI other) -> self
|
|
26
|
+
|
|
27
|
+
def sub!: (_ToI other) -> self
|
|
28
|
+
|
|
29
|
+
def mul!: (_ToI other) -> self
|
|
30
|
+
|
|
31
|
+
def div!: (_ToI other) -> self
|
|
32
|
+
|
|
33
|
+
def +@: () -> self
|
|
34
|
+
|
|
35
|
+
def -@: () -> ModInt
|
|
36
|
+
|
|
37
|
+
# Answers the Integer `0` when the modulus is 1. Declared `ModInt` all the same: a union return reached
|
|
38
|
+
# `1 + m ** 2` as an argument, where overload selection reads a union whole (#1356), typed the sum `Integer`
|
|
39
|
+
# and reported `call.undefined-method` for `.val` on correct code.
|
|
40
|
+
def **: (Integer other) -> ModInt
|
|
41
|
+
alias pow **
|
|
42
|
+
|
|
43
|
+
def inv: () -> ModInt
|
|
44
|
+
|
|
45
|
+
def coerce: (_ToI other) -> [ModInt, ModInt]
|
|
46
|
+
|
|
47
|
+
def +: (_ToI other) -> ModInt
|
|
48
|
+
|
|
49
|
+
def -: (_ToI other) -> ModInt
|
|
50
|
+
|
|
51
|
+
def *: (_ToI other) -> ModInt
|
|
52
|
+
|
|
53
|
+
def /: (_ToI other) -> ModInt
|
|
54
|
+
|
|
55
|
+
def ==: (untyped other) -> bool
|
|
56
|
+
|
|
57
|
+
def pred: () -> ModInt
|
|
58
|
+
|
|
59
|
+
def succ: () -> ModInt
|
|
60
|
+
|
|
61
|
+
def zero?: () -> bool
|
|
62
|
+
|
|
63
|
+
def dup: () -> ModInt
|
|
64
|
+
|
|
65
|
+
def to_int: () -> Integer
|
|
66
|
+
|
|
67
|
+
def to_s: () -> String
|
|
68
|
+
|
|
69
|
+
def inspect: () -> String
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# The core extensions under `ac-library-rb/core_ext/`. `ac-library-rb/modint` loads the `ModInt` conversions;
|
|
2
|
+
# `Integer#divisors` and the `Array` conversions need `ac-library-rb/core_ext/all` (or `integer`).
|
|
3
|
+
class Object
|
|
4
|
+
private
|
|
5
|
+
|
|
6
|
+
def ModInt: (_ToI val) -> AcLibraryRb::ModInt
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class Array[unchecked out Elem]
|
|
10
|
+
def to_fenwick_tree: () -> AcLibraryRb::FenwickTree
|
|
11
|
+
alias to_fetree to_fenwick_tree
|
|
12
|
+
|
|
13
|
+
# The element type is not carried over: a literal array would pin it to the literals it starts with.
|
|
14
|
+
def to_priority_queue: () -> AcLibraryRb::PriorityQueue[untyped]
|
|
15
|
+
alias to_pq to_priority_queue
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class Integer
|
|
19
|
+
# `ModInt#coerce` makes an Integer on the left answer a `ModInt`.
|
|
20
|
+
def +: (AcLibraryRb::ModInt) -> AcLibraryRb::ModInt | ...
|
|
21
|
+
def -: (AcLibraryRb::ModInt) -> AcLibraryRb::ModInt | ...
|
|
22
|
+
def *: (AcLibraryRb::ModInt) -> AcLibraryRb::ModInt | ...
|
|
23
|
+
def /: (AcLibraryRb::ModInt) -> AcLibraryRb::ModInt | ...
|
|
24
|
+
|
|
25
|
+
def divisors: () -> Array[Integer]
|
|
26
|
+
|
|
27
|
+
def each_divisor: () { (Integer) -> void } -> Array[Integer]
|
|
28
|
+
| () -> Enumerator[Integer, Array[Integer]]
|
|
29
|
+
|
|
30
|
+
def to_modint: () -> AcLibraryRb::ModInt
|
|
31
|
+
alias to_m to_modint
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class String
|
|
35
|
+
def to_modint: () -> AcLibraryRb::ModInt
|
|
36
|
+
alias to_m to_modint
|
|
37
|
+
end
|
|
@@ -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:)
|
|
@@ -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
|