rails-dev-boost 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,5 @@
1
+ require 'active_support/dependencies'
2
+
1
3
  module RailsDevelopmentBoost
2
4
  module DependenciesPatch
3
5
  module LoadablePatch
@@ -9,6 +11,8 @@ module RailsDevelopmentBoost
9
11
  end
10
12
 
11
13
  def self.apply!
14
+ return if applied?
15
+
12
16
  # retain the original method in case the application overwrites it on its modules/klasses
13
17
  Module.send :alias_method, :_mod_name, :name
14
18
 
@@ -34,34 +38,130 @@ module RailsDevelopmentBoost
34
38
  end
35
39
 
36
40
  def self.debug!
37
- if ActiveSupport::Dependencies < DependenciesPatch
41
+ if applied?
38
42
  InstrumentationPatch.apply!
39
43
  else
40
44
  @do_instrument = true
41
45
  end
42
46
  end
43
47
 
44
- autoload :InstrumentationPatch, 'rails_development_boost/dependencies_patch/instrumentation_patch'
45
-
46
- mattr_accessor :module_cache
47
- self.module_cache = []
48
+ def self.async!
49
+ @async = true
50
+ end
51
+
52
+ def self.async?
53
+ @async
54
+ end
48
55
 
49
- mattr_accessor :file_map
50
- self.file_map = {}
56
+ def self.applied?
57
+ ActiveSupport::Dependencies < self
58
+ end
51
59
 
60
+ autoload :InstrumentationPatch, 'rails_development_boost/dependencies_patch/instrumentation_patch'
61
+
52
62
  mattr_accessor :constants_being_removed
53
63
  self.constants_being_removed = []
54
64
 
55
65
  mattr_accessor :explicit_dependencies
56
66
  self.explicit_dependencies = {}
57
67
 
58
- def unload_modified_files!
59
- log_call
60
- file_map.values.each do |file|
61
- unload_modified_file(file) if file.changed?
68
+ mattr_accessor :currently_loading
69
+ self.currently_loading = []
70
+
71
+ module Util
72
+ extend self
73
+
74
+ def anonymous_const?(mod)
75
+ anonymous_const_name?(mod._mod_name)
76
+ end
77
+
78
+ def anonymous_const_name?(const_name)
79
+ !const_name || const_name.empty?
80
+ end
81
+
82
+ def first_non_anonymous_superclass(klass)
83
+ while (klass = klass.superclass) && anonymous_const?(klass); end
84
+ klass
85
+ end
86
+
87
+ NOTHING = ''
88
+ def in_autoloaded_namespace?(const_name) # careful, modifies passed in const_name!
89
+ begin
90
+ return true if LoadedFile.loaded_constant?(const_name)
91
+ end while const_name.sub!(/::[^:]+\Z/, NOTHING)
92
+ false
93
+ end
94
+ end
95
+
96
+ class ModuleCache
97
+ def initialize
98
+ @classes, @modules = [], []
99
+ ObjectSpace.each_object(Module) {|mod| self << mod if relevant?(mod)}
100
+ @singleton_ancestors = Hash.new {|h, klass| h[klass] = klass.singleton_class.ancestors}
101
+ end
102
+
103
+ def each_dependent_on(mod, &block)
104
+ arr = []
105
+ each_inheriting_from(mod) do |other|
106
+ mod_name = other._mod_name
107
+ arr << other if qualified_const_defined?(mod_name) && mod_name.constantize == other
108
+ end
109
+ arr.each(&block)
110
+ end
111
+
112
+ def remove_const(const_name, object)
113
+ if object && Class === object
114
+ remove_const_from_colletion(@classes, const_name, object)
115
+ else
116
+ [@classes, @modules].each {|collection| remove_const_from_colletion(collection, const_name, object)}
117
+ end
118
+ end
119
+
120
+ def <<(mod)
121
+ (Class === mod ? @classes : @modules) << mod
122
+ end
123
+
124
+ private
125
+ def relevant?(mod)
126
+ const_name = mod._mod_name
127
+ !Util.anonymous_const_name?(const_name) && Util.in_autoloaded_namespace?(const_name)
128
+ end
129
+
130
+ def remove_const_from_colletion(collection, const_name, object)
131
+ if object
132
+ collection.delete(object)
133
+ else
134
+ collection.delete_if {|mod| mod._mod_name == const_name}
135
+ end
136
+ end
137
+
138
+ def each_inheriting_from(mod_or_class)
139
+ if Class === mod_or_class
140
+ @classes.each do |other_class|
141
+ yield other_class if other_class < mod_or_class && Util.first_non_anonymous_superclass(other_class) == mod_or_class
142
+ end
143
+ else
144
+ [@classes, @modules].each do |collection|
145
+ collection.each do |other|
146
+ yield other if other < mod_or_class || @singleton_ancestors[other].include?(mod_or_class)
147
+ end
148
+ end
149
+ end
150
+ end
151
+
152
+ def qualified_const_defined?(const_name)
153
+ ActiveSupport::Dependencies.qualified_const_defined?(const_name)
62
154
  end
