rbs_macros 0.1.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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +49 -0
- data/.rubocop_new.yml +269 -0
- data/CHANGELOG.md +6 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +123 -0
- data/Rakefile +41 -0
- data/Steepfile +22 -0
- data/lib/rbs_macros/config.rb +19 -0
- data/lib/rbs_macros/environment.rb +50 -0
- data/lib/rbs_macros/exec_ctx.rb +66 -0
- data/lib/rbs_macros/library_registry.rb +52 -0
- data/lib/rbs_macros/macro.rb +12 -0
- data/lib/rbs_macros/macros/forwardable.rb +197 -0
- data/lib/rbs_macros/meta_module.rb +65 -0
- data/lib/rbs_macros/project.rb +110 -0
- data/lib/rbs_macros/version.rb +5 -0
- data/lib/rbs_macros.rb +137 -0
- data/rbs_collection.lock.yaml +60 -0
- data/rbs_collection.yaml +24 -0
- data/sig/generated/rbs_macros/library_registry.rbs +5 -0
- data/sig/rbs_macros/config.rbs +13 -0
- data/sig/rbs_macros/environment.rbs +42 -0
- data/sig/rbs_macros/exec_ctx.rbs +14 -0
- data/sig/rbs_macros/library_registry.rbs +16 -0
- data/sig/rbs_macros/macro.rbs +5 -0
- data/sig/rbs_macros/macros/forwardable.rbs +29 -0
- data/sig/rbs_macros/meta_module.rbs +29 -0
- data/sig/rbs_macros/project.rbs +22 -0
- data/sig/rbs_macros/version.rbs +3 -0
- data/sig/rbs_macros.rbs +7 -0
- data/sig-private/test/environment_test.rbs +3 -0
- data/sig-private/test/library_registry_test.rbs +11 -0
- data/sig-private/test/macros/forwardable_test.rbs +7 -0
- data/sig-private/test/rbs_macros_test.rbs +13 -0
- metadata +147 -0
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "prism"
|
4
|
+
|
5
|
+
module RbsMacros
|
6
|
+
# An environment for the Ruby program being analyzed.
|
7
|
+
class Environment
|
8
|
+
attr_reader :rbs, :object_class, :decls
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@rbs = RBS::Environment.new
|
12
|
+
@object_class = MetaClass.new(self, "Object", is_class: true)
|
13
|
+
@decls = []
|
14
|
+
@exact_handlers = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def register_handler(name, handler)
|
18
|
+
(@exact_handlers[name.to_sym] ||= []) << handler
|
19
|
+
end
|
20
|
+
|
21
|
+
def invoke(params)
|
22
|
+
handlers = @exact_handlers[params.name]
|
23
|
+
handlers&.each do |handler|
|
24
|
+
handler.(params)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def meta_eval_ruby(code, filename:)
|
29
|
+
result = Prism.parse(code, filepath: "#{filename}.rb")
|
30
|
+
raise ArgumentError, "Parse error: #{result.errors}" if result.failure?
|
31
|
+
|
32
|
+
ExecCtx.new(
|
33
|
+
env: self,
|
34
|
+
filename:,
|
35
|
+
self: nil, # TODO
|
36
|
+
cref: @object_class,
|
37
|
+
cref_dynamic: @object_class,
|
38
|
+
locals: {}
|
39
|
+
).eval_node(result.value)
|
40
|
+
end
|
41
|
+
|
42
|
+
def add_decl(decl, mod:, file:)
|
43
|
+
@decls << DeclarationEntry.new(declaration: decl, mod:, file:)
|
44
|
+
end
|
45
|
+
|
46
|
+
HandlerParams = _ = Data.define(:env, :filename, :receiver, :name, :positional, :keyword, :block) # rubocop:disable Naming/ConstantName
|
47
|
+
|
48
|
+
DeclarationEntry = _ = Data.define(:declaration, :mod, :file) # rubocop:disable Naming/ConstantName
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RbsMacros
|
4
|
+
ExecCtx = _ = Data.define(:env, :filename, :self, :cref, :cref_dynamic, :locals) # rubocop:disable Naming/ConstantName
|
5
|
+
|
6
|
+
# Context, including self, class, and local variables
|
7
|
+
class ExecCtx
|
8
|
+
def eval_node(node)
|
9
|
+
case node
|
10
|
+
when nil
|
11
|
+
# do nothing
|
12
|
+
when Prism::CallNode
|
13
|
+
recv = \
|
14
|
+
if node.receiver
|
15
|
+
eval_node(node.receiver)
|
16
|
+
else
|
17
|
+
self.self
|
18
|
+
end
|
19
|
+
positional = [] # : Array[Object]
|
20
|
+
keyword = {} # : Hash[Object, Object]
|
21
|
+
node.arguments&.arguments&.each do |arg|
|
22
|
+
positional << eval_node(arg)
|
23
|
+
end
|
24
|
+
env.invoke(
|
25
|
+
Environment::HandlerParams.new(
|
26
|
+
env:,
|
27
|
+
filename:,
|
28
|
+
receiver: recv,
|
29
|
+
name: node.name,
|
30
|
+
positional:,
|
31
|
+
keyword:,
|
32
|
+
block: nil
|
33
|
+
)
|
34
|
+
)
|
35
|
+
when Prism::ClassNode
|
36
|
+
klass = cref.define_module(node.name)
|
37
|
+
klass.class!
|
38
|
+
with(
|
39
|
+
self: klass,
|
40
|
+
cref: klass,
|
41
|
+
cref_dynamic: klass,
|
42
|
+
locals: {}
|
43
|
+
).eval_node(node.body)
|
44
|
+
when Prism::ModuleNode
|
45
|
+
mod = cref.define_module(node.name)
|
46
|
+
mod.module!
|
47
|
+
with(
|
48
|
+
self: mod,
|
49
|
+
cref: mod,
|
50
|
+
cref_dynamic: mod,
|
51
|
+
locals: {}
|
52
|
+
).eval_node(node.body)
|
53
|
+
when Prism::ProgramNode
|
54
|
+
eval_node(node.statements)
|
55
|
+
when Prism::StatementsNode
|
56
|
+
node.body.each { |stmt| eval_node(stmt) }
|
57
|
+
when Prism::StringNode
|
58
|
+
node.unescaped.dup.freeze
|
59
|
+
when Prism::SymbolNode
|
60
|
+
node.unescaped.to_sym
|
61
|
+
else
|
62
|
+
$stderr.puts "Dismissing node: #{node.inspect}" # rubocop:disable Style/StderrPuts
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
|
5
|
+
module RbsMacros
|
6
|
+
# RbsMacros allow you to define a reusable set of macro definitions
|
7
|
+
# called a library.
|
8
|
+
# This is usually registered to the global singleton of LibraryRegistry
|
9
|
+
# like:
|
10
|
+
#
|
11
|
+
# # my_library/rbs_macros.rb
|
12
|
+
# RbsMacros::LibraryRegistry.register_macros("my_library/rbs_macros") do |macros|
|
13
|
+
# macros << MyMacro1
|
14
|
+
# macros << MyMacro2
|
15
|
+
# end
|
16
|
+
class LibraryRegistry
|
17
|
+
extend SingleForwardable
|
18
|
+
def_single_delegators :@global, :register_macros
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@libraries = {}
|
22
|
+
end
|
23
|
+
|
24
|
+
def register_macros(name, macros = [], &block)
|
25
|
+
a = @libraries.fetch(name) { |k| @libraries[k] = [] }
|
26
|
+
a.push(*macros)
|
27
|
+
block&.(a)
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
|
31
|
+
def lookup_macros(name, soft_fail: false)
|
32
|
+
unless @libraries.key?(name)
|
33
|
+
require_library(name, soft_fail:)
|
34
|
+
raise ArgumentError, "Unknown library: #{name}" if !@libraries.key?(name) && !soft_fail
|
35
|
+
end
|
36
|
+
|
37
|
+
@libraries[name] || []
|
38
|
+
end
|
39
|
+
|
40
|
+
def require_library(name, soft_fail: false)
|
41
|
+
# To be implemented by subclasses
|
42
|
+
raise ArgumentError, "Unknown library: #{name}" unless soft_fail
|
43
|
+
end
|
44
|
+
|
45
|
+
@global = LibraryRegistry.new
|
46
|
+
def @global.require_library(name, soft_fail: false)
|
47
|
+
require name
|
48
|
+
rescue LoadError
|
49
|
+
raise unless soft_fail
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RbsMacros
|
4
|
+
# Base class for macro implementations.
|
5
|
+
# Macros react to method invocations in Ruby code (usually in module/class bodies)
|
6
|
+
# and generate RBS declarations for them.
|
7
|
+
class Macro
|
8
|
+
def setup(env) # rubocop:disable Lint/UnusedMethodArgument
|
9
|
+
raise NoMethodError, "Not implemented: #{self.class}#setup"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rbs_macros"
|
4
|
+
|
5
|
+
module RbsMacros
|
6
|
+
module Macros
|
7
|
+
# TODO: resolve duplication between ForwardableMacros and SingleForwardableMacros
|
8
|
+
# TODO: fallback to untyped even when something is wrong
|
9
|
+
|
10
|
+
# Implements macros for the `Forwardable` module.
|
11
|
+
class ForwardableMacros < Macro
|
12
|
+
def setup(env)
|
13
|
+
env.register_handler(:def_delegator, method(:meta_def_delegator))
|
14
|
+
env.register_handler(:def_instance_delegator, method(:meta_def_delegator))
|
15
|
+
|
16
|
+
env.register_handler(:def_delegators, method(:meta_def_delegators))
|
17
|
+
env.register_handler(:def_instance_delegators, method(:meta_def_delegators))
|
18
|
+
end
|
19
|
+
|
20
|
+
def meta_def_delegator(params)
|
21
|
+
recv = params.receiver
|
22
|
+
return unless recv.is_a?(MetaModule)
|
23
|
+
|
24
|
+
accessor = params.positional[0]
|
25
|
+
method = params.positional[1]
|
26
|
+
ali = params.positional[2] || method
|
27
|
+
return unless accessor.is_a?(Symbol) || accessor.is_a?(String)
|
28
|
+
return unless method.is_a?(Symbol) || method.is_a?(String)
|
29
|
+
return unless ali.is_a?(Symbol) || ali.is_a?(String)
|
30
|
+
|
31
|
+
builder = RBS::DefinitionBuilder.new(env: params.env.rbs)
|
32
|
+
self_instance = builder.build_instance(recv.rbs_type)
|
33
|
+
accessor_type =
|
34
|
+
case accessor.to_s
|
35
|
+
when /\A@/
|
36
|
+
ivar = self_instance.instance_variables[accessor.to_sym]
|
37
|
+
return unless ivar
|
38
|
+
|
39
|
+
ivar.type
|
40
|
+
else
|
41
|
+
# TODO
|
42
|
+
return
|
43
|
+
end
|
44
|
+
accessor_instance = widened_instance(accessor_type, builder:)
|
45
|
+
return unless accessor_instance
|
46
|
+
|
47
|
+
meth = accessor_instance[0].methods[method.to_sym]&.sub(accessor_instance[1])
|
48
|
+
return unless meth
|
49
|
+
|
50
|
+
params.env.add_decl(
|
51
|
+
RBS::AST::Members::MethodDefinition.new(
|
52
|
+
name: ali.to_sym,
|
53
|
+
kind: :instance,
|
54
|
+
overloads:
|
55
|
+
meth.defs.map do |typedef|
|
56
|
+
RBS::AST::Members::MethodDefinition::Overload.new(
|
57
|
+
method_type: typedef.type,
|
58
|
+
annotations: []
|
59
|
+
)
|
60
|
+
end,
|
61
|
+
annotations: [],
|
62
|
+
location: nil,
|
63
|
+
comment: nil,
|
64
|
+
overloading: false,
|
65
|
+
visibility: nil
|
66
|
+
),
|
67
|
+
mod: recv,
|
68
|
+
file: params.filename
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
72
|
+
def meta_def_delegators(params)
|
73
|
+
recv = params.receiver
|
74
|
+
return unless recv.is_a?(MetaModule)
|
75
|
+
|
76
|
+
accessor = params.positional[0]
|
77
|
+
methods = params.positional[1..] || []
|
78
|
+
methods.each do |method|
|
79
|
+
params.env.invoke(
|
80
|
+
params.with(
|
81
|
+
name: :def_instance_delegator,
|
82
|
+
positional: [accessor, method],
|
83
|
+
keyword: {},
|
84
|
+
block: nil
|
85
|
+
)
|
86
|
+
)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def widened_instance(type, builder:)
|
93
|
+
case type
|
94
|
+
when RBS::Types::ClassInstance
|
95
|
+
# Using public_send because tapp_subst is declared as private although defined as public.
|
96
|
+
[builder.build_instance(type.name), builder.public_send(:tapp_subst, type.name, type.args)]
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Implements macros for the `SingleForwardable` module.
|
102
|
+
class SingleForwardableMacros < Macro
|
103
|
+
def setup(env)
|
104
|
+
env.register_handler(:def_delegator, method(:meta_def_delegator))
|
105
|
+
env.register_handler(:def_single_delegator, method(:meta_def_delegator))
|
106
|
+
|
107
|
+
env.register_handler(:def_delegators, method(:meta_def_delegators))
|
108
|
+
env.register_handler(:def_single_delegators, method(:meta_def_delegators))
|
109
|
+
end
|
110
|
+
|
111
|
+
def meta_def_delegator(params)
|
112
|
+
recv = params.receiver
|
113
|
+
return unless recv.is_a?(MetaModule)
|
114
|
+
|
115
|
+
accessor = params.positional[0]
|
116
|
+
method = params.positional[1]
|
117
|
+
ali = params.positional[2] || method
|
118
|
+
return unless accessor.is_a?(Symbol) || accessor.is_a?(String)
|
119
|
+
return unless method.is_a?(Symbol) || method.is_a?(String)
|
120
|
+
return unless ali.is_a?(Symbol) || ali.is_a?(String)
|
121
|
+
|
122
|
+
builder = RBS::DefinitionBuilder.new(env: params.env.rbs)
|
123
|
+
singleton = builder.build_singleton(recv.rbs_type)
|
124
|
+
accessor_type =
|
125
|
+
case accessor.to_s
|
126
|
+
when /\A@/
|
127
|
+
ivar = singleton.instance_variables[accessor.to_sym]
|
128
|
+
return unless ivar
|
129
|
+
|
130
|
+
ivar.type
|
131
|
+
else
|
132
|
+
# TODO
|
133
|
+
return
|
134
|
+
end
|
135
|
+
accessor_instance = widened_instance(accessor_type, builder:)
|
136
|
+
return unless accessor_instance
|
137
|
+
|
138
|
+
meth = accessor_instance[0].methods[method.to_sym]&.sub(accessor_instance[1])
|
139
|
+
return unless meth
|
140
|
+
|
141
|
+
params.env.add_decl(
|
142
|
+
RBS::AST::Members::MethodDefinition.new(
|
143
|
+
name: ali.to_sym,
|
144
|
+
kind: :singleton,
|
145
|
+
overloads:
|
146
|
+
meth.defs.map do |typedef|
|
147
|
+
RBS::AST::Members::MethodDefinition::Overload.new(
|
148
|
+
method_type: typedef.type,
|
149
|
+
annotations: []
|
150
|
+
)
|
151
|
+
end,
|
152
|
+
annotations: [],
|
153
|
+
location: nil,
|
154
|
+
comment: nil,
|
155
|
+
overloading: false,
|
156
|
+
visibility: nil
|
157
|
+
),
|
158
|
+
mod: recv,
|
159
|
+
file: params.filename
|
160
|
+
)
|
161
|
+
end
|
162
|
+
|
163
|
+
def meta_def_delegators(params)
|
164
|
+
recv = params.receiver
|
165
|
+
return unless recv.is_a?(MetaModule)
|
166
|
+
|
167
|
+
accessor = params.positional[0]
|
168
|
+
methods = params.positional[1..] || []
|
169
|
+
methods.each do |method|
|
170
|
+
params.env.invoke(
|
171
|
+
params.with(
|
172
|
+
name: :def_single_delegator,
|
173
|
+
positional: [accessor, method],
|
174
|
+
keyword: {},
|
175
|
+
block: nil
|
176
|
+
)
|
177
|
+
)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
private
|
182
|
+
|
183
|
+
def widened_instance(type, builder:)
|
184
|
+
case type
|
185
|
+
when RBS::Types::ClassInstance
|
186
|
+
# Using public_send because tapp_subst is declared as private although defined as public.
|
187
|
+
[builder.build_instance(type.name), builder.public_send(:tapp_subst, type.name, type.args)]
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
RbsMacros::LibraryRegistry.register_macros("rbs_macros/macros/forwardable") do |macros|
|
195
|
+
macros << RbsMacros::Macros::ForwardableMacros
|
196
|
+
macros << RbsMacros::Macros::SingleForwardableMacros
|
197
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RbsMacros
|
4
|
+
# Designates a module in a Ruby program being analyzed.
|
5
|
+
class MetaModule
|
6
|
+
attr_reader :env, :name, :is_class, :superclass
|
7
|
+
|
8
|
+
def initialize(env, name, is_class: nil, superclass: nil)
|
9
|
+
@env = env
|
10
|
+
@name = name
|
11
|
+
@is_class = is_class
|
12
|
+
@superclass = superclass
|
13
|
+
@constants = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def rbs_type
|
17
|
+
return @rbs_type if defined?(@rbs_type)
|
18
|
+
|
19
|
+
*ns, name = (@name || raise("Anonymous module given")).split("::")
|
20
|
+
@rbs_type = RBS::TypeName.new(
|
21
|
+
name: (name || raise("Anonymous module gien")).to_sym,
|
22
|
+
namespace: RBS::Namespace.new(
|
23
|
+
path: ns.map(&:to_sym),
|
24
|
+
absolute: true
|
25
|
+
)
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
def class!
|
30
|
+
@is_class = true if @is_class.nil?
|
31
|
+
end
|
32
|
+
|
33
|
+
def module!
|
34
|
+
@is_class = false if @is_class.nil?
|
35
|
+
end
|
36
|
+
|
37
|
+
def define_module(name)
|
38
|
+
@constants[name] ||= MetaModule.new(@env, child_module_name(name.to_s))
|
39
|
+
end
|
40
|
+
|
41
|
+
def meta_const_set(name, value)
|
42
|
+
@constants[name] = value
|
43
|
+
end
|
44
|
+
|
45
|
+
def meta_const_get(name)
|
46
|
+
@constants[name]
|
47
|
+
end
|
48
|
+
|
49
|
+
def meta_constants
|
50
|
+
@constants.keys
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def child_module_name(child_name)
|
56
|
+
if child_name && (name && name != "Object")
|
57
|
+
"#{name}::#{child_name}"
|
58
|
+
elsif child_name
|
59
|
+
child_name
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
MetaClass = MetaModule
|
65
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fileutils"
|
4
|
+
require "pathname"
|
5
|
+
|
6
|
+
module RbsMacros
|
7
|
+
# Refers to the filesystem RbsMacros operates on.
|
8
|
+
class AbstractProject
|
9
|
+
# rubocop:disable Lint/UnusedMethodArgument
|
10
|
+
def glob(
|
11
|
+
ext:,
|
12
|
+
include:,
|
13
|
+
exclude:,
|
14
|
+
&block
|
15
|
+
)
|
16
|
+
raise NoMethodError, "Not implemented: #{self.class}#each_rbs"
|
17
|
+
end
|
18
|
+
|
19
|
+
def write(path, content)
|
20
|
+
raise NoMethodError, "Not implemented: #{self.class}#write"
|
21
|
+
end
|
22
|
+
|
23
|
+
def read(path)
|
24
|
+
raise NoMethodError, "Not implemented: #{self.class}#read"
|
25
|
+
end
|
26
|
+
# rubocop:enable Lint/UnusedMethodArgument
|
27
|
+
end
|
28
|
+
|
29
|
+
# A project based on real FS.
|
30
|
+
class Project < AbstractProject
|
31
|
+
attr_accessor :base_dir
|
32
|
+
|
33
|
+
def initialize(base_dir: Pathname(Dir.pwd))
|
34
|
+
super()
|
35
|
+
@base_dir = base_dir
|
36
|
+
end
|
37
|
+
|
38
|
+
def glob(
|
39
|
+
ext:,
|
40
|
+
include:,
|
41
|
+
exclude:,
|
42
|
+
&block
|
43
|
+
)
|
44
|
+
return enum_for(:glob, ext:, include:, exclude:) unless block
|
45
|
+
|
46
|
+
loaded = Set.new # : Set[String]
|
47
|
+
include.each do |incl_dir|
|
48
|
+
Dir.glob(
|
49
|
+
"#{incl_dir}/**/*#{ext}",
|
50
|
+
base: @base_dir
|
51
|
+
).sort.each do |path|
|
52
|
+
next unless File.file?(@base_dir + path)
|
53
|
+
next if loaded.include?(path)
|
54
|
+
|
55
|
+
loaded << path
|
56
|
+
is_excluded = exclude.any? do |excl_dir|
|
57
|
+
"#{path}/".start_with?("#{excl_dir}/")
|
58
|
+
end
|
59
|
+
block.(path) unless is_excluded
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def write(path, content)
|
65
|
+
full_path = @base_dir + path
|
66
|
+
FileUtils.mkdir_p(full_path.dirname)
|
67
|
+
File.write(full_path.to_s, content)
|
68
|
+
end
|
69
|
+
|
70
|
+
def read(path)
|
71
|
+
File.read((@base_dir + path).to_s)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# An in-memory project.
|
76
|
+
class FakeProject < AbstractProject
|
77
|
+
def initialize
|
78
|
+
super
|
79
|
+
@files = {}
|
80
|
+
end
|
81
|
+
|
82
|
+
def glob(
|
83
|
+
ext:,
|
84
|
+
include:,
|
85
|
+
exclude:,
|
86
|
+
&block
|
87
|
+
)
|
88
|
+
return enum_for(:glob, ext:, include:, exclude:) unless block
|
89
|
+
|
90
|
+
@files.each_key do |path|
|
91
|
+
has_ext = path.end_with?(ext)
|
92
|
+
is_incl = include.any? do |incl_dir|
|
93
|
+
"#{path}/".start_with?("#{incl_dir}/")
|
94
|
+
end
|
95
|
+
is_excl = exclude.any? do |excl_dir|
|
96
|
+
"#{path}/".start_with?("#{excl_dir}/")
|
97
|
+
end
|
98
|
+
block.(path) if has_ext && is_incl && !is_excl
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def write(path, content)
|
103
|
+
@files[path] = content
|
104
|
+
end
|
105
|
+
|
106
|
+
def read(path)
|
107
|
+
@files[path] || raise(Errno::ENOENT, path)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
data/lib/rbs_macros.rb
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "rbs_macros/version"
|
4
|
+
require_relative "rbs_macros/config"
|
5
|
+
require_relative "rbs_macros/environment"
|
6
|
+
require_relative "rbs_macros/exec_ctx"
|
7
|
+
require_relative "rbs_macros/library_registry"
|
8
|
+
require_relative "rbs_macros/macro"
|
9
|
+
require_relative "rbs_macros/meta_module"
|
10
|
+
require_relative "rbs_macros/project"
|
11
|
+
|
12
|
+
require "stringio"
|
13
|
+
require "rbs"
|
14
|
+
|
15
|
+
# RbsMacros is a utility that looks for metaprogramming-related
|
16
|
+
# method invocation in your Ruby code and generates RBS files for them.
|
17
|
+
module RbsMacros
|
18
|
+
def self.run(&block)
|
19
|
+
config = Config.new
|
20
|
+
block&.(config)
|
21
|
+
|
22
|
+
env = Environment.new
|
23
|
+
config.loader.load(env: env.rbs) if config.use_loader
|
24
|
+
config.macros.each do |macro|
|
25
|
+
macro.setup(env)
|
26
|
+
end
|
27
|
+
|
28
|
+
config.project.glob(ext: ".rbs", include: config.sigs, exclude: [config.output_dir]) do |filename|
|
29
|
+
source = config.project.read(filename)
|
30
|
+
buffer = RBS::Buffer.new(name: filename, content: source)
|
31
|
+
_, directives, decls = RBS::Parser.parse_signature(buffer)
|
32
|
+
env.rbs.add_signature(buffer:, directives:, decls:)
|
33
|
+
end
|
34
|
+
# TODO: streamline this private method invocation
|
35
|
+
env.instance_variable_set(:@rbs, env.rbs.resolve_type_names)
|
36
|
+
config.project.glob(ext: ".rb", include: config.load_dirs, exclude: []) do |filename|
|
37
|
+
relative_filename = filename
|
38
|
+
config.load_dirs.each do |load_dir|
|
39
|
+
if filename.start_with?("#{load_dir}/")
|
40
|
+
relative_filename = filename.delete_prefix("#{load_dir}/")
|
41
|
+
break
|
42
|
+
end
|
43
|
+
end
|
44
|
+
source = config.project.read(filename)
|
45
|
+
env.meta_eval_ruby(source, filename: relative_filename.sub(/\.rb\z/, ""))
|
46
|
+
end
|
47
|
+
|
48
|
+
files = {} # : Hash[String, Array[RBS::AST::Declarations::t]]
|
49
|
+
env.decls.each do |entry|
|
50
|
+
file_decls = (files[entry.file] ||= [])
|
51
|
+
current_mod = env.object_class
|
52
|
+
container = nil # : (RBS::AST::Declarations::Module | RBS::AST::Declarations::Class)?
|
53
|
+
(entry.mod.name || "").split("::").each do |name|
|
54
|
+
inner_mod = current_mod.meta_const_get(name.to_sym)
|
55
|
+
raise "Not found: #{current_mod.name}::#{name}" unless inner_mod
|
56
|
+
raise "Not a module: #{current_mod.name}::#{name}" unless inner_mod.is_a?(MetaModule)
|
57
|
+
|
58
|
+
current_mod = inner_mod
|
59
|
+
|
60
|
+
current_decls = container&.members || file_decls
|
61
|
+
container = nil
|
62
|
+
current_decls.each do |decl|
|
63
|
+
if (decl.is_a?(RBS::AST::Declarations::Class) || decl.is_a?(RBS::AST::Declarations::Module)) \
|
64
|
+
&& decl.name.name == name.to_sym
|
65
|
+
container = decl
|
66
|
+
end
|
67
|
+
end
|
68
|
+
next if container
|
69
|
+
|
70
|
+
if inner_mod.is_class
|
71
|
+
container = c = RBS::AST::Declarations::Class.new(
|
72
|
+
name: RBS::TypeName.new(name: name.to_sym, namespace: RBS::Namespace.empty),
|
73
|
+
type_params: [],
|
74
|
+
super_class: nil,
|
75
|
+
members: [],
|
76
|
+
annotations: [],
|
77
|
+
location: nil,
|
78
|
+
comment: nil
|
79
|
+
)
|
80
|
+
current_decls << c
|
81
|
+
else
|
82
|
+
container = m = RBS::AST::Declarations::Module.new(
|
83
|
+
name: RBS::TypeName.new(name: name.to_sym, namespace: RBS::Namespace.empty),
|
84
|
+
type_params: [],
|
85
|
+
members: [],
|
86
|
+
self_types: [],
|
87
|
+
annotations: [],
|
88
|
+
location: nil,
|
89
|
+
comment: nil
|
90
|
+
)
|
91
|
+
current_decls << m
|
92
|
+
end
|
93
|
+
end
|
94
|
+
if container
|
95
|
+
container.members << entry.declaration
|
96
|
+
else
|
97
|
+
d = entry.declaration
|
98
|
+
case d
|
99
|
+
when RBS::AST::Declarations::Class,
|
100
|
+
RBS::AST::Declarations::Module,
|
101
|
+
RBS::AST::Declarations::Interface,
|
102
|
+
RBS::AST::Declarations::Constant,
|
103
|
+
RBS::AST::Declarations::Global,
|
104
|
+
RBS::AST::Declarations::TypeAlias,
|
105
|
+
RBS::AST::Declarations::ClassAlias,
|
106
|
+
RBS::AST::Declarations::ModuleAlias
|
107
|
+
file_decls << d
|
108
|
+
else
|
109
|
+
raise "Not allowed here: #{d.class}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
decls = [] # : Array[RBS::AST::Declarations::t]
|
115
|
+
env.object_class.meta_constants.each do |name|
|
116
|
+
value = env.object_class.meta_const_get(name)
|
117
|
+
next unless value.is_a?(MetaModule)
|
118
|
+
|
119
|
+
decls << RBS::AST::Declarations::Module.new(
|
120
|
+
name: RBS::TypeName.new(name:, namespace: RBS::Namespace.empty),
|
121
|
+
type_params: [],
|
122
|
+
members: [],
|
123
|
+
self_types: [],
|
124
|
+
annotations: [],
|
125
|
+
location: nil,
|
126
|
+
comment: nil
|
127
|
+
)
|
128
|
+
end
|
129
|
+
|
130
|
+
files.each do |filename, file_decls|
|
131
|
+
out = StringIO.new(+"", "w")
|
132
|
+
writer = RBS::Writer.new(out:)
|
133
|
+
writer.write file_decls
|
134
|
+
config.project.write("#{config.output_dir}/#{filename}.rbs", out.string)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|