im 0.1.6 → 0.2.1
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 +242 -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: e1708fffea9fb79c809dbfa34669e5471556e4d969ae8d95e689cfccc5c6f3e4
|
4
|
+
data.tar.gz: bce10f72bc879a53bda624ced7534bd0c74fc5666c6ff8545ea9371521edc8b0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a112f1ee72916d690c0227a94a16f87d283b93a059f1e01d8681649ac9405ce46add46650234b2e337ace8b8571c64ba4f722f256099263eea04cfaafb3f02e1
|
7
|
+
data.tar.gz: b7a03df97bcbac97f09c8637f9c8b82fbaaddcd58640aecc2406e821910fe5cb2ed4200ddc2d6490bc0d0d2d42e6a65173bb7a52ba78c7ed2c05126a0edc669b
|
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,274 @@
|
|
1
1
|
# Im
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
requires a patched version of Ruby which you will need to build in order to use
|
6
|
-
the gem.
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/im.svg)][gem]
|
4
|
+
[![Build Status](https://github.com/shioyama/im/actions/workflows/ci.yml/badge.svg)][actions]
|
7
5
|
|
8
|
-
|
6
|
+
[gem]: https://rubygems.org/gems/im
|
7
|
+
[actions]: https://github.com/shioyama/im/actions
|
9
8
|
|
10
|
-
|
9
|
+
<!-- TOC -->
|
11
10
|
|
12
|
-
|
11
|
+
- [Introduction](#introduction)
|
12
|
+
- [Synopsis](#synopsis)
|
13
|
+
- [File structure](#file-structure)
|
14
|
+
- [File paths match constant paths under loader](#file-paths-match-constant-paths-under-loader)
|
15
|
+
- [Root directories](#root-directories)
|
16
|
+
- [Relative and absolute cpaths](#relative-and-absolute-cpaths)
|
17
|
+
- [Usage](#usage)
|
18
|
+
- [Motivation](#motivation)
|
19
|
+
- [License](#license)
|
13
20
|
|
14
|
-
|
21
|
+
<!-- /TOC -->
|
15
22
|
|
16
|
-
|
23
|
+
<a id="markdown-introduction" name="introduction"></a>
|
24
|
+
## Introduction
|
17
25
|
|
18
|
-
|
26
|
+
Im is a thread-safe code loader for anonymous-rooted namespaces in Ruby. It
|
27
|
+
allows you to share any nested, autoloaded set of code without polluting or in
|
28
|
+
any way touching the global namespace.
|
29
|
+
|
30
|
+
To do this, Im leverages code autoloading, Zeitwerk conventions around file
|
31
|
+
structure and naming, and two features added in Ruby 3.2: `Kernel#load`
|
32
|
+
with a module argument[^1] and `Module#const_added`[^2]. Since these Ruby
|
33
|
+
features are essential to its design, Im is not usable with earlier versions
|
34
|
+
of Ruby.
|
35
|
+
|
36
|
+
Im started its life as a fork of Zeitwerk and has a very similar interface. Im
|
37
|
+
and Zeitwerk can be used alongside each other provided there is no overlap
|
38
|
+
between file paths managed by each gem.
|
39
|
+
|
40
|
+
Im is in active development and should be considered experimental until the
|
41
|
+
eventual release of version 1.0. Versions 0.1.6 and earlier of the gem were
|
42
|
+
part of a different experiment and are unrelated to the current gem.
|
43
|
+
|
44
|
+
<a id="markdown-synopsis" name="synopsis"></a>
|
45
|
+
## Synopsis
|
46
|
+
|
47
|
+
Im's public interface is in most respects identical to that of Zeitwerk. The
|
48
|
+
central difference is that whereas Zeitwerk loads constants into the global
|
49
|
+
namespace (rooted in `Object`), Im loads them into anonymous namespaces rooted
|
50
|
+
on the loader itself. `Im::Loader` is a subclass of `Module`, and thus each
|
51
|
+
loader instance can define its own namespace. Since there can be arbitrarily
|
52
|
+
many loaders, there can also be arbitrarily many autoloaded namespaces.
|
53
|
+
|
54
|
+
Im's gem interface looks like this:
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
# lib/my_gem.rb (main file)
|
58
|
+
|
59
|
+
require "im"
|
60
|
+
loader = Im::Loader.for_gem
|
61
|
+
loader.setup # ready!
|
62
|
+
|
63
|
+
module loader::MyGem
|
64
|
+
# ...
|
65
|
+
end
|
66
|
+
|
67
|
+
loader.eager_load # optionally
|
68
|
+
```
|
69
|
+
|
70
|
+
The generic interface is identical to Zeitwerk's:
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
loader = Zeitwerk::Loader.new
|
74
|
+
loader.push_dir(...)
|
75
|
+
loader.setup # ready!
|
76
|
+
```
|
77
|
+
|
78
|
+
Other than gem names, the only difference here is in the definition of `MyGem`
|
79
|
+
under the loader namespace in the gem code. Unlike Zeitwerk, with Im the gem
|
80
|
+
namespace is not defined at toplevel:
|
19
81
|
|
20
|
-
|
82
|
+
```ruby
|
83
|
+
Object.const_defined?(:MyGem)
|
84
|
+
#=> false
|
85
|
+
```
|
86
|
+
|
87
|
+
In order to prevent leakage, the gem's entrypoint, in this case
|
88
|
+
`lib/my_gem.rb`, must not define anything at toplevel, hence the use of
|
89
|
+
`module loader::MyGem`.
|
90
|
+
|
91
|
+
Once the entrypoint has been required, all constants defined within the gem's
|
92
|
+
file structure are autoloadable from the loader itself:
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
# lib/my_gem/foo.rb
|
96
|
+
|
97
|
+
module MyGem
|
98
|
+
class Foo
|
99
|
+
def hello_world
|
100
|
+
"Hello World!"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
```
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
foo = loader::MyGem::Foo
|
108
|
+
# loads `Foo` from lib/my_gem/foo.rb
|
109
|
+
|
110
|
+
foo.new.hello_world
|
111
|
+
#=> "Hello World!"
|
112
|
+
```
|
113
|
+
|
114
|
+
Constants under the loader can be given permanent names that are different from
|
115
|
+
the one defined in the gem itself:
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
Bar = loader::MyGem::Foo
|
119
|
+
Bar.new.hello_world
|
120
|
+
#=> "Hello World!"
|
121
|
+
```
|
122
|
+
|
123
|
+
Like Zeitwerk, Im keeps a registry of all loaders, so the loader objects won't
|
124
|
+
be garbage collected. For convenience, Im also provides a method, `Im#import`,
|
125
|
+
to fetch a loader for a given file path:
|
21
126
|
|
22
127
|
```ruby
|
23
128
|
require "im"
|
129
|
+
require "my_gem"
|
24
130
|
|
25
131
|
extend Im
|
132
|
+
my_gem = import "my_gem"
|
133
|
+
#=> my_gem::MyGem is autoloadable
|
134
|
+
```
|
26
135
|
|
27
|
-
|
28
|
-
|
136
|
+
Reloading works like Zeitwerk:
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
loader = Im::Loader.new
|
140
|
+
loader.push_dir(...)
|
141
|
+
loader.enable_reloading # you need to opt-in before setup
|
142
|
+
loader.setup
|
143
|
+
...
|
144
|
+
loader.reload
|
29
145
|
```
|
30
146
|
|
31
|
-
|
147
|
+
You can assign a permanent name to an autoloaded constant, and it will be
|
148
|
+
reloaded when the loader is reloaded:
|
32
149
|
|
33
150
|
```ruby
|
34
|
-
|
35
|
-
|
151
|
+
Foo = loader::Foo
|
152
|
+
loader.reload # Object::Foo is replaced by an autoload
|
153
|
+
Foo #=> autoload is triggered, reloading loader::Foo
|
154
|
+
```
|
36
155
|
|
37
|
-
|
38
|
-
|
156
|
+
Like Zeitwerk, you can eager-load all the code at once:
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
loader.eager_load
|
39
160
|
```
|
40
161
|
|
41
|
-
|
162
|
+
Alternatively, you can broadcast `eager_load` to all loader instances:
|
42
163
|
|
43
164
|
```ruby
|
44
|
-
|
165
|
+
Im::Loader.eager_load_all
|
166
|
+
```
|
45
167
|
|
46
|
-
|
47
|
-
|
168
|
+
<a id="markdown-file-structure" name="file-structure"></a>
|
169
|
+
## File structure
|
48
170
|
|
49
|
-
|
50
|
-
|
51
|
-
end
|
171
|
+
<a id="markdown-the-idea-file-paths-match-constant-paths-under-loader" name="the-idea-file-paths-match-constant-paths-under-loader"></a>
|
172
|
+
### File paths match constant paths under loader
|
52
173
|
|
53
|
-
|
54
|
-
|
55
|
-
|
174
|
+
File structure is identical to Zeitwerk, again with the difference that
|
175
|
+
constants are loaded from the loader's namespace rather than the root one:
|
176
|
+
|
177
|
+
```
|
178
|
+
lib/my_gem.rb -> loader::MyGem
|
179
|
+
lib/my_gem/foo.rb -> loader::MyGem::Foo
|
180
|
+
lib/my_gem/bar_baz.rb -> loader::MyGem::BarBaz
|
181
|
+
lib/my_gem/woo/zoo.rb -> loader::MyGem::Woo::Zoo
|
182
|
+
```
|
183
|
+
|
184
|
+
Im inherits support for collapsing directories and custom inflection, see
|
185
|
+
Zeitwerk's documentation for details on usage of these features.
|
186
|
+
|
187
|
+
<a id="markdown-root-directories" name="root-directories"></a>
|
188
|
+
### Root directories
|
189
|
+
|
190
|
+
Internally, each loader in Im can have one or more _root directories_ from which
|
191
|
+
it loads code onto itself. Root directories are added to the loader using
|
192
|
+
`Im::Loader#push_dir`:
|
193
|
+
|
194
|
+
```ruby
|
195
|
+
loader.push_dir("#{__dir__}/models")
|
196
|
+
loader.push_dir("#{__dir__}/serializers"))
|
197
|
+
```
|
198
|
+
|
199
|
+
Note that concept of a _root namespace_, which Zeitwerk uses to load code
|
200
|
+
under a given node of the global namespace, is absent in Im. Custom root
|
201
|
+
namespaces are likewise not supported. These features were removed as they add
|
202
|
+
complexity for little gain given Im's flexibility to anchor a namespace
|
203
|
+
anywhere in the global namespace.
|
204
|
+
|
205
|
+
<a id="markdown-relative-and-absolute-cpaths" name="relative-and-absolute-cpaths"></a>
|
206
|
+
### Relative and absolute cpaths
|
207
|
+
|
208
|
+
Im uses two types of constant paths: relative and absolute, wherever possible
|
209
|
+
defaulting to relative ones. A _relative cpath_ is a constant name relative to
|
210
|
+
the loader in which it was originally defined, regardless of any other names it
|
211
|
+
was later assigned. Whereas Zeitwerk uses absolute cpaths, Im uses relative
|
212
|
+
cpaths for all external loader APIs (see usage for examples).
|
56
213
|
|
57
|
-
|
58
|
-
|
59
|
-
|
214
|
+
To understand these concepts, it is important first to distinguish between two
|
215
|
+
types of names in Ruby: _temporary names_ and _permanent names_.
|
216
|
+
|
217
|
+
A _temporary name_ is a constant name on an anonymous-rooted namespace, for
|
218
|
+
example a loader:
|
219
|
+
|
220
|
+
```ruby
|
221
|
+
my_gem = import "my_gem"
|
222
|
+
my_gem::Foo
|
223
|
+
my_gem::Foo.name
|
224
|
+
#=> "#<Im::Loader ...>::Foo"
|
225
|
+
```
|
226
|
+
|
227
|
+
Here, the string `"#<Im::Loader ...>::Foo"` is called a temporary name. We can
|
228
|
+
give this module a _permanent name_ by assigning it to a toplevel constant:
|
229
|
+
|
230
|
+
```ruby
|
231
|
+
Bar = my_gem::Foo
|
232
|
+
my_gem::Foo.name
|
233
|
+
#=> "Bar"
|
234
|
+
```
|
235
|
+
|
236
|
+
Now its name is `"Bar"`, and it is near impossible to get back its original
|
237
|
+
temporary name.
|
238
|
+
|
239
|
+
This property of module naming in Ruby is problematic since cpaths are used as
|
240
|
+
keys in Im's internal registries to index constants and their autoloads, which
|
241
|
+
is critical for successful autoloading.
|
242
|
+
|
243
|
+
To get around this issue, Im tracks all module names and uses relative naming
|
244
|
+
inside loader code. Internally, Im has a method, `relative_cpath`, which can
|
245
|
+
generate any module name under a module in the loader namespace:
|
246
|
+
|
247
|
+
```ruby
|
248
|
+
my_gem.send(:relative_cpath, loader::Foo, :Baz)
|
249
|
+
#=> "Foo::Baz"
|
60
250
|
```
|
61
251
|
|
252
|
+
Using relative cpaths frees Im from depending on `Module#name` for
|
253
|
+
registry keys like Zeitwerk does, which does not work with anonymous
|
254
|
+
namespaces. All public methods in Im that take a `cpath` take the _relative_
|
255
|
+
cpath, i.e. the cpath relative to the loader as toplevel, regardless of any
|
256
|
+
toplevel-rooted constant a module may have been assigned to.
|
257
|
+
|
258
|
+
<a id="markdown-usage" name="usage"></a>
|
259
|
+
## Usage
|
260
|
+
|
261
|
+
(TODO)
|
262
|
+
|
263
|
+
<a id="markdown-motivation" name="motivation"></a>
|
264
|
+
## Motivation
|
265
|
+
|
266
|
+
(TODO)
|
267
|
+
|
268
|
+
<a id="markdown-license" name="license"></a>
|
62
269
|
## License
|
63
270
|
|
64
|
-
|
271
|
+
Released under the MIT License, Copyright (c) 2023 Chris Salzberg and 2019–<i>ω</i> Xavier Noria.
|
272
|
+
|
273
|
+
[^1]: https://bugs.ruby-lang.org/issues/6210
|
274
|
+
[^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