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
|
@@ -29,9 +29,19 @@ module Rigor
|
|
|
29
29
|
# floor — the recorded string is the input to a later slice's
|
|
30
30
|
# precision promotion via ADR-13's `Plugin::TypeNodeResolver`.
|
|
31
31
|
class SyntheticMethodIndex
|
|
32
|
-
attr_reader :entries
|
|
32
|
+
attr_reader :entries, :class_names
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
# @param entries [Array<SyntheticMethod>]
|
|
35
|
+
# @param class_names [Array<String>, Set<String>] names of
|
|
36
|
+
# classes the substrate synthesises wholesale (ADR-36
|
|
37
|
+
# nested-class emission — the variant subclasses that have
|
|
38
|
+
# no RBS/source declaration of their own). Recorded so
|
|
39
|
+
# `Environment#class_known?` can resolve them as classes
|
|
40
|
+
# (their constant reference + `.new` dispatch) even though
|
|
41
|
+
# nothing else in the type universe declares them. Tier B/C
|
|
42
|
+
# method emissions leave this empty (their receiver classes
|
|
43
|
+
# are already real).
|
|
44
|
+
def initialize(entries: [], class_names: [])
|
|
35
45
|
unless entries.is_a?(Array) && entries.all?(SyntheticMethod)
|
|
36
46
|
raise ArgumentError,
|
|
37
47
|
"SyntheticMethodIndex#entries must be an Array of SyntheticMethod, got #{entries.inspect}"
|
|
@@ -40,11 +50,20 @@ module Rigor
|
|
|
40
50
|
@entries = Ractor.make_shareable(entries.dup)
|
|
41
51
|
@by_instance = Ractor.make_shareable(bucket(entries, SyntheticMethod::INSTANCE))
|
|
42
52
|
@by_singleton = Ractor.make_shareable(bucket(entries, SyntheticMethod::SINGLETON))
|
|
53
|
+
@class_names = Ractor.make_shareable(class_names.to_a.map(&:to_s).uniq.freeze)
|
|
54
|
+
@class_name_set = Ractor.make_shareable(@class_names.to_set)
|
|
43
55
|
freeze
|
|
44
56
|
end
|
|
45
57
|
|
|
46
58
|
def empty?
|
|
47
|
-
entries.empty?
|
|
59
|
+
entries.empty? && class_names.empty?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# True when `name` is a substrate-synthesised class (an
|
|
63
|
+
# ADR-36 variant subclass). Used by `Environment#class_known?`
|
|
64
|
+
# so the constant resolves and `.new` dispatches.
|
|
65
|
+
def knows_class?(name)
|
|
66
|
+
@class_name_set.include?(name.to_s)
|
|
48
67
|
end
|
|
49
68
|
|
|
50
69
|
# Returns an Array of matching {SyntheticMethod} records in
|
|
@@ -59,7 +78,7 @@ module Rigor
|
|
|
59
78
|
end
|
|
60
79
|
|
|
61
80
|
def to_h
|
|
62
|
-
{ "entries" => entries.map(&:to_h) }
|
|
81
|
+
{ "entries" => entries.map(&:to_h), "class_names" => class_names }
|
|
63
82
|
end
|
|
64
83
|
|
|
65
84
|
EMPTY_ROW = [].freeze
|
|
@@ -4,6 +4,7 @@ require "prism"
|
|
|
4
4
|
|
|
5
5
|
require_relative "../plugin/macro/heredoc_template"
|
|
6
6
|
require_relative "../plugin/macro/trait_registry"
|
|
7
|
+
require_relative "../source/literals"
|
|
7
8
|
require_relative "synthetic_method"
|
|
8
9
|
require_relative "synthetic_method_index"
|
|
9
10
|
|
|
@@ -69,13 +70,15 @@ module Rigor
|
|
|
69
70
|
def scan(plugin_registry:, paths:, environment: nil, fact_store: nil, buffer: nil)
|
|
70
71
|
templates = collect_templates(plugin_registry)
|
|
71
72
|
registries = collect_trait_registries(plugin_registry)
|
|
72
|
-
|
|
73
|
+
nested_templates = collect_nested_class_templates(plugin_registry)
|
|
74
|
+
return SyntheticMethodIndex::EMPTY if templates.empty? && registries.empty? && nested_templates.empty?
|
|
73
75
|
|
|
74
76
|
asts = parse_paths(paths, buffer: buffer)
|
|
75
77
|
hierarchy = build_hierarchy(asts)
|
|
76
78
|
concern_index = build_concern_index(asts)
|
|
77
79
|
|
|
78
80
|
entries = []
|
|
81
|
+
class_names = []
|
|
79
82
|
asts.each do |path, ast|
|
|
80
83
|
walk_class_bodies(ast) do |class_name, call_node|
|
|
81
84
|
collect_entries(entries, templates, class_name, call_node, hierarchy, environment, path, fact_store)
|
|
@@ -85,9 +88,10 @@ module Rigor
|
|
|
85
88
|
templates, registries, hierarchy, environment, path, fact_store
|
|
86
89
|
)
|
|
87
90
|
end
|
|
91
|
+
collect_nested_class_entries(entries, class_names, nested_templates, ast, path) unless nested_templates.empty?
|
|
88
92
|
end
|
|
89
93
|
|
|
90
|
-
SyntheticMethodIndex.new(entries: entries)
|
|
94
|
+
SyntheticMethodIndex.new(entries: entries, class_names: class_names)
|
|
91
95
|
end
|
|
92
96
|
|
|
93
97
|
# Aggregates `(plugin_id, template)` pairs across every
|
|
@@ -119,6 +123,146 @@ module Rigor
|
|
|
119
123
|
end
|
|
120
124
|
end
|
|
121
125
|
|
|
126
|
+
# ADR-36 — aggregates `(plugin_id, template)` pairs across
|
|
127
|
+
# every plugin's `manifest.nested_class_templates`. Empty when
|
|
128
|
+
# no plugin contributes the nested-class emission tier.
|
|
129
|
+
def collect_nested_class_templates(plugin_registry)
|
|
130
|
+
return [] if plugin_registry.nil? || plugin_registry.empty?
|
|
131
|
+
|
|
132
|
+
plugin_registry.plugins.flat_map do |plugin|
|
|
133
|
+
# rigor:disable undefined-method
|
|
134
|
+
plugin.manifest.nested_class_templates.map do |template|
|
|
135
|
+
[plugin.manifest.id, template]
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# ADR-36 nested-class emission. For each class that `extend`s a
|
|
141
|
+
# template's `receiver_constraint` and carries a
|
|
142
|
+
# `<block_method> do ... end` block, mint one synthetic
|
|
143
|
+
# subclass per `<variant_method> <Const>, <Type>` row:
|
|
144
|
+
#
|
|
145
|
+
# class Shape
|
|
146
|
+
# extend Mangrove::Enum
|
|
147
|
+
# variants do
|
|
148
|
+
# variant Circle, Float
|
|
149
|
+
# end
|
|
150
|
+
# end
|
|
151
|
+
#
|
|
152
|
+
# yields synthetic class `Shape::Circle` + instance method
|
|
153
|
+
# `Shape::Circle#inner -> Float`. The variant subclass name is
|
|
154
|
+
# recorded in `class_names` so `Environment#class_known?`
|
|
155
|
+
# resolves the constant (and `.new` dispatches through
|
|
156
|
+
# `meta_new`); `#inner`'s return type is the literal constant
|
|
157
|
+
# type argument (non-constant inner shapes degrade to
|
|
158
|
+
# `Dynamic[Top]` per the slice-A floor).
|
|
159
|
+
def collect_nested_class_entries(entries, class_names, nested_templates, ast, path)
|
|
160
|
+
return if ast.nil?
|
|
161
|
+
|
|
162
|
+
walk_classes(ast) do |class_name, class_node|
|
|
163
|
+
body = class_body_statements(class_node)
|
|
164
|
+
next if body.empty?
|
|
165
|
+
|
|
166
|
+
nested_templates.each do |(plugin_id, template)|
|
|
167
|
+
next unless body_extends?(body, template.receiver_constraint)
|
|
168
|
+
|
|
169
|
+
each_variant_call(body, template) do |variant_const, inner_node|
|
|
170
|
+
emit_variant(entries, class_names, class_name, variant_const, inner_node, template, plugin_id, path)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Walks every class declaration, yielding its fully-qualified
|
|
177
|
+
# name and the `Prism::ClassNode`. Mirrors `walk_class_bodies`'
|
|
178
|
+
# scope-stack bookkeeping but hands back the class node itself.
|
|
179
|
+
def walk_classes(node, scope_stack = [], &)
|
|
180
|
+
return unless node.respond_to?(:compact_child_nodes)
|
|
181
|
+
|
|
182
|
+
case node
|
|
183
|
+
when Prism::ClassNode
|
|
184
|
+
name = class_name_from(node, scope_stack)
|
|
185
|
+
yield name, node if name
|
|
186
|
+
new_stack = scope_stack + [node]
|
|
187
|
+
node.body&.compact_child_nodes&.each { |child| walk_classes(child, new_stack, &) }
|
|
188
|
+
when Prism::ModuleNode
|
|
189
|
+
new_stack = scope_stack + [node]
|
|
190
|
+
node.body&.compact_child_nodes&.each { |child| walk_classes(child, new_stack, &) }
|
|
191
|
+
else
|
|
192
|
+
node.compact_child_nodes.each { |child| walk_classes(child, scope_stack, &) }
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def class_body_statements(class_node)
|
|
197
|
+
body = class_node.body
|
|
198
|
+
body.respond_to?(:body) ? body.body.compact : []
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# True when the class body carries `extend <constraint>`
|
|
202
|
+
# (receiverless `extend` call with the constraint constant as
|
|
203
|
+
# its first argument).
|
|
204
|
+
def body_extends?(body, constraint)
|
|
205
|
+
body.any? do |stmt|
|
|
206
|
+
stmt.is_a?(Prism::CallNode) && stmt.receiver.nil? && stmt.name == :extend &&
|
|
207
|
+
const_name_string(first_arg(stmt)) == constraint
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Yields `(variant_const_name, inner_type_node)` for every
|
|
212
|
+
# `<variant_method> <Const>, <Type>` call inside the template's
|
|
213
|
+
# `<block_method> do ... end` block(s).
|
|
214
|
+
def each_variant_call(body, template, &)
|
|
215
|
+
body.each do |stmt|
|
|
216
|
+
next unless variants_block_call?(stmt, template)
|
|
217
|
+
|
|
218
|
+
block_body_statements(stmt.block).each { |call| yield_variant(call, template, &) }
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def variants_block_call?(stmt, template)
|
|
223
|
+
stmt.is_a?(Prism::CallNode) && stmt.receiver.nil? &&
|
|
224
|
+
stmt.name == template.block_method && stmt.block.is_a?(Prism::BlockNode)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def yield_variant(call, template)
|
|
228
|
+
return unless call.is_a?(Prism::CallNode) && call.receiver.nil? && call.name == template.variant_method
|
|
229
|
+
|
|
230
|
+
args = call.arguments&.arguments || []
|
|
231
|
+
variant_const = const_name_string(args[template.name_arg_position])
|
|
232
|
+
return if variant_const.nil?
|
|
233
|
+
|
|
234
|
+
yield variant_const, args[template.inner_arg_position]
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def block_body_statements(block_node)
|
|
238
|
+
body = block_node.body
|
|
239
|
+
body.respond_to?(:body) ? body.body.compact : []
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def emit_variant(entries, class_names, enclosing, variant_const, inner_node, template, plugin_id, path) # rubocop:disable Metrics/ParameterLists
|
|
243
|
+
variant_class = "#{enclosing}::#{variant_const}"
|
|
244
|
+
class_names << variant_class
|
|
245
|
+
inner_type = const_name_string(inner_node) || "untyped"
|
|
246
|
+
|
|
247
|
+
entries << SyntheticMethod.new(
|
|
248
|
+
class_name: variant_class,
|
|
249
|
+
method_name: template.inner_reader,
|
|
250
|
+
return_type: inner_type,
|
|
251
|
+
kind: SyntheticMethod::INSTANCE,
|
|
252
|
+
provenance: {
|
|
253
|
+
plugin_id: plugin_id,
|
|
254
|
+
tier: "nested_class",
|
|
255
|
+
enclosing: enclosing,
|
|
256
|
+
variant: variant_const,
|
|
257
|
+
source_path: path
|
|
258
|
+
}
|
|
259
|
+
)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def first_arg(call_node)
|
|
263
|
+
call_node.arguments&.arguments&.first
|
|
264
|
+
end
|
|
265
|
+
|
|
122
266
|
def parse_paths(paths, buffer: nil)
|
|
123
267
|
paths.to_h do |path|
|
|
124
268
|
physical = buffer ? buffer.resolve(path) : path
|
|
@@ -399,9 +543,7 @@ module Rigor
|
|
|
399
543
|
end
|
|
400
544
|
|
|
401
545
|
def literal_symbol_value(node)
|
|
402
|
-
|
|
403
|
-
when Prism::SymbolNode, Prism::StringNode then node.unescaped.to_sym
|
|
404
|
-
end
|
|
546
|
+
Source::Literals.symbol_or_string(node)
|
|
405
547
|
end
|
|
406
548
|
|
|
407
549
|
def emit_trait_module_entries(entries, class_name, modules, registry, plugin_id, path, call_node, environment) # rubocop:disable Metrics/ParameterLists
|
|
@@ -584,15 +726,7 @@ module Rigor
|
|
|
584
726
|
end
|
|
585
727
|
|
|
586
728
|
def literal_symbol_arg(call_node, index)
|
|
587
|
-
|
|
588
|
-
return nil if args_node.nil?
|
|
589
|
-
|
|
590
|
-
arg = args_node.arguments[index]
|
|
591
|
-
return nil unless arg
|
|
592
|
-
|
|
593
|
-
case arg
|
|
594
|
-
when Prism::SymbolNode, Prism::StringNode then arg.unescaped.to_sym
|
|
595
|
-
end
|
|
729
|
+
Source::Literals.symbol_arg(call_node, index)
|
|
596
730
|
end
|
|
597
731
|
end
|
|
598
732
|
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Plugin
|
|
5
|
+
# ADR-38 declaration: "on `receiver_constraint` (and its
|
|
6
|
+
# subclasses), every method named in `methods` also establishes
|
|
7
|
+
# instance-variable state — treat it like `initialize` for the
|
|
8
|
+
# read-before-write nil soundness gate."
|
|
9
|
+
#
|
|
10
|
+
# Authored on a plugin manifest:
|
|
11
|
+
#
|
|
12
|
+
# manifest(
|
|
13
|
+
# id: "minitest",
|
|
14
|
+
# version: "0.1.0",
|
|
15
|
+
# additional_initializers: [
|
|
16
|
+
# Rigor::Plugin::AdditionalInitializer.new(
|
|
17
|
+
# receiver_constraint: "Minitest::Test",
|
|
18
|
+
# methods: [:setup]
|
|
19
|
+
# )
|
|
20
|
+
# ]
|
|
21
|
+
# )
|
|
22
|
+
#
|
|
23
|
+
# The Ruby analogue of PHPStan's `AdditionalConstructorsExtension`.
|
|
24
|
+
# `Rigor::Inference::ScopeIndexer` consults the aggregated set at
|
|
25
|
+
# its single read-before-write gate: for a `def` whose name is in
|
|
26
|
+
# `methods` on a class that equals or inherits from
|
|
27
|
+
# `receiver_constraint` (matched via `Environment#class_ordering`,
|
|
28
|
+
# the same mechanism ADR-16 Tier A uses), the method's ivar writes
|
|
29
|
+
# are folded into the class's `init_writes` set, so a sibling
|
|
30
|
+
# method reading those ivars no longer gets a `Constant[nil]`
|
|
31
|
+
# widening.
|
|
32
|
+
#
|
|
33
|
+
# The contribution can only ever *suppress* a nil widening — it
|
|
34
|
+
# never makes the analyzer stricter — so a missed or over-broad
|
|
35
|
+
# match is false-positive-safe by construction (ADR-38 § "Why this
|
|
36
|
+
# is FP-safe").
|
|
37
|
+
#
|
|
38
|
+
# ## Fields
|
|
39
|
+
#
|
|
40
|
+
# - `receiver_constraint` — fully-qualified class name (String).
|
|
41
|
+
# The entry applies to that class and its subclasses.
|
|
42
|
+
# - `methods` — Array of Symbol method names treated as
|
|
43
|
+
# initializers on a matching class.
|
|
44
|
+
#
|
|
45
|
+
# ## Ractor-shareability
|
|
46
|
+
#
|
|
47
|
+
# Both fields are frozen at construction (ADR-15 Phase 1);
|
|
48
|
+
# `Ractor.shareable?` returns true after `#initialize`, so the
|
|
49
|
+
# value object survives `Plugin::Registry.materialize` into a
|
|
50
|
+
# worker Ractor.
|
|
51
|
+
class AdditionalInitializer
|
|
52
|
+
attr_reader :receiver_constraint, :methods
|
|
53
|
+
|
|
54
|
+
def initialize(receiver_constraint:, methods:)
|
|
55
|
+
validate_receiver_constraint!(receiver_constraint)
|
|
56
|
+
validate_methods!(methods)
|
|
57
|
+
|
|
58
|
+
@receiver_constraint = receiver_constraint.dup.freeze
|
|
59
|
+
@methods = methods.map(&:to_sym).freeze
|
|
60
|
+
freeze
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# True when `method_name` (a Symbol) is declared an initializer
|
|
64
|
+
# by this entry. The class-constraint match is the caller's
|
|
65
|
+
# responsibility (it needs the environment's class graph).
|
|
66
|
+
def covers_method?(method_name)
|
|
67
|
+
methods.include?(method_name)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def to_h
|
|
71
|
+
{
|
|
72
|
+
"receiver_constraint" => receiver_constraint,
|
|
73
|
+
"methods" => methods.map(&:to_s)
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def ==(other)
|
|
78
|
+
other.is_a?(AdditionalInitializer) && to_h == other.to_h
|
|
79
|
+
end
|
|
80
|
+
alias eql? ==
|
|
81
|
+
|
|
82
|
+
def hash
|
|
83
|
+
to_h.hash
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def validate_receiver_constraint!(value)
|
|
89
|
+
return if value.is_a?(String) && !value.empty?
|
|
90
|
+
|
|
91
|
+
raise ArgumentError,
|
|
92
|
+
"Plugin::AdditionalInitializer#receiver_constraint must be a non-empty String, " \
|
|
93
|
+
"got #{value.inspect}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def validate_methods!(value)
|
|
97
|
+
if value.is_a?(Array) && !value.empty? &&
|
|
98
|
+
value.all? { |m| m.is_a?(Symbol) || (m.is_a?(String) && !m.empty?) }
|
|
99
|
+
return
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
raise ArgumentError,
|
|
103
|
+
"Plugin::AdditionalInitializer#methods must be a non-empty Array of " \
|
|
104
|
+
"Symbol/non-empty String, got #{value.inspect}"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|