lib/sord/rbi_generator.rb in sord-0.6.0 vs lib/sord/rbi_generator.rb in sord-0.7.0

- old
+ new

@@ -10,57 +10,86 @@ # @return [Array<String>] The lines of the generated RBI file so far. attr_reader :rbi_contents # @return [Integer] The number of objects this generator has processed so # far. - attr_reader :object_count + def object_count + @namespace_count + @method_count + end + # @return [Array<Array(String, YARD::CodeObjects::Base, Integer)>] The + # errors encountered by by the generator. Each element is of the form + # [message, item, line]. + attr_reader :warnings + # Create a new RBI generator. # @param [Hash] options # @return [RbiGenerator] def initialize(options) @rbi_contents = ['# typed: strong'] - @object_count = 0 + @namespace_count = 0 + @method_count = 0 + @warnings = [] # Hook the logger so that messages are added as comments to the RBI file - Logging.add_hook do |type, msg, item| - rbi_contents << " # sord #{type} - #{msg}" + Logging.add_hook do |type, msg, item, indent_level = 0| + rbi_contents << "#{' ' * (indent_level + 1)}# sord #{type} - #{msg}" end if options.comments + + # Hook the logger so that warnings are collected + Logging.add_hook do |type, msg, item, indent_level = 0| + warnings << [msg, item, rbi_contents.length] \ + if type == :warn + end end - # Increment the object counter. + # Increment the namespace counter. # @return [void] - def count_object - @object_count += 1 + def count_namespace + @namespace_count += 1 end + # Increment the method counter. + # @return [void] + def count_method + @method_count += 1 + end + # Given a YARD CodeObject, add lines defining its mixins (that is, extends # and includes) to the current RBI file. # @param [YARD::CodeObjects::Base] item + # @param [Integer] indent_level # @return [void] - def add_mixins(item) + def add_mixins(item, indent_level) extends = item.instance_mixins includes = item.class_mixins extends.each do |this_extend| - rbi_contents << " extend #{this_extend.path}" + rbi_contents << "#{' ' * (indent_level + 1)}extend #{this_extend.path}" end includes.each do |this_include| - rbi_contents << " include #{this_include.path}" + rbi_contents << "#{' ' * (indent_level + 1)}include #{this_include.path}" end end # Given a YARD NamespaceObject, add lines defining its methods and their # signatures to the current RBI file. # @param [YARD::CodeObjects::NamespaceObject] item + # @param [Integer] indent_level # @return [void] - def add_methods(item) + def add_methods(item, indent_level) # TODO: block documentation item.meths.each do |meth| - count_object + count_method + # If the method is an alias, skip it so we don't define it as a + # separate method. Sorbet will handle it automatically. + if meth.is_alias? + next + end + parameter_list = meth.parameters.map do |name, default| # Handle these three main cases: # - def method(param) or def method(param:) # - def method(param: 'default') # - def method(param = 'default') @@ -93,65 +122,66 @@ # Look for the matching getter method getter_path = meth.path[0...-1] getter = item.meths.find { |m| m.path == getter_path } unless getter - Logging.omit("no YARD type given for #{name.inspect}, using T.untyped", meth) + Logging.omit("no YARD type given for #{name.inspect}, using T.untyped", meth, indent_level) next "#{name}: T.untyped" end inferred_type = TypeConverter.yard_to_sorbet( getter.tags('return').flat_map(&:types), meth) - Logging.infer("inferred type of parameter #{name.inspect} as #{inferred_type} using getter's return type", meth) + Logging.infer("inferred type of parameter #{name.inspect} as #{inferred_type} using getter's return type", meth, indent_level) # Get rid of : on keyword arguments. name = name.chop if name.end_with?(':') "#{name}: #{inferred_type}" else - Logging.omit("no YARD type given for #{name.inspect}, using T.untyped", meth) + Logging.omit("no YARD type given for #{name.inspect}, using T.untyped", meth, indent_level) # Get rid of : on keyword arguments. name = name.chop if name.end_with?(':') "#{name}: T.untyped" end end.join(", ") return_tags = meth.tags('return') returns = if return_tags.length == 0 "void" - elsif return_tags.length == 1 && return_tags.first.types.first.downcase == "void" + elsif return_tags.length == 1 && return_tags&.first&.types&.first&.downcase == "void" "void" else "returns(#{TypeConverter.yard_to_sorbet(meth.tag('return').types, meth)})" end prefix = meth.scope == :class ? 'self.' : '' - sig = sig_params_list.empty? ? " sig { #{returns} }" : " sig { params(#{sig_params_list}).#{returns} }" + sig = sig_params_list.empty? ? "#{' ' * (indent_level + 1)}sig { #{returns} }" : "#{' ' * (indent_level + 1)}sig { params(#{sig_params_list}).#{returns} }" rbi_contents << sig - rbi_contents << " def #{prefix}#{meth.name}(#{parameter_list}); end" + rbi_contents << "#{' ' * (indent_level + 1)}def #{prefix}#{meth.name}(#{parameter_list}); end" end end # Given a YARD NamespaceObject, add lines defining its mixins, methods # and children to the RBI file. # @param [YARD::CodeObjects::NamespaceObject] item - def add_namespace(item) - count_object + # @param [Integer] indent_level + def add_namespace(item, indent_level = 0) + count_namespace if item.type == :class && item.superclass.to_s != "Object" - rbi_contents << "class #{item.name} < #{item.superclass.path}" + rbi_contents << "#{' ' * indent_level}class #{item.name} < #{item.superclass.path}" else - rbi_contents << "#{item.type} #{item.name}" + rbi_contents << "#{' ' * indent_level}#{item.type} #{item.name}" end - add_mixins(item) - add_methods(item) + add_mixins(item, indent_level) + add_methods(item, indent_level) item.children.select { |x| [:class, :module].include?(x.type) } - .each { |child| add_namespace(child) } + .each { |child| add_namespace(child, indent_level + 1) } - rbi_contents << "end" + rbi_contents << "#{' ' * indent_level}end" end # Generates the RBI file and writes it to the given file path. # @param [String] filename # @return [void] @@ -169,14 +199,32 @@ .each { |child| add_namespace(child) } # Write the file File.write(filename, rbi_contents.join(?\n)) - Logging.done("Processed #{object_count} objects") + if object_count.zero? + Logging.warn("No objects processed.") + Logging.warn("Have you definitely generated the YARD documentation for this project?") + Logging.warn("Run `yard` to generate docs.") + end + + Logging.done("Processed #{object_count} objects (#{@namespace_count} namespaces and #{@method_count} methods)") + + Logging.hooks.clear + + unless warnings.empty? + Logging.warn("There were #{warnings.length} important warnings in the RBI file, listed below.") + Logging.warn("The types which caused them have been replaced with SORD_ERROR_ constants.") + Logging.warn("Please edit the file near the line numbers given to fix these errors.") + Logging.warn("Alternatively, edit your YARD documentation so that your types are valid and re-run Sord.") + warnings.each do |(msg, item, line)| + puts " #{"Line #{line} |".light_black} (#{item.path.bold}) #{msg}" + end + end rescue Logging.error($!) $@.each do |line| - puts " #{line}".light_white + puts " #{line}" end end end end