lib/sord/rbi_generator.rb in sord-1.0.0 vs lib/sord/rbi_generator.rb in sord-2.0.0
- old
+ new
@@ -38,10 +38,11 @@
@replace_errors_with_untyped = options[:replace_errors_with_untyped]
@replace_unresolved_with_untyped = options[:replace_unresolved_with_untyped]
@keep_original_comments = options[:keep_original_comments]
@skip_constants = options[:skip_constants]
+ @use_original_initialize_return = options[:use_original_initialize_return]
# Hook the logger so that messages are added as comments to the RBI file
Logging.add_hook do |type, msg, item|
@current_object.add_comment_to_next_child("sord #{type} - #{msg}")
end if options[:sord_comments]
@@ -94,10 +95,101 @@
c.add_comments(constant.docstring.all.split("\n"))
end
end
end
+ # Adds comments to an RBI object based on a docstring.
+ # @param [YARD::CodeObjects::NamespaceObject] item
+ # @param [Parlour::RbiGenerator::RbiObject] rbi_object
+ # @return [void]
+ def add_comments(item, rbi_object)
+ if @keep_original_comments
+ rbi_object.add_comments(item.docstring.all.split("\n"))
+ else
+ parser = YARD::Docstring.parser
+ parser.parse(item.docstring.all)
+
+ docs_array = parser.text.split("\n")
+
+ # Add @param tags if there are any with names and descriptions.
+ params = parser.tags.select { |tag| tag.tag_name == 'param' && tag.is_a?(YARD::Tags::Tag) && !tag.name.nil? }
+ # Add a blank line if there's anything before the params.
+ docs_array << '' if docs_array.length.positive? && params.length.positive?
+ params.each do |param|
+ docs_array << '' if docs_array.last != '' && docs_array.length.positive?
+ # Output params in the form of:
+ # _@param_ `foo` — Lorem ipsum.
+ # _@param_ `foo`
+ if param.text.nil? || param.text == ''
+ docs_array << "_@param_ `#{param.name}`"
+ else
+ docs_array << "_@param_ `#{param.name}` — #{param.text.gsub("\n", " ")}"
+ end
+ end
+
+ # Add @return tags (there could possibly be more than one, despite this not being supported)
+ returns = parser.tags.select { |tag| tag.tag_name == 'return' && tag.is_a?(YARD::Tags::Tag) && !tag.text.nil? && tag.text.strip != '' }
+ # Add a blank line if there's anything before the returns.
+ docs_array << '' if docs_array.length.positive? && returns.length.positive?
+ returns.each do |retn|
+ docs_array << '' if docs_array.last != '' && docs_array.length.positive?
+ # Output returns in the form of:
+ # _@return_ — Lorem ipsum.
+ docs_array << "_@return_ — #{retn.text}"
+ end
+
+ # Iterate through the @example tags for a given YARD doc and output them in Markdown codeblocks.
+ examples = parser.tags.select { |tag| tag.tag_name == 'example' && tag.is_a?(YARD::Tags::Tag) }
+ examples.each do |example|
+ # Only add a blank line if there's anything before the example.
+ docs_array << '' if docs_array.length.positive?
+ # Include the example's 'name' if there is one.
+ docs_array << example.name unless example.name.nil? || example.name == ""
+ docs_array << "```ruby"
+ docs_array.concat(example.text.split("\n"))
+ docs_array << "```"
+ end if examples.length.positive?
+
+ # Add @note and @deprecated tags.
+ notice_tags = parser.tags.select { |tag| ['note', 'deprecated'].include?(tag.tag_name) && tag.is_a?(YARD::Tags::Tag) }
+ # Add a blank line if there's anything before the params.
+ docs_array << '' if docs_array.last != '' && docs_array.length.positive? && notice_tags.length.positive?
+ notice_tags.each do |notice_tag|
+ docs_array << '' if docs_array.last != ''
+ # Output note/deprecated/see in the form of:
+ # _@note_ — Lorem ipsum.
+ # _@note_
+ if notice_tag.text.nil?
+ docs_array << "_@#{notice_tag.tag_name}_"
+ else
+ docs_array << "_@#{notice_tag.tag_name}_ — #{notice_tag.text}"
+ end
+ end
+
+ # Add @see tags.
+ see_tags = parser.tags.select { |tag| tag.tag_name == 'see' && tag.is_a?(YARD::Tags::Tag) }
+ # Add a blank line if there's anything before the params.
+ docs_array << '' if docs_array.last != '' && docs_array.length.positive? && see_tags.length.positive?
+ see_tags.each do |see_tag|
+ docs_array << '' if docs_array.last != ''
+ # Output note/deprecated/see in the form of:
+ # _@see_ `B` — Lorem ipsum.
+ # _@see_ `B`
+ if see_tag.text.nil?
+ docs_array << "_@see_ `#{see_tag.name}`"
+ else
+ docs_array << "_@see_ `#{see_tag.name}` — #{see_tag.text}"
+ end
+ end
+
+ # fix: yard text may contains multiple line. should deal \n.
+ # else generate text will be multiple line and only first line is commented
+ docs_array = docs_array.flat_map {|line| line.empty? ? [""] : line.split("\n")}
+ rbi_object.add_comments(docs_array)
+ end
+ end
+
# Given a YARD NamespaceObject, add lines defining its methods and their
# signatures to the current RBI file.
# @param [YARD::CodeObjects::NamespaceObject] item
# @return [void]
def add_methods(item)
@@ -108,10 +200,15 @@
# separate method. Sorbet will handle it automatically.
if meth.is_alias?
next
end
+ # If the method is an attribute, it'll be handled by add_attributes
+ if meth.is_attribute?
+ next
+ end
+
# Sort parameters
meth.parameters.reverse.sort! { |pair1, pair2| sort_params(pair1, pair2) }
# 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)
@@ -189,11 +286,13 @@
end
end
end
return_tags = meth.tags('return')
- returns = if return_tags.length == 0
+ returns = if meth.name == :initialize && !@use_original_initialize_return
+ nil
+ elsif return_tags.length == 0
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"
nil
else
@@ -219,93 +318,69 @@
meth.name.to_s,
parameters: parlour_params,
returns: returns,
class_method: meth.scope == :class
) do |m|
- if @keep_original_comments
- m.add_comments(meth.docstring.all.split("\n"))
- else
- parser = YARD::Docstring.parser
- parser.parse(meth.docstring.all)
+ add_comments(meth, m)
+ end
+ end
+ end
- docs_array = parser.text.split("\n")
+ # Given a YARD NamespaceObject, add lines defining either its class
+ # and instance attributes and their signatures to the current RBI file.
+ # @param [YARD::CodeObjects::NamespaceObject] item
+ # @return [void]
+ def add_attributes(item)
+ [:class, :instance].each do |attr_loc|
+ # Grab attributes for the current location (class or instance)
+ attrs = item.attributes[attr_loc]
+ attrs.each do |name, attribute|
+ reader = attribute[:read]
+ writer = attribute[:write]
- # Add @param tags if there are any with names and descriptions.
- params = parser.tags.select { |tag| tag.tag_name == 'param' && tag.is_a?(YARD::Tags::Tag) && !tag.name.nil? }
- # Add a blank line if there's anything before the params.
- docs_array << '' if docs_array.length.positive? && params.length.positive?
- params.each do |param|
- docs_array << '' if docs_array.last != '' && docs_array.length.positive?
- # Output params in the form of:
- # _@param_ `foo` — Lorem ipsum.
- # _@param_ `foo`
- if param.text.nil? || param.text == ''
- docs_array << "_@param_ `#{param.name}`"
- else
- docs_array << "_@param_ `#{param.name}` — #{param.text.gsub("\n", " ")}"
- end
- end
+ unless reader || writer
+ Logging.warn("attribute is not readable or writable somehow, skipping", attribute)
+ next
+ end
- # Add @return tags (there could possibly be more than one, despite this not being supported)
- returns = parser.tags.select { |tag| tag.tag_name == 'return' && tag.is_a?(YARD::Tags::Tag) && !tag.text.nil? && tag.text.strip != '' }
- # Add a blank line if there's anything before the returns.
- docs_array << '' if docs_array.length.positive? && returns.length.positive?
- returns.each do |retn|
- docs_array << '' if docs_array.last != '' && docs_array.length.positive?
- # Output returns in the form of:
- # _@return_ — Lorem ipsum.
- docs_array << "_@return_ — #{retn.text}"
- end
+ # Get all given types
+ yard_types = []
+ if reader
+ yard_types += reader.tags('return').flat_map(&:types).compact.reject { |x| x.downcase == 'void' } +
+ reader.tags('param').flat_map(&:types)
+ end
+ if writer
+ yard_types += writer.tags('return').flat_map(&:types).compact.reject { |x| x.downcase == 'void' } +
+ writer.tags('param').flat_map(&:types)
+ end
- # Iterate through the @example tags for a given YARD doc and output them in Markdown codeblocks.
- examples = parser.tags.select { |tag| tag.tag_name == 'example' && tag.is_a?(YARD::Tags::Tag) }
- examples.each do |example|
- # Only add a blank line if there's anything before the example.
- docs_array << '' if docs_array.length.positive?
- # Include the example's 'name' if there is one.
- docs_array << example.name unless example.name.nil? || example.name == ""
- docs_array << "```ruby"
- docs_array.concat(example.text.split("\n"))
- docs_array << "```"
- end if examples.length.positive?
+ # Use T.untyped if not types specified anywhere, otherwise try to
+ # compute Sorbet type given all these types
+ if yard_types.empty?
+ Logging.omit("no YARD type given for #{name.inspect}, using T.untyped", reader || writer)
+ sorbet_type = 'T.untyped'
+ else
+ sorbet_type = TypeConverter.yard_to_sorbet(
+ yard_types, reader || writer, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
+ end
- # Add @note and @deprecated tags.
- notice_tags = parser.tags.select { |tag| ['note', 'deprecated'].include?(tag.tag_name) && tag.is_a?(YARD::Tags::Tag) }
- # Add a blank line if there's anything before the params.
- docs_array << '' if docs_array.last != '' && docs_array.length.positive? && notice_tags.length.positive?
- notice_tags.each do |notice_tag|
- docs_array << '' if docs_array.last != ''
- # Output note/deprecated/see in the form of:
- # _@note_ — Lorem ipsum.
- # _@note_
- if notice_tag.text.nil?
- docs_array << "_@#{notice_tag.tag_name}_"
- else
- docs_array << "_@#{notice_tag.tag_name}_ — #{notice_tag.text}"
- end
- end
+ # Generate attribute
+ if reader && writer
+ kind = :accessor
+ elsif reader
+ kind = :reader
+ elsif writer
+ kind = :writer
+ end
- # Add @see tags.
- see_tags = parser.tags.select { |tag| tag.tag_name == 'see' && tag.is_a?(YARD::Tags::Tag) }
- # Add a blank line if there's anything before the params.
- docs_array << '' if docs_array.last != '' && docs_array.length.positive? && see_tags.length.positive?
- see_tags.each do |see_tag|
- docs_array << '' if docs_array.last != ''
- # Output note/deprecated/see in the form of:
- # _@see_ `B` — Lorem ipsum.
- # _@see_ `B`
- if see_tag.text.nil?
- docs_array << "_@see_ `#{see_tag.name}`"
- else
- docs_array << "_@see_ `#{see_tag.name}` — #{see_tag.text}"
- end
- end
-
- # fix: yard text may contains multiple line. should deal \n.
- # else generate text will be multiple line and only first line is commented
- docs_array = docs_array.flat_map {|line| line.empty? ? [""] : line.split("\n")}
- m.add_comments(docs_array)
+ @current_object.create_attribute(
+ name.to_s,
+ kind: kind,
+ type: sorbet_type,
+ class_attribute: (attr_loc == :class)
+ ) do |m|
+ add_comments(reader || writer, m)
end
end
end
end
@@ -325,9 +400,10 @@
: parent.create_module(item.name.to_s)
@current_object.add_comments(item.docstring.all.split("\n"))
add_mixins(item)
add_methods(item)
+ add_attributes(item)
add_constants(item) unless @skip_constants
item.children.select { |x| [:class, :module].include?(x.type) }
.each { |child| add_namespace(child) }