rigortype 0.1.15 → 0.1.16
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 +4 -2
- data/exe/rigor +19 -0
- data/lib/rigor/analysis/check_rules.rb +25 -1
- data/lib/rigor/analysis/diagnostic.rb +40 -0
- data/lib/rigor/analysis/runner.rb +61 -2
- data/lib/rigor/analysis/worker_session.rb +3 -2
- data/lib/rigor/cache/descriptor.rb +6 -2
- data/lib/rigor/cli/plugins_command.rb +51 -4
- data/lib/rigor/cli/plugins_renderer.rb +86 -1
- data/lib/rigor/cli.rb +135 -5
- data/lib/rigor/environment/rbs_loader.rb +259 -1
- data/lib/rigor/environment.rb +8 -2
- data/lib/rigor/inference/budget_trace.rb +137 -0
- data/lib/rigor/inference/expression_typer.rb +9 -2
- data/lib/rigor/inference/hkt_reducer.rb +2 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +23 -6
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +81 -14
- data/lib/rigor/inference/method_dispatcher.rb +57 -10
- data/lib/rigor/inference/precision_scanner.rb +60 -1
- data/lib/rigor/inference/scope_indexer.rb +127 -8
- data/lib/rigor/inference/statement_evaluator.rb +13 -8
- data/lib/rigor/inference/synthetic_method_index.rb +23 -4
- data/lib/rigor/inference/synthetic_method_scanner.rb +148 -14
- data/lib/rigor/plugin/additional_initializer.rb +108 -0
- data/lib/rigor/plugin/base.rb +321 -2
- data/lib/rigor/plugin/box.rb +64 -0
- data/lib/rigor/plugin/inflector.rb +121 -0
- data/lib/rigor/plugin/isolation.rb +191 -0
- data/lib/rigor/plugin/macro/nested_class_template.rb +140 -0
- data/lib/rigor/plugin/macro.rb +1 -0
- data/lib/rigor/plugin/manifest.rb +120 -23
- data/lib/rigor/plugin/node_context.rb +62 -0
- data/lib/rigor/plugin/registry.rb +10 -0
- data/lib/rigor/plugin.rb +3 -0
- data/lib/rigor/sig_gen/generator.rb +2 -3
- data/lib/rigor/sig_gen/observation_collector.rb +2 -2
- data/lib/rigor/source/literals.rb +118 -0
- data/lib/rigor/source/node_walker.rb +26 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/combinator.rb +6 -1
- data/lib/rigor/type/union.rb +65 -1
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +31 -53
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +21 -23
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +38 -59
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +7 -13
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +22 -33
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +298 -413
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +69 -71
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +24 -34
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +18 -16
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +4 -46
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +1 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +17 -12
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -8
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +2 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +2 -6
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +4 -3
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +40 -45
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +7 -17
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +20 -42
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +7 -4
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +4 -8
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +188 -0
- data/plugins/rigor-mangrove/lib/rigor-mangrove.rb +3 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +4 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +24 -8
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +31 -48
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +21 -23
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +54 -82
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +25 -25
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +63 -147
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -17
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +23 -114
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +36 -31
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +6 -3
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +4 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +13 -12
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +28 -40
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +44 -47
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +11 -10
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +45 -87
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +11 -12
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +29 -42
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +20 -19
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +73 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +43 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +21 -29
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +36 -96
- data/sig/rigor/plugin/access_denied_error.rbs +3 -1
- data/sig/rigor/plugin/base.rbs +58 -3
- data/sig/rigor/plugin/io_boundary.rbs +3 -0
- data/sig/rigor/plugin/manifest.rbs +31 -1
- data/sig/rigor/source.rbs +12 -0
- data/sig/rigor.rbs +5 -0
- data/skills/rigor-plugin-author/SKILL.md +13 -9
- data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +6 -5
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +159 -75
- data/skills/rigor-plugin-author/references/03-test-and-ship.md +3 -3
- metadata +52 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/inflector.rb +0 -114
|
@@ -115,14 +115,31 @@ module Rigor
|
|
|
115
115
|
# overload-list position.
|
|
116
116
|
overloads = ReceiverAffinity.reorder(overloads, self_type: self_type, environment: environment)
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
passes = lambda do |require_block|
|
|
119
|
+
run_selection_passes(
|
|
120
|
+
overloads, arg_types: arg_types, self_type: self_type, instance_type: instance_type,
|
|
121
|
+
type_vars: type_vars, block_required: require_block, param_overrides: param_overrides
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
match = passes.call(block_required)
|
|
122
126
|
return match if match
|
|
123
|
-
return overloads.find { |mt| overload_has_block?(mt) } if block_required
|
|
124
127
|
|
|
125
|
-
#
|
|
128
|
+
# A block at the call site that no block-declaring overload
|
|
129
|
+
# matched: Ruby ignores a block handed to a method that never
|
|
130
|
+
# yields it, so retry treating the block as ignorable rather
|
|
131
|
+
# than failing the dispatch. Without this, a block-bearing
|
|
132
|
+
# call to a method whose RBS declares no block (e.g.
|
|
133
|
+
# `define_command(:x) do … end` against
|
|
134
|
+
# `def define_command: (Symbol) -> Symbol`) degraded to
|
|
135
|
+
# `Dynamic[Top]` — and on a self-send suppressed the whole
|
|
136
|
+
# method's return type.
|
|
137
|
+
if block_required
|
|
138
|
+
match = passes.call(false)
|
|
139
|
+
return match if match
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# No (usable) block at the call site: prefer an overload that does
|
|
126
143
|
# not REQUIRE a block over `overloads.first`. Methods like
|
|
127
144
|
# `Array#filter` / `Enumerable#map` declare the block-
|
|
128
145
|
# bearing overload first (`() { ... } -> Array[Elem]`) and
|
|
@@ -64,6 +64,19 @@ module Rigor
|
|
|
64
64
|
module RbsDispatch
|
|
65
65
|
module_function
|
|
66
66
|
|
|
67
|
+
# ADR-43 — ancestor classes whose RBS is authoritative and
|
|
68
|
+
# COMPLETE, so a call a subclass makes that the ancestor's RBS
|
|
69
|
+
# does not declare is a genuine mistake rather than a gap.
|
|
70
|
+
# Membership unlocks inherited-method resolution (and thus
|
|
71
|
+
# `call.undefined-method`) for Ruby-source subclasses of these
|
|
72
|
+
# classes; every other RBS ancestor stays on the Dynamic
|
|
73
|
+
# fallback. Seeded with the plugin contract base — this repo
|
|
74
|
+
# owns both the class and `sig/rigor/plugin/base.rbs`, and the
|
|
75
|
+
# `lib` self-check keeps them in lock-step. NOT a place for
|
|
76
|
+
# third-party/core classes whose objects answer to methods
|
|
77
|
+
# their RBS omits (`ActionController::Base`, `Hash`, …).
|
|
78
|
+
ALLOWED_RBS_COMPLETE_ANCESTORS = ["Rigor::Plugin::Base"].freeze
|
|
79
|
+
|
|
67
80
|
# @param receiver [Rigor::Type]
|
|
68
81
|
# @param method_name [Symbol]
|
|
69
82
|
# @param args [Array<Rigor::Type>]
|
|
@@ -94,8 +107,19 @@ module Rigor
|
|
|
94
107
|
# @return [Rigor::Type, nil] inferred return type, or `nil`
|
|
95
108
|
# when no rule resolves (no class name, no method, dispatch
|
|
96
109
|
# on a Top/Dynamic[Top] receiver, etc.).
|
|
97
|
-
|
|
98
|
-
|
|
110
|
+
# @param scope [Rigor::Scope, nil] when supplied, enables
|
|
111
|
+
# ADR-43 RBS-complete-ancestor resolution: a call on a
|
|
112
|
+
# Ruby-source subclass not known to RBS, whose discovered
|
|
113
|
+
# superclass chain reaches an allow-listed RBS-complete
|
|
114
|
+
# ancestor (e.g. `Rigor::Plugin::Base`), resolves against
|
|
115
|
+
# that ancestor's RBS. `nil` (the default for every caller
|
|
116
|
+
# that does not thread a scope) keeps the legacy behaviour —
|
|
117
|
+
# such an inherited call stays unresolved and degrades to
|
|
118
|
+
# `Dynamic[Top]`, which is the false-positive-safe default
|
|
119
|
+
# for the open hierarchies (`< ActionController::Base`, …)
|
|
120
|
+
# the allow-list deliberately excludes.
|
|
121
|
+
def try_dispatch(receiver:, method_name:, args:, environment:, block_type: nil, self_type_override: nil, # rubocop:disable Metrics/ParameterLists
|
|
122
|
+
public_only: false, scope: nil)
|
|
99
123
|
return nil if environment.nil?
|
|
100
124
|
return nil unless environment.rbs_loader
|
|
101
125
|
|
|
@@ -106,7 +130,8 @@ module Rigor
|
|
|
106
130
|
environment: environment,
|
|
107
131
|
block_type: block_type,
|
|
108
132
|
self_type_override: self_type_override,
|
|
109
|
-
public_only: public_only
|
|
133
|
+
public_only: public_only,
|
|
134
|
+
scope: scope
|
|
110
135
|
)
|
|
111
136
|
end
|
|
112
137
|
|
|
@@ -148,37 +173,37 @@ module Rigor
|
|
|
148
173
|
class << self
|
|
149
174
|
private
|
|
150
175
|
|
|
151
|
-
def dispatch_for(receiver:, method_name:, args:, environment:, block_type:, self_type_override: nil,
|
|
152
|
-
public_only: false)
|
|
176
|
+
def dispatch_for(receiver:, method_name:, args:, environment:, block_type:, self_type_override: nil, # rubocop:disable Metrics/ParameterLists
|
|
177
|
+
public_only: false, scope: nil)
|
|
153
178
|
args ||= []
|
|
154
179
|
case receiver
|
|
155
180
|
when Type::Union
|
|
156
181
|
dispatch_union(receiver, method_name, args, environment, block_type, self_type_override,
|
|
157
|
-
public_only: public_only)
|
|
182
|
+
public_only: public_only, scope: scope)
|
|
158
183
|
else
|
|
159
184
|
dispatch_one(receiver, method_name, args, environment, block_type, self_type_override,
|
|
160
|
-
public_only: public_only)
|
|
185
|
+
public_only: public_only, scope: scope)
|
|
161
186
|
end
|
|
162
187
|
end
|
|
163
188
|
|
|
164
|
-
def dispatch_union(receiver, method_name, args, environment, block_type, self_type_override = nil,
|
|
165
|
-
public_only: false)
|
|
189
|
+
def dispatch_union(receiver, method_name, args, environment, block_type, self_type_override = nil, # rubocop:disable Metrics/ParameterLists
|
|
190
|
+
public_only: false, scope: nil)
|
|
166
191
|
results = receiver.members.map do |member|
|
|
167
192
|
dispatch_one(member, method_name, args, environment, block_type, self_type_override,
|
|
168
|
-
public_only: public_only)
|
|
193
|
+
public_only: public_only, scope: scope)
|
|
169
194
|
end
|
|
170
195
|
return nil if results.any?(&:nil?)
|
|
171
196
|
|
|
172
197
|
Type::Combinator.union(*results)
|
|
173
198
|
end
|
|
174
199
|
|
|
175
|
-
def dispatch_one(receiver, method_name, args, environment, block_type, self_type_override = nil,
|
|
176
|
-
public_only: false)
|
|
200
|
+
def dispatch_one(receiver, method_name, args, environment, block_type, self_type_override = nil, # rubocop:disable Metrics/ParameterLists
|
|
201
|
+
public_only: false, scope: nil)
|
|
177
202
|
descriptor = receiver_descriptor(receiver)
|
|
178
203
|
return nil unless descriptor
|
|
179
204
|
|
|
180
205
|
class_name, kind, receiver_args = descriptor
|
|
181
|
-
method_definition = lookup_method(environment, class_name, kind, method_name)
|
|
206
|
+
method_definition = lookup_method(environment, class_name, kind, method_name, scope)
|
|
182
207
|
return nil unless method_definition
|
|
183
208
|
return nil if public_only && method_private?(method_definition)
|
|
184
209
|
|
|
@@ -267,7 +292,26 @@ module Rigor
|
|
|
267
292
|
method_definition.accessibility == :private
|
|
268
293
|
end
|
|
269
294
|
|
|
270
|
-
def lookup_method(environment, class_name, kind, method_name)
|
|
295
|
+
def lookup_method(environment, class_name, kind, method_name, scope = nil)
|
|
296
|
+
direct = lookup_method_on(environment, class_name, kind, method_name)
|
|
297
|
+
return direct if direct
|
|
298
|
+
|
|
299
|
+
# ADR-43 — scoped inherited-method resolution. The direct
|
|
300
|
+
# lookup misses when `class_name` is a Ruby-source subclass
|
|
301
|
+
# absent from RBS (so no ancestor walk runs). If its
|
|
302
|
+
# discovered superclass chain reaches an allow-listed
|
|
303
|
+
# RBS-complete ancestor, resolve the method there so
|
|
304
|
+
# inherited contract calls (`self.manifest` on a plugin)
|
|
305
|
+
# resolve and the normal call rules apply. Bounded to the
|
|
306
|
+
# allow-list, so open hierarchies stay on the Dynamic
|
|
307
|
+
# fallback (no false positive on `< ActionController::Base`).
|
|
308
|
+
ancestor = allowed_rbs_complete_ancestor(environment, class_name, scope)
|
|
309
|
+
return nil unless ancestor
|
|
310
|
+
|
|
311
|
+
lookup_method_on(environment, ancestor, kind, method_name)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def lookup_method_on(environment, class_name, kind, method_name)
|
|
271
315
|
case kind
|
|
272
316
|
when :instance
|
|
273
317
|
Rigor::Reflection.instance_method_definition(class_name, method_name, environment: environment)
|
|
@@ -276,6 +320,29 @@ module Rigor
|
|
|
276
320
|
end
|
|
277
321
|
end
|
|
278
322
|
|
|
323
|
+
# The first allow-listed, RBS-complete ancestor reachable from
|
|
324
|
+
# `class_name` through `scope.discovered_superclasses`, or nil.
|
|
325
|
+
# Returns nil when no scope is threaded, when `class_name` is
|
|
326
|
+
# itself RBS-known (the direct lookup already had authority),
|
|
327
|
+
# or when the discovered chain reaches no allow-listed class.
|
|
328
|
+
# The walk carries a visited set so a malformed cyclic
|
|
329
|
+
# `A < B < A` source cannot loop.
|
|
330
|
+
def allowed_rbs_complete_ancestor(environment, class_name, scope)
|
|
331
|
+
return nil if scope.nil?
|
|
332
|
+
return nil if Rigor::Reflection.rbs_class_known?(class_name, environment: environment)
|
|
333
|
+
|
|
334
|
+
supers = scope.discovered_superclasses
|
|
335
|
+
seen = {}
|
|
336
|
+
current = supers[class_name.to_s]
|
|
337
|
+
until current.nil? || seen[current]
|
|
338
|
+
return current if ALLOWED_RBS_COMPLETE_ANCESTORS.include?(current)
|
|
339
|
+
|
|
340
|
+
seen[current] = true
|
|
341
|
+
current = supers[current]
|
|
342
|
+
end
|
|
343
|
+
nil
|
|
344
|
+
end
|
|
345
|
+
|
|
279
346
|
# Slice 4 phase 2d substitution map. Zips the class's
|
|
280
347
|
# declared type-parameter names against the receiver's
|
|
281
348
|
# `type_args`. Returns an empty hash when either side is
|
|
@@ -71,7 +71,7 @@ module Rigor
|
|
|
71
71
|
# @param environment [Rigor::Environment, nil] required for
|
|
72
72
|
# RBS-backed dispatch; when nil only constant folding can fire.
|
|
73
73
|
# @return [Rigor::Type, nil] inferred result type, or `nil` for "no rule".
|
|
74
|
-
def dispatch(receiver_type:, method_name:, arg_types:, # rubocop:disable Metrics/MethodLength
|
|
74
|
+
def dispatch(receiver_type:, method_name:, arg_types:, # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
75
75
|
block_type: nil, environment: nil,
|
|
76
76
|
call_node: nil, scope: nil)
|
|
77
77
|
return nil if receiver_type.nil?
|
|
@@ -94,7 +94,7 @@ module Rigor
|
|
|
94
94
|
# consults the registry when both `call_node` and `scope`
|
|
95
95
|
# are supplied — the dispatcher's own internal callers
|
|
96
96
|
# (per-element block fold, etc.) skip this tier.
|
|
97
|
-
plugin_result = try_plugin_contribution(call_node, scope)
|
|
97
|
+
plugin_result = try_plugin_contribution(call_node, scope, receiver_type)
|
|
98
98
|
return plugin_result if plugin_result
|
|
99
99
|
|
|
100
100
|
# ADR-20 slice 3 — Rigor-bundled HKT-builtin return-
|
|
@@ -125,7 +125,7 @@ module Rigor
|
|
|
125
125
|
|
|
126
126
|
rbs_result = RbsDispatch.try_dispatch(
|
|
127
127
|
receiver: receiver_type, method_name: method_name, args: arg_types,
|
|
128
|
-
environment: environment, block_type: block_type
|
|
128
|
+
environment: environment, block_type: block_type, scope: scope
|
|
129
129
|
)
|
|
130
130
|
if rbs_result
|
|
131
131
|
record_boundary_cross_if_applicable(receiver_type, method_name, rbs_result, environment)
|
|
@@ -187,6 +187,19 @@ module Rigor
|
|
|
187
187
|
discovered_result = try_discovered_method(receiver_type, method_name, scope)
|
|
188
188
|
return discovered_result if discovered_result
|
|
189
189
|
|
|
190
|
+
# ADR-5 robustness — synthesized-stub-type tier. When the
|
|
191
|
+
# receiver is a type Rigor invented to make an otherwise-
|
|
192
|
+
# unbuildable project signature resolve (a missing-namespace
|
|
193
|
+
# module, or a stub for a referenced-but-undeclared type like
|
|
194
|
+
# an unavailable `DRb::DRbServer`), the stub carries no methods,
|
|
195
|
+
# so an unresolved call against it would otherwise mis-fire
|
|
196
|
+
# `call.undefined-method`. Resolve it to `Dynamic[Top]` instead
|
|
197
|
+
# — the same no-false-positive contract as the dependency-
|
|
198
|
+
# source tier. Sits below every real resolution tier so a
|
|
199
|
+
# genuine signature always wins.
|
|
200
|
+
stub_result = try_synthesized_stub_type(receiver_type, environment)
|
|
201
|
+
return stub_result if stub_result
|
|
202
|
+
|
|
190
203
|
# Slice 7 phase 10 — user-class ancestor fallback. When
|
|
191
204
|
# the receiver is `Nominal[T]` or `Singleton[T]` for a
|
|
192
205
|
# class not in the RBS environment (typically a
|
|
@@ -253,6 +266,31 @@ module Rigor
|
|
|
253
266
|
end
|
|
254
267
|
end
|
|
255
268
|
|
|
269
|
+
# ADR-5 robustness — returns `Dynamic[Top]` when the receiver is
|
|
270
|
+
# an instance or singleton of a type Rigor synthesized (a
|
|
271
|
+
# missing-namespace module or a referenced-type stub). The stub
|
|
272
|
+
# has no methods, so the call would otherwise reach the
|
|
273
|
+
# user-class fallback and surface `call.undefined-method`; the
|
|
274
|
+
# honest answer for a type Rigor invented is "unknown shape",
|
|
275
|
+
# i.e. `Dynamic[Top]`. Returns nil (declines) for any real type.
|
|
276
|
+
def try_synthesized_stub_type(receiver_type, environment)
|
|
277
|
+
return nil if environment.nil?
|
|
278
|
+
|
|
279
|
+
loader = environment.rbs_loader
|
|
280
|
+
return nil if loader.nil? || !loader.respond_to?(:synthesized_type_names)
|
|
281
|
+
|
|
282
|
+
names = loader.synthesized_type_names
|
|
283
|
+
return nil if names.empty?
|
|
284
|
+
|
|
285
|
+
class_name =
|
|
286
|
+
case receiver_type
|
|
287
|
+
when Type::Nominal, Type::Singleton then receiver_type.class_name.to_s.sub(/\A::/, "")
|
|
288
|
+
end
|
|
289
|
+
return nil unless class_name && names.include?(class_name)
|
|
290
|
+
|
|
291
|
+
Type::Combinator.untyped
|
|
292
|
+
end
|
|
293
|
+
|
|
256
294
|
# ADR-2 § "Flow Contribution Bundle" / v0.1.1 Track 2
|
|
257
295
|
# slice 7. Walks every loaded plugin's
|
|
258
296
|
# `#flow_contribution_for(call_node:, scope:)` hook,
|
|
@@ -340,13 +378,13 @@ module Rigor
|
|
|
340
378
|
end
|
|
341
379
|
end
|
|
342
380
|
|
|
343
|
-
def try_plugin_contribution(call_node, scope)
|
|
381
|
+
def try_plugin_contribution(call_node, scope, receiver_type)
|
|
344
382
|
return nil if call_node.nil? || scope.nil?
|
|
345
383
|
|
|
346
384
|
registry = scope.environment&.plugin_registry
|
|
347
385
|
return nil if registry.nil? || registry.empty?
|
|
348
386
|
|
|
349
|
-
contributions = collect_plugin_contributions(registry, call_node, scope)
|
|
387
|
+
contributions = collect_plugin_contributions(registry, call_node, scope, receiver_type)
|
|
350
388
|
return nil if contributions.empty?
|
|
351
389
|
|
|
352
390
|
FlowContribution::Merger.merge(contributions).return_type
|
|
@@ -622,12 +660,21 @@ module Rigor
|
|
|
622
660
|
end
|
|
623
661
|
end
|
|
624
662
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
663
|
+
# ADR-37 slice 2 — gathers each plugin's return-type contribution
|
|
664
|
+
# from BOTH the narrow `dynamic_return` DSL (receiver-gated, wrapped
|
|
665
|
+
# as a return-only `FlowContribution`) and the legacy
|
|
666
|
+
# `flow_contribution_for` escape valve, so migrated and unmigrated
|
|
667
|
+
# plugins compose through the same merger.
|
|
668
|
+
def collect_plugin_contributions(registry, call_node, scope, receiver_type)
|
|
669
|
+
registry.plugins.flat_map do |plugin|
|
|
670
|
+
contributions = []
|
|
671
|
+
legacy = plugin.flow_contribution_for(call_node: call_node, scope: scope)
|
|
672
|
+
contributions << legacy if legacy.is_a?(FlowContribution)
|
|
673
|
+
dynamic = plugin.dynamic_return_type(call_node: call_node, scope: scope, receiver_type: receiver_type)
|
|
674
|
+
contributions << FlowContribution.new(return_type: dynamic) if dynamic
|
|
675
|
+
contributions
|
|
629
676
|
rescue StandardError
|
|
630
|
-
|
|
677
|
+
[]
|
|
631
678
|
end
|
|
632
679
|
end
|
|
633
680
|
|
|
@@ -10,7 +10,11 @@ module Rigor
|
|
|
10
10
|
# engine recognises an AST node class (that is `CoverageScanner`'s job),
|
|
11
11
|
# but whether the type it produces carries useful static information.
|
|
12
12
|
#
|
|
13
|
-
# Each visited node is classified into one of eight
|
|
13
|
+
# Each visited *expression* node is classified into one of eight
|
|
14
|
+
# precision tiers (non-expression syntax nodes — argument /
|
|
15
|
+
# parameter lists, parameter declarations, hash pairs, statement
|
|
16
|
+
# wrappers, clause headers — are skipped; see
|
|
17
|
+
# {NON_EXPRESSION_NODE_TYPES}):
|
|
14
18
|
#
|
|
15
19
|
# :constant — Constant[T]: literal value known exactly
|
|
16
20
|
# :nominal — Nominal/Singleton: class identity known
|
|
@@ -33,6 +37,59 @@ module Rigor
|
|
|
33
37
|
dynamic_specific dynamic_top top
|
|
34
38
|
].freeze
|
|
35
39
|
|
|
40
|
+
# Prism node classes that do not denote a value-producing
|
|
41
|
+
# expression, so typing them is meaningless — they have no
|
|
42
|
+
# runtime value to carry a type. Counting them (they always fall
|
|
43
|
+
# to the `dynamic_top` fallback) silently diluted the precision
|
|
44
|
+
# ratio: on a real survey target (shugo/textbringer) they were
|
|
45
|
+
# ~49% of every "opaque" node, dragging the headline number ~13
|
|
46
|
+
# points below the true expression-level precision. We exclude
|
|
47
|
+
# them from BOTH numerator and denominator so the ratio measures
|
|
48
|
+
# what it claims to — the type quality of actual expressions.
|
|
49
|
+
#
|
|
50
|
+
# The set is deliberately CONSERVATIVE: only nodes that are
|
|
51
|
+
# unambiguously non-expressions in Ruby's grammar are listed —
|
|
52
|
+
# argument / parameter list containers and the parameter
|
|
53
|
+
# declarations inside them; the `key => value` pair node (its key
|
|
54
|
+
# and value are themselves walked and counted); the program /
|
|
55
|
+
# statements sequence wrappers (their value is the last child,
|
|
56
|
+
# already counted — listing them avoids double-counting); and the
|
|
57
|
+
# clause-header nodes whose body, not the header, carries the
|
|
58
|
+
# value. Anything that *could* be a value expression (`BlockNode`,
|
|
59
|
+
# `BeginNode`, `ImplicitNode`, `ParenthesesNode`, splats, …) is
|
|
60
|
+
# left in so a genuine inference gap stays visible.
|
|
61
|
+
#
|
|
62
|
+
# Compared by class NAME so a Prism version that lacks one of the
|
|
63
|
+
# newer node classes does not break loading.
|
|
64
|
+
NON_EXPRESSION_NODE_TYPES = %w[
|
|
65
|
+
Prism::ProgramNode
|
|
66
|
+
Prism::StatementsNode
|
|
67
|
+
Prism::ArgumentsNode
|
|
68
|
+
Prism::BlockArgumentNode
|
|
69
|
+
Prism::ParametersNode
|
|
70
|
+
Prism::BlockParametersNode
|
|
71
|
+
Prism::NumberedParametersNode
|
|
72
|
+
Prism::ItParametersNode
|
|
73
|
+
Prism::KeywordHashNode
|
|
74
|
+
Prism::RequiredParameterNode
|
|
75
|
+
Prism::OptionalParameterNode
|
|
76
|
+
Prism::RestParameterNode
|
|
77
|
+
Prism::KeywordRestParameterNode
|
|
78
|
+
Prism::BlockParameterNode
|
|
79
|
+
Prism::RequiredKeywordParameterNode
|
|
80
|
+
Prism::OptionalKeywordParameterNode
|
|
81
|
+
Prism::ForwardingParameterNode
|
|
82
|
+
Prism::NoKeywordsParameterNode
|
|
83
|
+
Prism::ImplicitRestNode
|
|
84
|
+
Prism::AssocNode
|
|
85
|
+
Prism::AssocSplatNode
|
|
86
|
+
Prism::WhenNode
|
|
87
|
+
Prism::InNode
|
|
88
|
+
Prism::ElseNode
|
|
89
|
+
Prism::EnsureNode
|
|
90
|
+
Prism::RescueNode
|
|
91
|
+
].to_set.freeze
|
|
92
|
+
|
|
36
93
|
TIER_RANK = TIERS.each_with_index.to_h.freeze
|
|
37
94
|
private_constant :TIER_RANK
|
|
38
95
|
|
|
@@ -87,6 +144,8 @@ module Rigor
|
|
|
87
144
|
total = 0
|
|
88
145
|
|
|
89
146
|
Source::NodeWalker.each(root) do |node|
|
|
147
|
+
next if NON_EXPRESSION_NODE_TYPES.include?(node.class.name)
|
|
148
|
+
|
|
90
149
|
type = scope_index[node].type_of(node)
|
|
91
150
|
tier = classify(type)
|
|
92
151
|
tier_counts[tier] += 1
|
|
@@ -107,7 +107,14 @@ module Rigor
|
|
|
107
107
|
# introduced method names. `rigor check` consults the
|
|
108
108
|
# table to suppress false positives for methods the
|
|
109
109
|
# user has defined but no RBS sig describes.
|
|
110
|
-
|
|
110
|
+
# Merged UNDER any cross-file pre-pass seed (like the def-node
|
|
111
|
+
# / include tables below) so a method `def`/`attr_reader`-
|
|
112
|
+
# declared in one file suppresses a false `undefined-method`
|
|
113
|
+
# for a call in another — `rigor check` seeds the project-wide
|
|
114
|
+
# table via `Runner#seed_project_scope`.
|
|
115
|
+
discovered_methods = deep_merge_class_methods(
|
|
116
|
+
default_scope.discovered_methods, build_discovered_methods(root)
|
|
117
|
+
)
|
|
111
118
|
seeded_scope = seeded_scope.with_discovered_methods(discovered_methods)
|
|
112
119
|
|
|
113
120
|
# v0.0.2 #5 + ADR-24 slice 2 — record per-instance-method
|
|
@@ -383,7 +390,7 @@ module Rigor
|
|
|
383
390
|
# class body has been walked, using `init_writes` as
|
|
384
391
|
# the soundness gate (an ivar written in `initialize`
|
|
385
392
|
# is initialised before any other method body runs).
|
|
386
|
-
collect_read_before_write_evidence(def_node, class_name, read_before_write, init_writes)
|
|
393
|
+
collect_read_before_write_evidence(def_node, class_name, read_before_write, init_writes, default_scope)
|
|
387
394
|
end
|
|
388
395
|
|
|
389
396
|
# Walks the method body in AST (== execution) order
|
|
@@ -394,14 +401,21 @@ module Rigor
|
|
|
394
401
|
# `init_writes` instead — used by the finalisation step
|
|
395
402
|
# to suppress nil contribution for ivars the constructor
|
|
396
403
|
# guarantees are initialised.
|
|
397
|
-
def collect_read_before_write_evidence(def_node, class_name, read_before_write, init_writes)
|
|
404
|
+
def collect_read_before_write_evidence(def_node, class_name, read_before_write, init_writes, default_scope = nil)
|
|
398
405
|
return if read_before_write.nil? || init_writes.nil?
|
|
399
406
|
|
|
400
407
|
seen_writes = Set.new
|
|
401
408
|
read_first = Set.new
|
|
402
409
|
detect_read_before_write(def_node.body, seen_writes, read_first)
|
|
403
410
|
|
|
404
|
-
|
|
411
|
+
# ADR-38 — `initialize` is the built-in initializer gate;
|
|
412
|
+
# a plugin may declare additional `def`-form initializer
|
|
413
|
+
# methods (minitest `setup`, Rails `after_initialize`, DI
|
|
414
|
+
# setters) on a constrained class. Both fold their writes
|
|
415
|
+
# into `init_writes`, suppressing the read-before-write nil
|
|
416
|
+
# contribution for sibling readers.
|
|
417
|
+
if def_node.name == :initialize ||
|
|
418
|
+
additional_initializer?(class_name, def_node.name, default_scope)
|
|
405
419
|
init_set = (init_writes[class_name] ||= Set.new)
|
|
406
420
|
seen_writes.each { |name| init_set << name }
|
|
407
421
|
return
|
|
@@ -413,6 +427,43 @@ module Rigor
|
|
|
413
427
|
read_first.each { |name| rbw_set << name }
|
|
414
428
|
end
|
|
415
429
|
|
|
430
|
+
# ADR-38 — true when a loaded plugin declares `method_name` an
|
|
431
|
+
# additional initializer for `class_name` (or an ancestor).
|
|
432
|
+
# Reads the plugin registry off the pre-pass scope's
|
|
433
|
+
# environment; the receiver-constraint match reuses
|
|
434
|
+
# `Environment#class_ordering` (the same mechanism ADR-16
|
|
435
|
+
# Tier A's `MacroBlockSelfType` uses). The whole lookup is
|
|
436
|
+
# wrapped so any resolution failure degrades to "no match" —
|
|
437
|
+
# since the gate only ever SUPPRESSES a nil contribution, a
|
|
438
|
+
# missed match is false-positive-safe (it merely leaves the
|
|
439
|
+
# existing nil widening in place).
|
|
440
|
+
def additional_initializer?(class_name, method_name, default_scope)
|
|
441
|
+
return false if class_name.nil? || default_scope.nil?
|
|
442
|
+
|
|
443
|
+
environment = default_scope.environment
|
|
444
|
+
registry = environment&.plugin_registry
|
|
445
|
+
return false if registry.nil?
|
|
446
|
+
return false if registry.respond_to?(:empty?) && registry.empty?
|
|
447
|
+
return false unless registry.respond_to?(:additional_initializers)
|
|
448
|
+
|
|
449
|
+
registry.additional_initializers.any? do |entry|
|
|
450
|
+
entry.covers_method?(method_name) &&
|
|
451
|
+
class_matches_constraint?(class_name, entry.receiver_constraint, environment)
|
|
452
|
+
end
|
|
453
|
+
rescue StandardError
|
|
454
|
+
false
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def class_matches_constraint?(class_name, constraint, environment)
|
|
458
|
+
return true if class_name == constraint
|
|
459
|
+
return false if environment.nil?
|
|
460
|
+
|
|
461
|
+
ordering = environment.class_ordering(class_name, constraint)
|
|
462
|
+
%i[equal subclass].include?(ordering)
|
|
463
|
+
rescue StandardError
|
|
464
|
+
false
|
|
465
|
+
end
|
|
466
|
+
|
|
416
467
|
IVAR_WRITE_NODES = [
|
|
417
468
|
Prism::InstanceVariableWriteNode,
|
|
418
469
|
Prism::InstanceVariableOrWriteNode,
|
|
@@ -802,7 +853,19 @@ module Rigor
|
|
|
802
853
|
accumulator.transform_values(&:freeze).freeze
|
|
803
854
|
end
|
|
804
855
|
|
|
805
|
-
#
|
|
856
|
+
# Merges two `class_name => { method => kind }` tables, unioning
|
|
857
|
+
# the per-class method maps (so a seeded cross-file table and the
|
|
858
|
+
# current file's table combine instead of clobbering).
|
|
859
|
+
def deep_merge_class_methods(base, overlay)
|
|
860
|
+
return overlay if base.nil? || base.empty?
|
|
861
|
+
return base if overlay.empty?
|
|
862
|
+
|
|
863
|
+
base.merge(overlay) do |_class_name, base_methods, overlay_methods|
|
|
864
|
+
base_methods.merge(overlay_methods)
|
|
865
|
+
end
|
|
866
|
+
end
|
|
867
|
+
|
|
868
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize
|
|
806
869
|
def walk_methods(node, qualified_prefix, in_singleton_class, accumulator)
|
|
807
870
|
return unless node.is_a?(Prism::Node)
|
|
808
871
|
|
|
@@ -836,6 +899,10 @@ module Rigor
|
|
|
836
899
|
return
|
|
837
900
|
when Prism::CallNode
|
|
838
901
|
record_define_method(node, qualified_prefix, in_singleton_class, accumulator) if node.name == :define_method
|
|
902
|
+
if ATTR_MACROS.include?(node.name)
|
|
903
|
+
record_attr_methods(node, qualified_prefix, in_singleton_class,
|
|
904
|
+
accumulator)
|
|
905
|
+
end
|
|
839
906
|
end
|
|
840
907
|
|
|
841
908
|
node.compact_child_nodes.each do |child|
|
|
@@ -866,7 +933,7 @@ module Rigor
|
|
|
866
933
|
end
|
|
867
934
|
end
|
|
868
935
|
end
|
|
869
|
-
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
936
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize
|
|
870
937
|
|
|
871
938
|
# v0.1.2 — when a `Const = Data.define(*sym) do ... end`
|
|
872
939
|
# / `Const = Struct.new(*sym) do ... end` constant write
|
|
@@ -1307,6 +1374,35 @@ module Rigor
|
|
|
1307
1374
|
accumulator[class_name][method_name] = in_singleton_class ? :singleton : :instance
|
|
1308
1375
|
end
|
|
1309
1376
|
|
|
1377
|
+
# The `attr_*` accessor macros that introduce methods Rigor must
|
|
1378
|
+
# treat as source-declared. Without this, a class that defines an
|
|
1379
|
+
# accessor with `attr_reader :x` AND carries RBS that omits `x`
|
|
1380
|
+
# (a common gap — the project ships an incomplete `sig/`) fires a
|
|
1381
|
+
# false `call.undefined-method` on `obj.x`, because the
|
|
1382
|
+
# undefined-method rule only suppressed `def` / `define_method` /
|
|
1383
|
+
# `alias_method`-discovered methods. `attr_reader` defines
|
|
1384
|
+
# readers, `attr_writer` writers (`x=`), `attr_accessor` both.
|
|
1385
|
+
ATTR_MACROS = %i[attr_reader attr_writer attr_accessor].freeze
|
|
1386
|
+
|
|
1387
|
+
def record_attr_methods(call_node, qualified_prefix, in_singleton_class, accumulator)
|
|
1388
|
+
return if qualified_prefix.empty?
|
|
1389
|
+
return unless call_node.receiver.nil? # only the implicit-self macro defines on the lexical class
|
|
1390
|
+
return if call_node.arguments.nil?
|
|
1391
|
+
|
|
1392
|
+
kind = in_singleton_class ? :singleton : :instance
|
|
1393
|
+
reader = call_node.name != :attr_writer
|
|
1394
|
+
writer = call_node.name != :attr_reader
|
|
1395
|
+
class_name = qualified_prefix.join("::")
|
|
1396
|
+
call_node.arguments.arguments.each do |arg|
|
|
1397
|
+
base = literal_method_name(arg)
|
|
1398
|
+
next if base.nil?
|
|
1399
|
+
|
|
1400
|
+
accumulator[class_name] ||= {}
|
|
1401
|
+
accumulator[class_name][base] = kind if reader
|
|
1402
|
+
accumulator[class_name][:"#{base}="] = kind if writer
|
|
1403
|
+
end
|
|
1404
|
+
end
|
|
1405
|
+
|
|
1310
1406
|
def literal_method_name(node)
|
|
1311
1407
|
return nil unless node.is_a?(Prism::SymbolNode) || node.is_a?(Prism::StringNode)
|
|
1312
1408
|
|
|
@@ -1386,7 +1482,7 @@ module Rigor
|
|
|
1386
1482
|
# @return [Hash{Symbol => Hash}]
|
|
1387
1483
|
# `{ def_nodes:, def_sources:, superclasses:, includes: }`
|
|
1388
1484
|
def discovered_def_index_for_paths(paths, buffer: nil)
|
|
1389
|
-
acc = { def_nodes: {}, def_sources: {}, superclasses: {}, includes: {}, method_visibilities: {} }
|
|
1485
|
+
acc = { def_nodes: {}, def_sources: {}, superclasses: {}, includes: {}, method_visibilities: {}, methods: {} }
|
|
1390
1486
|
paths.each do |path|
|
|
1391
1487
|
physical = buffer ? buffer.resolve(path) : path
|
|
1392
1488
|
root = Prism.parse(File.read(physical), filepath: path).value
|
|
@@ -1396,10 +1492,30 @@ module Rigor
|
|
|
1396
1492
|
# analyzer surfaces the parse error separately.
|
|
1397
1493
|
next
|
|
1398
1494
|
end
|
|
1399
|
-
|
|
1495
|
+
# Cross-file method suppression is for the project's OWN
|
|
1496
|
+
# accessors (attr_* / define_method / alias) — NOT for plain
|
|
1497
|
+
# `def`s. A cross-file `def` on a class is exactly the ADR-17
|
|
1498
|
+
# monkey-patch case the undefined-method rule deliberately
|
|
1499
|
+
# surfaces (fire + def-site annotation, nudging `pre_eval:`),
|
|
1500
|
+
# so dropping the `def`-declared names keeps that contract
|
|
1501
|
+
# intact while still letting `attr_reader :x` in one file
|
|
1502
|
+
# suppress a false undefined-method for `obj.x` in another.
|
|
1503
|
+
acc[:methods] = subtract_def_methods(acc[:methods], acc[:def_nodes])
|
|
1504
|
+
%i[def_nodes def_sources includes method_visibilities methods].each { |key| acc[key].each_value(&:freeze) }
|
|
1400
1505
|
acc.transform_values(&:freeze)
|
|
1401
1506
|
end
|
|
1402
1507
|
|
|
1508
|
+
# Removes, per class, the method names that have a project `def`
|
|
1509
|
+
# node, leaving only accessor/alias/define_method-introduced
|
|
1510
|
+
# methods in the cross-file suppression table.
|
|
1511
|
+
def subtract_def_methods(methods, def_nodes)
|
|
1512
|
+
methods.each_with_object({}) do |(class_name, table), out|
|
|
1513
|
+
defs = def_nodes[class_name] || {}
|
|
1514
|
+
kept = table.reject { |method_name, _kind| defs.key?(method_name) }
|
|
1515
|
+
out[class_name] = kept unless kept.empty?
|
|
1516
|
+
end
|
|
1517
|
+
end
|
|
1518
|
+
|
|
1403
1519
|
# Folds one file's class-keyed indexes into the cross-file
|
|
1404
1520
|
# accumulator. `method_visibilities` (ADR-35) is collected here so
|
|
1405
1521
|
# the override-visibility-reduced rule can read an ancestor's
|
|
@@ -1413,6 +1529,9 @@ module Rigor
|
|
|
1413
1529
|
build_discovered_method_visibilities(root).each do |class_name, table|
|
|
1414
1530
|
(acc[:method_visibilities][class_name] ||= {}).merge!(table)
|
|
1415
1531
|
end
|
|
1532
|
+
build_discovered_methods(root).each do |class_name, table|
|
|
1533
|
+
(acc[:methods][class_name] ||= {}).merge!(table)
|
|
1534
|
+
end
|
|
1416
1535
|
end
|
|
1417
1536
|
|
|
1418
1537
|
# Merges one file's `class → method → DefNode` map into the
|
|
@@ -1371,16 +1371,21 @@ module Rigor
|
|
|
1371
1371
|
end
|
|
1372
1372
|
end
|
|
1373
1373
|
|
|
1374
|
-
#
|
|
1375
|
-
# `
|
|
1376
|
-
#
|
|
1377
|
-
#
|
|
1378
|
-
#
|
|
1374
|
+
# ADR-37 slice 2 — gathers each plugin's post-return narrowing from
|
|
1375
|
+
# BOTH the narrow `type_specifier` DSL (method-gated, wrapped as a
|
|
1376
|
+
# facts-only `FlowContribution`) and the legacy
|
|
1377
|
+
# `flow_contribution_for` escape valve, swallowing per-plugin
|
|
1378
|
+
# exceptions so a buggy plugin can't abort the assertion path.
|
|
1379
1379
|
def collect_plugin_contributions(registry, call_node, current_scope)
|
|
1380
|
-
registry.plugins.
|
|
1381
|
-
|
|
1380
|
+
registry.plugins.flat_map do |plugin|
|
|
1381
|
+
contributions = []
|
|
1382
|
+
legacy = plugin.flow_contribution_for(call_node: call_node, scope: current_scope)
|
|
1383
|
+
contributions << legacy if legacy.is_a?(Rigor::FlowContribution)
|
|
1384
|
+
facts = plugin.type_specifier_facts(call_node: call_node, scope: current_scope)
|
|
1385
|
+
contributions << Rigor::FlowContribution.new(post_return_facts: facts) if facts && !facts.empty?
|
|
1386
|
+
contributions
|
|
1382
1387
|
rescue StandardError
|
|
1383
|
-
|
|
1388
|
+
[]
|
|
1384
1389
|
end
|
|
1385
1390
|
end
|
|
1386
1391
|
|