im 0.1.5 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Im::ModuleConstAdded
4
+ UNBOUND_METHOD_MODULE_NAME = Module.instance_method(:name)
5
+ private_constant :UNBOUND_METHOD_MODULE_NAME
6
+
7
+ # We patch Module#const_added to track every time a constant is added to a
8
+ # permanently-named module pointing to an Im-autoloaded constant. This is
9
+ # important because the moment that an Im-autoloaded constant is attached to
10
+ # a permanently named module, its name changes permanently. Although Im
11
+ # internally avoids the use of absolute cpaths, ExplicitNamespace must use
12
+ # them and thus we need to update its internal registry accordingly.
13
+ #
14
+ # @sig (Symbol) -> void
15
+ def const_added(const_name)
16
+ # If we are called from an autoload, no need to track.
17
+ return super if autoload?(const_name)
18
+
19
+ # Get the name of this module and only continue if it is a permanent name.
20
+ return unless cpath = Im.permanent_cpath(self)
21
+
22
+ # We know this is not an autoloaded constant, so it is safe to fetch the
23
+ # value. We fetch the value, get it's object_id, and check the registry to
24
+ # see if it is an Im-autoloaded module.
25
+ relative_cpath, loader, references = Im::Registry.autoloaded_modules[const_get(const_name).object_id]
26
+ return super unless loader
27
+
28
+ # Update the context for this const add. This is important for reloading so
29
+ # we can reset inbound references when the autoloaded module is unloaded.
30
+ references << [self, const_name]
31
+
32
+ # Update all absolute cpath references to this module by replacing all
33
+ # references to the original cpath with the new, permanently-named cpath.
34
+ #
35
+ # For example, if we had a module loader::Foo::Bar, and loader::Foo was
36
+ # assigned to Baz like this:
37
+ #
38
+ # Baz = loader::Foo
39
+ #
40
+ # then we must update cpaths from a string like
41
+ #
42
+ # "#<Im::Loader ...>::Foo::Bar"
43
+ #
44
+ # to
45
+ #
46
+ # "Baz::Bar"
47
+ #
48
+ # To do this, we take the loader's module_prefix ("#<Im::Loader ...>::"),
49
+ # append to it the relative cpath of the constant ("Foo") and replace that by the new
50
+ # name ("Baz"), roughly like this:
51
+ #
52
+ # "#<Im::Loader ...>::Foo::Bar".gsub(/^#{"#<Im::Loader ...>::Foo"}/, "Baz")
53
+ #
54
+ prefix = relative_cpath ? "#{loader.module_prefix}#{relative_cpath}" : loader.module_prefix.delete_suffix("::")
55
+ ::Im::ExplicitNamespace.__update_cpaths(prefix, "#{cpath}::#{const_name}")
56
+
57
+ super
58
+ rescue NameError
59
+ super
60
+ end
61
+ end
62
+
63
+ ::Module.prepend(Im::ModuleConstAdded)
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Im
4
+ module Registry # :nodoc: all
5
+ class << self
6
+ # Keeps track of all loaders. Useful to broadcast messages and to prevent
7
+ # them from being garbage collected.
8
+ #
9
+ # @private
10
+ # @sig Array[Im::Loader]
11
+ attr_reader :loaders
12
+
13
+ # Registers gem loaders to let `for_gem` be idempotent in case of reload.
14
+ #
15
+ # @private
16
+ # @sig Hash[String, Im::Loader]
17
+ attr_reader :gem_loaders_by_root_file
18
+
19
+ # Maps absolute paths to the loaders responsible for them.
20
+ #
21
+ # This information is used by our decorated `Kernel#require` to be able to
22
+ # invoke callbacks and autovivify modules.
23
+ #
24
+ # @private
25
+ # @sig Hash[String, Im::Loader]
26
+ attr_reader :autoloads
27
+
28
+ # @private
29
+ # @sig Hash[String, Im::Loader]
30
+ attr_reader :paths
31
+
32
+ # This hash table addresses an edge case in which an autoload is ignored.
33
+ #
34
+ # For example, let's suppose we want to autoload in a gem like this:
35
+ #
36
+ # # lib/my_gem.rb
37
+ # loader = Im::Loader.new
38
+ # loader.push_dir(__dir__)
39
+ # loader.setup
40
+ #
41
+ # module loader::MyGem
42
+ # end
43
+ #
44
+ # if you require "my_gem", as Bundler would do, this happens while setting
45
+ # up autoloads:
46
+ #
47
+ # 1. Object.autoload?(:MyGem) returns `nil` because the autoload for
48
+ # the constant is issued by Im while the same file is being
49
+ # required.
50
+ # 2. The constant `MyGem` is undefined while setup runs.
51
+ #
52
+ # Therefore, a directory `lib/my_gem` would autovivify a module according to
53
+ # the existing information. But that would be wrong.
54
+ #
55
+ # To overcome this fundamental limitation, we keep track of the constant
56
+ # paths that are in this situation ---in the example above, "MyGem"--- and
57
+ # take this collection into account for the autovivification logic.
58
+ #
59
+ # Note that you cannot generally address this by moving the setup code
60
+ # below the constant definition, because we want libraries to be able to
61
+ # use managed constants in the module body:
62
+ #
63
+ # module loader::MyGem
64
+ # include MyConcern
65
+ # end
66
+ #
67
+ # @private
68
+ # @sig Hash[String, [String, Im::Loader]]
69
+ attr_reader :inceptions
70
+
71
+ # @private
72
+ # @sig Hash[Integer, [Im::Loader, String, Array]]
73
+ attr_reader :autoloaded_modules
74
+
75
+ # Registers a loader.
76
+ #
77
+ # @private
78
+ # @sig (Im::Loader) -> void
79
+ def register_loader(loader)
80
+ loaders << loader
81
+ end
82
+
83
+ # @private
84
+ # @sig (Im::Loader) -> void
85
+ def unregister_loader(loader)
86
+ loaders.delete(loader)
87
+ gem_loaders_by_root_file.delete_if { |_, l| l == loader }
88
+ autoloads.delete_if { |_, l| l == loader }
89
+ paths.delete_if { |_, l| l == loader }
90
+ inceptions.delete_if { |_, (_, l)| l == loader }
91
+ autoloaded_modules.delete_if { |_, (_, l, _)| l == loader }
92
+ end
93
+
94
+ # This method returns always a loader, the same instance for the same root
95
+ # file. That is how Im::Loader.for_gem is idempotent.
96
+ #
97
+ # @private
98
+ # @sig (String) -> Im::Loader
99
+ def loader_for_gem(root_file, warn_on_extra_files:)
100
+ gem_loaders_by_root_file[root_file] ||= GemLoader._new(root_file, warn_on_extra_files: warn_on_extra_files)
101
+ end
102
+
103
+ # @private
104
+ # @sig (Im::Loader, String) -> String
105
+ def register_autoload(loader, abspath)
106
+ paths[abspath] = autoloads[abspath] = loader
107
+ end
108
+
109
+ # @private
110
+ # @sig (String) -> void
111
+ def unregister_autoload(abspath)
112
+ autoloads.delete(abspath)
113
+ end
114
+
115
+ # @private
116
+ # @sig (Im::Loader, String) -> String
117
+ def register_path(loader, abspath)
118
+ paths[abspath] = loader
119
+ end
120
+
121
+ # @private
122
+ # @sig (String) -> void
123
+ def unregister_path(abspath)
124
+ paths.delete(abspath)
125
+ end
126
+
127
+ # @private
128
+ # @sig (String, String, Im::Loader) -> void
129
+ def register_inception(cpath, abspath, loader)
130
+ inceptions[cpath] = [abspath, loader]
131
+ end
132
+
133
+ # @private
134
+ # @sig (String) -> String?
135
+ def inception?(cpath)
136
+ if pair = inceptions[cpath]
137
+ pair.first
138
+ end
139
+ end
140
+
141
+ def register_autoloaded_module(mod, module_name, loader)
142
+ autoloaded_modules[mod.object_id] = [module_name, loader, []]
143
+ end
144
+
145
+ # @private
146
+ # @sig (String) -> Im::Loader?
147
+ def loader_for(path)
148
+ paths[path]
149
+ end
150
+
151
+ # @private
152
+ # @sig (Im::Loader) -> void
153
+ def on_unload(loader)
154
+ autoloads.delete_if { |_path, object| object == loader }
155
+ inceptions.delete_if { |_cpath, (_path, object)| object == loader }
156
+ end
157
+ end
158
+
159
+ @loaders = []
160
+ @gem_loaders_by_root_file = {}
161
+ @autoloads = {}
162
+ @paths = {}
163
+ @inceptions = {}
164
+ @autoloaded_modules = {}
165
+ end
166
+ end
data/lib/im/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Im
4
- VERSION = "0.1.5"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/im.rb CHANGED
@@ -1,180 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Im
4
- load $LOAD_PATH.resolve_feature_path("im/ruby_version_check")[1], true
5
-
6
- @constants = Object.constants.reject { |c| Kernel.autoload?(c) }.dup
7
- @autoloads = {}
8
- @registry = {}
9
-
10
- require "im/version"
11
- require "im/kernel"
12
-
13
- class << self
14
- attr_reader :current_import, :registry, :autoloads, :constants
15
-
16
- def with_import(import)
17
- original_import = current_import
18
- @current_import = import || Im.current_import
19
-
20
- unless tracer_originally_enabled = @tracer.enabled?
21
- @tracer.enable
22
- end
23
-
24
- yield(import)
25
-
26
- ensure
27
- @current_import = original_import
28
- @tracer.disable unless tracer_originally_enabled
29
- end
30
-
31
- def importing?
32
- !!Im.current_import
33
- end
34
-
35
- def handle_require(path, caller_path)
36
- resolved_path = resolve_path(path)
37
-
38
- return yield unless resolved_path
39
-
40
- if autoloaded = autoloads.delete(resolved_path)
41
- return with_import(autoloaded) do |_import|
42
- !!import(path)
43
- end
44
- end
45
-
46
- if importing?
47
- registry[resolved_path] ||= Require.new(resolved_path, current_import)
48
- elsif registry.key?(caller_path)
49
- return with_import(registry[caller_path].import) do |_import|
50
- !!import(resolved_path)
51
- end
52
- end
53
-
54
- loaded = yield # super
55
-
56
- if required = registry[resolved_path]
57
- if loaded && registry.key?(caller_path)
58
- registry[caller_path].requires << required
59
- end
60
-
61
- required.requires.each { |r| require r.path }
62
- import = current_import || Object
63
-
64
- required.modules.each do |m|
65
- name = m.name
66
-
67
- # Do not assign constants that are aliased to root namespace
68
- root = name.split("::", 2)[0]
69
- next if Object.const_defined?(root) &&
70
- import.const_defined?(root, false) &&
71
- Object.const_get(root) == import.const_get(root)
72
-
73
- begin
74
- import.const_set(name, m) unless import.const_defined?(name, false)
75
- rescue NameError
76
- end
77
- end
78
- end
79
-
80
- loaded
81
- end
82
-
83
- def handle_autoload(path)
84
- if resolved_path = resolve_path(path)
85
- Im.autoloads[resolved_path] = Im.current_import
86
- end
87
- yield
88
- end
89
-
90
- def resolve_path(path)
91
- (resolved = $LOAD_PATH.resolve_feature_path(path)) && resolved[1]
92
- end
93
- end
94
-
95
- class Import < Module
96
- def initialize(root)
97
- @root = root
98
- super()
99
- Im.with_import(self) do
100
- Im.constants.each do |const|
101
- self.const_set(const, Object.const_get(const))
102
- end
103
- end
104
- end
105
-
106
- def inspect
107
- @inspect ||= "<#Im::Import root: #{@root}>"
108
- end
109
- end
110
-
111
- Require = Struct.new(:path, :import, :modules, :requires) do
112
- def initialize(path, import, modules = [], requires = [])
113
- super(path, import, modules, requires)
114
- end
115
- end
116
-
4
+ require_relative "im/const_path"
5
+ require_relative "im/internal"
6
+ require_relative "im/loader"
7
+ require_relative "im/gem_loader"
8
+ require_relative "im/registry"
9
+ require_relative "im/explicit_namespace"
10
+ require_relative "im/module_const_added"
11
+ require_relative "im/inflector"
12
+ require_relative "im/gem_inflector"
13
+ require_relative "im/kernel"
14
+ require_relative "im/error"
15
+ require_relative "im/version"
16
+
17
+ extend Im::ConstPath
18
+
19
+ # @sig (String) -> Im::Loader?
117
20
  def import(path)