63
155
  end
64
156
 
157
+ def unload_modified_files!
158
+ unloaded_something = unload_modified_files_internal!
159
+ load_failure = clear_load_failure
160
+ unloaded_something || load_failure
161
+ ensure
162
+ async_synchronize { @module_cache = nil }
163
+ end
164
+
65
165
  def remove_explicitely_unloadable_constants!
66
166
  explicitly_unloadable_constants.each { |const| remove_constant(const) }
67
167
  end
@@ -71,26 +171,20 @@ module RailsDevelopmentBoost
71
171
  end
72
172
 
73
173
  # Augmented `load_file'.
74
- def load_file_with_constant_tracking(path, *args, &block)
75
- result = now_loading(path) { load_file_without_constant_tracking(path, *args, &block) }
76
-
77
- unless load_once_path?(path)
78
- new_constants = autoloaded_constants - file_map.values.map(&:constants).flatten
79
-
80
- # Associate newly loaded constants to the file just loaded
81
- associate_constants_to_file(new_constants, path)
174
+ def load_file_with_constant_tracking(path, *args)
175
+ async_synchronize do
176
+ @module_cache = nil # nuking the module_cache helps to avoid any stale-class issues when the async mode is used in a console session
177
+ load_file_with_constant_tracking_internal(path, args)
82
178
  end
83
-
84
- result
85
179
  end
86
180
 
87
181
  def now_loading(path)
88
- @currently_loading, old_currently_loading = path, @currently_loading
182
+ currently_loading << path
89
183
  yield
90
184
  rescue Exception => e
91
- error_loading_file(@currently_loading, e)
185
+ error_loading_file(currently_loading.last, e)
92
186
  ensure
93
- @currently_loading = old_currently_loading
187
+ currently_loading.pop
94
188
  end
95
189
 
96
190
  def associate_constants_to_file(constants, file_path)
@@ -98,16 +192,13 @@ module RailsDevelopmentBoost
98
192
  constants.map!(&:freeze)
99
193
  file_path.freeze
100
194
 
101
- loaded_file_for(file_path).add_constants(constants)
102
- end
103
-
104
- def loaded_file_for(file_path)
105
- file_map[file_path] ||= LoadedFile.new(file_path)
195
+ LoadedFile.for(file_path).add_constants(constants)
106
196
  end
107
197
 
108
198
  # Augmented `remove_constant'.
109
199
  def remove_constant_with_handling_of_connections(const_name)
110
- fetch_module_cache do
200
+ async_synchronize do
201
+ module_cache # make sure module_cache has been created
111
202
  prevent_further_removal_of(const_name) do
112
203
  unprotected_remove_constant(const_name)
113
204
  end
@@ -116,31 +207,89 @@ module RailsDevelopmentBoost
116
207
 
117
208
  def required_dependency(file_name)
118
209
  # Rails uses require_dependency for loading helpers, we are however dealing with the helper problem elsewhere, so we can skip them
119
- if @currently_loading && @currently_loading !~ /_controller(?:\.rb)?\Z/ && file_name !~ /_helper(?:\.rb)?\Z/
120
- if full_path = ActiveSupport::Dependencies.search_for_file(file_name)
121
- loaded_file_for(@currently_loading).associate_with(loaded_file_for(full_path))
210
+ return if (curr_loading = currently_loading.last) && curr_loading =~ /_controller(?:\.rb)?\Z/ && file_name =~ /_helper(?:\.rb)?\Z/
211
+
212
+ if full_path = ActiveSupport::Dependencies.search_for_file(file_name)
213
+ RequiredDependency.new(curr_loading).related_files.each do |related_file|
214
+ LoadedFile.relate_files(related_file, full_path)
122
215
  end
123
216
  end
124
217
  end
125
218
 
126
219
  def add_explicit_dependency(parent, child)
