Skip to content

Commit 421fd6d

Browse files
committed
Support auto extending module
In Ruby language, developers need to use both "include" and "extend" to implement mix-in having instance methods and class methods. The auto extending module is a well-known technique to realize it (ex. `ActiveSupport::Concern`, "extend" call in `included` block, and so on). This supports the auto extending modules via modules having "autoextend:..." annotation. The `RBS::DefinitionBuilder` searches the extended modules from the annotations of the included modules.
1 parent ed748bf commit 421fd6d

6 files changed

+65
-5
lines changed

docs/syntax.md

+14
Original file line numberDiff line numberDiff line change
@@ -780,3 +780,17 @@ _annotation_ ::= `%a{` _annotation-text_ `}` # Annotation using {}
780780

781781
_annotation-text_ ::= /[^\x00]*/ # Any characters except NUL (and parenthesis)
782782
```
783+
784+
#### Auto extending modules
785+
786+
Module having "autoextend:..." annotation is considered as an auto extending module.
787+
When such auto extending modules are included, the including class will be extended by annotated modules.
788+
789+
```
790+
%a{autoextend:Mod::ClassMethods}
791+
module Mod
792+
module ClassMethods
793+
def foo: () -> void
794+
end
795+
end
796+
```

lib/rbs/definition_builder.rb

+13
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,19 @@ def build_singleton0(type_name)
239239
definition.class_variables.merge!(defn.class_variables)
240240
end
241241

242+
one_ancestors.each_included_module do |mod|
243+
included_module = env.class_decls[mod.name] or raise "Unknown name for one_ancestors.included_module: #{type_name}"
244+
included_module.decls.each do |decl|
245+
decl.decl.annotations.each do |annotation|
246+
if annotation.string.start_with? "autoextend:"
247+
mod_name = TypeName("::" + annotation.string.split(":", 2)[1])
248+
subst = tapp_subst(mod_name, [])
249+
define_instance(definition, mod_name, subst)
250+
end
251+
end
252+
end
253+
end
254+
242255
one_ancestors.each_extended_module do |mod|
243256
mod.args.each do |arg|
244257
validate_type_presence(arg)

lib/rbs/definition_builder/ancestor_builder.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ def self.singleton(type_name:, super_class:)
111111
params: nil,
112112
super_class: super_class,
113113
self_types: nil,
114-
included_modules: nil,
114+
included_modules: [],
115115
included_interfaces: nil,
116116
prepended_modules: nil,
117117
extended_modules: [],
@@ -306,7 +306,7 @@ def one_singleton_ancestors(type_name)
306306

307307
mixin_ancestors(entry,
308308
type_name,
309-
included_modules: nil,
309+
included_modules: ancestors.included_modules,
310310
included_interfaces: nil,
311311
prepended_modules: nil,
312312
extended_modules: ancestors.extended_modules,

test/rbs/ancestor_builder_test.rb

+4-2
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ class Hello[X] < Array[Integer]
6464

6565
assert_equal Ancestor::Singleton.new(name: type_name("::Array")),
6666
a.super_class
67-
assert_nil a.included_modules
67+
assert_equal [Ancestor::Instance.new(name: type_name("::Bar"), args: [parse_type("X", variables: [:X])], source: nil)],
68+
a.included_modules
6869
assert_nil a.included_interfaces
6970
assert_nil a.prepended_modules
7071
assert_equal [Ancestor::Instance.new(name: type_name("::Foo"), args: [parse_type("::String")], source: nil)],
@@ -130,7 +131,8 @@ module Hello[X] : _I1[Array[X]]
130131
assert_equal Ancestor::Instance.new(name: type_name("::Module"), args: [], source: nil),
131132
a.super_class
132133
assert_nil a.self_types
133-
assert_nil a.included_modules
134+
assert_equal [Ancestor::Instance.new(name: type_name("::M2"), args: [parse_type("X", variables: [:X])], source: nil)],
135+
a.included_modules
134136
assert_nil a.prepended_modules
135137
assert_equal [Ancestor::Instance.new(name: type_name("::M1"), args: [parse_type("::String")], source: nil)],
136138
a.extended_modules

test/rbs/ancestor_graph_test.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ module M
8383
graph.each_ancestor(InstanceNode("::M")).to_set
8484
)
8585
assert_equal(
86-
Set[InstanceNode("::B"), InstanceNode("::C")],
86+
Set[InstanceNode("::B"), InstanceNode("::C"), SingletonNode("::B"), SingletonNode("::C")],
8787
graph.each_descendant(InstanceNode("::M")).to_set
8888
)
8989

test/rbs/definition_builder_test.rb

+31
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,37 @@ def get: () -> X
281281
end
282282
end
283283

284+
def test_build_singleton_including_autoextending_module
285+
SignatureManager.new do |manager|
286+
manager.files[Pathname("foo.rbs")] = <<EOF
287+
%a{autoextend:Mod::ClassMethods}
288+
module Mod
289+
module ClassMethods
290+
def count: -> Integer
291+
end
292+
end
293+
294+
class Class
295+
include Mod
296+
end
297+
EOF
298+
manager.build do |env|
299+
builder = DefinitionBuilder.new(env: env)
300+
301+
builder.build_singleton(type_name("::Class")).yield_self do |definition|
302+
assert_instance_of Definition, definition
303+
304+
assert_equal [:__id__, :count, :initialize, :new, :puts, :respond_to_missing?, :to_i], definition.methods.keys.sort
305+
assert_method_definition definition.methods[:__id__], ["() -> ::Integer"]
306+
assert_method_definition definition.methods[:initialize], ["() -> void"]
307+
assert_method_definition definition.methods[:puts], ["(*untyped) -> nil"]
308+
assert_method_definition definition.methods[:respond_to_missing?], ["(::Symbol, bool) -> bool"]
309+
end
310+
end
311+
end
312+
end
313+
314+
284315
def test_build_instance_module_include_module
285316
SignatureManager.new do |manager|
286317
manager.files[Pathname("foo.rbs")] = <<EOF

0 commit comments

Comments
 (0)