118
- if resolved = $LOAD_PATH.resolve_feature_path(path)
119
- resolved_path = resolved[1]
120
- raise LoadError, "import can only import ruby files" unless resolved[0] == :rb
121
- else
122
- raise LoadError, "cannot load such file -- #{path}"
123
- end
124
-
125
- Im.registry.fetch(resolved_path) do
126
- # handle autoload within import
127
- return nil if $LOADED_FEATURES.include?(resolved_path)
128
-
129
- $LOADED_FEATURES << resolved_path
130
-
131
- import = Im.current_import || Import.new(path)
132
- (Im.registry[resolved_path] ||= Require.new(resolved_path, import)).tap do
133
- Im.with_import(import) do
134
- load resolved_path, import
135
- end
136
- end
137
- end&.import
21
+ _, feature_path = $:.resolve_feature_path(path)
22
+ Registry.loader_for(feature_path) if feature_path
138
23
  end
139
24
 
140
- # This is a hack to catch references to top-level constants (`::Foo`) and, if
141
- # the calling line is a key in the Im registry, replace the toplevel constant
142
- # reference with its corresponding constant under the import namespace.
143
- #
144
- # By doing this, a reference to `::Foo` in an imported file will resolve to
145
- # `mod::Foo`, where `mod` is the import module. Ideally this should
146
- # ultimately happen at the Ruby implementation level.
147
- module RootNamespaceRedirect
148
- def const_missing(name)
149
-
150
- # Take top five locations to allow for other `const_missing` overrides
151
- # in the backtrace. If there are more than five, then this will no longer
152
- # work. This is currently very inefficient.
153
- if location = caller_locations(1, 5).find { |l| Im.registry.key?(l.path) }
154
- Im.registry[location.path].import.const_get(name)
155
- else
156
- super
157
- end
158
- end
159
- end
160
-
161
- @tracer = TracePoint.new(:class) do |event|
162
- next unless (name = event.self.name)
163
- next if Im.constants.include?(name.to_sym)
164
-
165
- if resolved = $LOAD_PATH.resolve_feature_path(event.path)
166
- resolved_path = resolved[1]
167
- if Im.registry.key?(resolved_path)
168
- (Im.registry[resolved_path].modules << event.self).uniq!
169
- else
170
- Im.registry[resolved_path] = Require.new(resolved_path, Im.current_import, [event.self])
171
- end
172
- end
173
- end
174
-
175
- require "im/module"
176
-
177
25
  extend self
