diff --git a/lib/rbs/prototype/rbi.rb b/lib/rbs/prototype/rbi.rb index 2359e9529..bfbac40ed 100644 --- a/lib/rbs/prototype/rbi.rb +++ b/lib/rbs/prototype/rbi.rb @@ -9,10 +9,22 @@ class RBI attr_reader :modules attr_reader :last_sig + class Context + attr_accessor :singleton + attr_accessor :visibility + + def initialize(singleton:, visibility:) + @singleton = singleton + @visibility = visibility + end + end + def initialize @decls = [] @modules = [] + @contexts = [] + @emitted_visibility = {} end def parse(string) @@ -20,19 +32,17 @@ def parse(string) process RubyVM::AbstractSyntaxTree.parse(string), comments: comments end - def nested_name(name) - (current_namespace + const_to_name(name).to_namespace).to_type_name.relative! - end - - def current_namespace - modules.inject(Namespace.empty) do |parent, mod| - parent + mod.name.to_namespace + def append_decl(decl) + if mod = current_module + mod.members << decl + else + decls << decl end end def push_class(name, super_class, comment:) class_decl = AST::Declarations::Class.new( - name: nested_name(name), + name: const_to_name(name), super_class: super_class && AST::Declarations::Class::Super.new(name: const_to_name(super_class), args: [], location: nil), type_params: [], members: [], @@ -41,17 +51,20 @@ def push_class(name, super_class, comment:) comment: comment ) + append_decl class_decl modules << class_decl - decls << class_decl + @contexts << Context.new(singleton: false, visibility: :public) + @emitted_visibility[class_decl.object_id] = :public yield ensure + @contexts.pop modules.pop end def push_module(name, comment:) module_decl = AST::Declarations::Module.new( - name: nested_name(name), + name: const_to_name(name), type_params: [], members: [], annotations: [], @@ -60,11 +73,14 @@ def push_module(name, comment:) comment: comment ) + append_decl module_decl modules << module_decl - decls << module_decl + @contexts << Context.new(singleton: false, visibility: :public) + @emitted_visibility[module_decl.object_id] = :public yield ensure + @contexts.pop modules.pop end @@ -76,6 +92,34 @@ def current_module! current_module or raise end + def current_context + @contexts.last + end + + def current_context! + current_context or raise + end + + def sync_visibility(visibility) + # RBS has no protected visibility. Private is the conservative fallback. + visibility = :private if visibility == :protected + + mod = current_module! + return if @emitted_visibility[mod.object_id] == visibility + + member = case visibility + when :public + AST::Members::Public.new(location: nil) + when :private + AST::Members::Private.new(location: nil) + else + raise "Unexpected visibility: #{visibility}" + end + + mod.members << member + @emitted_visibility[mod.object_id] = visibility + end + def push_sig(node) if last_sig = @last_sig last_sig << node @@ -107,6 +151,15 @@ def process(node, outer: [], comments:) push_module node.children[0], comment: comment do process node.children[1], outer: outer + [node], comments: comments end + when :SCLASS + if node.children[0].type == :SELF + @contexts << Context.new(singleton: true, visibility: :public) + begin + process node.children[1], outer: outer + [node], comments: comments + ensure + @contexts.pop + end + end when :FCALL case node.children[0] when :include @@ -125,9 +178,9 @@ def process(node, outer: [], comments:) end when :extend each_arg node.children[1] do |arg| - if arg.type == :CONST || arg.type == :COLON2 + if arg.type == :CONST || arg.type == :COLON2 || arg.type == :COLON3 name = const_to_name(arg) - unless name.to_s == "T::Generic" || name.to_s == "T::Sig" + unless ["T::Generic", "T::Helpers", "T::Sig"].include?(name.to_s.delete_prefix("::")) member = AST::Members::Extend.new( name: name, args: [], @@ -142,6 +195,10 @@ def process(node, outer: [], comments:) when :sig out = outer.last or raise push_sig out.children.last.children.last + when :attr_reader, :attr_writer, :attr_accessor + process_attribute node, comments: comments + when :private, :protected, :public + process_visibility node, outer: outer, comments: comments when :alias_method new, old = each_arg(node.children[1]).map {|x| x.children[0] } current_module!.members << AST::Members::Alias.new( @@ -149,14 +206,20 @@ def process(node, outer: [], comments:) old_name: old, location: nil, annotations: [], - kind: :instance, + kind: current_context!.singleton ? :singleton : :instance, comment: nil ) end + when :VCALL + case node.children[0] + when :private, :protected, :public + current_context!.visibility = node.children[0] + end when :DEFS sigs = pop_sig if sigs + sync_visibility(:public) comment = join_comments(sigs, comments) args = node.children[2] @@ -178,6 +241,8 @@ def process(node, outer: [], comments:) sigs = pop_sig if sigs + context = current_context! + sync_visibility(context.visibility) comment = join_comments(sigs, comments) args = node.children[1] @@ -188,7 +253,7 @@ def process(node, outer: [], comments:) location: nil, annotations: [], overloads: types.map {|type| AST::Members::MethodDefinition::Overload.new(annotations: [], method_type: type) }, - kind: :instance, + kind: context.singleton ? :singleton : :instance, comment: comment, overloading: false, visibility: nil @@ -222,11 +287,7 @@ def process(node, outer: [], comments:) end else name = node.children[0].yield_self do |n| - if n.is_a?(Symbol) - TypeName.new(namespace: current_namespace, name: n) - else - const_to_name(n) - end + n.is_a?(Symbol) ? TypeName.new(namespace: Namespace.empty, name: n) : const_to_name(n) end value_node = node.children.last type = if value_node && value_node.type == :CALL && value_node.children[1] == :let @@ -235,7 +296,7 @@ def process(node, outer: [], comments:) else Types::Bases::Any.new(location: nil) end - decls << AST::Declarations::Constant.new( + append_decl AST::Declarations::Constant.new( name: name, type: type, location: nil, @@ -244,12 +305,13 @@ def process(node, outer: [], comments:) ) end when :ALIAS + sync_visibility(current_context!.visibility) current_module!.members << AST::Members::Alias.new( new_name: node.children[0].children[0], old_name: node.children[1].children[0], location: nil, annotations: [], - kind: :instance, + kind: current_context!.singleton ? :singleton : :instance, comment: nil ) else @@ -259,6 +321,93 @@ def process(node, outer: [], comments:) end end + def process_visibility(node, outer:, comments:) + visibility = node.children[0] + args = each_arg(node.children[1]).to_a + context = current_context! + + if args.empty? + context.visibility = visibility + else + previous_visibility = context.visibility + context.visibility = visibility + + begin + args.each do |arg| + if arg.type == :DEFN || arg.type == :DEFS + process arg, outer: outer + [node], comments: comments + end + end + ensure + context.visibility = previous_visibility + end + end + end + + def process_attribute(node, comments:) + sigs = pop_sig + kind = node.children[0] + context = current_context! + sync_visibility(context.visibility) + + type = attribute_type(kind, sigs) + comment = join_comments(sigs, comments) if sigs + member_class = case kind + when :attr_reader + AST::Members::AttrReader + when :attr_writer + AST::Members::AttrWriter + when :attr_accessor + AST::Members::AttrAccessor + else + raise "Unexpected attribute kind: #{kind}" + end + + each_arg node.children[1] do |arg| + if name = symbol_literal_node?(arg) + current_module!.members << member_class.new( + name: name, + type: type, + ivar_name: nil, + kind: context.singleton ? :singleton : :instance, + annotations: [], + location: nil, + comment: comment, + visibility: nil + ) + end + end + end + + def attribute_type(kind, sigs) + any = Types::Bases::Any.new(location: nil) + return any unless sigs + + method_types = sigs.filter_map do |sig| + method_type(nil, sig, variables: current_module!.type_params, overloads: sigs.size) + end + function = method_types.last&.type + return any unless function.is_a?(Types::Function) + + parameter_type = function.required_positionals.first&.type + return_type = function.return_type + + case kind + when :attr_reader + return_type + when :attr_writer + parameter_type || return_type + when :attr_accessor + if return_type.is_a?(Types::Bases::Any) || return_type.is_a?(Types::Bases::Void) + parameter_type || any + else + return_type + end + else + any + end + end + def method_type(args_node, type_node, variables:, overloads:) if type_node if type_node.type == :CALL @@ -463,8 +612,10 @@ def type_of(type_node, variables:) case when type.is_a?(Types::ClassInstance) && type.name.name == BuiltinNames::BasicObject.name.name Types::Bases::Any.new(location: nil) - when type.is_a?(Types::ClassInstance) && type.name.to_s == "T::Boolean" + when type.is_a?(Types::ClassInstance) && type.name.to_s.delete_prefix("::") == "T::Boolean" Types::Bases::Bool.new(location: nil) + when type.is_a?(Types::ClassInstance) && type.name.to_s.delete_prefix("::") == "T::Class" + Types::Bases::Any.new(location: nil) else type end @@ -482,6 +633,11 @@ def type_of0(type_node, variables:) Types::ClassInstance.new(name: const_to_name(type_node), args: [], location: nil) when call_node?(type_node, name: :[], receiver: -> (_) { true }) # The type_node represents a type application + receiver = type_node.children[0] + if [:CONST, :COLON2, :COLON3].include?(receiver.type) && const_to_name(receiver).to_s.delete_prefix("::") == "T::Class" + return Types::Bases::Any.new(location: nil) + end + type = type_of(type_node.children[0], variables: variables) type.is_a?(Types::ClassInstance) or raise @@ -559,7 +715,7 @@ def const_to_name(node) type_name = TypeName.new(name: node.children[1], namespace: namespace) - case type_name.to_s + case type_name.to_s.delete_prefix("::") when "T::Array" BuiltinNames::Array.name when "T::Hash" diff --git a/sig/prototype/rbi.rbs b/sig/prototype/rbi.rbs index d8b37e9e8..bbf078775 100644 --- a/sig/prototype/rbi.rbs +++ b/sig/prototype/rbi.rbs @@ -2,7 +2,17 @@ module RBS module Prototype class RBI include Helpers - + + type visibility = :private | :protected | :public + + class Context + attr_accessor singleton: bool + + attr_accessor visibility: visibility + + def initialize: (singleton: bool, visibility: visibility) -> void + end + attr_reader decls: Array[AST::Declarations::t] type module_decl = AST::Declarations::Class | AST::Declarations::Module @@ -13,13 +23,15 @@ module RBS # Last subsequent `sig` calls attr_reader last_sig: Array[RubyVM::AbstractSyntaxTree::Node]? + @contexts: Array[Context] + + @emitted_visibility: Hash[Integer, visibility] + def initialize: () -> void def parse: (String) -> void - def nested_name: (RubyVM::AbstractSyntaxTree::Node name) -> TypeName - - def current_namespace: () -> Namespace + def append_decl: (AST::Declarations::t decl) -> void def push_class: ( RubyVM::AbstractSyntaxTree::Node name, @@ -35,6 +47,12 @@ module RBS # The inner most module/class definition, raises on toplevel def current_module!: () -> module_decl + def current_context: () -> Context? + + def current_context!: () -> Context + + def sync_visibility: (visibility) -> void + # Put a `sig` call to current list. def push_sig: (RubyVM::AbstractSyntaxTree::Node node) -> void @@ -45,6 +63,12 @@ module RBS def process: (RubyVM::AbstractSyntaxTree::Node node, comments: Hash[Integer, AST::Comment], ?outer: Array[RubyVM::AbstractSyntaxTree::Node]) -> void + def process_visibility: (RubyVM::AbstractSyntaxTree::Node node, outer: Array[RubyVM::AbstractSyntaxTree::Node], comments: Hash[Integer, AST::Comment]) -> void + + def process_attribute: (RubyVM::AbstractSyntaxTree::Node node, comments: Hash[Integer, AST::Comment]) -> void + + def attribute_type: (:attr_reader | :attr_writer | :attr_accessor kind, Array[RubyVM::AbstractSyntaxTree::Node]? sigs) -> Types::t + def method_type: (RubyVM::AbstractSyntaxTree::Node? args_node, RubyVM::AbstractSyntaxTree::Node? type_node, variables: Array[AST::TypeParam], overloads: Integer) -> MethodType? def parse_params: (RubyVM::AbstractSyntaxTree::Node args_node, RubyVM::AbstractSyntaxTree::Node args, MethodType method_type, variables: Array[AST::TypeParam], overloads: Integer) -> MethodType diff --git a/test/rbs/rbi_prototype_test.rb b/test/rbs/rbi_prototype_test.rb index c8bed6dec..99e9b4368 100644 --- a/test/rbs/rbi_prototype_test.rb +++ b/test/rbs/rbi_prototype_test.rb @@ -70,9 +70,8 @@ module Bar assert_write parser.decls, <<-EOF module Foo -end - -module Foo::Bar + module Bar + end end EOF end @@ -91,9 +90,8 @@ module ::Bar assert_write parser.decls, <<-EOF module Foo -end - -module Bar + module ::Bar + end end EOF end @@ -112,11 +110,10 @@ module Foo assert_write parser.decls, <<-EOF module Foo -end + ABBR_DAYNAMES: Array -Foo::ABBR_DAYNAMES: Array - -Foo::ABBR_MONTHNAMES: Integer + ABBR_MONTHNAMES: Integer +end EOF end @@ -550,6 +547,251 @@ class Dir[out X, in Y, Z] EOF end + def test_nested_declarations_preserve_lexical_resolution + parser = RBI.new + + parser.parse <<-EOF +module Demo + class Parent; end + module Helpers; end + class Value; end + + class Child < Parent + include Helpers + + sig { params(value: Value).returns(Value) } + def convert(value); end + end +end + EOF + + assert_write parser.decls, <<-EOF +module Demo + class Parent + end + + module Helpers + end + + class Value + end + + class Child < Parent + include Helpers + + def convert: (Value value) -> Value + end +end + EOF + end + + def test_nested_constant + parser = RBI.new + + parser.parse <<-EOF +module Demo + module Modes + VALUE = T.let(:value, Symbol) + end +end + EOF + + assert_write parser.decls, <<-EOF +module Demo + module Modes + VALUE: Symbol + end +end + EOF + end + + def test_ignores_t_helpers + parser = RBI.new + + parser.parse <<-EOF +module Factory + extend T::Helpers + extend OtherHelpers +end + EOF + + assert_write parser.decls, <<-EOF +module Factory + extend OtherHelpers +end + EOF + end + + def test_t_class_falls_back_to_untyped + parser = RBI.new + + parser.parse <<-EOF +module Factory + sig do + type_parameters(:Config) + .params(config_class: T::Class[T.type_parameter(:Config)]) + .returns(T.type_parameter(:Config)) + end + def make(config_class); end +end + EOF + + assert_write parser.decls, <<-EOF +module Factory + def make: [Config] (untyped config_class) -> Config +end + EOF + end + + def test_singleton_class_method + parser = RBI.new + + parser.parse <<-EOF +class Registry + class << self + sig { returns(T.attached_class) } + def build; end + end +end + EOF + + assert_write parser.decls, <<-EOF +class Registry + def self.build: () -> instance +end + EOF + end + + def test_typed_attribute_consumes_signature + parser = RBI.new + + parser.parse <<-EOF +class Cache + sig { returns(T.nilable(Integer)) } + attr_reader :size + + sig { returns(String) } + attr_accessor :name + + sig { params(value: Integer).void } + attr_writer :count + + sig { params(size: T.nilable(Integer)).void } + def initialize(size: nil); end +end + EOF + + assert_write parser.decls, <<-EOF +class Cache + attr_reader size: Integer? + + attr_accessor name: String + + attr_writer count: Integer + + def initialize: (?size: Integer? size) -> void +end + EOF + end + + def test_method_visibility + parser = RBI.new + + parser.parse <<-EOF +module Factory + private + + sig { void } + def helper; end + + public + + sig { void } + def make; end +end + EOF + + assert_write parser.decls, <<-EOF +module Factory + private + + def helper: () -> void + + public + + def make: () -> void +end + EOF + end + + def test_generated_reproduction_can_be_loaded + parser = RBI.new + + parser.parse <<-EOF +module Demo + class Parent; end + module Helpers; end + + class Child < Parent + include Helpers + end + + module Modes + VALUE = T.let(:value, Symbol) + end + + class Registry + class << self + sig { returns(T.attached_class) } + def build; end + end + end + + class Cache + sig { returns(T.nilable(Integer)) } + attr_reader :size + + sig { params(size: T.nilable(Integer)).void } + def initialize(size: nil); end + end + + module Factory + extend T::Helpers + + sig do + type_parameters(:Config) + .params(config_class: T::Class[T.type_parameter(:Config)]) + .returns(T.type_parameter(:Config)) + end + def make(config_class); end + + private + + sig { void } + def helper; end + end +end + EOF + + out = StringIO.new + RBS::Writer.new(out: out).write(parser.decls) + refute_match(/\bT::/, out.string) + + SignatureManager.new do |manager| + manager.add_file("repro.rbs", out.string) + manager.build do |env| + builder = RBS::DefinitionBuilder.new(env: env) + + ["::Demo::Child", "::Demo::Registry", "::Demo::Cache", "::Demo::Factory"].each do |name| + builder.build_instance(type_name(name)) + builder.build_singleton(type_name(name)) + end + + assert_include env.constant_decls.keys, type_name("::Demo::Modes::VALUE") + end + end + end + def test_masgn parser = RBI.new @@ -561,13 +803,12 @@ class Test assert_write parser.decls, <<-EOF class Test -end + A: untyped -Test::A: untyped + B: untyped -Test::B: untyped - -Test::C: untyped + C: untyped +end EOF end end