127
- (explicit_dependencies[parent._mod_name] ||= []) << child._mod_name
220
+ if !Util.anonymous_const_name?(child_mod_name = child._mod_name) && !Util.anonymous_const_name?(parent_mod_name = parent._mod_name)
221
+ ((explicit_dependencies[parent_mod_name] ||= []) << child_mod_name).uniq!
222
+ end
128
223
  end
129
224
 
130
225
  def handle_already_autoloaded_constants! # we might be late to the party and other gems/plugins might have already triggered autoloading of some constants
131
226
  loaded.each do |require_path|
132
- associate_constants_to_file(autoloaded_constants, "#{require_path}.rb") # slightly heavy-handed..
227
+ unless load_once_path?(require_path)
228
+ associate_constants_to_file(autoloaded_constants, "#{require_path}.rb") # slightly heavy-handed..
229
+ end
230
+ end
231
+ end
232
+
233
+ def in_autoload_path?(expanded_file_path)
234
+ autoload_paths.any? do |autoload_path|
235
+ autoload_path = autoload_path.to_s # handle Pathnames
236
+ expanded_file_path.starts_with?(autoload_path.ends_with?('/') ? autoload_path : "#{autoload_path}/")
237
+ end
238
+ end
239
+
240
+ def load_file_from_explicit_load(expanded_path)
241
+ unless LoadedFile.loaded?(expanded_path)
242
+ load_file(expanded_path)
243
+ if LoadedFile.loaded?(expanded_path) && (file = LoadedFile.for(expanded_path)).decorator_like?
244
+ file.associate_to_greppable_constants
245
+ end
133
246
  end
134
247
  end
135
248
 
136
249
  private
250
+ def unload_modified_files_internal!
251
+ log_call
252
+ if DependenciesPatch.async?
253
+ # because of the forking ruby servers (threads don't survive the forking),
254
+ # the Async heartbeat/init check needs to be here (instead of it being a boot time thing)
255
+ Async.heartbeat_check!
256
+ else
257
+ LoadedFile.unload_modified!
258
+ end
259
+ end
260
+
261
+ def clear_load_failure
262
+ @load_failure.tap { @load_failure = false }
263
+ end
264
+
265
+ def load_file_with_constant_tracking_internal(path, args)
266
+ result = now_loading(path) { load_file_without_constant_tracking(path, *args) }
267
+
268
+ unless load_once_path?(path)
269
+ new_constants = autoloaded_constants - LoadedFile.loaded_constants
270
+
271
+ # Associate newly loaded constants to the file just loaded
272
+ associate_constants_to_file(new_constants, path)
273
+ end
274
+
275
+ result
276
+ end
277
+
278
+ def async_synchronize
279
+ if DependenciesPatch.async?
280
+ Async.synchronize { yield }
281
+ else
282
+ yield
283
+ end
284
+ end
285
+
137
286
  def unprotected_remove_constant(const_name)
138
287
  if qualified_const_defined?(const_name) && object = const_name.constantize
139
288
  handle_connected_constants(object, const_name)
140
- remove_same_file_constants(const_name)
289
+ LoadedFile.unload_files_with_const!(const_name)
141
290
  if object.kind_of?(Module)
142
291
  remove_parent_modules_if_autoloaded(object)
143
- remove_child_module_constants(object)
292
+ remove_child_module_constants(object, const_name)
144
293
  end
145
294
  end
146
295
  result = remove_constant_without_handling_of_connections(const_name)
@@ -148,14 +297,9 @@ module RailsDevelopmentBoost
148
297
  result
149
298
  end
150
299
 
151
- def unload_file(file)
152
- file.constants.dup.each {|const| remove_constant(const)}
153
- clean_up_if_no_constants(file)
154
- end
155
- alias_method :unload_modified_file, :unload_file
156
-
157
300
  def error_loading_file(file_path, e)
158
- loaded_file_for(file_path).stale!
301
+ LoadedFile.for(file_path).stale! if LoadedFile.loaded?(file_path)
302
+ @load_failure = true
159
303
  raise e
160
304
  end
161
305
 
@@ -164,32 +308,29 @@ module RailsDevelopmentBoost
164
308
  remove_explicit_dependencies_of(const_name)
165
309
  remove_dependent_modules(object)
166
310
  update_activerecord_related_references(object)
311
+ update_mongoid_related_references(object)
167
312
  remove_nested_constants(const_name)
168
313
  end
169
314
 
170
315
  def remove_nested_constants(const_name)