178
26
  end
179
-
180
- Object.extend Im::RootNamespaceRedirect
metadata CHANGED
@@ -1,84 +1,48 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: im
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Salzberg
8
8
  autorequire:
9
- bindir: exe
9
+ bindir: bin
10
10
  cert_chain: []
11
- date: 2022-09-22 00:00:00.000000000 Z
12
- dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: rake
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: '13.0'
20
- type: :development
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: '13.0'
27
- - !ruby/object:Gem::Dependency
28
- name: rake-compiler
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
- - !ruby/object:Gem::Dependency
42
- name: rspec
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: '3.0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '3.0'
55
- description: Import code without side-effects.
56
- email:
57
- - chris@dejimata.com
11
+ date: 2023-01-28 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: " Im is a thread-safe code loader for anonymous-rooted namespaces.\n"
14
+ email: chris@dejimata.com
58
15
  executables: []
59
16
  extensions: []
60
17
  extra_rdoc_files: []
61
18
  files:
62
- - CHANGELOG.md
63
- - Gemfile
64
- - Gemfile.lock
65
- - LICENSE.txt
19
+ - MIT-LICENSE
66
20
  - README.md
67
- - Rakefile
68
21
  - lib/im.rb
22
+ - lib/im/const_path.rb
23
+ - lib/im/error.rb
24
+ - lib/im/explicit_namespace.rb
25
+ - lib/im/gem_inflector.rb
26
+ - lib/im/gem_loader.rb
27
+ - lib/im/inflector.rb
28
+ - lib/im/internal.rb
69
29
  - lib/im/kernel.rb
