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.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -2
  3. data/exe/rigor +19 -0
  4. data/lib/rigor/analysis/check_rules.rb +25 -1
  5. data/lib/rigor/analysis/diagnostic.rb +40 -0
  6. data/lib/rigor/analysis/runner.rb +61 -2
  7. data/lib/rigor/analysis/worker_session.rb +3 -2
  8. data/lib/rigor/cache/descriptor.rb +6 -2
  9. data/lib/rigor/cli/plugins_command.rb +51 -4
  10. data/lib/rigor/cli/plugins_renderer.rb +86 -1
  11. data/lib/rigor/cli.rb +135 -5
  12. data/lib/rigor/environment/rbs_loader.rb +259 -1
  13. data/lib/rigor/environment.rb +8 -2
  14. data/lib/rigor/inference/budget_trace.rb +137 -0
  15. data/lib/rigor/inference/expression_typer.rb +9 -2
  16. data/lib/rigor/inference/hkt_reducer.rb +2 -0
  17. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +23 -6
  18. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +81 -14
  19. data/lib/rigor/inference/method_dispatcher.rb +57 -10
  20. data/lib/rigor/inference/precision_scanner.rb +60 -1
  21. data/lib/rigor/inference/scope_indexer.rb +127 -8
  22. data/lib/rigor/inference/statement_evaluator.rb +13 -8
  23. data/lib/rigor/inference/synthetic_method_index.rb +23 -4
  24. data/lib/rigor/inference/synthetic_method_scanner.rb +148 -14
  25. data/lib/rigor/plugin/additional_initializer.rb +108 -0
  26. data/lib/rigor/plugin/base.rb +321 -2
  27. data/lib/rigor/plugin/box.rb +64 -0
  28. data/lib/rigor/plugin/inflector.rb +121 -0
  29. data/lib/rigor/plugin/isolation.rb +191 -0
  30. data/lib/rigor/plugin/macro/nested_class_template.rb +140 -0
  31. data/lib/rigor/plugin/macro.rb +1 -0
  32. data/lib/rigor/plugin/manifest.rb +120 -23
  33. data/lib/rigor/plugin/node_context.rb +62 -0
  34. data/lib/rigor/plugin/registry.rb +10 -0
  35. data/lib/rigor/plugin.rb +3 -0
  36. data/lib/rigor/sig_gen/generator.rb +2 -3
  37. data/lib/rigor/sig_gen/observation_collector.rb +2 -2
  38. data/lib/rigor/source/literals.rb +118 -0
  39. data/lib/rigor/source/node_walker.rb +26 -0
  40. data/lib/rigor/source.rb +1 -0
  41. data/lib/rigor/type/combinator.rb +6 -1
  42. data/lib/rigor/type/union.rb +65 -1
  43. data/lib/rigor/version.rb +1 -1
  44. data/lib/rigor.rb +1 -0
  45. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +31 -53
  46. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +21 -23
  47. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +38 -59
  48. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +7 -13
  49. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +22 -33
  50. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +298 -413
  51. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +69 -71
  52. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +24 -34
  53. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +18 -16
  54. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +4 -46
  55. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
  56. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +1 -1
  57. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +17 -12
  58. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -8
  59. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +2 -7
  60. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +2 -6
  61. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +4 -3
  62. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
  63. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +40 -45
  64. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +7 -17
  65. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +20 -42
  66. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +7 -4
  67. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +4 -8
  68. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +188 -0
  69. data/plugins/rigor-mangrove/lib/rigor-mangrove.rb +3 -0
  70. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +4 -0
  71. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +24 -8
  72. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +31 -48
  73. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +21 -23
  74. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +54 -82
  75. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +25 -25
  76. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +63 -147
  77. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -17
  78. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +23 -114
  79. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +36 -31
  80. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
  81. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +6 -3
  82. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +4 -2
  83. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +13 -12
  84. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +28 -40
  85. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +44 -47
  86. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +11 -10
  87. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +45 -87
  88. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +11 -12
  89. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +29 -42
  90. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +20 -19
  91. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +73 -0
  92. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +43 -1
  93. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +21 -29
  94. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +36 -96
  95. data/sig/rigor/plugin/access_denied_error.rbs +3 -1
  96. data/sig/rigor/plugin/base.rbs +58 -3
  97. data/sig/rigor/plugin/io_boundary.rbs +3 -0
  98. data/sig/rigor/plugin/manifest.rbs +31 -1
  99. data/sig/rigor/source.rbs +12 -0
  100. data/sig/rigor.rbs +5 -0
  101. data/skills/rigor-plugin-author/SKILL.md +13 -9
  102. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +6 -5
  103. data/skills/rigor-plugin-author/references/02-walker-and-types.md +159 -75
  104. data/skills/rigor-plugin-author/references/03-test-and-ship.md +3 -3
  105. metadata +52 -2
  106. 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
- def initialize(entries: [])
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
- return SyntheticMethodIndex::EMPTY if templates.empty? && registries.empty?
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
- case node
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
- args_node = call_node.arguments
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