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 +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +1 -1
- data/lib/as_minimizer/census.rb +6 -4
- data/lib/as_minimizer/check.rb +147 -0
- data/lib/as_minimizer/unroller.rb +91 -48
- data/lib/as_minimizer/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8bb3bd538359f52f42e0b8e2f591c3ea3b4b3cabf2740ce4ce5d51fbd810817c
|
|
4
|
+
data.tar.gz: 598f2c2e3897cef1e12d3bcdcd02d95a760ac695c202084c08a83dae1346a4b1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
80
|
+
[Japan Voyage](https://japanvoyage.jp)'s test suite (4317 runs) gives the same results with it on.
|
data/lib/as_minimizer/census.rb
CHANGED
|
@@ -28,12 +28,14 @@ module AsMinimizer
|
|
|
28
28
|
klass.__callbacks.each do |kind, chain|
|
|
29
29
|
next if chain.empty?
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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}
|
|
38
|
+
fallbacks[e.message] << "#{name}##{kind}"
|
|
37
39
|
end
|
|
38
40
|
end
|
|
39
41
|
end
|
data/lib/as_minimizer/check.rb
CHANGED
|
@@ -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
|
|
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 =
|
|
82
|
+
hit = cache[kind]
|
|
69
83
|
return hit[1] if hit && hit[0].equal?(chain)
|
|
70
84
|
|
|
71
85
|
MUTEX.synchronize do
|
|
72
|
-
|
|
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,
|
|
77
|
-
|
|
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
|
|
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,
|
|
87
|
-
generator = Generator.new(klass, kind,
|
|
105
|
+
def generate(klass, kind, chain)
|
|
106
|
+
generator = Generator.new(klass, kind, chain)
|
|
88
107
|
body = generator.source
|
|
89
|
-
warn_missing(klass, kind,
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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,
|
|
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
|
|
134
|
+
"names no method; running the chain will raise NoMethodError")
|
|
112
135
|
end
|
|
113
136
|
end
|
|
114
137
|
|
|
115
|
-
def file_for(klass, kind,
|
|
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,
|
|
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
|
-
|
|
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
|
|
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:
|
|
171
|
-
|
|
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
|
|
223
|
-
#
|
|
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
|
-
|
|
227
|
-
code =
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
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
|
|
240
|
-
return nil
|
|
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
|
|
247
|
-
|
|
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
|
|
256
|
-
@
|
|
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
|
-
|
|
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)}#{"
|
|
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)}(#{
|
|
327
|
+
else "#{object(filter)}.#{scope_method(callback)}(self)#{" #{block}" if block}"
|
|
285
328
|
end
|
|
286
329
|
end
|
|
287
330
|
|
data/lib/as_minimizer/version.rb
CHANGED