70
- - lib/im/module.rb
71
- - lib/im/ruby_version_check.rb
30
+ - lib/im/loader.rb
31
+ - lib/im/loader/callbacks.rb
32
+ - lib/im/loader/config.rb
33
+ - lib/im/loader/eager_load.rb
34
+ - lib/im/loader/helpers.rb
35
+ - lib/im/module_const_added.rb
36
+ - lib/im/registry.rb
72
37
  - lib/im/version.rb
73
38
  homepage: https://github.com/shioyama/im
74
39
  licenses:
75
40
  - MIT
76
41
  metadata:
77
42
  homepage_uri: https://github.com/shioyama/im
43
+ changelog_uri: https://github.com/shioyama/im/blob/master/CHANGELOG.md
78
44
  source_code_uri: https://github.com/shioyama/im
79
45
  bug_tracker_uri: https://github.com/shioyama/im/issues
80
- changelog_uri: https://github.com/shioyama/im/CHANGELOG.md
81
- rubygems_mfa_required: 'true'
82
46
  post_install_message:
83
47
  rdoc_options: []
84
48
  require_paths:
@@ -87,7 +51,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
87
51
  requirements:
88
52
  - - ">="
89
53
  - !ruby/object:Gem::Version
90
- version: 3.1.0
54
+ version: '3.2'
91
55
  required_rubygems_version: !ruby/object:Gem::Requirement
