im 0.1.5 → 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.
- checksums.yaml +4 -4
- data/MIT-LICENSE +21 -0
- data/README.md +245 -32
- data/lib/im/const_path.rb +48 -0
- data/lib/im/error.rb +24 -0
- data/lib/im/explicit_namespace.rb +96 -0
- data/lib/im/gem_inflector.rb +17 -0
- data/lib/im/gem_loader.rb +65 -0
- data/lib/im/inflector.rb +46 -0
- data/lib/im/internal.rb +12 -0
- data/lib/im/kernel.rb +34 -9
- data/lib/im/loader/callbacks.rb +93 -0
- data/lib/im/loader/config.rb +346 -0
- data/lib/im/loader/eager_load.rb +214 -0
- data/lib/im/loader/helpers.rb +123 -0
- data/lib/im/loader.rb +586 -0
- data/lib/im/module_const_added.rb +63 -0
- data/lib/im/registry.rb +166 -0
- data/lib/im/version.rb +1 -1
- data/lib/im.rb +18 -172
- metadata +24 -60
- data/CHANGELOG.md +0 -28
- data/Gemfile +0 -10
- data/Gemfile.lock +0 -182
- data/LICENSE.txt +0 -21
- data/Rakefile +0 -8
- data/lib/im/module.rb +0 -9
- data/lib/im/ruby_version_check.rb +0 -22
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a146ab43403d85ad1ba24849e235c4c3f9c6f1f344c350f93f7815163ad0ec17
|
4
|
+
data.tar.gz: 88f76dc7b193e2566b6603f4828ba07cade314dc08853b4941bce01df442954f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 53b9300337db2c3c716b9527f16ad78b29e65629da09306e22f48312907ef18d9e66f34e5b7a627dd7cf5daee00fd79953b222984937e43c49dd9f20934d6af2
|
7
|
+
data.tar.gz: 706be184a19a39e665c8324c69b98bcd847f908c206382338b933470e803a7503c3b376227c6be6913ebe8630c83f4c865d9f943a76bcaaa224931139b8e09f9
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
Copyright (c) 2023 Chris Salzberg
|
2
|
+
Copyright (c) 2019 Xavier Noria
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
a copy of this software and associated documentation files (the
|
6
|
+
"Software"), to deal in the Software without restriction, including
|
7
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
CHANGED
@@ -1,64 +1,277 @@
|
|
1
1
|
# Im
|
2
2
|
|
3
|
-
|
4
|
-
similar to systems like CommonJS, ESmodules, and RequireJS in Javascript. It
|
5
|
-
requires a patched version of Ruby which you will need to build in order to use
|
6
|
-
the gem.
|
3
|
+
[][actions]
|
7
4
|
|
8
|
-
|
5
|
+
[actions]: https://github.com/shioyama/im/actions
|
9
6
|
|
10
|
-
|
7
|
+
<!-- TOC -->
|
11
8
|
|
12
|
-
|
9
|
+
- [Introduction](#introduction)
|
10
|
+
- [Synopsis](#synopsis)
|
11
|
+
- [File structure](#file-structure)
|
12
|
+
- [File paths match constant paths under loader](#file-paths-match-constant-paths-under-loader)
|
13
|
+
- [Root directories](#root-directories)
|
14
|
+
- [Relative and absolute cpaths](#relative-and-absolute-cpaths)
|
15
|
+
- [Usage](#usage)
|
16
|
+
- [Motivation](#motivation)
|
17
|
+
- [License](#license)
|
13
18
|
|
14
|
-
|
19
|
+
<!-- /TOC -->
|
15
20
|
|
16
|
-
|
21
|
+
<a id="markdown-introduction" name="introduction"></a>
|
22
|
+
## Introduction
|
17
23
|
|
18
|
-
|
24
|
+
Im is a thread-safe code loader for anonymous-rooted namespaces in Ruby. It
|
25
|
+
allows you to share any nested, autoloaded set of code without polluting or in
|
26
|
+
any way touching the global namespace.
|
27
|
+
|
28
|
+
To do this, Im leverages code autoloading, Zeitwerk conventions around file
|
29
|
+
structure and naming, and two features added in Ruby 3.2: `Kernel#load`
|
30
|
+
with a module argument[^1] and `Module#const_added`[^2]. Since these Ruby
|
31
|
+
features are essential to its design, it cannot be used with earlier versions
|
32
|
+
of Ruby.
|
33
|
+
|
34
|
+
Im started its life as a fork of Zeitwerk and has a very similar interface. The
|
35
|
+
gem strives to follow the Zeitwerk pattern as much as possible. Im and Zeitwerk
|
36
|
+
can be used alongside each other provided there is no overlap between file
|
37
|
+
paths managed by each gem.
|
38
|
+
|
39
|
+
Im is in active development and should be considered experimental until the
|
40
|
+
eventual release of version 1.0. Versions 0.1.6 and earlier of the gem were
|
41
|
+
part of a different experiment and are unrelated to the current gem.
|
42
|
+
|
43
|
+
<a id="markdown-synopsis" name="synopsis"></a>
|
44
|
+
## Synopsis
|
45
|
+
|
46
|
+
Im follows an interface that is in most respects identical to Zeitwerk.
|
47
|
+
The central difference is that whereas Zeitwerk loads constants into the global
|
48
|
+
namespace (rooted in `Object`), Im loads them into anonymous namespaces rooted
|
49
|
+
on the loader itself. Loaders in Im are a subclass of the `Module` class, and
|
50
|
+
thus each one can define its own namespace. Since there can be arbitrarily many
|
51
|
+
loaders, there can also be arbitrarily many autoloaded namespaces.
|
19
52
|
|
20
|
-
|
53
|
+
Im's gem interface looks like this:
|
21
54
|
|
22
55
|
```ruby
|
56
|
+
# lib/my_gem.rb (main file)
|
57
|
+
|
23
58
|
require "im"
|
59
|
+
loader = Im::Loader.for_gem
|
60
|
+
loader.setup # ready!
|
61
|
+
|
62
|
+
module loader::MyGem
|
63
|
+
# ...
|
64
|
+
end
|
65
|
+
|
66
|
+
loader.eager_load # optionally
|
67
|
+
```
|
68
|
+
|
69
|
+
The generic interface is likewise identical to Zeitwerk's:
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
loader = Zeitwerk::Loader.new
|
73
|
+
loader.push_dir(...)
|
74
|
+
loader.setup # ready!
|
75
|
+
```
|
76
|
+
|
77
|
+
Other than gem names, the only difference here is in the definition of `MyGem`
|
78
|
+
under the loader namespace in the gem code. Unlike Zeitwerk, with Im the gem
|
79
|
+
namespace is not defined at toplevel:
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
Object.const_defined?(:MyGem)
|
83
|
+
#=> false
|
84
|
+
```
|
85
|
+
|
86
|
+
In order to prevent leakage, the gem's entrypoint, in this case
|
87
|
+
`lib/my_gem.rb`), must not define anything at toplevel, hence the use of
|
88
|
+
`module loader::MyGem`.
|
89
|
+
|
90
|
+
Once the entrypoint has been required, all constants defined within the gem's
|
91
|
+
file structure are autoloadable from the loader itself:
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
# lib/my_gem/foo.rb
|
95
|
+
|
96
|
+
module MyGem
|
97
|
+
class Foo
|
98
|
+
def hello_world
|
99
|
+
"Hello World!"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
```
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
foo = loader::MyGem::Foo
|
107
|
+
# loads `Foo` from lib/my_gem/foo.rb
|
108
|
+
|
109
|
+
foo.new.hello_world
|
110
|
+
# "Hello World!"
|
111
|
+
```
|
112
|
+
|
113
|
+
Constants under the loader can be given permanent names that are different from
|
114
|
+
the one defined in the gem itself:
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
Bar = loader::MyGem::Foo
|
118
|
+
Bar.new.hello_world
|
119
|
+
# "Hello World!"
|
120
|
+
```
|
121
|
+
|
122
|
+
Since Im uses loaders as its namespace roots, it is important that consumers of
|
123
|
+
gems have a way to fetch the loader for a given file path.
|
124
|
+
|
125
|
+
The loader variable can go out of scope. Like Zeitwerk, Im keeps a registry
|
126
|
+
with all of them, and so the object won't be garbage collected. For
|
127
|
+
convenience, Im also provides a method, `Im#import`, to fetch a loader for
|
128
|
+
a given file path:
|
129
|
+
|
130
|
+
```ruby
|
131
|
+
require "im"
|
132
|
+
require "my_gem"
|
24
133
|
|
25
134
|
extend Im
|
135
|
+
my_gem = import "my_gem"
|
136
|
+
#=> loader::MyGem is autoloadable
|
137
|
+
```
|
26
138
|
|
27
|
-
|
28
|
-
|
139
|
+
Reloading works like Zeitwerk:
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
loader = Im::Loader.new
|
143
|
+
loader.push_dir(...)
|
144
|
+
loader.enable_reloading # you need to opt-in before setup
|
145
|
+
loader.setup
|
146
|
+
...
|
147
|
+
loader.reload
|
29
148
|
```
|
30
149
|
|
31
|
-
|
150
|
+
You can assign a permanent name to an autoloaded constant, and it will be
|
151
|
+
reloaded when the loader is reloaded:
|
32
152
|
|
33
153
|
```ruby
|
34
|
-
|
35
|
-
|
154
|
+
Foo = loader::Foo
|
155
|
+
loader.reload # Object::Foo is replaced by an autoload
|
156
|
+
Foo #=> autoload is triggered, reloading loader::Foo
|
157
|
+
```
|
36
158
|
|
37
|
-
|
38
|
-
|
159
|
+
Like Zeitwerk, you can eager-load all the code at once:
|
160
|
+
|
161
|
+
```ruby
|
162
|
+
loader.eager_load
|
39
163
|
```
|
40
164
|
|
41
|
-
|
165
|
+
Alternatively, you can broadcast `eager_load` to all loader instances:
|
42
166
|
|
43
167
|
```ruby
|
44
|
-
|
168
|
+
Im::Loader.eager_load_all
|
169
|
+
```
|
45
170
|
|
46
|
-
|
47
|
-
|
171
|
+
<a id="markdown-file-structure" name="file-structure"></a>
|
172
|
+
## File structure
|
48
173
|
|
49
|
-
|
50
|
-
|
51
|
-
end
|
174
|
+
<a id="markdown-the-idea-file-paths-match-constant-paths-under-loader" name="the-idea-file-paths-match-constant-paths-under-loader"></a>
|
175
|
+
### File paths match constant paths under loader
|
52
176
|
|
53
|
-
|
54
|
-
|
55
|
-
|
177
|
+
File structure is identical to Zeitwerk, again with the difference that
|
178
|
+
constants are loaded from the loader's namespace rather than the root one:
|
179
|
+
|
180
|
+
```
|
181
|
+
lib/my_gem.rb -> loader::MyGem
|
182
|
+
lib/my_gem/foo.rb -> loader::MyGem::Foo
|
183
|
+
lib/my_gem/bar_baz.rb -> loader::MyGem::BarBaz
|
184
|
+
lib/my_gem/woo/zoo.rb -> loader::MyGem::Woo::Zoo
|
185
|
+
```
|
186
|
+
|
187
|
+
Im inherits support for collapsing directories and custom inflection, see
|
188
|
+
Zeitwerk's documentation for details on usage of these features.
|
189
|
+
|
190
|
+
<a id="markdown-root-directories" name="root-directories"></a>
|
191
|
+
### Root directories
|
192
|
+
|
193
|
+
Internally, each loader in Im can have one or more _root directories_ from which
|
194
|
+
it loads code onto itself. Root directories are added to the loader using
|
195
|
+
`Im::Loader#push_dir`:
|
196
|
+
|
197
|
+
```ruby
|
198
|
+
loader.push_dir("#{__dir__}/models")
|
199
|
+
loader.push_dir("#{__dir__}/serializers"))
|
200
|
+
```
|
201
|
+
|
202
|
+
Note that concept of a _root namespace_, which Zeitwerk uses to load code
|
203
|
+
under a given node of the global namespace, is absent in Im. Custom root
|
204
|
+
namespaces are likewise not supported. These features were removed as they add
|
205
|
+
complexity for little gain given Im's flexibility to anchor a namespace
|
206
|
+
anywhere in the global namespace.
|
207
|
+
|
208
|
+
<a id="markdown-relative-and-absolute-cpaths" name="relative-and-absolute-cpaths"></a>
|
209
|
+
### Relative and absolute cpaths
|
210
|
+
|
211
|
+
Im uses two types of constant paths: relative and absolute, wherever possible
|
212
|
+
defaulting to relative ones. A relative cpath is a constant name relative to
|
213
|
+
the loader in which it was originally defined, regardless of any other names it
|
214
|
+
was assigned. Whereas Zeitwerk uses absolute cpaths, Im uses relative cpaths for
|
215
|
+
all external loader APIs (see usage for examples).
|
56
216
|
|
57
|
-
|
58
|
-
|
59
|
-
|
217
|
+
To understand these concepts, it is important first to distinguish between two
|
218
|
+
types of names in Ruby: _temporary names_ and _permanent names_.
|
219
|
+
|
220
|
+
A temporary name is a constant name on an anonymous-rooted namespace, for
|
221
|
+
example a loader:
|
222
|
+
|
223
|
+
```ruby
|
224
|
+
my_gem = import "my_gem"
|
225
|
+
my_gem::Foo
|
226
|
+
my_gem::Foo.name
|
227
|
+
#=> "#<Im::Loader ...>::Foo"
|
228
|
+
```
|
229
|
+
|
230
|
+
Here, the string `"#<Im::Loader ...>::Foo"` is called a temporary name. We can
|
231
|
+
give this module a permanent name by assigning it to a toplevel constant:
|
232
|
+
|
233
|
+
```ruby
|
234
|
+
Bar = my_gem::Foo
|
235
|
+
my_gem::Foo.name
|
236
|
+
#=> "Bar"
|
237
|
+
```
|
238
|
+
|
239
|
+
Now its name is `"Bar"`, and it is near impossible to get back its original
|
240
|
+
temporary name.
|
241
|
+
|
242
|
+
This property of module naming in Ruby is problematic since cpaths are used as
|
243
|
+
keys in Im's internal registries to index constants and their autoloads, which
|
244
|
+
is critical for successful autoloading.
|
245
|
+
|
246
|
+
To get around this issue, Im tracks all module names and uses relative naming
|
247
|
+
inside loader code. You can get the name of a module relative to the loader
|
248
|
+
that loaded it with `Im::Loader#relative_cpath`:
|
249
|
+
|
250
|
+
```ruby
|
251
|
+
my_gem.relative_cpath(my_gem::Foo)
|
252
|
+
#=> "Foo"
|
60
253
|
```
|
61
254
|
|
255
|
+
Using relative cpaths frees Im from depending on `Module#name` for
|
256
|
+
registry keys like Zeitwerk does, which does not work with anonymous
|
257
|
+
namespaces. All public methods in Im that take a `cpath` take the _relative_
|
258
|
+
cpath, i.e. the cpath relative to the loader as toplevel, regardless of any
|
259
|
+
toplevel-rooted constant a module may have been assigned to.
|
260
|
+
|
261
|
+
<a id="markdown-usage" name="usage"></a>
|
262
|
+
## Usage
|
263
|
+
|
264
|
+
(TODO)
|
265
|
+
|
266
|
+
<a id="markdown-motivation" name="motivation"></a>
|
267
|
+
## Motivation
|
268
|
+
|
269
|
+
(TODO)
|
270
|
+
|
271
|
+
<a id="markdown-license" name="license"></a>
|
62
272
|
## License
|
63
273
|
|
64
|
-
|
274
|
+
Released under the MIT License, Copyright (c) 2023 Chris Salzberg and 2019–<i>ω</i> Xavier Noria.
|
275
|
+
|
276
|
+
[^1]: https://bugs.ruby-lang.org/issues/6210
|
277
|
+
[^2]: https://bugs.ruby-lang.org/issues/17881
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Im
|
4
|
+
module ConstPath
|
5
|
+
UNBOUND_METHOD_MODULE_NAME = Module.instance_method(:name)
|
6
|
+
UNBOUND_METHOD_MODULE_TO_S = Module.instance_method(:to_s)
|
7
|
+
private_constant :UNBOUND_METHOD_MODULE_NAME, :UNBOUND_METHOD_MODULE_TO_S
|
8
|
+
|
9
|
+
# @sig (Module) -> String
|
10
|
+
def cpath(mod)
|
11
|
+
real_mod_name(mod) || real_mod_to_s(mod)
|
12
|
+
end
|
13
|
+
|
14
|
+
# @sig (Module) -> String?
|
15
|
+
def permanent_cpath(mod)
|
16
|
+
name = real_mod_name(mod)
|
17
|
+
return name unless temporary_name?(name)
|
18
|
+
end
|
19
|
+
|
20
|
+
# @sig (Module) -> Boolean
|
21
|
+
def permanent_cpath?(mod)
|
22
|
+
!temporary_cpath?(mod)
|
23
|
+
end
|
24
|
+
|
25
|
+
# @sig (Module) -> Boolean
|
26
|
+
def temporary_cpath?(mod)
|
27
|
+
temporary_name?(real_mod_name(mod))
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# @sig (Module) -> String
|
33
|
+
def real_mod_to_s(mod)
|
34
|
+
UNBOUND_METHOD_MODULE_TO_S.bind_call(mod)
|
35
|
+
end
|
36
|
+
|
37
|
+
# @sig (Module) -> String?
|
38
|
+
def real_mod_name(mod)
|
39
|
+
UNBOUND_METHOD_MODULE_NAME.bind_call(mod)
|
40
|
+
end
|
41
|
+
|
42
|
+
# @sig (String) -> Boolean
|
43
|
+
def temporary_name?(name)
|
44
|
+
# There should be a nicer way to get this in Ruby.
|
45
|
+
name.nil? || name.start_with?("#")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/im/error.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Im
|
4
|
+
class Error < StandardError
|
5
|
+
end
|
6
|
+
|
7
|
+
class ReloadingDisabledError < Error
|
8
|
+
def initialize
|
9
|
+
super("can't reload, please call loader.enable_reloading before setup")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class NameError < ::NameError
|
14
|
+
end
|
15
|
+
|
16
|
+
class SetupRequired < Error
|
17
|
+
def initialize
|
18
|
+
super("please, finish your configuration and call Im::Loader#setup once all is ready")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class InvalidModuleName < Error
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Im
|
4
|
+
# Centralizes the logic for the trace point used to detect the creation of
|
5
|
+
# explicit namespaces, needed to descend into matching subdirectories right
|
6
|
+
# after the constant has been defined.
|
7
|
+
#
|
8
|
+
# The implementation assumes an explicit namespace is managed by one loader.
|
9
|
+
# Loaders that reopen namespaces owned by other projects are responsible for
|
10
|
+
# loading their constant before setup. This is documented.
|
11
|
+
module ExplicitNamespace # :nodoc: all
|
12
|
+
class << self
|
13
|
+
extend Internal
|
14
|
+
|
15
|
+
# Maps constant paths that correspond to explicit namespaces according to
|
16
|
+
# the file system, to the loader responsible for them.
|
17
|
+
#
|
18
|
+
# @sig Hash[String, [String, Im::Loader]]
|
19
|
+
attr_reader :cpaths
|
20
|
+
private :cpaths
|
21
|
+
|
22
|
+
# @sig Mutex
|
23
|
+
attr_reader :mutex
|
24
|
+
private :mutex
|
25
|
+
|
26
|
+
# @sig TracePoint
|
27
|
+
attr_reader :tracer
|
28
|
+
private :tracer
|
29
|
+
|
30
|
+
# Asserts `cpath` corresponds to an explicit namespace for which `loader`
|
31
|
+
# is responsible.
|
32
|
+
#
|
33
|
+
# @sig (String, Im::Loader) -> void
|
34
|
+
internal def register(cpath, module_name, loader)
|
35
|
+
mutex.synchronize do
|
36
|
+
cpaths[cpath] = [module_name, loader]
|
37
|
+
# We check enabled? because, looking at the C source code, enabling an
|
38
|
+
# enabled tracer does not seem to be a simple no-op.
|
39
|
+
tracer.enable unless tracer.enabled?
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# @sig (Im::Loader) -> void
|
44
|
+
internal def unregister_loader(loader)
|
45
|
+
cpaths.delete_if { |_cpath, (_, l)| l == loader }
|
46
|
+
disable_tracer_if_unneeded
|
47
|
+
end
|
48
|
+
|
49
|
+
# @sig (String, String) -> void
|
50
|
+
internal def update_cpaths(prefix, replacement)
|
51
|
+
pattern = /^#{prefix}/
|
52
|
+
mutex.synchronize do
|
53
|
+
cpaths.transform_keys! do |key|
|
54
|
+
key.start_with?(prefix) ? key.gsub(pattern, replacement) : key
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# @sig () -> void
|
60
|
+
private def disable_tracer_if_unneeded
|
61
|
+
mutex.synchronize do
|
62
|
+
tracer.disable if cpaths.empty?
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# @sig (TracePoint) -> void
|
67
|
+
private def tracepoint_class_callback(event)
|
68
|
+
# If the class is a singleton class, we won't do anything with it so we
|
69
|
+
# can bail out immediately. This is several orders of magnitude faster
|
70
|
+
# than accessing its name.
|
71
|
+
return if event.self.singleton_class?
|
72
|
+
|
73
|
+
# It might be tempting to return if name.nil?, to avoid the computation
|
74
|
+
# of a hash code and delete call. But Ruby does not trigger the :class
|
75
|
+
# event on Class.new or Module.new, so that would incur in an extra call
|
76
|
+
# for nothing.
|
77
|
+
#
|
78
|
+
# On the other hand, if we were called, cpaths is not empty. Otherwise
|
79
|
+
# the tracer is disabled. So we do need to go ahead with the hash code
|
80
|
+
# computation and delete call.
|
81
|
+
relative_cpath, loader = cpaths.delete(Im.cpath(event.self))
|
82
|
+
if loader
|
83
|
+
loader.on_namespace_loaded(relative_cpath)
|
84
|
+
disable_tracer_if_unneeded
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
@cpaths = {}
|
90
|
+
@mutex = Mutex.new
|
91
|
+
|
92
|
+
# We go through a method instead of defining a block mainly to have a better
|
93
|
+
# label when profiling.
|
94
|
+
@tracer = TracePoint.new(:class, &method(:tracepoint_class_callback))
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Im
|
4
|
+
class GemInflector < Inflector
|
5
|
+
# @sig (String) -> void
|
6
|
+
def initialize(root_file)
|
7
|
+
namespace = File.basename(root_file, ".rb")
|
8
|
+
lib_dir = File.dirname(root_file)
|
9
|
+
@version_file = File.join(lib_dir, namespace, "version.rb")
|
10
|
+
end
|
11
|
+
|
12
|
+
# @sig (String, String) -> String
|
13
|
+
def camelize(basename, abspath)
|
14
|
+
abspath == @version_file ? "VERSION" : super
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Im
|
4
|
+
# @private
|
5
|
+
class GemLoader < Loader
|
6
|
+
# Users should not create instances directly, the public interface is
|
7
|
+
# `Im::Loader.for_gem`.
|
8
|
+
private_class_method :new
|
9
|
+
|
10
|
+
# @private
|
11
|
+
# @sig (String, bool) -> Im::GemLoader
|
12
|
+
def self._new(root_file, warn_on_extra_files:)
|
13
|
+
new(root_file, warn_on_extra_files: warn_on_extra_files)
|
14
|
+
end
|
15
|
+
|
16
|
+
# @sig (String, bool) -> void
|
17
|
+
def initialize(root_file, warn_on_extra_files:)
|
18
|
+
super()
|
19
|
+
|
20
|
+
@tag = File.basename(root_file, ".rb")
|
21
|
+
@inflector = GemInflector.new(root_file)
|
22
|
+
@root_file = File.expand_path(root_file)
|
23
|
+
@lib = File.dirname(root_file)
|
24
|
+
@warn_on_extra_files = warn_on_extra_files
|
25
|
+
|
26
|
+
push_dir(@lib)
|
27
|
+
end
|
28
|
+
|
29
|
+
# @sig () -> void
|
30
|
+
def setup
|
31
|
+
warn_on_extra_files if @warn_on_extra_files
|
32
|
+
super
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# @sig () -> void
|
38
|
+
def warn_on_extra_files
|
39
|
+
expected_namespace_dir = @root_file.delete_suffix(".rb")
|
40
|
+
|
41
|
+
ls(@lib) do |basename, abspath|
|
42
|
+
next if abspath == @root_file
|
43
|
+
next if abspath == expected_namespace_dir
|
44
|
+
|
45
|
+
basename_without_ext = basename.delete_suffix(".rb")
|
46
|
+
cname = inflector.camelize(basename_without_ext, abspath).to_sym
|
47
|
+
ftype = dir?(abspath) ? "directory" : "file"
|
48
|
+
|
49
|
+
warn(<<~EOS)
|
50
|
+
WARNING: Im defines the constant #{cname} after the #{ftype}
|
51
|
+
|
52
|
+
#{abspath}
|
53
|
+
|
54
|
+
To prevent that, please configure the loader to ignore it:
|
55
|
+
|
56
|
+
loader.ignore("\#{__dir__}/#{basename}")
|
57
|
+
|
58
|
+
Otherwise, there is a flag to silence this warning:
|
59
|
+
|
60
|
+
Im::Loader.for_gem(warn_on_extra_files: false)
|
61
|
+
EOS
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/im/inflector.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Im
|
4
|
+
class Inflector
|
5
|
+
# Very basic snake case -> camel case conversion.
|
6
|
+
#
|
7
|
+
# inflector = Im::Inflector.new
|
8
|
+
# inflector.camelize("post", ...) # => "Post"
|
9
|
+
# inflector.camelize("users_controller", ...) # => "UsersController"
|
10
|
+
# inflector.camelize("api", ...) # => "Api"
|
11
|
+
#
|
12
|
+
# Takes into account hard-coded mappings configured with `inflect`.
|
13
|
+
#
|
14
|
+
# @sig (String, String) -> String
|
15
|
+
def camelize(basename, _abspath)
|
16
|
+
overrides[basename] || basename.split('_').each(&:capitalize!).join
|
17
|
+
end
|
18
|
+
|
19
|
+
# Configures hard-coded inflections:
|
20
|
+
#
|
21
|
+
# inflector = Im::Inflector.new
|
22
|
+
# inflector.inflect(
|
23
|
+
# "html_parser" => "HTMLParser",
|
24
|
+
# "mysql_adapter" => "MySQLAdapter"
|
25
|
+
# )
|
26
|
+
#
|
27
|
+
# inflector.camelize("html_parser", abspath) # => "HTMLParser"
|
28
|
+
# inflector.camelize("mysql_adapter", abspath) # => "MySQLAdapter"
|
29
|
+
# inflector.camelize("users_controller", abspath) # => "UsersController"
|
30
|
+
#
|
31
|
+
# @sig (Hash[String, String]) -> void
|
32
|
+
def inflect(inflections)
|
33
|
+
overrides.merge!(inflections)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
# Hard-coded basename to constant name user maps that override the default
|
39
|
+
# inflection logic.
|
40
|
+
#
|
41
|
+
# @sig () -> Hash[String, String]
|
42
|
+
def overrides
|
43
|
+
@overrides ||= {}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/im/internal.rb
ADDED