activesupport-minimizer 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 057c323c4f9d34ec9172be627d4a8362422d43fcc0ff40a2e6df1bdc46ea85dc
4
- data.tar.gz: ce5c57db54cde067e88a7396afe0c38934fb8cc1b322a3835db4a79837a94221
3
+ metadata.gz: 8bb3bd538359f52f42e0b8e2f591c3ea3b4b3cabf2740ce4ce5d51fbd810817c
4
+ data.tar.gz: 598f2c2e3897cef1e12d3bcdcd02d95a760ac695c202084c08a83dae1346a4b1
5
5
  SHA512:
6
- metadata.gz: d89cf16715590c6689131ec5d6f7ffa15226f7a171fcfc736b3d82331abc58d48eb62d1fbb0a5d5a5a7bcd2c6155a4337e868e246786adc5b1f51de2218e099c
7
- data.tar.gz: bcc03a8edb2d24e2e996ef3312046a8aa45516b83d1a767a33fbff1d772a866584083fc1df57f2bd43522d4e46215014ab614fb215f275aaedd8953b7e8208f3
6
+ metadata.gz: f183c2881281f4687a85312464e95e954383507e4d7bc1d3fe2cd4263b99c977a827b3e16b9b7059621ffede37536f357e84858d2c7a5aeb1467b2710c5b41d4
7
+ data.tar.gz: a0b386f48da881a5df8a74afdcaa7491afb12f0e16cffbb5ee143d10193ab0e64a033f6d9fe6f12cc4d864a5da71a054c77bd4607384382792c0f6d262885991
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.2 (2026-10-18)
4
+
5
+ - Rails main: the controller terminator (now `AbstractController::Callbacks::TERMINATOR`, a lambda in 8.1) is recognized again, so a before_action is a plain call followed by `performed?` instead of a lambda passed to the terminator: one block less per callback to compile, one allocation less per call.
6
+ - The part of a chain an around callback wraps is a method of its own, which the around gets as a one-line block, instead of a proc holding the rest of the chain: no callbacks in blocks nested in blocks, and no proc allocated per around per run.
7
+
8
+ ## 0.1.1 (2026-10-18)
9
+
10
+ - Unrolled callbacks: one method per class and chain instead of one per controller action (`only:`/`except:` become a Set lookup), and classes whose chains are the same share one method body. Fewer methods for the JIT to compile.
11
+ - `Check.long_chains`: chains with more than a limit (default 5) of the app's own callbacks. Callbacks inherited from excluded classes (default `ApplicationController`) don't count; a base class over the limit is reported once, with the classes that inherit it.
12
+ - `Check.callback_loops`: model callbacks that call `save`, `update`, `touch`, ... on their own record, which runs the callbacks again (found with Prism), with the guard if there is one.
13
+ - Census counts per class, not per action.
14
+
3
15
  ## 0.1.0 (2026-10-18)
4
16
 
5
17
  First release.
data/README.md CHANGED
@@ -77,4 +77,4 @@ validations, 2 before_save, 2 after_save, after_initialize):
77
77
  A controller action behind 9 callbacks: 12.6 µs of callback overhead → 4.0 µs
78
78
  (6.4 → 2.3 µs with YJIT).
79
79
 