92
56
  requirements:
93
57
  - - ">="
@@ -97,5 +61,5 @@ requirements: []
97
61
  rubygems_version: 3.4.0.dev
98
62
  signing_key:
99
63
  specification_version: 4
100
- summary: Module import system.
64
+ summary: Multiverse autoloader
101
65
  test_files: []
data/CHANGELOG.md DELETED
@@ -1,28 +0,0 @@
1
- ## [Unreleased]
2
-
3
- ## [0.1.5] - 2022-09-22
4
- - Rename Im::Dependency -> Im::Require
5
- - Handle dynamic requires (require called from method body after file has been imported)
6
- - Refactor: move require/autoload patch logic to Im handler methods
7
-
8
-
9
- ## [0.1.4] - 2022-09-19
10
- - Correctly assign constants imported via recursive requires
11
-
12
- ## [0.1.3] - 2022-09-16
13
- - Fix gemspec
14
-
15
- ## [0.1.1] - 2022-09-16
16
-
17
- - Fix TracePoint event
18
- - Simplify import inspect
19
- - Remove unnecessary Kernel#autoload patch
20
- - Encode comprehensive list of top-level constants
21
-
22
- ## [0.1.0] - 2022-09-15
23
-
24
- - Initial release, requires patched version of Ruby.
25
-
26
- ## [0.0.1] - 2022-07-13
27
-
28
- - Stub
data/Gemfile DELETED
@@ -1,10 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- source "https://rubygems.org"
4
-
5
- # Specify your gem's dependencies in im.gemspec
6
- gemspec
7
-
8
- gem "rspec", "~> 3.0"
9
- gem "rails"
10
- gem "sqlite3"