im 0.1.6 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 -185
- metadata +25 -61
- data/CHANGELOG.md +0 -31
- data/Gemfile +0 -8
- data/Gemfile.lock +0 -37
- 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
|
+
[![Build Status](https://github.com/shioyama/im/actions/workflows/ci.yml/badge.svg)][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