-
Notifications
You must be signed in to change notification settings - Fork 46
/
dependencies_patch.rb
348 lines (289 loc) · 12 KB
/
dependencies_patch.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
require 'active_support/dependencies'
module RailsDevelopmentBoost
module DependenciesPatch
module LoadablePatch
def require_dependency_with_constant_tracking(*args)
ActiveSupport::Dependencies.required_dependency(args.first)
# handle plugins such as concerned_with
require_dependency_without_constant_tracking(*args)
end
end
def self.apply!
return if applied?
# retain the original method in case the application overwrites it on its modules/klasses
Module.send :alias_method, :_mod_name, :name
patch = self
ActiveSupport::Dependencies.module_eval do
alias_method :local_const_defined?, :uninherited_const_defined? unless method_defined?(:local_const_defined?) # pre 4da45060 compatibility
remove_possible_method :remove_unloadable_constants!
remove_possible_method :clear
include patch
alias_method_chain :load_file, 'constant_tracking'
alias_method_chain :remove_constant, 'handling_of_connections'
extend patch
end
ActiveSupport::Dependencies::Loadable.module_eval do
include LoadablePatch
alias_method_chain :require_dependency, 'constant_tracking'
end
InstrumentationPatch.apply! if @do_instrument
ActiveSupport::Dependencies.handle_already_autoloaded_constants!
end
def self.debug!
if applied?
InstrumentationPatch.apply!
else
@do_instrument = true
end
end
def self.applied?
ActiveSupport::Dependencies < self
end
autoload :InstrumentationPatch, 'rails_development_boost/dependencies_patch/instrumentation_patch'
mattr_accessor :constants_being_removed
self.constants_being_removed = []
mattr_accessor :explicit_dependencies
self.explicit_dependencies = {}
class ModuleCache
def initialize
@classes, @modules = [], []
ObjectSpace.each_object(Module) {|mod| self << mod if relevant?(mod)}
@singleton_ancestors = Hash.new {|h, klass| h[klass] = klass.singleton_class.ancestors}
end
def each_dependent_on(mod)
each_inheriting_from(mod) do |other|
mod_name = other._mod_name
yield other if qualified_const_defined?(mod_name) && mod_name.constantize == other
end
end
def remove_const(const_name, object)
if object && Class === object
remove_const_from_colletion(@classes, const_name, object)
else
[@classes, @modules].each {|collection| remove_const_from_colletion(collection, const_name, object)}
end
end
def <<(mod)
(Class === mod ? @classes : @modules) << mod
end
private
def relevant?(mod)
const_name = mod._mod_name
!anonymous_const_name?(const_name) && in_autoloaded_namespace?(const_name)
end
def remove_const_from_colletion(collection, const_name, object)
if object
collection.delete(object)
else
collection.delete_if {|mod| mod._mod_name == const_name}
end
end
def each_inheriting_from(mod_or_class)
if Class === mod_or_class
@classes.dup.each do |other_class|
yield other_class if other_class < mod_or_class && first_non_anonymous_superclass(other_class) == mod_or_class
end
else
[@classes, @modules].each do |collection|
collection.dup.each do |other|
yield other if other < mod_or_class || @singleton_ancestors[other].include?(mod_or_class)
end
end
end
end
def first_non_anonymous_superclass(klass)
while (klass = klass.superclass) && anonymous_const_name?(klass._mod_name); end
klass
end
NOTHING = ''
def in_autoloaded_namespace?(const_name) # careful, modifies passed in const_name!
begin
return true if LoadedFile.loaded_constant?(const_name)
end while const_name.sub!(/::[^:]+\Z/, NOTHING)
false
end
def anonymous_const_name?(const_name)
!const_name || const_name.empty?
end
def qualified_const_defined?(const_name)
ActiveSupport::Dependencies.qualified_const_defined?(const_name)
end
end
def unload_modified_files!
log_call
LoadedFile.unload_modified!
ensure
@module_cache = nil
end
def remove_explicitely_unloadable_constants!
explicitly_unloadable_constants.each { |const| remove_constant(const) }
end
# Overridden.
def clear
end
# Augmented `load_file'.
def load_file_with_constant_tracking(path, *args, &block)
result = now_loading(path) { load_file_without_constant_tracking(path, *args, &block) }
unless load_once_path?(path)
new_constants = autoloaded_constants - LoadedFile.loaded_constants
# Associate newly loaded constants to the file just loaded
associate_constants_to_file(new_constants, path)
end
result
end
def now_loading(path)
@currently_loading, old_currently_loading = path, @currently_loading
yield
rescue Exception => e
error_loading_file(@currently_loading, e)
ensure
@currently_loading = old_currently_loading
end
def associate_constants_to_file(constants, file_path)
# freezing strings before using them as Hash keys is slightly more memory efficient
constants.map!(&:freeze)
file_path.freeze
LoadedFile.for(file_path).add_constants(constants)
end
# Augmented `remove_constant'.
def remove_constant_with_handling_of_connections(const_name)
module_cache # make sure module_cache has been created
prevent_further_removal_of(const_name) do
unprotected_remove_constant(const_name)
end
end
def required_dependency(file_name)
# Rails uses require_dependency for loading helpers, we are however dealing with the helper problem elsewhere, so we can skip them
return if @currently_loading && @currently_loading =~ /_controller(?:\.rb)?\Z/ && file_name =~ /_helper(?:\.rb)?\Z/
if full_path = ActiveSupport::Dependencies.search_for_file(file_name)
RequiredDependency.new(@currently_loading).related_files.each do |related_file|
LoadedFile.relate_files(related_file, full_path)
end
end
end
def add_explicit_dependency(parent, child)
(explicit_dependencies[parent._mod_name] ||= []) << child._mod_name
end
def handle_already_autoloaded_constants! # we might be late to the party and other gems/plugins might have already triggered autoloading of some constants
loaded.each do |require_path|
unless load_once_path?(require_path)
associate_constants_to_file(autoloaded_constants, "#{require_path}.rb") # slightly heavy-handed..
end
end
end
private
def unprotected_remove_constant(const_name)
if qualified_const_defined?(const_name) && object = const_name.constantize
handle_connected_constants(object, const_name)
LoadedFile.unload_files_with_const!(const_name)
if object.kind_of?(Module)
remove_parent_modules_if_autoloaded(object)
remove_child_module_constants(object, const_name)
end
end
result = remove_constant_without_handling_of_connections(const_name)
clear_tracks_of_removed_const(const_name, object)
result
end
def error_loading_file(file_path, e)
LoadedFile.for(file_path).stale! if LoadedFile.loaded?(file_path)
raise e
end
def handle_connected_constants(object, const_name)
return unless Module === object && qualified_const_defined?(const_name)
remove_explicit_dependencies_of(const_name)
remove_dependent_modules(object)
update_activerecord_related_references(object)
remove_nested_constants(const_name)
end
def remove_nested_constants(const_name)
autoloaded_constants.grep(/\A#{const_name}::/) { |const| remove_nested_constant(const_name, const) }
end
def remove_nested_constant(parent_const, child_const)
remove_constant(child_const)
end
# AS::Dependencies doesn't track same-file nested constants, so we need to look out for them on our own.
# For example having loaded an abc.rb that looks like this:
# class Abc; class Inner; end; end
# AS::Dependencies would only add "Abc" constant name to its autoloaded_constants list, completely ignoring Abc::Inner. This in turn
# can cause problems for classes inheriting from Abc::Inner somewhere else in the app.
def remove_parent_modules_if_autoloaded(object)
unless autoloaded_object?(object)
initial_object = object
while (object = object.parent) != Object
if autoloaded_object?(object)
remove_autoloaded_parent_module(initial_object, object)
break
end
end
end
end
def remove_autoloaded_parent_module(initial_object, parent_object)
remove_constant(parent_object._mod_name)
end
def autoloaded_object?(object) # faster than going through Dependencies.autoloaded?
LoadedFile.loaded_constant?(object._mod_name)
end
# AS::Dependencies doesn't track same-file nested constants, so we need to look out for them on our own and remove any dependent modules/constants
def remove_child_module_constants(object, object_const_name)
object.constants.each do |child_const_name|
# we only care about "namespace" constants (classes/modules)
if local_const_defined?(object, child_const_name) && (child_const = object.const_get(child_const_name)).kind_of?(Module)
# make sure this is not "const alias" created like this: module Y; end; module A; X = Y; end, const A::X is not a proper "namespacing module",
# but only an alias to Y module
if (full_child_const_name = child_const._mod_name) == "#{object_const_name}::#{child_const_name}"
remove_child_module_constant(object, full_child_const_name)
end
end
end
end
def remove_child_module_constant(parent_object, full_child_const_name)
remove_constant(full_child_const_name)
end
def remove_explicit_dependencies_of(const_name)
if dependencies = explicit_dependencies.delete(const_name)
dependencies.uniq.each {|depending_const| remove_explicit_dependency(const_name, depending_const)}
end
end
def remove_explicit_dependency(const_name, depending_const)
remove_constant(depending_const)
end
def clear_tracks_of_removed_const(const_name, object = nil)
autoloaded_constants.delete(const_name)
@module_cache.remove_const(const_name, object)
LoadedFile.const_unloaded(const_name)
end
def remove_dependent_modules(mod)
module_cache.each_dependent_on(mod) {|other| remove_dependent_constant(mod, other)}
end
def remove_dependent_constant(original_module, dependent_module)
remove_constant(dependent_module._mod_name)
end
# egrep -ohR '@\w*([ck]lass|refl|target|own)\w*' activerecord | sort | uniq
def update_activerecord_related_references(klass)
return unless defined?(ActiveRecord)
return unless klass < ActiveRecord::Base
# Reset references held by macro reflections (klass is lazy loaded, so
# setting its cache to nil will force the name to be resolved again).
ActiveRecord::Base.descendants.each do |model|
model.reflections.each_value do |reflection|
reflection.instance_eval do
@klass = nil if @klass == klass
end
end
end
end
def module_cache
@module_cache ||= ModuleCache.new
end
def prevent_further_removal_of(const_name)
return if constants_being_removed.include?(const_name)
constants_being_removed << const_name
begin
yield
ensure
constants_being_removed.delete(const_name)
end
end
end
end