80
- golf-buddy's test suite (4317 runs) gives the same results with it on.
80
+ [Japan Voyage](https://japanvoyage.jp)'s test suite (4317 runs) gives the same results with it on.
@@ -28,12 +28,14 @@ module AsMinimizer
28
28
  klass.__callbacks.each do |kind, chain|
29
29
  next if chain.empty?
30
30
 
31
- actions = kind == :process_action && klass.respond_to?(:action_methods) ? controller_actions(klass, routed) : [nil]
32
- actions.each do |action|
33
- Unroller::Generator.new(klass, kind, action, chain).source
31
+ # Controllers no route reaches (abstract ones included) never run their chain.
32
+ next if kind == :process_action && klass.respond_to?(:action_methods) && controller_actions(klass, routed).empty?
33
+
34
+ begin
35
+ Unroller::Generator.new(klass, kind, chain).source
34
36
  unrolled += 1
35
37
  rescue Unroller::Unsupported => e
36
- fallbacks[e.message] << "#{name}##{kind}#{"[#{action}]" if action}"
38
+ fallbacks[e.message] << "#{name}##{kind}"
37
39
  end
38
40
  end
39
41
  end
@@ -14,6 +14,33 @@ module AsMinimizer
14
14
  end
15
15
  end
16
16
 
17
+ # A chain with more of the app's own callbacks than the limit.
18
+ LongChain = Data.define(:klass, :chain, :callbacks, :limit, :action, :also) do
19
+ def to_s
20
+ where = action ? " (#{action})" : ""
21
+ others = also.empty? ? "" : " (inherited by #{also.size}: #{also.first(3).map { Check.name_of(it) }.join(", ")}#{", ..." if also.size > 3})"
22
+ "#{Check.name_of(klass)}#{others}: #{chain} chain has #{callbacks.size} callbacks#{where}, more than #{limit}\n" +
23
+ callbacks.map { " #{it}" }.join("\n")
24
+ end
25
+ end
26
+
27
+ # A model callback that saves (updates, touches, ...) the record it runs
28
+ # for: that runs the chain again, so it repeats until something stops it.
29
+ Loop = Data.define(:klass, :chain, :callback, :call, :location, :guard) do
30
+ def to_s
31
+ "#{Check.name_of(klass)}: #{callback} calls #{call} on the record itself (#{location}), " \
32
+ "which runs the #{chain} callbacks again#{"; guarded by #{guard}, check it can't stay true" if guard}"
33
+ end
34
+ end
35
+
36
+ # Chains whose callbacks are validations, not callbacks.
37
+ SKIP_CHAINS = %i[validate].freeze
38
+ # Model chains a save, update or touch of the same record runs again.
39
+ REENTERED_CHAINS = %i[validation save create update commit touch].freeze
40
+ # Calls on the record itself that run its callbacks (update_column(s) and
41
+ # update_all don't).
42
+ SELF_SAVES = %i[save save! update update! update_attribute update_attributes update_attributes! touch increment! decrement! toggle!].freeze
43
+
17
44
  # method_missing implementations whose answers are known: attribute methods.
18
45
  ATTRIBUTE_METHOD_MISSING = %w[ActiveModel::AttributeMethods ActiveRecord::AttributeMethods ArMinimizer::Slim::Record].freeze
19
46
  PLAIN_METHOD_MISSING = %w[BasicObject Kernel].freeze
@@ -55,6 +82,67 @@ module AsMinimizer
55
82
  end
56
83
  end
57
84
 
85
+ # Chains with more than +limit+ of the app's own callbacks (defined under
86
+ # +root+, not in gems). Controllers: their action with the most.
87
+ # Callbacks a subclass inherits from an +exclude+d class (the
88
+ # ApplicationController default: time zone, locale, current user, ...,
89
+ # the per-request basics) don't count. A class over the limit is reported
90
+ # once, with how many classes inherit its chain; those aren't repeated.
91
+ def long_chains(classes = callback_classes, limit: 5, root:, routed: nil, exclude: ["ApplicationController"])
92
+ @routed = routed
93
+ excluded = exclude.filter_map { Object.const_get(it) if Object.const_defined?(it) }
94
+ reported = {} # [class, chain] => LongChain
95
+ classes.sort_by { it.ancestors.size }.each do |klass|
96
+ next if excluded.include?(klass)
97
+
98
+ klass.__callbacks.each do |chain_name, chain|
99
+ next if SKIP_CHAINS.include?(chain_name)
100
+ next if chain.none?
101
+
102
+ # Already over the limit in a superclass: covered by that report.
103
+ parent = klass.ancestors.drop(1).find { reported.key?([it, chain_name]) }
104
+ if parent
105
+ reported[[parent, chain_name]].also << klass
106
+ next
107
+ end
108
+
109
+ inherited = excluded.select { klass < it && it.respond_to?(:__callbacks) }
110
+ .flat_map { it.__callbacks[chain_name].to_a }.to_set
111
+ actions = controller_actions(klass, chain_name)
112
+ candidates = actions.nil? || actions.empty? ? [nil] : actions # a base class: all of its callbacks
113
+ action, own = candidates.map do |act|
114
+ [act, chain.select { |cb| !inherited.include?(cb) && (act.nil? || applies?(cb, act)) && own_location(klass, cb, root) }]
115
+ end.max_by { it[1].size }
116
+ next unless own.size > limit
117
+
118
+ reported[[klass, chain_name]] = LongChain.new(klass, chain_name, own.map { describe(klass, it, root) }, limit, action, [])
119
+ end
120
+ end
121
+ reported.values
122
+ end
123
+
124
+ # Model callbacks (under +root+) that call save, update, touch, ... on
125
+ # the record they run for. allow: ["Trip#recalculate", ...] to accept one.
126
+ def callback_loops(classes = callback_classes, root:, allow: [])
127
+ classes.flat_map do |klass|
128
+ next [] unless defined?(ActiveRecord::Base) && klass < ActiveRecord::Base
129
+
130
+ klass.__callbacks.flat_map do |chain_name, chain|
131
+ next [] unless REENTERED_CHAINS.include?(chain_name)
132
+
133
+ chain.flat_map do |callback|
134
+ next [] unless (location = own_location(klass, callback, root))
135
+ next [] if allow.include?("#{name_of(klass)}##{callback.filter}")
136
+
137
+ self_saves(*location).map do |call, line|
138
+ Loop.new(klass, chain_name, "#{callback.kind}_#{chain_name} #{callback.filter.is_a?(Symbol) ? callback.filter.inspect : "block"}",
139
+ call, "#{relative(location[0], root)}:#{line}", guard(callback))
140
+ end
141
+ end
142
+ end
143
+ end.uniq { [it.klass, it.callback, it.call, it.location] }
144
+ end
145
+
58
146
  def callback_classes
59
147
  ObjectSpace.each_object(Class).filter_map do |klass|
60
148
  name = Module.instance_method(:name).bind_call(klass) # some classes redefine .name (and raise)
@@ -113,6 +201,65 @@ module AsMinimizer
113
201
 
114
202
  def listed?(filter, action) = filter.instance_variable_get(:@actions).include?(action)
115
203
 
204
+ # [file, line] of the callback's code when it's the app's own (under root, not a gem), else nil.
205
+ def own_location(klass, callback, root)
206
+ filter = callback.filter
207
+ location =
208
+ case filter
209
+ when Symbol then (klass.instance_method(filter).source_location rescue nil)
210
+ when Proc then filter.source_location
211
+ end
212
+ return unless location && location[0].start_with?(root.to_s) && !location[0].match?(%r{/(vendor|gems)/})
213
+
214
+ location
215
+ end
216
+
217
+ def describe(klass, callback, root)
218
+ file, line = own_location(klass, callback, root)
219
+ name = callback.filter.is_a?(Symbol) ? callback.filter.inspect : "block"
220
+ "#{callback.kind}_#{callback.name} #{name} (#{relative(file, root)}:#{line})"
221
+ end
222
+
223
+ def relative(file, root) = file.delete_prefix("#{root}/")
224
+
225
+ # The :if/:unless that might stop a loop, as written. Rails' own
226
+ # condition on model after_* callbacks (skip after a halt) isn't one.
227
+ def guard(callback)
228
+ conditions = (callback.instance_variable_get(:@if).map { ["if", it] } + callback.instance_variable_get(:@unless).map { ["unless", it] })
229
+ .reject { |_, c| c.is_a?(ActiveSupport::Callbacks::Conditionals::Value) }
230
+ conditions.filter_map { |kind, c| "#{kind}: #{c.inspect}" if c.is_a?(Symbol) }.first ||
231
+ (conditions.any? ? "a #{conditions.first[0]}: condition" : nil)
232
+ end
233
+
234
+ # [[call, line]] for calls in SELF_SAVES on self in the method or block
235
+ # starting at file:line.
236
+ def self_saves(file, line)
237
+ require "prism"
238
+ @parsed ||= {}
239
+ root_node = (@parsed[file] ||= Prism.parse_file(file).value)
240
+ body = find_starting(root_node, line) or return []
241
+ found = []
242
+ walk = lambda do |node|
243
+ if node.is_a?(Prism::CallNode) && SELF_SAVES.include?(node.name) && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
244
+ found << [node.name.to_s, node.location.start_line]
245
+ end
246
+ node.compact_child_nodes.each(&walk)
247
+ end
248
+ walk.(body)
249
+ found
250
+ end
251
+
252
+ def find_starting(node, line)
253
+ return node if (node.is_a?(Prism::DefNode) || node.is_a?(Prism::LambdaNode) || node.is_a?(Prism::BlockNode)) && node.location.start_line == line
254
+
255
+ node.compact_child_nodes.each do |child|
256
+ next unless child.location.start_line <= line && child.location.end_line >= line
257
+
258
+ found = find_starting(child, line) and return found
259
+ end
260
+ nil
261
+ end
262
+
116
263
  # name_changed?, saved_change_to_title?, ... for a real attribute (Slim may not have generated it).
117
264
  def attribute_method?(klass, name)
118
265
  return false unless klass.respond_to?(:attribute_method_patterns) && klass.respond_to?(:attribute_names)
@@ -36,13 +36,28 @@ module AsMinimizer
36
36
  # throw :abort for the rest, or any custom terminator), around callbacks
37
37
  # nesting the rest of the chain, after callbacks last-defined first and
38
38
  # skipped after a halt when the chain says so. For controllers, `only:` and
39
- # `except:` are decided per action when the method is generated.
39
+ # `except:` are a lookup of action_name in the filter's action set.
40
+ #
41
+ # The part of the chain an around callback wraps is a method of its own
42
+ # (__as_save_1, ...), which the around gets as a one-line block: the
43
+ # callbacks run in plain methods rather than blocks nested in blocks, and
44
+ # nothing is allocated for an around callback that only yields.
45
+ #
46
+ # One method per class and chain (not per controller action), and one body
47
+ # per distinct chain: classes whose chains come out the same (subclasses of
48
+ # a controller that add no callbacks of their own, ...) get the same method
49
+ # body, so a JIT compiles it once.
40
50
  #
41
51
  # The generated code is named in backtraces ("unrolled: Post#save"), and with
42
52
  # source_dir set, written to a file there, so backtraces point at code you can open.
43
53
  module Unroller
44
54
  # Filters and conditions that aren't symbols are referenced from here.
45
55
  OBJECTS = []
56
+ # What a nested level returns when the chain halted in it.
57
+ HALTED = Object.new.freeze
58
+ # [kind, body] => a module holding the generated method; classes with the
59
+ # same chain define theirs from its UnboundMethod, sharing the body.
60
+ SHARED = {}
46
61
  MUTEX = Mutex.new
47
62
  Unsupported = Class.new(StandardError)
48
63
 
@@ -62,44 +77,52 @@ module AsMinimizer
62
77
  def method_for(target, kind)
63
78
  klass = target.class
64
79
  chain = target.__callbacks[kind]
65
- action = target.action_name if kind == :process_action && target.respond_to?(:action_name)
66
80
  cache = klass.instance_variable_get(:@__as_unrolled) ||
67
81
  MUTEX.synchronize { klass.instance_variable_get(:@__as_unrolled) || klass.instance_variable_set(:@__as_unrolled, {}) }
68
- hit = (per_kind = cache[kind]) && per_kind[action]
82
+ hit = cache[kind]
69
83
  return hit[1] if hit && hit[0].equal?(chain)
70
84
 
71
85
  MUTEX.synchronize do
72
- per_kind = (cache[kind] ||= {})
73
- hit = per_kind[action]
86
+ hit = cache[kind]
74
87
  return hit[1] if hit && hit[0].equal?(chain)
75
88
 
76
- name = generate(klass, kind, action, chain)
77
- per_kind[action] = [chain, name]
89
+ name = generate(klass, kind, chain)
90
+ cache[kind] = [chain, name]
78
91
  name
79
92
  end
80
93
  end
81
94
 
82
- def source_for(klass, kind, action = nil) = Generator.new(klass, kind, action, klass.__callbacks[kind]).source
95
+ def source_for(klass, kind) = Generator.new(klass, kind, klass.__callbacks[kind]).source
96
+
97
+ # Generated methods, and the distinct bodies behind them.
98
+ def stats
99
+ classes = ObjectSpace.each_object(Class).select { it.instance_variable_defined?(:@__as_unrolled) }
100
+ { methods: classes.sum { it.instance_variable_get(:@__as_unrolled).count { |_, (_, name)| name } }, bodies: SHARED.size }
101
+ end
83
102
 
84
103
  private
85
104
 
86
- def generate(klass, kind, action, chain)
87
- generator = Generator.new(klass, kind, action, chain)
105
+ def generate(klass, kind, chain)
106
+ generator = Generator.new(klass, kind, chain)
88
107
  body = generator.source
89
- warn_missing(klass, kind, action, generator.targets)
90
- name = :"__as_#{kind}#{"_#{action.unpack1("H*")}" if action}"
91
- label = "#{klass.name || "anonymous"}##{kind}#{"[#{action}]" if action}"
92
- code = "# #{label} callbacks, unrolled by activesupport-minimizer\ndef #{name}\n#{body}\nend\n"
93
- klass.class_eval(code, file_for(klass, kind, action, label, code), 1)
94
- klass.send(:private, name)
95
- name
108
+ warn_missing(klass, kind, generator.targets)
109
+ shared = SHARED[[kind, body]] ||= begin
110
+ label = "#{klass.name || "anonymous"}##{kind}"
111
+ code = "# #{label} callbacks (and every class whose chain is the same), unrolled by activesupport-minimizer\n#{body}"
112
+ Module.new.tap { it.module_eval(code, file_for(klass, kind, label, code), 1) }
113
+ end
114
+ shared.instance_methods(false).each do |name|
115
+ klass.send(:define_method, name, shared.instance_method(name))
116
+ klass.send(:private, name)
117
+ end
118
+ :"__as_#{kind}"
96
119
  rescue Unsupported
97
120
  nil
98
121
  end
99
122
 
100
123
  # Rails finds out when the chain runs (NoMethodError, a 500 for a
101
124
  # controller); say so when the method is generated, once per target.
102
- def warn_missing(klass, kind, action, targets)
125
+ def warn_missing(klass, kind, targets)
103
126
  targets.each do |name, role, callback_kind|
104
127
  next unless Check.status(klass, name) == :missing
105
128
 
@@ -108,15 +131,14 @@ module AsMinimizer
108
131
 
109
132
  @warned[[klass, kind, name]] = true
110
133
  AsMinimizer.warn("activesupport-minimizer: #{klass.name || klass}'s #{callback_kind}_#{kind} #{role} #{name.inspect} " \
111
- "names no method#{" (action #{action})" if action}; running the chain will raise NoMethodError")
134
+ "names no method; running the chain will raise NoMethodError")
112
135
  end
113
136
  end
114
137
 
115
- def file_for(klass, kind, action, label, code)
138
+ def file_for(klass, kind, label, code)
116
139
  return "unrolled: #{label}" unless source_dir
117
140
 
118
- path = File.join(source_dir.to_s, (klass.name || "anonymous_#{klass.object_id}").gsub("::", "/"),
119
- "#{kind}#{"_#{action}" if action}.rb")
141
+ path = File.join(source_dir.to_s, (klass.name || "anonymous_#{klass.object_id}").gsub("::", "/"), "#{kind}.rb")
120
142
  FileUtils.mkdir_p(File.dirname(path))
121
143
  File.write(path, code)
122
144
  path
@@ -139,19 +161,20 @@ module AsMinimizer
139
161
  # Symbols the generated code calls (filters and conditions that apply).
140
162
  attr_reader :targets
141
163
 
142
- def initialize(klass, kind, action, chain)
164
+ def initialize(klass, kind, chain)
143
165
  @klass = klass
144
166
  @kind = kind
145
- @action = action
146
167
  @chain = chain
147
168
  @config = chain.config
148
- @lambdas = 0
149
169
  @targets = []
150
170
  end
151
171
 
172
+ # The method definitions: __as_<kind>, and one more per around callback.
152
173
  def source
153
174
  terminator!
154
- "halted = false\nvalue = nil\n#{level(levels)}value"
175
+ @methods = []
176
+ @methods << "def __as_#{@kind}(&)\nhalted = false\nvalue = nil\n#{level(levels)}value\nend\n"
177
+ @methods.join
155
178
  end
156
179
 
157
180
  private
@@ -161,14 +184,24 @@ module AsMinimizer
161
184
  @terminator =
162
185
  if terminator.equal?(ActiveSupport::Callbacks::CallbackChain.const_get(:DEFAULT_TERMINATOR))
163
186
  :abort
164
- elsif terminator.is_a?(Proc) && terminator.source_location&.first&.end_with?("abstract_controller/callbacks.rb")
187
+ elsif performed_terminator?(terminator)
165
188
  :performed
166
189
  else
167
190
  object(terminator)
168
191
  end
169
192
  @skip_after = @config[:skip_after_callbacks_if_terminated]
170
- # With raise_on_missing_callback_actions, only:/except: check the action list at runtime.
171
- @static_actions = @action && !(@klass.respond_to?(:raise_on_missing_callback_actions) && @klass.raise_on_missing_callback_actions)
193
+ # With raise_on_missing_callback_actions, only:/except: go through the
194
+ # filter object, which raises for actions that don't exist.
195
+ @inline_actions = !(@klass.respond_to?(:raise_on_missing_callback_actions) && @klass.raise_on_missing_callback_actions)
196
+ end
197
+
198
+ # Rails 8.1's controller terminator is a lambda; main's is TERMINATOR.
199
+ def performed_terminator?(terminator)
200
+ if terminator.is_a?(Proc)
201
+ terminator.source_location&.first&.end_with?("abstract_controller/callbacks.rb")
202
+ else
203
+ defined?(AbstractController::Callbacks::TERMINATOR) && terminator.equal?(AbstractController::Callbacks::TERMINATOR)
204
+ end
172
205
  end
173
206
 
174
207
  # What CallbackChain#compile builds: each around opens a nested level;
@@ -219,32 +252,40 @@ module AsMinimizer
219
252
  check.empty? ? "#{call}\n" : "if #{check.join(" && ")}\n #{call}\nend\n"
220
253
  end
221
254
 
222
- # The rest of the chain goes in a proc: the around callback gets it as
223
- # its block, and it runs directly when the around is skipped.
255
+ # The rest of the chain is a method of its own: the around callback gets
256
+ # a block calling it, and it's called directly when the around is
257
+ # skipped. It returns the chain's value, or HALTED.
224
258
  def around(callback, inner)
259
+ name = "__as_#{@kind}_#{@methods.size}"
260
+ @methods << "def #{name}(halted, &)\nvalue = nil\n#{inner}halted ? ::AsMinimizer::Unroller::HALTED : value\nend\n"
225
261
  cond = conditions(callback)
226
- inner_name = "inner#{@lambdas += 1}"
227
- code = "#{inner_name} = proc do\n#{inner}end\n" # a Proc, as ActiveSupport passes: extra arguments are ignored
228
- return code << "#{inner_name}.()\n" unless cond
229
-
230
- skip = cond == "true" ? "halted" : "halted || !(#{cond})"
231
- code << "if #{skip}\n #{inner_name}.()\nelse\n #{call(callback, callback.filter, inner_name)}\nend\n"
262
+ direct = "inner = #{name}(halted, &)\n"
263
+ code =
264
+ if cond
265
+ skip = cond == "true" ? "halted" : "halted || !(#{cond})"
266
+ "inner = nil\nif #{skip}\n #{direct.chomp}\nelse\n #{call(callback, callback.filter, "{ inner = #{name}(halted, &) }")}\nend\n"
267
+ else
268
+ direct
269
+ end
270
+ code << "if ::AsMinimizer::Unroller::HALTED.equal?(inner)\n halted = true\n value = false\nelse\n value = inner\nend\n"
232
271
  end
233
272
 
234
273
  # Ruby for the callback's :if/:unless, "true" when there are none, nil
235
- # when it never runs (an `only:`/`except:` that rules out this action).
274
+ # when it can never run (an `only:` with no actions).
236
275
  def conditions(callback)
237
276
  parts = []
238
277
  callback.instance_variable_get(:@if).each do |c|
239
- if static_action_filter?(c)
240
- return nil unless action_listed?(c)
278
+ if inline_action_filter?(c)
279
+ return nil if actions_of(c).empty?
280
+
281
+ parts << "#{object(actions_of(c))}.include?(action_name)"
241
282
  else
242
283
  parts << condition(callback, c)
243
284
  end
244
285
  end
245
286
  callback.instance_variable_get(:@unless).each do |c|
246
- if static_action_filter?(c)
247
- return nil if action_listed?(c)
287
+ if inline_action_filter?(c)
288
+ parts << "!#{object(actions_of(c))}.include?(action_name)" unless actions_of(c).empty?
248
289
  else
249
290
  parts << "!(#{condition(callback, c)})"
250
291
  end
@@ -252,11 +293,12 @@ module AsMinimizer
252
293
  parts.empty? ? "true" : parts.join(" && ")
253
294
  end
254
295
 
255
- def static_action_filter?(c)
256
- @static_actions && defined?(AbstractController::Callbacks::ActionFilter) && c.is_a?(AbstractController::Callbacks::ActionFilter)
296
+ def inline_action_filter?(c)
297
+ @inline_actions && defined?(AbstractController::Callbacks::ActionFilter) && c.is_a?(AbstractController::Callbacks::ActionFilter)
257
298
  end
258
299
 
259
- def action_listed?(filter) = filter.instance_variable_get(:@actions).include?(@action)
300
+ # only:/except: actions, as the frozen Set of Strings Rails keeps.
301
+ def actions_of(filter) = filter.instance_variable_get(:@actions)
260
302
 
261
303
  # As CallTemplate.build + make_lambda.
262
304
  def condition(callback, c)
@@ -270,18 +312,19 @@ module AsMinimizer
270
312
  end
271
313
  end
272
314
 
315
+ # block: a block literal ("{ ... }") an around callback runs the rest of the chain with.
273
316
  def call(callback, filter, block = nil)
274
317
  case filter
275
- when Symbol then (@targets << [filter, :filter, callback.kind]; "self.#{method_call(filter)}#{"(&#{block})" if block}")
318
+ when Symbol then (@targets << [filter, :filter, callback.kind]; "self.#{method_call(filter)}#{" #{block}" if block}")
276
319
  when Proc
277
320
  case filter.arity
278
321
  when 2
279
322
  raise Unsupported, "a before/after proc with 2 parameters" unless block # ActiveSupport raises ArgumentError here
280
- "instance_exec(self, #{block}, &#{object(filter)})"
323
+ "instance_exec(self, proc #{block}, &#{object(filter)})"
281
324
  when 1, -2 then "instance_exec(self, &#{object(filter)})"
282
325
  else "instance_exec(&#{object(filter)})"
283
326
  end
284
- else "#{object(filter)}.#{scope_method(callback)}(#{["self", ("&#{block}" if block)].compact.join(", ")})"
327
+ else "#{object(filter)}.#{scope_method(callback)}(self)#{" #{block}" if block}"
285
328
  end
286
329
  end
287
330
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AsMinimizer
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.2"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activesupport-minimizer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sampo Kuokkanen