rails-dev-boost 0.1.1 → 0.2.0

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.
@@ -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)