171
- autoloaded_constants.grep(/\A#{const_name}::/).each { |const| remove_nested_constant(const_name, const) }
316
+ autoloaded_constants.grep(/\A#{const_name}::/) { |const| remove_nested_constant(const_name, const) }
172
317
  end
173
318
 
174
319
  def remove_nested_constant(parent_const, child_const)
175
320
  remove_constant(child_const)
176
321
  end
177
322
 
178
- def autoloaded_namespace_object?(object) # faster than going through Dependencies.autoloaded?
179
- LoadedFile.constants_to_files[object._mod_name]
180
- end
181
-
182
323
  # AS::Dependencies doesn't track same-file nested constants, so we need to look out for them on our own.
183
324
  # For example having loaded an abc.rb that looks like this:
184
325
  # class Abc; class Inner; end; end
185
326
  # AS::Dependencies would only add "Abc" constant name to its autoloaded_constants list, completely ignoring Abc::Inner. This in turn
186
327
  # can cause problems for classes inheriting from Abc::Inner somewhere else in the app.
187
328
  def remove_parent_modules_if_autoloaded(object)
188
- unless autoloaded_namespace_object?(object)
329
+ unless autoloaded_object?(object)
189
330
  initial_object = object
190
331
 
191
332
  while (object = object.parent) != Object
192
- if autoloaded_namespace_object?(object)
333
+ if autoloaded_object?(object)
193
334
  remove_autoloaded_parent_module(initial_object, object)
194
335
  break
195
336
  end
@@ -201,39 +342,45 @@ module RailsDevelopmentBoost
201
342
  remove_constant(parent_object._mod_name)
202
343
  end
203
344
 
345
+ def autoloaded_object?(object) # faster than going through Dependencies.autoloaded?
346
+ LoadedFile.loaded_constant?(object._mod_name)
347
+ end
348
+
204
349
  # 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
205
- def remove_child_module_constants(object)
206
- object.constants.each do |const_name|
350
+ def remove_child_module_constants(object, object_const_name)
351
+ object.constants.each do |child_const_name|
207
352
  # we only care about "namespace" constants (classes/modules)
208
- if local_const_defined?(object, const_name) && (child_const = object.const_get(const_name)).kind_of?(Module)
209
- remove_child_module_constant(object, child_const)
353
+ if (child_const = get_child_const(object, child_const_name)).kind_of?(Module)
354
+ # 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",
355
+ # but only an alias to Y module
356
+ if (full_child_const_name = child_const._mod_name) == "#{object_const_name}::#{child_const_name}"
357
+ remove_child_module_constant(object, full_child_const_name)
358
+ end
210
359
  end
211
360
  end
212
361
  end
213
362
 
214
- def remove_child_module_constant(parent_object, child_constant)
215
- remove_constant(child_constant._mod_name)
216
- end
217
-
218
- def in_autoloaded_namespace?(object)
219
- while object != Object
220
- return true if autoloaded_namespace_object?(object)
221
- object = object.parent
363
+ def get_child_const(object, child_const_name)
364
+ if local_const_defined?(object, child_const_name)
365
+ begin
366
+ object.const_get(child_const_name)
367
+ rescue NameError
368
+ # Apparently even though we get a list of constants through the native Module#constants and do a local_const_defined? check the const_get
369
+ # can still fail with a NameError (const undefined etc.)
370
+ # See https://github.com/thedarkone/rails-dev-boost/pull/33 for more details.
371
+ end
222
372
  end
223
- false
224
- end
225
-
226
- def remove_same_file_constants(const_name)
227
- LoadedFile.each_file_with_const(const_name) {|file| unload_containing_file(const_name, file)}
228
373
  end
229
374
 
230
- def unload_containing_file(const_name, file)
231
- unload_file(file)
375
+ def remove_child_module_constant(parent_object, full_child_const_name)
376
+ remove_constant(full_child_const_name)
232
377
  end
233
378
 
234
379
  def remove_explicit_dependencies_of(const_name)
235
380
  if dependencies = explicit_dependencies.delete(const_name)
236
- dependencies.uniq.each {|depending_const| remove_explicit_dependency(const_name, depending_const)}
381
+ dependencies.each do |depending_const|
382
+ remove_explicit_dependency(const_name, depending_const) if LoadedFile.loaded_constant?(depending_const)
383
+ end
237
384
  end
238
385
  end
239
386
 
@@ -241,50 +388,21 @@ module RailsDevelopmentBoost
241
388
  remove_constant(depending_const)
242
389
  end
243
390
 
244
- def clear_tracks_of_removed_const(const_name, object)
391
+ def clear_tracks_of_removed_const(const_name, object = nil)
245
392
  autoloaded_constants.delete(const_name)
246
- module_cache.delete_if { |mod| mod._mod_name == const_name }
247
- clean_up_references(const_name, object)
248
-
249
- LoadedFile.each_file_with_const(const_name) do |file|
250
- file.delete_constant(const_name)
251
- clean_up_if_no_constants(file)
252
- end
253
- end
254
-
255
- def clean_up_if_no_constants(file)
256
- if file.constants.empty?
257
- loaded.delete(file.require_path)
258
- file_map.delete(file.path)
259
- end
260
- end
261
-
262
- def clean_up_references(const_name, object)
263
- ActiveSupport::Dependencies::Reference.loose!(const_name)
264
- ActiveSupport::DescendantsTracker.delete(object)
393
+ @module_cache.remove_const(const_name, object)
394
+ LoadedFile.const_unloaded(const_name)
265
395
  end
266
396
 
267
397
  def remove_dependent_modules(mod)
268
- fetch_module_cache do |modules|
269
- modules.dup.each do |other|
270
- next unless other < mod || other.singleton_class.ancestors.include?(mod)
271
- next unless first_non_anonymous_superclass(other) == mod if Class === mod
272
- next unless qualified_const_defined?(other._mod_name) && other._mod_name.constantize == other
273
- next unless in_autoloaded_namespace?(other)
274
- remove_dependent_constant(mod, other)
275
- end
276
- end
398
+ module_cache.each_dependent_on(mod) {|other| remove_dependent_constant(mod, other)}
277
399
  end
278
400
 
279
401
  def remove_dependent_constant(original_module, dependent_module)
280
402
  remove_constant(dependent_module._mod_name)
281
403
  end
282
404
 
283
- def first_non_anonymous_superclass(klass)
284
- while (klass = klass.superclass) && anonymous?(klass); end
285
- klass
286
- end
287
-
405
+ AR_REFLECTION_CACHES = [:@klass]
288
406
  # egrep -ohR '@\w*([ck]lass|refl|target|own)\w*' activerecord | sort | uniq
289
407
  def update_activerecord_related_references(klass)
290
408
  return unless defined?(ActiveRecord)
@@ -293,30 +411,34 @@ module RailsDevelopmentBoost
293
411
  # Reset references held by macro reflections (klass is lazy loaded, so
294
412
  # setting its cache to nil will force the name to be resolved again).
295
413
  ActiveRecord::Base.descendants.each do |model|
296
- model.reflections.each_value do |reflection|
297
- reflection.instance_eval do
298
- @klass = nil if @klass == klass
299
- end
300
- end
414
+ clean_up_relation_caches(model.reflections, klass, AR_REFLECTION_CACHES)
301
415
  end
302
416
  end
303
417
 
304
- def anonymous?(mod)
305
- !(name = mod._mod_name) || name.empty?
418
+ MONGOID_RELATION_CACHES = [:@klass, :@inverse_klass]
419
+ def update_mongoid_related_references(klass)
420
+ if defined?(Mongoid::Document) && klass < Mongoid::Document
421
+ while (superclass = Util.first_non_anonymous_superclass(superclass || klass)) != Object && superclass < Mongoid::Document
422
+ remove_constant(superclass._mod_name) # this is necessary to nuke the @_types caches
423
+ end
424
+
425
+ module_cache.each_dependent_on(Mongoid::Document) do |model|
426
+ clean_up_relation_caches(model.relations, klass, MONGOID_RELATION_CACHES)
427
+ end
428
+ end
306
429
  end
307
-
308
- private
309
-
310
- def fetch_module_cache
311
- return(yield(module_cache)) if module_cache.any?
312
-
313
- ObjectSpace.each_object(Module) { |mod| module_cache << mod unless anonymous?(mod) }
314
- begin
315
- yield module_cache
316
- ensure
317
- module_cache.clear
430
+
431
+ def clean_up_relation_caches(relations, klass, ivar_names)
432
+ relations.each_value do |relation|
433
+ ivar_names.each do |ivar_name|
434
+ relation.instance_variable_set(ivar_name, nil) if relation.instance_variable_get(ivar_name) == klass
435
+ end
318
436
  end
319
437
  end
438
+
439
+ def module_cache
440
+ @module_cache ||= ModuleCache.new
441
+ end
320
442
 
321
443
  def prevent_further_removal_of(const_name)
322
444
  return if constants_being_removed.include?(const_name)