lib/sord/rbi_generator.rb in sord-0.8.0 vs lib/sord/rbi_generator.rb in sord-0.9.0
- old
+ new
@@ -1,17 +1,15 @@
# typed: true
require 'yard'
require 'sord/type_converter'
-require 'colorize'
require 'sord/logging'
+require 'parlour'
+require 'rainbow'
module Sord
# Converts the current working directory's YARD registry into an RBI file.
- class RbiGenerator
- # @return [Array<String>] The lines of the generated RBI file so far.
- attr_reader :rbi_contents
-
+ class RbiGenerator
# @return [Integer] The number of objects this generator has processed so
# far.
def object_count
@namespace_count + @method_count
end
@@ -19,39 +17,39 @@
# @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
- # @return [Boolean] A boolean indicating whether the next item is the first
- # in its namespace. This is used to determine whether to insert a blank
- # line before it or not.
- attr_accessor :next_item_is_first_in_namespace
-
# Create a new RBI generator.
# @param [Hash] options
# @option options [Integer] break_params
# @option options [Boolean] replace_errors_with_untyped
+ # @option options [Boolean] replace_unresolved_with_untyped
# @option options [Boolean] comments
+ # @option options [Parlour::RbiGenerator] generator
+ # @option options [Parlour::RbiGenerator::Namespace] root
# @return [void]
def initialize(options)
- @rbi_contents = ['# typed: strong']
+ @parlour = options[:parlour] || Parlour::RbiGenerator.new
+ @current_object = options[:root] || @parlour.root
+
@namespace_count = 0
@method_count = 0
- @break_params = options[:break_params]
- @replace_errors_with_untyped = options[:replace_errors_with_untyped]
@warnings = []
- @next_item_is_first_in_namespace = true
+ @replace_errors_with_untyped = options[:replace_errors_with_untyped]
+ @replace_unresolved_with_untyped = options[:replace_unresolved_with_untyped]
+
# Hook the logger so that messages are added as comments to the RBI file
- Logging.add_hook do |type, msg, item, indent_level = 0|
- rbi_contents << "#{' ' * (indent_level + 1)}# sord #{type} - #{msg}"
+ Logging.add_hook do |type, msg, item|
+ @current_object.add_comment_to_next_child("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
+ Logging.add_hook do |type, msg, item|
+ # TODO: is it possible to get line numbers here?
+ warnings << [msg, item, 0] if type == :warn
end
end
# Increment the namespace counter.
# @return [void]
@@ -63,245 +61,213 @@
# @return [void]
def count_method
@method_count += 1
end
- # Adds a single blank line to the RBI file, unless this item is the first
- # in its namespace.
- # @return [void]
- def add_blank
- rbi_contents << '' unless next_item_is_first_in_namespace
- self.next_item_is_first_in_namespace = false
- end
-
# Given a YARD CodeObject, add lines defining its mixins (that is, extends
# and includes) to the current RBI file. Returns the number of mixins.
# @param [YARD::CodeObjects::Base] item
- # @param [Integer] indent_level
# @return [Integer]
- def add_mixins(item, indent_level)
- includes = item.instance_mixins
- extends = item.class_mixins
-
- extends.reverse_each do |this_extend|
- rbi_contents << "#{' ' * (indent_level + 1)}extend #{this_extend.path}"
+ def add_mixins(item)
+ item.instance_mixins.reverse_each do |i|
+ @current_object.create_include(i.path.to_s)
end
- includes.reverse_each do |this_include|
- rbi_contents << "#{' ' * (indent_level + 1)}include #{this_include.path}"
+ item.class_mixins.reverse_each do |e|
+ @current_object.create_extend(e.path.to_s)
end
- extends.length + includes.length
+ item.instance_mixins.length + item.class_mixins.length
end
- # Given an array of parameters and a return type, inserts the signature for
- # a method with those properties into the current RBI file.
- # @param [Array<String>] params
- # @param [String] returns
- # @param [Integer] indent_level
+ # Given a YARD NamespaceObject, add lines defining constants.
+ # @param [YARD::CodeObjects::NamespaceObject] item
# @return [void]
- def add_signature(params, returns, indent_level)
- if params.empty?
- rbi_contents << "#{' ' * (indent_level + 1)}sig { #{returns} }"
- return
+ def add_constants(item)
+ item.constants.each do |constant|
+ # Take a constant (like "A::B::CONSTANT"), split it on each '::', and
+ # set the constant name to the last string in the array.
+ constant_name = constant.to_s.split('::').last
+
+ # Add the constant to the current object being generated.
+ @current_object.create_constant(constant_name, value: "T.let(#{constant.value}, T.untyped)")
end
-
- if params.length >= @break_params
- rbi_contents << "#{' ' * (indent_level + 1)}sig do"
- rbi_contents << "#{' ' * (indent_level + 2)}params("
- params.each.with_index do |param, i|
- terminator = params.length - 1 == i ? '' : ','
- rbi_contents << "#{' ' * (indent_level + 3)}#{param}#{terminator}"
- end
- rbi_contents << "#{' ' * (indent_level + 2)}).#{returns}"
- rbi_contents << "#{' ' * (indent_level + 1)}end"
- else
- rbi_contents << "#{' ' * (indent_level + 1)}sig { params(#{params.join(', ')}).#{returns} }"
- 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, indent_level)
- item.meths.each do |meth|
+ def add_methods(item)
+ item.meths(inherited: false).each do |meth|
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
- add_blank
-
- 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')
- if default.nil?
- "#{name}"
- elsif !default.nil? && name.end_with?(':')
- "#{name} #{default}"
- else
- "#{name} = #{default}"
- end
- end.join(", ")
-
# This is better than iterating over YARD's "@param" tags directly
# because it includes parameters without documentation
# (The gsubs allow for better splat-argument compatibility)
- parameter_names_to_tags = meth.parameters.map do |name, _|
- [name, meth.tags('param')
- .find { |p| p.name&.gsub('*', '') == name.gsub('*', '') }]
+ parameter_names_and_defaults_to_tags = meth.parameters.map do |name, default|
+ [[name, default], meth.tags('param')
+ .find { |p| p.name&.gsub('*', '')&.gsub(':', '') == name.gsub('*', '').gsub(':', '') }]
end.to_h
- sig_params_list = parameter_names_to_tags.map do |name, tag|
- name = name.gsub('*', '')
+ parameter_types = parameter_names_and_defaults_to_tags.map do |name_and_default, tag|
+ name = name_and_default.first
if tag
- "#{name}: #{TypeConverter.yard_to_sorbet(tag.types, meth, indent_level, @replace_errors_with_untyped)}"
+ TypeConverter.yard_to_sorbet(tag.types, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
elsif name.start_with? '&'
- # Cut the ampersand from the block parameter name
- name = name.gsub('&', '')
-
# Find yieldparams and yieldreturn
yieldparams = meth.tags('yieldparam')
yieldreturn = meth.tag('yieldreturn')&.types
yieldreturn = nil if yieldreturn&.length == 1 &&
yieldreturn&.first&.downcase == 'void'
# Create strings
params_string = yieldparams.map do |param|
- "#{param.name.gsub('*', '')}: #{TypeConverter.yard_to_sorbet(param.types, meth, indent_level, @replace_errors_with_untyped)}" unless param.name.nil?
+ "#{param.name.gsub('*', '')}: #{TypeConverter.yard_to_sorbet(param.types, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)}" unless param.name.nil?
end.join(', ')
- return_string = TypeConverter.yard_to_sorbet(yieldreturn, meth, indent_level, @replace_errors_with_untyped)
+ return_string = TypeConverter.yard_to_sorbet(yieldreturn, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
# Create proc types, if possible
if yieldparams.empty? && yieldreturn.nil?
- "#{name}: T.untyped"
+ 'T.untyped'
elsif yieldreturn.nil?
- "#{name}: T.proc#{params_string.empty? ? '' : ".params(#{params_string})"}.void"
+ "T.proc#{params_string.empty? ? '' : ".params(#{params_string})"}.void"
else
- "#{name}: T.proc#{params_string.empty? ? '' : ".params(#{params_string})"}.returns(#{return_string})"
+ "T.proc#{params_string.empty? ? '' : ".params(#{params_string})"}.returns(#{return_string})"
end
elsif meth.path.end_with? '='
# Look for the matching getter method
getter_path = meth.path[0...-1]
getter = item.meths.find { |m| m.path == getter_path }
unless getter
- if parameter_names_to_tags.length == 1 \
+ if parameter_names_and_defaults_to_tags.length == 1 \
&& meth.tags('param').length == 1 \
&& meth.tag('param').types
- Logging.infer("argument name in single @param inferred as #{parameter_names_to_tags.first.first.inspect}", meth, indent_level)
- next "#{name}: #{TypeConverter.yard_to_sorbet(meth.tag('param').types, meth, indent_level, @replace_errors_with_untyped)}"
+ Logging.infer("argument name in single @param inferred as #{parameter_names_and_defaults_to_tags.first.first.first.inspect}", meth)
+ next TypeConverter.yard_to_sorbet(meth.tag('param').types, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
else
- Logging.omit("no YARD type given for #{name.inspect}, using T.untyped", meth, indent_level)
- next "#{name}: T.untyped"
+ Logging.omit("no YARD type given for #{name.inspect}, using T.untyped", meth)
+ next 'T.untyped'
end
end
inferred_type = TypeConverter.yard_to_sorbet(
- getter.tags('return').flat_map(&:types), meth, indent_level, @replace_errors_with_untyped)
+ getter.tags('return').flat_map(&:types), meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
- 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}"
+ Logging.infer("inferred type of parameter #{name.inspect} as #{inferred_type} using getter's return type", meth)
+ inferred_type
else
# Is this the only argument, and was a @param specified without an
# argument name? If so, infer it
- if parameter_names_to_tags.length == 1 \
+ if parameter_names_and_defaults_to_tags.length == 1 \
&& meth.tags('param').length == 1 \
&& meth.tag('param').types
- Logging.infer("argument name in single @param inferred as #{parameter_names_to_tags.first.first.inspect}", meth, indent_level)
- "#{name}: #{TypeConverter.yard_to_sorbet(meth.tag('param').types, meth, indent_level, @replace_errors_with_untyped)}"
+ Logging.infer("argument name in single @param inferred as #{parameter_names_and_defaults_to_tags.first.first.first.inspect}", meth)
+ TypeConverter.yard_to_sorbet(meth.tag('param').types, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
else
- 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"
+ Logging.omit("no YARD type given for #{name.inspect}, using T.untyped", meth)
+ 'T.untyped'
end
end
end
return_tags = meth.tags('return')
returns = if return_tags.length == 0
- Logging.omit("no YARD return type given, using T.untyped", meth, indent_level)
- "returns(T.untyped)"
+ Logging.omit("no YARD return type given, using T.untyped", meth)
+ 'T.untyped'
elsif return_tags.length == 1 && return_tags&.first&.types&.first&.downcase == "void"
- "void"
+ nil
else
- "returns(#{TypeConverter.yard_to_sorbet(meth.tag('return').types, meth, indent_level, @replace_errors_with_untyped)})"
+ TypeConverter.yard_to_sorbet(meth.tag('return').types, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
end
- prefix = meth.scope == :class ? 'self.' : ''
+ parlour_params = parameter_names_and_defaults_to_tags
+ .zip(parameter_types)
+ .map do |((name, default), _), type|
+ # If the default is "nil" but the type is not nilable, then it
+ # should become nilable
+ # (T.untyped can include nil, so don't alter that)
+ type = "T.nilable(#{type})" \
+ if default == 'nil' && !type.start_with?('T.nilable') && type != 'T.untyped'
+ Parlour::RbiGenerator::Parameter.new(
+ name.to_s,
+ type: type,
+ default: default
+ )
+ end
- add_signature(sig_params_list, returns, indent_level)
-
- rbi_contents << "#{' ' * (indent_level + 1)}def #{prefix}#{meth.name}(#{parameter_list}); end"
+ @current_object.create_method(
+ meth.name.to_s,
+ parameters: parlour_params,
+ returns: returns,
+ class_method: meth.scope == :class
+ )
end
end
# Given a YARD NamespaceObject, add lines defining its mixins, methods
# and children to the RBI file.
# @param [YARD::CodeObjects::NamespaceObject] item
- # @param [Integer] indent_level
# @return [void]
- def add_namespace(item, indent_level = 0)
+ def add_namespace(item)
count_namespace
- add_blank
- if item.type == :class && item.superclass.to_s != "Object"
- rbi_contents << "#{' ' * indent_level}class #{item.name} < #{item.superclass.path}"
- else
- rbi_contents << "#{' ' * indent_level}#{item.type} #{item.name}"
- end
+ superclass = nil
+ superclass = item.superclass.path.to_s if item.type == :class && item.superclass.to_s != "Object"
- self.next_item_is_first_in_namespace = true
- if add_mixins(item, indent_level) > 0
- self.next_item_is_first_in_namespace = false
- end
- add_methods(item, indent_level)
+ parent = @current_object
+ @current_object = item.type == :class \
+ ? parent.create_class(item.name.to_s, superclass: superclass)
+ : parent.create_module(item.name.to_s)
+ add_mixins(item)
+ add_methods(item)
+ add_constants(item)
+
item.children.select { |x| [:class, :module].include?(x.type) }
- .each { |child| add_namespace(child, indent_level + 1) }
+ .each { |child| add_namespace(child) }
- self.next_item_is_first_in_namespace = false
-
- rbi_contents << "#{' ' * indent_level}end"
+ @current_object = parent
end
- # Generates the RBI file from the loading registry and returns its contents.
- # You must load a registry first!
- # @return [String]
- def generate
+ # Populates the RBI generator with the contents of the YARD registry. You
+ # must load the YARD registry first!
+ # @return [void]
+ def populate
# Generate top-level modules, which recurses to all modules
YARD::Registry.root.children
.select { |x| [:class, :module].include?(x.type) }
.each { |child| add_namespace(child) }
+ end
- rbi_contents.join("\n")
+ # Populates the RBI generator with the contents of the YARD registry, then
+ # uses the loaded Parlour::RbiGenerator to generate the RBI file. You must
+ # load the YARD registry first!
+ # @return [void]
+ def generate
+ populate
+ @parlour.rbi
end
- # Generates the RBI file and writes it to the given file path, printing a
- # summary and any warnings at the end. The registry is also loaded.
- # @param [String, nil] filename
+ # Loads the YARD registry, populates the RBI file, and prints any relevant
+ # final logs.
# @return [void]
- def run(filename)
- raise 'No filename specified' unless filename
-
+ def run
# Get YARD ready
YARD::Registry.load!
- # Write the file
- File.write(filename, generate)
+ # Populate the RBI
+ populate
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.")
@@ -316,13 +282,13 @@
if @replace_errors_with_untyped
Logging.warn("The types which caused them have been replaced with T.untyped.")
else
Logging.warn("The types which caused them have been replaced with SORD_ERROR_ constants.")
end
- Logging.warn("Please edit the file near the line numbers given to fix these errors.")
+ Logging.warn("Please edit the file 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}"
+ warnings.each do |(msg, item, _)|
+ puts " (#{Rainbow(item&.path).bold}) #{msg}"
end
end
rescue
Logging.error($!)
$@.each do |line|