rigortype 0.1.3 → 0.1.5
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 +154 -33
- data/lib/rigor/analysis/check_rules.rb +10 -18
- data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +75 -0
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +47 -21
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
- data/lib/rigor/analysis/dependency_source_inference/index.rb +32 -3
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +1 -1
- data/lib/rigor/analysis/dependency_source_inference.rb +1 -0
- data/lib/rigor/analysis/diagnostic.rb +0 -2
- data/lib/rigor/analysis/fact_store.rb +26 -6
- data/lib/rigor/analysis/result.rb +11 -3
- data/lib/rigor/analysis/rule_catalog.rb +2 -2
- data/lib/rigor/analysis/run_stats.rb +193 -0
- data/lib/rigor/analysis/runner.rb +498 -12
- data/lib/rigor/analysis/worker_session.rb +327 -0
- data/lib/rigor/builtins/imported_refinements.rb +364 -55
- data/lib/rigor/builtins/regex_refinement.rb +17 -12
- data/lib/rigor/cache/descriptor.rb +1 -1
- data/lib/rigor/cache/rbs_descriptor.rb +3 -1
- data/lib/rigor/cache/store.rb +39 -6
- data/lib/rigor/cli/diff_command.rb +1 -1
- data/lib/rigor/cli/sig_gen_command.rb +173 -0
- data/lib/rigor/cli/type_of_command.rb +1 -1
- data/lib/rigor/cli/type_scan_renderer.rb +1 -1
- data/lib/rigor/cli/type_scan_report.rb +2 -2
- data/lib/rigor/cli.rb +61 -3
- data/lib/rigor/configuration/dependencies.rb +2 -2
- data/lib/rigor/configuration.rb +131 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +198 -0
- data/lib/rigor/environment/class_registry.rb +12 -3
- data/lib/rigor/environment/lockfile_resolver.rb +125 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +126 -0
- data/lib/rigor/environment/rbs_coverage_report.rb +112 -0
- data/lib/rigor/environment/rbs_loader.rb +194 -6
- data/lib/rigor/environment/reflection.rb +152 -0
- data/lib/rigor/environment.rb +109 -6
- data/lib/rigor/flow_contribution/conflict.rb +2 -2
- data/lib/rigor/flow_contribution/element.rb +1 -1
- data/lib/rigor/flow_contribution/fact.rb +1 -1
- data/lib/rigor/flow_contribution/merge_result.rb +1 -1
- data/lib/rigor/flow_contribution/merger.rb +3 -3
- data/lib/rigor/flow_contribution.rb +2 -2
- data/lib/rigor/inference/acceptance.rb +35 -1
- data/lib/rigor/inference/block_parameter_binder.rb +0 -2
- data/lib/rigor/inference/builtins/method_catalog.rb +12 -5
- data/lib/rigor/inference/builtins/numeric_catalog.rb +15 -4
- data/lib/rigor/inference/coverage_scanner.rb +1 -1
- data/lib/rigor/inference/expression_typer.rb +77 -11
- data/lib/rigor/inference/fallback.rb +1 -1
- data/lib/rigor/inference/macro_block_self_type.rb +96 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +3 -5
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +29 -41
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -3
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -4
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +135 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +7 -12
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +27 -11
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +46 -44
- data/lib/rigor/inference/method_dispatcher.rb +274 -5
- data/lib/rigor/inference/method_parameter_binder.rb +22 -14
- data/lib/rigor/inference/narrowing.rb +129 -12
- data/lib/rigor/inference/rbs_type_translator.rb +0 -2
- data/lib/rigor/inference/scope_indexer.rb +14 -9
- data/lib/rigor/inference/statement_evaluator.rb +7 -7
- data/lib/rigor/inference/synthetic_method.rb +86 -0
- data/lib/rigor/inference/synthetic_method_index.rb +82 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +521 -0
- data/lib/rigor/plugin/blueprint.rb +60 -0
- data/lib/rigor/plugin/io_boundary.rb +0 -2
- data/lib/rigor/plugin/loader.rb +5 -3
- data/lib/rigor/plugin/macro/block_as_method.rb +131 -0
- data/lib/rigor/plugin/macro/external_file.rb +143 -0
- data/lib/rigor/plugin/macro/heredoc_template.rb +201 -0
- data/lib/rigor/plugin/macro/trait_registry.rb +198 -0
- data/lib/rigor/plugin/macro.rb +31 -0
- data/lib/rigor/plugin/manifest.rb +102 -10
- data/lib/rigor/plugin/registry.rb +43 -2
- data/lib/rigor/plugin/services.rb +1 -1
- data/lib/rigor/plugin/type_node_resolver.rb +52 -0
- data/lib/rigor/plugin.rb +2 -0
- data/lib/rigor/rbs_extended/reporter.rb +91 -0
- data/lib/rigor/rbs_extended.rb +131 -32
- data/lib/rigor/scope.rb +25 -8
- data/lib/rigor/sig_gen/classification.rb +36 -0
- data/lib/rigor/sig_gen/generator.rb +1048 -0
- data/lib/rigor/sig_gen/layout_index.rb +108 -0
- data/lib/rigor/sig_gen/method_candidate.rb +62 -0
- data/lib/rigor/sig_gen/observation_collector.rb +391 -0
- data/lib/rigor/sig_gen/observed_call.rb +62 -0
- data/lib/rigor/sig_gen/path_mapper.rb +116 -0
- data/lib/rigor/sig_gen/renderer.rb +157 -0
- data/lib/rigor/sig_gen/type_elaborator.rb +92 -0
- data/lib/rigor/sig_gen/write_result.rb +48 -0
- data/lib/rigor/sig_gen/writer.rb +530 -0
- data/lib/rigor/sig_gen.rb +25 -0
- data/lib/rigor/trinary.rb +15 -11
- data/lib/rigor/type/bot.rb +6 -3
- data/lib/rigor/type/bound_method.rb +79 -0
- data/lib/rigor/type/combinator.rb +207 -3
- data/lib/rigor/type/constant.rb +13 -0
- data/lib/rigor/type/hash_shape.rb +0 -2
- data/lib/rigor/type/integer_range.rb +7 -7
- data/lib/rigor/type/refined.rb +18 -12
- data/lib/rigor/type/top.rb +4 -3
- data/lib/rigor/type/union.rb +20 -1
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_node/generic.rb +68 -0
- data/lib/rigor/type_node/identifier.rb +38 -0
- data/lib/rigor/type_node/indexed_access.rb +41 -0
- data/lib/rigor/type_node/integer_literal.rb +29 -0
- data/lib/rigor/type_node/name_scope.rb +52 -0
- data/lib/rigor/type_node/resolver_chain.rb +56 -0
- data/lib/rigor/type_node/string_literal.rb +32 -0
- data/lib/rigor/type_node/symbol_literal.rb +28 -0
- data/lib/rigor/type_node/union.rb +42 -0
- data/lib/rigor/type_node.rb +29 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +2 -0
- data/sig/rigor/analysis/check_rules/always_truthy_condition_collector.rbs +10 -0
- data/sig/rigor/analysis/check_rules/dead_assignment_collector.rbs +10 -0
- data/sig/rigor/analysis/dependency_source_inference/gem_resolver.rbs +25 -0
- data/sig/rigor/analysis/dependency_source_inference/index.rbs +9 -0
- data/sig/rigor/cli/diff_command.rbs +4 -0
- data/sig/rigor/cli/explain_command.rbs +4 -0
- data/sig/rigor/cli/sig_gen_command.rbs +4 -0
- data/sig/rigor/cli/type_scan_command.rbs +3 -0
- data/sig/rigor/environment.rbs +8 -2
- data/sig/rigor/inference/builtins/method_catalog.rbs +4 -0
- data/sig/rigor/inference/builtins/numeric_catalog.rbs +3 -0
- data/sig/rigor/inference/builtins.rbs +2 -0
- data/sig/rigor/plugin/access_denied_error.rbs +3 -0
- data/sig/rigor/plugin/base.rbs +6 -0
- data/sig/rigor/plugin/blueprint.rbs +7 -0
- data/sig/rigor/plugin/fact_store.rbs +11 -0
- data/sig/rigor/plugin/io_boundary.rbs +4 -0
- data/sig/rigor/plugin/load_error.rbs +6 -0
- data/sig/rigor/plugin/loader.rbs +20 -0
- data/sig/rigor/plugin/manifest.rbs +9 -0
- data/sig/rigor/plugin/registry.rbs +16 -0
- data/sig/rigor/plugin/services.rbs +3 -0
- data/sig/rigor/plugin/trust_policy.rbs +4 -0
- data/sig/rigor/plugin/type_node_resolver.rbs +3 -0
- data/sig/rigor/plugin.rbs +8 -0
- data/sig/rigor/scope.rbs +4 -2
- data/sig/rigor/type.rbs +28 -6
- data/sig/rigor.rbs +35 -2
- metadata +90 -1
|
@@ -12,6 +12,7 @@ require_relative "method_dispatcher/iterator_dispatch"
|
|
|
12
12
|
require_relative "method_dispatcher/block_folding"
|
|
13
13
|
require_relative "method_dispatcher/file_folding"
|
|
14
14
|
require_relative "method_dispatcher/kernel_dispatch"
|
|
15
|
+
require_relative "method_dispatcher/method_folding"
|
|
15
16
|
|
|
16
17
|
module Rigor
|
|
17
18
|
module Inference
|
|
@@ -61,11 +62,18 @@ module Rigor
|
|
|
61
62
|
# @param environment [Rigor::Environment, nil] required for
|
|
62
63
|
# RBS-backed dispatch; when nil only constant folding can fire.
|
|
63
64
|
# @return [Rigor::Type, nil] inferred result type, or `nil` for "no rule".
|
|
64
|
-
def dispatch(receiver_type:, method_name:, arg_types:,
|
|
65
|
+
def dispatch(receiver_type:, method_name:, arg_types:,
|
|
65
66
|
block_type: nil, environment: nil,
|
|
66
67
|
call_node: nil, scope: nil)
|
|
67
68
|
return nil if receiver_type.nil?
|
|
68
69
|
|
|
70
|
+
bound_method_result = MethodFolding.try_backward(
|
|
71
|
+
receiver: receiver_type, method_name: method_name, args: arg_types,
|
|
72
|
+
block_type: block_type, environment: environment,
|
|
73
|
+
call_node: call_node, scope: scope
|
|
74
|
+
)
|
|
75
|
+
return bound_method_result if bound_method_result
|
|
76
|
+
|
|
69
77
|
precise = dispatch_precise_tiers(receiver_type, method_name, arg_types, block_type)
|
|
70
78
|
return precise if precise
|
|
71
79
|
|
|
@@ -84,7 +92,24 @@ module Rigor
|
|
|
84
92
|
receiver: receiver_type, method_name: method_name, args: arg_types,
|
|
85
93
|
environment: environment, block_type: block_type
|
|
86
94
|
)
|
|
87
|
-
|
|
95
|
+
if rbs_result
|
|
96
|
+
record_boundary_cross_if_applicable(receiver_type, method_name, rbs_result, environment)
|
|
97
|
+
return rbs_result
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# ADR-16 Tier B / Tier C — synthetic-method tier. Sits
|
|
101
|
+
# BELOW RBS dispatch (per WD13: user-authored RBS overrides
|
|
102
|
+
# substrate synthesis) and ABOVE the dependency-source
|
|
103
|
+
# inference tier so a plugin's declared emit table beats
|
|
104
|
+
# the generic gem-source fallback for the same class. Slice
|
|
105
|
+
# 6a-TierB (origin_module dispatch) lands precise return
|
|
106
|
+
# types for Tier B emissions; Tier C emissions still return
|
|
107
|
+
# `Dynamic[T]` at this tier (slice 6b is the Tier C
|
|
108
|
+
# promotion via ADR-13's resolver chain).
|
|
109
|
+
synthetic_result = try_synthetic_method(
|
|
110
|
+
receiver_type, method_name, arg_types, block_type, environment
|
|
111
|
+
)
|
|
112
|
+
return synthetic_result if synthetic_result
|
|
88
113
|
|
|
89
114
|
# ADR-10 slice 2b-ii — dependency-source inference tier.
|
|
90
115
|
# Sits BELOW RBS dispatch (RBS / RBS::Inline / generated
|
|
@@ -98,6 +123,21 @@ module Rigor
|
|
|
98
123
|
dep_source_result = try_dependency_source(receiver_type, method_name, environment)
|
|
99
124
|
return dep_source_result if dep_source_result
|
|
100
125
|
|
|
126
|
+
# v0.1.3 — discovered-method dispatch tier. When the
|
|
127
|
+
# receiver class has no RBS BUT scope_indexer recorded
|
|
128
|
+
# `def method_name` for that class (or singleton), the
|
|
129
|
+
# call dispatches to `Dynamic[top]` rather than falling
|
|
130
|
+
# through to the user-class fallback. Sits below RBS /
|
|
131
|
+
# dependency-source so authoritative signatures still win.
|
|
132
|
+
# The scope-indexer-built table records every project-side
|
|
133
|
+
# `def`, `define_method`, and `alias_method`; the
|
|
134
|
+
# `discovered_method?` consult here closes the
|
|
135
|
+
# fail-soft-event hot spot on implicit-self calls
|
|
136
|
+
# (`sibling_private(...)`) inside `lib/rigor/`'s own
|
|
137
|
+
# internals (analyser private helpers don't have RBS).
|
|
138
|
+
discovered_result = try_discovered_method(receiver_type, method_name, scope)
|
|
139
|
+
return discovered_result if discovered_result
|
|
140
|
+
|
|
101
141
|
# Slice 7 phase 10 — user-class ancestor fallback. When
|
|
102
142
|
# the receiver is `Nominal[T]` or `Singleton[T]` for a
|
|
103
143
|
# class not in the RBS environment (typically a
|
|
@@ -112,6 +152,58 @@ module Rigor
|
|
|
112
152
|
try_user_class_fallback(receiver_type, method_name, arg_types, environment, block_type)
|
|
113
153
|
end
|
|
114
154
|
|
|
155
|
+
# v0.1.3 — discovered-method dispatch tier. `scope` carries
|
|
156
|
+
# the `discovered_methods` table built once per program by
|
|
157
|
+
# `ScopeIndexer` (a `Hash[String, Hash[Symbol, :instance |
|
|
158
|
+
# :singleton]]`). When the receiver names a discovered
|
|
159
|
+
# class AND the requested method is recorded for that
|
|
160
|
+
# class's appropriate kind, return `Type::Combinator.untyped`
|
|
161
|
+
# — the dispatcher cannot infer a more precise return type
|
|
162
|
+
# from the bare `def` shape, but the call site stops being a
|
|
163
|
+
# fail-soft hot spot.
|
|
164
|
+
#
|
|
165
|
+
# Returns `nil` when scope / receiver class is unavailable,
|
|
166
|
+
# when the method is not in the discovered table, OR when
|
|
167
|
+
# `discovered_def_nodes` carries a re-typable body for the
|
|
168
|
+
# method (so the downstream
|
|
169
|
+
# `ExpressionTyper#try_user_method_inference` tier can
|
|
170
|
+
# re-type the body for a precise return type rather than
|
|
171
|
+
# collapsing to `Dynamic[top]` here).
|
|
172
|
+
#
|
|
173
|
+
# The tier does NOT gate on `rbs_class_known?`. RBS dispatch
|
|
174
|
+
# already had its turn upstream and returned `nil` (otherwise
|
|
175
|
+
# we wouldn't be here). When RBS knows the class but the
|
|
176
|
+
# particular method is missing from the sig — common for
|
|
177
|
+
# internal helpers and for auto-generated stubs that emit
|
|
178
|
+
# `class X` without enumerating every method — falling
|
|
179
|
+
# through to the user-class fallback would mistakenly fire
|
|
180
|
+
# `call.undefined-method`. Honoring the discovered table
|
|
181
|
+
# here keeps the sibling-private call resolution working
|
|
182
|
+
# under partial RBS coverage.
|
|
183
|
+
def try_discovered_method(receiver_type, method_name, scope)
|
|
184
|
+
return nil if scope.nil?
|
|
185
|
+
|
|
186
|
+
class_name, kind = discovered_method_lookup(receiver_type)
|
|
187
|
+
return nil if class_name.nil?
|
|
188
|
+
return nil unless scope.discovered_method?(class_name, method_name, kind)
|
|
189
|
+
return nil if kind == :instance && scope.user_def_for(class_name, method_name)
|
|
190
|
+
|
|
191
|
+
Type::Combinator.untyped
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Resolves the `(class_name, kind)` pair scope_indexer keys
|
|
195
|
+
# its `discovered_methods` table on. `Nominal[X]` looks up
|
|
196
|
+
# instance methods on X; `Singleton[X]` looks up singleton
|
|
197
|
+
# methods on X. Other carriers return `[nil, nil]` so the
|
|
198
|
+
# tier declines.
|
|
199
|
+
def discovered_method_lookup(receiver_type)
|
|
200
|
+
case receiver_type
|
|
201
|
+
when Type::Nominal then [receiver_type.class_name, :instance]
|
|
202
|
+
when Type::Singleton then [receiver_type.class_name, :singleton]
|
|
203
|
+
else [nil, nil]
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
115
207
|
# ADR-2 § "Flow Contribution Bundle" / v0.1.1 Track 2
|
|
116
208
|
# slice 7. Walks every loaded plugin's
|
|
117
209
|
# `#flow_contribution_for(call_node:, scope:)` hook,
|
|
@@ -147,6 +239,101 @@ module Rigor
|
|
|
147
239
|
# publish as ground-truth `T`). Returns `nil` when the
|
|
148
240
|
# environment carries no index, the index has no entry, or
|
|
149
241
|
# the receiver has no nominal class to look up.
|
|
242
|
+
# ADR-16 synthetic-method tier. Slice 2b shipped the floor —
|
|
243
|
+
# a match short-circuits at the right precedence (above
|
|
244
|
+
# dep-source / discovered / user-class-fallback; below RBS)
|
|
245
|
+
# and returns `Dynamic[T]`. Slice 6 (precision promotion):
|
|
246
|
+
# - Tier B path (slice 6a, `provenance[:origin_module]`
|
|
247
|
+
# recorded by the slice-3b scanner): redispatch on
|
|
248
|
+
# `Nominal[origin_module]` via `RbsDispatch` so the
|
|
249
|
+
# module's authored RBS return type wins. Devise's
|
|
250
|
+
# `valid_password?` returns `bool`, not `Dynamic[T]`.
|
|
251
|
+
# - Tier C path (slice 6b, plain `return_type:` string from
|
|
252
|
+
# the manifest's emit table): look up
|
|
253
|
+
# `environment.nominal_for_name(return_type)` so
|
|
254
|
+
# `attribute :avatar, Types::String` emits a synthetic
|
|
255
|
+
# reader returning `Nominal[ActiveStorage::Attached::One]`
|
|
256
|
+
# (when the class is in RBS). Unparameterised class names
|
|
257
|
+
# only — parameterised forms (`Array[String]`,
|
|
258
|
+
# `Hash[K, V]`) and plugin-supplied utility-type names
|
|
259
|
+
# (`Pick<T, K>`) require routing through the full ADR-13
|
|
260
|
+
# `Plugin::TypeNodeResolver` chain, which slice 6 does
|
|
261
|
+
# not yet wire in (the resolver chain is consulted only
|
|
262
|
+
# for `%a{rigor:v1:…}` payloads as of ADR-13 slice 3).
|
|
263
|
+
def try_synthetic_method(receiver_type, method_name, arg_types, block_type, environment)
|
|
264
|
+
index = environment&.synthetic_method_index
|
|
265
|
+
return nil if index.nil? || index.empty?
|
|
266
|
+
|
|
267
|
+
class_name = synthetic_method_class_name(receiver_type)
|
|
268
|
+
return nil if class_name.nil?
|
|
269
|
+
|
|
270
|
+
matches = case receiver_type
|
|
271
|
+
when Type::Singleton then index.lookup_singleton(class_name, method_name)
|
|
272
|
+
else index.lookup_instance(class_name, method_name)
|
|
273
|
+
end
|
|
274
|
+
return nil if matches.empty?
|
|
275
|
+
|
|
276
|
+
promoted = promote_synthetic_match(matches, method_name, arg_types, block_type, environment)
|
|
277
|
+
promoted || Type::Combinator.untyped
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# First non-nil promotion wins. Tier B (origin_module) and
|
|
281
|
+
# Tier C (return_type nominal lookup) are tried in the
|
|
282
|
+
# same registration-order pass per WD11 first-wins —
|
|
283
|
+
# the slice-3b scanner sets `origin_module` for Tier B
|
|
284
|
+
# entries and leaves it absent for Tier C, so the two
|
|
285
|
+
# paths self-route per match.
|
|
286
|
+
def promote_synthetic_match(matches, method_name, arg_types, block_type, environment)
|
|
287
|
+
return nil if environment.nil?
|
|
288
|
+
|
|
289
|
+
matches.each do |synthetic|
|
|
290
|
+
promoted =
|
|
291
|
+
promote_via_origin_module(synthetic, method_name, arg_types, block_type, environment) ||
|
|
292
|
+
promote_via_return_type(synthetic, environment)
|
|
293
|
+
return promoted if promoted
|
|
294
|
+
end
|
|
295
|
+
nil
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Slice 6a-TierB. For Tier B emissions (origin_module
|
|
299
|
+
# recorded in provenance), redispatch the call on the
|
|
300
|
+
# included module's `Nominal[...]` type via `RbsDispatch`.
|
|
301
|
+
# Returns nil when the SyntheticMethod is not a Tier B
|
|
302
|
+
# entry or when the origin_module is not in the RBS env.
|
|
303
|
+
def promote_via_origin_module(synthetic, method_name, arg_types, block_type, environment)
|
|
304
|
+
module_name = synthetic.provenance[:origin_module]
|
|
305
|
+
return nil unless module_name
|
|
306
|
+
|
|
307
|
+
module_type = Type::Combinator.nominal_of(module_name)
|
|
308
|
+
RbsDispatch.try_dispatch(
|
|
309
|
+
receiver: module_type, method_name: method_name, args: arg_types,
|
|
310
|
+
environment: environment, block_type: block_type
|
|
311
|
+
)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Slice 6b-TierC. For Tier C emissions, look up the
|
|
315
|
+
# manifest-declared `return_type:` string via
|
|
316
|
+
# `environment.nominal_for_name`. Skips the placeholder
|
|
317
|
+
# `"untyped"` (Tier B's record-but-do-not-resolve marker
|
|
318
|
+
# from the slice-3b scanner) and the `"void"` keyword
|
|
319
|
+
# (RBS-style absent return). Falls back to nil when the
|
|
320
|
+
# class is not in the env — caller then returns Dynamic[T].
|
|
321
|
+
TIER_C_PLACEHOLDER_RETURNS = %w[untyped void].freeze
|
|
322
|
+
private_constant :TIER_C_PLACEHOLDER_RETURNS
|
|
323
|
+
|
|
324
|
+
def promote_via_return_type(synthetic, environment)
|
|
325
|
+
return_type = synthetic.return_type
|
|
326
|
+
return nil if return_type.nil? || TIER_C_PLACEHOLDER_RETURNS.include?(return_type)
|
|
327
|
+
|
|
328
|
+
environment.nominal_for_name(return_type)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def synthetic_method_class_name(receiver_type)
|
|
332
|
+
case receiver_type
|
|
333
|
+
when Type::Nominal, Type::Singleton then receiver_type.class_name
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
150
337
|
def try_dependency_source(receiver_type, method_name, environment)
|
|
151
338
|
index = environment&.dependency_source_index
|
|
152
339
|
return nil if index.nil? || index.empty?
|
|
@@ -177,6 +364,71 @@ module Rigor
|
|
|
177
364
|
budget_silence_result(class_name, index, environment)
|
|
178
365
|
end
|
|
179
366
|
|
|
367
|
+
# ADR-10 slice 5c — record a
|
|
368
|
+
# `dynamic.dependency-source.boundary-cross` event when
|
|
369
|
+
# RBS dispatch resolves a call AND the receiver class
|
|
370
|
+
# belongs to a `mode: :full` opt-in gem whose Walker
|
|
371
|
+
# also catalogued the same `(class_name, method_name)`.
|
|
372
|
+
# The dispatcher still returns the RBS answer (per
|
|
373
|
+
# ADR-10's tier order: authoritative-source wins), but
|
|
374
|
+
# the reporter accumulates the crossing for end-of-run
|
|
375
|
+
# audit diagnostics.
|
|
376
|
+
#
|
|
377
|
+
# Five honest fall-throughs keep the gate narrow:
|
|
378
|
+
#
|
|
379
|
+
# - environment / index / reporter missing — slice 5c
|
|
380
|
+
# needs all three.
|
|
381
|
+
# - receiver has no nominal class name (Dynamic-only
|
|
382
|
+
# carriers) — nothing to look up.
|
|
383
|
+
# - receiver class doesn't belong to a `mode: :full` gem
|
|
384
|
+
# — the user didn't opt this gem into the distinct
|
|
385
|
+
# dispatch path.
|
|
386
|
+
# - the gem-source catalog has no entry for the method —
|
|
387
|
+
# only RBS knows about it; nothing to cross.
|
|
388
|
+
# - the RBS-side result is itself `Dynamic[Top]` — the
|
|
389
|
+
# "agreement" is trivially `untyped ≈ untyped`, no
|
|
390
|
+
# meaningful divergence to flag.
|
|
391
|
+
def record_boundary_cross_if_applicable(receiver_type, method_name, rbs_result, environment)
|
|
392
|
+
class_name = boundary_cross_class_name(receiver_type, environment, rbs_result)
|
|
393
|
+
return if class_name.nil?
|
|
394
|
+
|
|
395
|
+
index = environment.dependency_source_index
|
|
396
|
+
return unless index.full_mode?(class_name)
|
|
397
|
+
return unless index.contribution_for(class_name: class_name, method_name: method_name)
|
|
398
|
+
|
|
399
|
+
environment.boundary_cross_reporter.record(
|
|
400
|
+
class_name: class_name, method_name: method_name,
|
|
401
|
+
gem_name: index.gem_for(class_name),
|
|
402
|
+
rbs_display: rbs_display_for(rbs_result)
|
|
403
|
+
)
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# Composite preflight for {#record_boundary_cross_if_applicable}.
|
|
407
|
+
# Returns the receiver class name only when every prerequisite
|
|
408
|
+
# for emitting the diagnostic is satisfied (environment carries
|
|
409
|
+
# an index + reporter, receiver is a nominal carrier, RBS-side
|
|
410
|
+
# result is not the trivial `Dynamic[Top]` envelope). Returns
|
|
411
|
+
# `nil` to short-circuit otherwise.
|
|
412
|
+
def boundary_cross_class_name(receiver_type, environment, rbs_result)
|
|
413
|
+
return nil if environment.nil?
|
|
414
|
+
return nil if environment.dependency_source_index.nil?
|
|
415
|
+
return nil if environment.dependency_source_index.empty?
|
|
416
|
+
return nil if environment.boundary_cross_reporter.nil?
|
|
417
|
+
return nil if rbs_result_untyped?(rbs_result)
|
|
418
|
+
|
|
419
|
+
dep_source_class_name(receiver_type)
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def rbs_result_untyped?(rbs_result)
|
|
423
|
+
rbs_result.is_a?(Type::Dynamic) && rbs_result.static_facet.is_a?(Type::Top)
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def rbs_display_for(rbs_result)
|
|
427
|
+
return "untyped" if rbs_result.nil?
|
|
428
|
+
|
|
429
|
+
rbs_result.respond_to?(:describe) ? rbs_result.describe : rbs_result.inspect
|
|
430
|
+
end
|
|
431
|
+
|
|
180
432
|
def budget_silence_result(class_name, index, _environment)
|
|
181
433
|
return nil unless index.budget_overrun_strategy == :dependency_silence
|
|
182
434
|
|
|
@@ -242,6 +494,7 @@ module Rigor
|
|
|
242
494
|
ShapeDispatch.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
243
495
|
FileFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
244
496
|
KernelDispatch.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
497
|
+
MethodFolding.try_forward(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
245
498
|
BlockFolding.try_fold(
|
|
246
499
|
receiver: receiver_type, method_name: method_name, args: arg_types, block_type: block_type
|
|
247
500
|
)
|
|
@@ -325,9 +578,25 @@ module Rigor
|
|
|
325
578
|
Type::Combinator.nominal_of(receiver_type.class_name)
|
|
326
579
|
end
|
|
327
580
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
581
|
+
# ADR-15 Phase 4b.x — `Ractor.make_shareable` on both the
|
|
582
|
+
# outer Hash and each lambda value. A plain `.freeze` leaves
|
|
583
|
+
# the Procs unshareable; reading `CONSTANT_CONSTRUCTORS[class]`
|
|
584
|
+
# from a worker Ractor would raise `Ractor::IsolationError`,
|
|
585
|
+
# which the `rescue StandardError` in
|
|
586
|
+
# `constant_constructor_lift` silently swallows — `meta_new`
|
|
587
|
+
# then falls back to `Nominal[Pathname]` in pool mode while
|
|
588
|
+
# sequential builds the `Constant<Pathname>` lift. The
|
|
589
|
+
# divergence surfaces downstream as a spurious
|
|
590
|
+
# `call.argument-type-mismatch` (sequential's
|
|
591
|
+
# `argument_type_diagnostic` short-circuits on Constant<Pathname>
|
|
592
|
+
# because Pathname is not in its CONSTANT_CLASSES table; pool's
|
|
593
|
+
# Nominal[Pathname] doesn't short-circuit). Surfaced on GitLab
|
|
594
|
+
# FOSS via `lib/gitlab/mail_room.rb:17`.
|
|
595
|
+
CONSTANT_CONSTRUCTORS = Ractor.make_shareable({
|
|
596
|
+
"Pathname" => Ractor.make_shareable(lambda { |arg|
|
|
597
|
+
Pathname.new(arg)
|
|
598
|
+
})
|
|
599
|
+
})
|
|
331
600
|
private_constant :CONSTANT_CONSTRUCTORS
|
|
332
601
|
|
|
333
602
|
def constant_constructor_lift(class_name, arg_types)
|
|
@@ -32,7 +32,6 @@ module Rigor
|
|
|
32
32
|
# `#singleton_method`.
|
|
33
33
|
#
|
|
34
34
|
# See docs/internal-spec/inference-engine.md for the binding contract.
|
|
35
|
-
# rubocop:disable Metrics/ClassLength
|
|
36
35
|
class MethodParameterBinder
|
|
37
36
|
# @param environment [Rigor::Environment]
|
|
38
37
|
# @param class_path [String, nil] the qualified name of the class
|
|
@@ -175,7 +174,7 @@ module Rigor
|
|
|
175
174
|
# `non-empty-array[Integer]` describe the parameter binding
|
|
176
175
|
# they actually want, not its element type.
|
|
177
176
|
def apply_param_overrides(types, slots, rbs_method)
|
|
178
|
-
override_map = RbsExtended.param_type_override_map(rbs_method)
|
|
177
|
+
override_map = RbsExtended.param_type_override_map(rbs_method, environment: @environment)
|
|
179
178
|
return if override_map.empty?
|
|
180
179
|
|
|
181
180
|
slots.each do |slot|
|
|
@@ -212,20 +211,30 @@ module Rigor
|
|
|
212
211
|
# (`?by:`) while the Ruby `def` lists it as required (or vice
|
|
213
212
|
# versa); the binding is by-name regardless of which side
|
|
214
213
|
# defines it.
|
|
215
|
-
KEYWORD_PROVIDER = lambda do |fn, slot|
|
|
214
|
+
KEYWORD_PROVIDER = Ractor.make_shareable(lambda do |fn, slot|
|
|
216
215
|
fn.required_keywords[slot.name]&.type || fn.optional_keywords[slot.name]&.type
|
|
217
|
-
end
|
|
216
|
+
end)
|
|
218
217
|
private_constant :KEYWORD_PROVIDER
|
|
219
218
|
|
|
220
|
-
RBS_TYPE_PROVIDERS = {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
219
|
+
RBS_TYPE_PROVIDERS = Ractor.make_shareable({
|
|
220
|
+
required_positional: Ractor.make_shareable(lambda { |fn, slot|
|
|
221
|
+
fn.required_positionals[slot.index]&.type
|
|
222
|
+
}),
|
|
223
|
+
optional_positional: Ractor.make_shareable(lambda { |fn, slot|
|
|
224
|
+
fn.optional_positionals[slot.index]&.type
|
|
225
|
+
}),
|
|
226
|
+
rest_positional: Ractor.make_shareable(lambda { |fn, _slot|
|
|
227
|
+
fn.rest_positionals&.type
|
|
228
|
+
}),
|
|
229
|
+
trailing_positional: Ractor.make_shareable(lambda { |fn, slot|
|
|
230
|
+
fn.trailing_positionals[slot.index]&.type
|
|
231
|
+
}),
|
|
232
|
+
required_keyword: KEYWORD_PROVIDER,
|
|
233
|
+
optional_keyword: KEYWORD_PROVIDER,
|
|
234
|
+
rest_keyword: Ractor.make_shareable(lambda { |fn, _slot|
|
|
235
|
+
fn.rest_keywords&.type
|
|
236
|
+
})
|
|
237
|
+
})
|
|
229
238
|
private_constant :RBS_TYPE_PROVIDERS
|
|
230
239
|
|
|
231
240
|
def rbs_type_for_slot(function, slot)
|
|
@@ -275,6 +284,5 @@ module Rigor
|
|
|
275
284
|
end
|
|
276
285
|
end
|
|
277
286
|
end
|
|
278
|
-
# rubocop:enable Metrics/ClassLength
|
|
279
287
|
end
|
|
280
288
|
end
|
|
@@ -376,7 +376,7 @@ module Rigor
|
|
|
376
376
|
# the predicate shape is recognised, or `nil` to signal "no
|
|
377
377
|
# narrowing" so the public surface can fall back to the entry
|
|
378
378
|
# scope.
|
|
379
|
-
def analyse(node, scope)
|
|
379
|
+
def analyse(node, scope)
|
|
380
380
|
case node
|
|
381
381
|
when Prism::ParenthesesNode
|
|
382
382
|
analyse_parentheses(node, scope)
|
|
@@ -384,6 +384,14 @@ module Rigor
|
|
|
384
384
|
analyse_statements(node, scope)
|
|
385
385
|
when Prism::LocalVariableReadNode
|
|
386
386
|
analyse_local_read(node, scope)
|
|
387
|
+
when Prism::LocalVariableWriteNode
|
|
388
|
+
analyse_local_write(node, scope)
|
|
389
|
+
when Prism::InstanceVariableWriteNode
|
|
390
|
+
analyse_ivar_write(node, scope)
|
|
391
|
+
when Prism::ClassVariableWriteNode
|
|
392
|
+
analyse_cvar_write(node, scope)
|
|
393
|
+
when Prism::GlobalVariableWriteNode
|
|
394
|
+
analyse_global_write(node, scope)
|
|
387
395
|
when Prism::CallNode
|
|
388
396
|
analyse_call(node, scope)
|
|
389
397
|
when Prism::AndNode
|
|
@@ -445,7 +453,6 @@ module Rigor
|
|
|
445
453
|
# intersects each half with the integer-domain parts of
|
|
446
454
|
# `current_type`. Non-integer parts of a Union receiver
|
|
447
455
|
# (nil, String, …) survive unchanged.
|
|
448
|
-
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
449
456
|
def complement_integer_range(current_type, range)
|
|
450
457
|
halves = integer_range_complement_halves(range)
|
|
451
458
|
parts = current_type.is_a?(Type::Union) ? current_type.members : [current_type]
|
|
@@ -466,7 +473,6 @@ module Rigor
|
|
|
466
473
|
|
|
467
474
|
Type::Combinator.union(*survivors)
|
|
468
475
|
end
|
|
469
|
-
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
470
476
|
|
|
471
477
|
# Returns the two open halves of an IntegerRange's
|
|
472
478
|
# complement: the left half `int<-∞, a-1>` (when `a` is
|
|
@@ -738,6 +744,59 @@ module Rigor
|
|
|
738
744
|
]
|
|
739
745
|
end
|
|
740
746
|
|
|
747
|
+
# Assignment-in-condition: `if name = expr` and the more
|
|
748
|
+
# frequent `if cond && (name = expr)` / `if cond && name =
|
|
749
|
+
# expr` Redmine-style guard. By the time narrowing runs,
|
|
750
|
+
# `StatementEvaluator#eval_local_write` has already bound
|
|
751
|
+
# the assigned local in `scope` to the rvalue type. The
|
|
752
|
+
# write's own truthiness IS the assigned value's
|
|
753
|
+
# truthiness, so the truthy edge narrows the local by
|
|
754
|
+
# `narrow_truthy(current)` and the falsey edge by
|
|
755
|
+
# `narrow_falsey(current)`. Mirrors `analyse_local_read`
|
|
756
|
+
# because the only meaningful difference between
|
|
757
|
+
# "predicate is `var`" and "predicate is `var = expr`" is
|
|
758
|
+
# which scope holds the just-bound value; the narrowing
|
|
759
|
+
# contract on the surrounding `if` is the same.
|
|
760
|
+
def analyse_local_write(node, scope)
|
|
761
|
+
current = scope.local(node.name)
|
|
762
|
+
return nil if current.nil?
|
|
763
|
+
|
|
764
|
+
[
|
|
765
|
+
scope.with_local(node.name, narrow_truthy(current)),
|
|
766
|
+
scope.with_local(node.name, narrow_falsey(current))
|
|
767
|
+
]
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
def analyse_ivar_write(node, scope)
|
|
771
|
+
current = scope.ivar(node.name)
|
|
772
|
+
return nil if current.nil?
|
|
773
|
+
|
|
774
|
+
[
|
|
775
|
+
scope.with_ivar(node.name, narrow_truthy(current)),
|
|
776
|
+
scope.with_ivar(node.name, narrow_falsey(current))
|
|
777
|
+
]
|
|
778
|
+
end
|
|
779
|
+
|
|
780
|
+
def analyse_cvar_write(node, scope)
|
|
781
|
+
current = scope.cvar(node.name)
|
|
782
|
+
return nil if current.nil?
|
|
783
|
+
|
|
784
|
+
[
|
|
785
|
+
scope.with_cvar(node.name, narrow_truthy(current)),
|
|
786
|
+
scope.with_cvar(node.name, narrow_falsey(current))
|
|
787
|
+
]
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
def analyse_global_write(node, scope)
|
|
791
|
+
current = scope.global(node.name)
|
|
792
|
+
return nil if current.nil?
|
|
793
|
+
|
|
794
|
+
[
|
|
795
|
+
scope.with_global(node.name, narrow_truthy(current)),
|
|
796
|
+
scope.with_global(node.name, narrow_falsey(current))
|
|
797
|
+
]
|
|
798
|
+
end
|
|
799
|
+
|
|
741
800
|
# `if /(?<x>...)/ =~ str` — Prism wraps the `=~` call in a
|
|
742
801
|
# `MatchWriteNode` listing the named-capture targets. The
|
|
743
802
|
# parent `eval_match_write` has already bound each target
|
|
@@ -918,12 +977,12 @@ module Rigor
|
|
|
918
977
|
# zero-arg predicates on `Numeric`. We model them as
|
|
919
978
|
# comparisons against the literal 0 so the existing range
|
|
920
979
|
# narrowing handles them uniformly.
|
|
921
|
-
ZERO_CLASS_PREDICATE_RULES = {
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
980
|
+
ZERO_CLASS_PREDICATE_RULES = Ractor.make_shareable({
|
|
981
|
+
positive?: { truthy: [:>, 0], falsey: [:<=, 0] },
|
|
982
|
+
negative?: { truthy: [:<, 0], falsey: [:>=, 0] },
|
|
983
|
+
zero?: { truthy: [:eq, 0], falsey: [:ne, 0] },
|
|
984
|
+
nonzero?: { truthy: [:ne, 0], falsey: [:eq, 0] }
|
|
985
|
+
})
|
|
927
986
|
private_constant :ZERO_CLASS_PREDICATE_RULES
|
|
928
987
|
|
|
929
988
|
def analyse_zero_class_predicate(node, scope, predicate:)
|
|
@@ -1204,15 +1263,73 @@ module Rigor
|
|
|
1204
1263
|
return nil if node.arguments.nil?
|
|
1205
1264
|
return nil unless node.arguments.arguments.size == 1
|
|
1206
1265
|
|
|
1207
|
-
|
|
1208
|
-
return nil if
|
|
1266
|
+
bare_name = static_class_name(node.arguments.arguments.first)
|
|
1267
|
+
return nil if bare_name.nil?
|
|
1209
1268
|
|
|
1210
1269
|
current = scope.local(node.receiver.name)
|
|
1211
1270
|
return nil if current.nil?
|
|
1212
1271
|
|
|
1272
|
+
# Resolve `bare_name` through the lexical-scope chain
|
|
1273
|
+
# so a name shadowed by the current class / enclosing
|
|
1274
|
+
# module wins over the top-level constant. Mirrors
|
|
1275
|
+
# Ruby's `Module.nesting`-driven constant lookup. The
|
|
1276
|
+
# canonical motivating case: inside
|
|
1277
|
+
# `Rigor::Type::Singleton#==`, `is_a?(Singleton)`
|
|
1278
|
+
# should resolve to `Rigor::Type::Singleton`, not the
|
|
1279
|
+
# top-level stdlib `Singleton` mixin (which would
|
|
1280
|
+
# surface as a spurious `undefined-method` on
|
|
1281
|
+
# subsequent `other.class_name` calls).
|
|
1282
|
+
class_name = resolve_class_name_lexically(bare_name, scope)
|
|
1213
1283
|
class_predicate_scopes(scope, node.receiver.name, current, class_name, exact: exact)
|
|
1214
1284
|
end
|
|
1215
1285
|
|
|
1286
|
+
# Walks the lexical-nesting chain derived from
|
|
1287
|
+
# `scope.self_type` and returns the first
|
|
1288
|
+
# `<prefix>::<bare_name>` (or bare `<bare_name>` at the
|
|
1289
|
+
# top level) that the environment recognises. Falls back
|
|
1290
|
+
# to `bare_name` itself when nothing in the chain
|
|
1291
|
+
# resolves; the downstream `narrow_class` then yields
|
|
1292
|
+
# the conservative answer for unknown receivers.
|
|
1293
|
+
def resolve_class_name_lexically(bare_name, scope)
|
|
1294
|
+
return bare_name if bare_name.include?("::") # Already qualified.
|
|
1295
|
+
|
|
1296
|
+
chain = lexical_nesting_for(scope)
|
|
1297
|
+
chain.each do |prefix|
|
|
1298
|
+
candidate = "#{prefix}::#{bare_name}"
|
|
1299
|
+
return candidate if class_known_to_scope?(scope, candidate)
|
|
1300
|
+
end
|
|
1301
|
+
bare_name
|
|
1302
|
+
end
|
|
1303
|
+
|
|
1304
|
+
# Combines the environment's RBS-known set with the
|
|
1305
|
+
# scope's in-source `discovered_classes` table so a
|
|
1306
|
+
# lexical-nesting candidate matches a class the project
|
|
1307
|
+
# declares but has no RBS for.
|
|
1308
|
+
def class_known_to_scope?(scope, candidate)
|
|
1309
|
+
return true if scope.environment.class_known?(candidate)
|
|
1310
|
+
|
|
1311
|
+
scope.discovered_classes.key?(candidate)
|
|
1312
|
+
end
|
|
1313
|
+
|
|
1314
|
+
# Approximates `Module.nesting` from the inferable
|
|
1315
|
+
# `self_type`. Today's implementation handles the common
|
|
1316
|
+
# case: when the surrounding method is a regular
|
|
1317
|
+
# instance method (`self_type = Nominal[T]`) or a
|
|
1318
|
+
# class-body / singleton (`self_type = Singleton[T]`),
|
|
1319
|
+
# the chain is `T`'s namespace path — `Foo::Bar::Baz`
|
|
1320
|
+
# → `["Foo::Bar::Baz", "Foo::Bar", "Foo"]`. Returns an
|
|
1321
|
+
# empty array when `self_type` is unknown.
|
|
1322
|
+
def lexical_nesting_for(scope)
|
|
1323
|
+
self_type = scope.self_type
|
|
1324
|
+
base = case self_type
|
|
1325
|
+
when Type::Nominal, Type::Singleton then self_type.class_name
|
|
1326
|
+
end
|
|
1327
|
+
return [] if base.nil? || base.empty?
|
|
1328
|
+
|
|
1329
|
+
parts = base.split("::")
|
|
1330
|
+
parts.each_index.map { |i| parts[0..-(i + 1)].join("::") }
|
|
1331
|
+
end
|
|
1332
|
+
|
|
1216
1333
|
def class_predicate_scopes(scope, name, current, class_name, exact:)
|
|
1217
1334
|
[
|
|
1218
1335
|
scope.with_local(
|
|
@@ -1337,7 +1454,7 @@ module Rigor
|
|
|
1337
1454
|
method_def = resolve_rbs_extended_method(node, scope)
|
|
1338
1455
|
return nil if method_def.nil?
|
|
1339
1456
|
|
|
1340
|
-
contribution = RbsExtended.read_flow_contribution(method_def)
|
|
1457
|
+
contribution = RbsExtended.read_flow_contribution(method_def, environment: scope.environment)
|
|
1341
1458
|
return nil if contribution.nil?
|
|
1342
1459
|
|
|
1343
1460
|
result = Rigor::FlowContribution::Merger.merge([contribution])
|
|
@@ -41,7 +41,6 @@ module Rigor
|
|
|
41
41
|
# `Nominal[C]` regardless of which method body we are in.
|
|
42
42
|
# When either argument is omitted, the corresponding token degrades
|
|
43
43
|
# to Dynamic[Top].
|
|
44
|
-
# rubocop:disable Metrics/ModuleLength
|
|
45
44
|
module RbsTypeTranslator
|
|
46
45
|
# Hash-based dispatch keeps `translate` linear and dodges the
|
|
47
46
|
# bookkeeping costs of a 20-arm `case` (RuboCop AbcSize/CCN/Length
|
|
@@ -214,6 +213,5 @@ module Rigor
|
|
|
214
213
|
end
|
|
215
214
|
end
|
|
216
215
|
end
|
|
217
|
-
# rubocop:enable Metrics/ModuleLength
|
|
218
216
|
end
|
|
219
217
|
end
|