Skip to content

Commit 408dea7

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 5f992e6 commit 408dea7

File tree

6 files changed

+85
-5
lines changed

6 files changed

+85
-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/ancestor_builder.rb

+33-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,
@@ -334,7 +334,32 @@ def one_interface_ancestors(type_name)
334334
end
335335
end
336336

337+
def auto_extended_modules(type_name, resolver)
338+
auto_extended_modules = [] #: Array[Definition::Ancestor::Instance]
339+
340+
mod = env.class_decls[type_name] or raise "Unknown name for include: #{type_name}"
341+
mod.decls.each do |mod_decl|
342+
mod_decl.decl.annotations.each do |annotation|
343+
if annotation.string.start_with? "autoextend:"
344+
auto_extended_mod_name = TypeName(annotation.string.split(":", 2)[1])
345+
auto_extended_mod_name = resolver.resolve(auto_extended_mod_name, context: mod_decl.context) || auto_extended_mod_name
346+
347+
auto_extend_source = AST::Members::Extend.new(
348+
name: auto_extended_mod_name,
349+
args: [],
350+
annotations: [],
351+
location: nil,
352+
comment: nil,
353+
)
354+
auto_extended_modules << Definition::Ancestor::Instance.new(name: auto_extended_mod_name, args: [], source: auto_extend_source)
355+
end
356+
end
357+
end
358+
auto_extended_modules
359+
end
360+
337361
def mixin_ancestors0(decl, type_name, align_params:, included_modules:, included_interfaces:, extended_modules:, prepended_modules:, extended_interfaces:)
362+
resolver = Resolver::TypeNameResolver.new(env)
338363
decl.each_mixin do |member|
339364
case member
340365
when AST::Members::Include
@@ -348,6 +373,12 @@ def mixin_ancestors0(decl, type_name, align_params:, included_modules:, included
348373

349374
module_name = env.normalize_module_name(module_name)
350375
included_modules << Definition::Ancestor::Instance.new(name: module_name, args: module_args, source: member)
376+
377+
if extended_modules
378+
auto_extended_modules(module_name, resolver).each do |auto_extended_module|
379+
extended_modules << auto_extended_module
380+
end
381+
end
351382
when member.name.interface? && included_interfaces
352383
NoMixinFoundError.check!(member.name, env: env, member: member)
353384

sig/ancestor_builder.rbs

+2
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,8 @@ module RBS
138138

139139
def validate_super_class!: (TypeName, Environment::ClassEntry) -> void
140140

141+
def auto_extended_modules: (TypeName, Resolver::TypeNameResolver) -> Array[Definition::Ancestor::Instance]
142+
141143
def mixin_ancestors: (Environment::ClassEntry | Environment::ModuleEntry,
142144
TypeName,
143145
included_modules: Array[Definition::Ancestor::Instance]?,

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)