yard-sorbet 0.4.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f5adecef50618ea39bdb103aa89d5a6561e838ad9860b7b4103fc39eeb268f5b
4
- data.tar.gz: 33a6ce0b55c6a9934e3491a1a1feff06da39919d6b5196a67bab5d3861cd9425
3
+ metadata.gz: ee5168e45f37fe7d16db534727b57fad2a08e05acda8298ef421589aa72f0354
4
+ data.tar.gz: ce10ba6df63c35ba4b52eb4f7d8b02878cfb01fea7424e966aad739691d1d767
5
5
  SHA512:
6
- metadata.gz: '0139df44b176ef888fb74101915593dd43349133abc0cec0e60a16be72c72ddbbfdefa1ded2dc535764ef898a44156e8a5d74ff40d62ab109818277666f2e44f'
7
- data.tar.gz: 2015b2b80680fcea678afc3b5d1a68350218904cc97a93ef4dec35cb1e07918d8766c77f309b21371dd52609c98a348bcb932899c360f721e0a00d3aedf0f175
6
+ metadata.gz: faa0b506388d501f6567ddfd08144185b1e761a1d4d4cfec12e3256f3985f626ddbc2da1423b37896fbe132a1e729a3c44b995b957eb0391aa48ded9519978d4
7
+ data.tar.gz: 2afbc9875afecab271abccfd12b5023c3e7db429f23f689fdbed18ed2e0fd7f3b51137ee86b649bb501ea921c469774e2096887d1d063599ae72939951b2f719
data/lib/yard-sorbet.rb CHANGED
@@ -4,11 +4,11 @@
4
4
  require 'sorbet-runtime'
5
5
  require 'yard'
6
6
 
7
- # top-level namespace
8
- module YARDSorbet; end
7
+ require_relative 'yard-sorbet/version'
9
8
 
10
9
  require_relative 'yard-sorbet/directives'
11
- require_relative 'yard-sorbet/sig_handler'
10
+ require_relative 'yard-sorbet/handlers'
11
+ require_relative 'yard-sorbet/node_utils'
12
12
  require_relative 'yard-sorbet/sig_to_yard'
13
- require_relative 'yard-sorbet/struct_handler'
14
- require_relative 'yard-sorbet/version'
13
+ require_relative 'yard-sorbet/t_struct_prop'
14
+ require_relative 'yard-sorbet/tag_utils'
@@ -1,28 +1,30 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- # Extract & re-add directives to a docstring
5
- module YARDSorbet::Directives
6
- extend T::Sig
4
+ module YARDSorbet
5
+ # Extract & re-add directives to a docstring
6
+ module Directives
7
+ extend T::Sig
7
8
 
8
- sig { params(docstring: T.nilable(String)).returns([YARD::Docstring, T::Array[String]]) }
9
- def self.extract_directives(docstring)
10
- parser = YARD::DocstringParser.new.parse(docstring)
11
- # Directives are already parsed at this point, and there doesn't
12
- # seem to be an API to tweeze them from one node to another without
13
- # managing YARD internal state. Instead, we just extract them from
14
- # the raw text and re-attach them.
15
- directives = parser.raw_text&.split("\n")&.select do |line|
16
- line.start_with?('@!')
17
- end || []
9
+ sig { params(docstring: T.nilable(String)).returns([YARD::Docstring, T::Array[String]]) }
10
+ def self.extract_directives(docstring)
11
+ parser = YARD::DocstringParser.new.parse(docstring)
12
+ # Directives are already parsed at this point, and there doesn't
13
+ # seem to be an API to tweeze them from one node to another without
14
+ # managing YARD internal state. Instead, we just extract them from
15
+ # the raw text and re-attach them.
16
+ directives = parser.raw_text&.split("\n")&.select do |line|
17
+ line.start_with?('@!')
18
+ end || []
18
19
 
19
- [parser.to_docstring, directives]
20
- end
20
+ [parser.to_docstring, directives]
21
+ end
21
22
 
22
- sig { params(docstring: String, directives: T::Array[String]).void }
23
- def self.add_directives(docstring, directives)
24
- directives.each do |directive|
25
- docstring.concat("\n#{directive}")
23
+ sig { params(docstring: String, directives: T::Array[String]).void }
24
+ def self.add_directives(docstring, directives)
25
+ directives.each do |directive|
26
+ docstring.concat("\n#{directive}")
27
+ end
26
28
  end
27
29
  end
28
30
  end
@@ -0,0 +1,14 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ module YARDSorbet
5
+ # Custom YARD Handlers
6
+ # @see https://rubydoc.info/gems/yard/YARD/Handlers/Base YARD Base Handler documentation
7
+ module Handlers; end
8
+ end
9
+
10
+ require_relative 'handlers/abstract_dsl_handler'
11
+ require_relative 'handlers/enums_handler'
12
+ require_relative 'handlers/sig_handler'
13
+ require_relative 'handlers/struct_class_handler'
14
+ require_relative 'handlers/struct_prop_handler'
@@ -0,0 +1,30 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module YARDSorbet
5
+ module Handlers
6
+ # Apllies an `@abstract` tag to `abstract!`/`interface!` modules (if not alerady present).
7
+ class AbstractDSLHandler < YARD::Handlers::Ruby::Base
8
+ extend T::Sig
9
+
10
+ handles method_call(:abstract!), method_call(:interface!)
11
+ namespace_only
12
+
13
+ # The text accompanying the `@abstract` tag.
14
+ # @see https://github.com/lsegal/yard/blob/main/templates/default/docstring/html/abstract.erb
15
+ # The `@abstract` tag template
16
+ TAG_TEXT = 'Subclasses must implement the `abstract` methods below.'
17
+ # Extra text for class namespaces
18
+ CLASS_TAG_TEXT = T.let("It cannont be directly instantiated. #{TAG_TEXT}", String)
19
+
20
+ sig { void }
21
+ def process
22
+ return if namespace.has_tag?(:abstract)
23
+
24
+ text = namespace.is_a?(YARD::CodeObjects::ClassObject) ? CLASS_TAG_TEXT : TAG_TEXT
25
+ tag = YARD::Tags::Tag.new(:abstract, text)
26
+ namespace.add_tag(tag)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,34 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module YARDSorbet
5
+ module Handlers
6
+ # Handle `enums` calls, registering enum values as constants
7
+ class EnumsHandler < YARD::Handlers::Ruby::Base
8
+ extend T::Sig
9
+
10
+ handles method_call(:enums)
11
+ namespace_only
12
+
13
+ sig { void }
14
+ def process
15
+ statement.traverse do |node|
16
+ if const_assign_node?(node)
17
+ register YARD::CodeObjects::ConstantObject.new(namespace, node.first.source) do |obj|
18
+ obj.docstring = node.docstring
19
+ obj.source = node
20
+ obj.value = node.last.source
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ sig { params(node: YARD::Parser::Ruby::AstNode).returns(T::Boolean) }
29
+ def const_assign_node?(node)
30
+ node.type == :assign && node[0][0].type == :const
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,70 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module YARDSorbet
5
+ module Handlers
6
+ # A YARD Handler for Sorbet type declarations
7
+ class SigHandler < YARD::Handlers::Ruby::Base
8
+ extend T::Sig
9
+
10
+ handles method_call(:sig)
11
+ namespace_only
12
+
13
+ # These node types attached to sigs represent attr_* declarations
14
+ ATTR_NODE_TYPES = T.let(%i[command fcall], T::Array[Symbol])
15
+ private_constant :ATTR_NODE_TYPES
16
+
17
+ # Swap the method definition docstring and the sig docstring.
18
+ # Parse relevant parts of the `sig` and include them as well.
19
+ sig { void }
20
+ def process
21
+ method_node = NodeUtils.get_method_node(NodeUtils.sibling_node(statement))
22
+ docstring, directives = Directives.extract_directives(statement.docstring)
23
+ parse_sig(method_node, docstring)
24
+ method_node.docstring = docstring.to_raw
25
+ Directives.add_directives(method_node.docstring, directives)
26
+ statement.docstring = nil
27
+ end
28
+
29
+ private
30
+
31
+ sig { params(method_node: YARD::Parser::Ruby::AstNode, docstring: YARD::Docstring).void }
32
+ def parse_sig(method_node, docstring)
33
+ NodeUtils.bfs_traverse(statement) do |n|
34
+ case n.source
35
+ when 'abstract'
36
+ YARDSorbet::TagUtils.upsert_tag(docstring, 'abstract')
37
+ when 'params'
38
+ parse_params(method_node, n, docstring)
39
+ when 'returns', 'void'
40
+ parse_return(n, docstring)
41
+ end
42
+ end
43
+ end
44
+
45
+ sig do
46
+ params(
47
+ method_node: YARD::Parser::Ruby::AstNode,
48
+ node: YARD::Parser::Ruby::AstNode,
49
+ docstring: YARD::Docstring
50
+ ).void
51
+ end
52
+ def parse_params(method_node, node, docstring)
53
+ return if ATTR_NODE_TYPES.include?(method_node.type)
54
+
55
+ sibling = NodeUtils.sibling_node(node)
56
+ sibling[0][0].each do |p|
57
+ param_name = p[0][0]
58
+ types = SigToYARD.convert(p.last)
59
+ TagUtils.upsert_tag(docstring, 'param', types, param_name)
60
+ end
61
+ end
62
+
63
+ sig { params(node: YARD::Parser::Ruby::AstNode, docstring: YARD::Docstring).void }
64
+ def parse_return(node, docstring)
65
+ type = node.source == 'void' ? ['void'] : SigToYARD.convert(NodeUtils.sibling_node(node))
66
+ TagUtils.upsert_tag(docstring, 'return', type)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,72 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module YARDSorbet
5
+ module Handlers
6
+ # Class-level handler that folds all `const` and `prop` declarations into the constructor documentation
7
+ # this needs to be injected as a module otherwise the default Class handler will overwrite documentation
8
+ #
9
+ # @note this module is included in `YARD::Handlers::Ruby::ClassHandler`
10
+ module StructClassHandler
11
+ extend T::Sig
12
+
13
+ sig { void }
14
+ def process
15
+ super
16
+ return if extra_state.prop_docs.nil?
17
+
18
+ # lookup the full YARD path for the current class
19
+ class_ns = YARD::CodeObjects::ClassObject.new(namespace, statement[0].source.gsub(/\s/, ''))
20
+ props = extra_state.prop_docs[class_ns]
21
+ return if props.empty?
22
+
23
+ process_t_struct_props(props, class_ns)
24
+ end
25
+
26
+ private
27
+
28
+ # Create a virtual `initialize` method with all the `prop`/`const` arguments
29
+ sig { params(props: T::Array[TStructProp], class_ns: YARD::CodeObjects::ClassObject).void }
30
+ def process_t_struct_props(props, class_ns)
31
+ # having the name :initialize & the scope :instance marks this as the constructor.
32
+ object = YARD::CodeObjects::MethodObject.new(class_ns, :initialize, :instance)
33
+ # There is a chance that there is a custom initializer, so make sure we steal the existing docstring
34
+ # and source
35
+ docstring, directives = Directives.extract_directives(object.docstring)
36
+ object.tags.each { |tag| docstring.add_tag(tag) }
37
+ props.each do |prop|
38
+ TagUtils.upsert_tag(docstring, 'param', prop.types, prop.prop_name, prop.doc)
39
+ end
40
+ TagUtils.upsert_tag(docstring, 'return', ['void'])
41
+ decorate_t_struct_init(object, props, docstring, directives)
42
+ end
43
+
44
+ sig do
45
+ params(
46
+ object: YARD::CodeObjects::MethodObject,
47
+ props: T::Array[TStructProp],
48
+ docstring: YARD::Docstring,
49
+ directives: T::Array[String]
50
+ ).void
51
+ end
52
+ def decorate_t_struct_init(object, props, docstring, directives)
53
+ # Use kwarg style arguments, with optionals being marked with a default (unless an actual default was specified)
54
+ object.parameters = to_object_parameters(props)
55
+ # The "source" of our constructor is the field declarations
56
+ object.source ||= props.map(&:source).join("\n")
57
+ object.docstring = docstring
58
+ Directives.add_directives(object.docstring, directives)
59
+ end
60
+
61
+ sig { params(props: T::Array[TStructProp]).returns(T::Array[[String, T.nilable(String)]]) }
62
+ def to_object_parameters(props)
63
+ props.map do |prop|
64
+ default = prop.default || (prop.types.include?('nil') ? 'nil' : nil)
65
+ ["#{prop.prop_name}:", default]
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ YARD::Handlers::Ruby::ClassHandler.include YARDSorbet::Handlers::StructClassHandler
@@ -0,0 +1,5 @@
1
+ # typed: strict
2
+ # This is in an rbi so the runtime doesn't depend on experimental sorbet features
3
+ module YARDSorbet::Handlers::StructClassHandler
4
+ requires_ancestor YARD::Handlers::Ruby::ClassHandler
5
+ end
@@ -0,0 +1,72 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module YARDSorbet
5
+ module Handlers
6
+ # Handles all `const`/`prop` calls, creating accessor methods, and compiles them for later usage at the class level
7
+ # in creating a constructor
8
+ class StructPropHandler < YARD::Handlers::Ruby::Base
9
+ extend T::Sig
10
+
11
+ handles method_call(:const), method_call(:prop)
12
+ namespace_only
13
+
14
+ sig { void }
15
+ def process
16
+ name = statement.parameters.first.last.last.source
17
+ prop = make_prop(name)
18
+ update_state(prop)
19
+ object = YARD::CodeObjects::MethodObject.new(namespace, name, scope)
20
+ decorate_object(object, prop)
21
+ register_attrs(object, name)
22
+ end
23
+
24
+ private
25
+
26
+ # Add the source and docstring to the method object
27
+ sig { params(object: YARD::CodeObjects::MethodObject, prop: TStructProp).void }
28
+ def decorate_object(object, prop)
29
+ object.source = prop.source
30
+ # TODO: this should use `+` to delimit the attribute name when markdown is disabled
31
+ reader_docstring = prop.doc.empty? ? "Returns the value of attribute `#{prop.prop_name}`." : prop.doc
32
+ docstring = YARD::DocstringParser.new.parse(reader_docstring).to_docstring
33
+ docstring.add_tag(YARD::Tags::Tag.new(:return, '', prop.types))
34
+ object.docstring = docstring.to_raw
35
+ end
36
+
37
+ # Get the default prop value
38
+ sig { returns(T.nilable(String)) }
39
+ def default_value
40
+ default_node = statement.traverse { |n| break n if n.type == :label && n.source == 'default:' }
41
+ default_node.parent[1].source if default_node
42
+ end
43
+
44
+ sig { params(name: String).returns(TStructProp) }
45
+ def make_prop(name)
46
+ TStructProp.new(
47
+ default: default_value,
48
+ doc: statement.docstring.to_s,
49
+ prop_name: name,
50
+ source: statement.source,
51
+ types: SigToYARD.convert(statement.parameters[1])
52
+ )
53
+ end
54
+
55
+ # Register the field explicitly as an attribute.
56
+ # While `const` attributes are immutable, `prop` attributes may be reassigned.
57
+ sig { params(object: YARD::CodeObjects::MethodObject, name: String).void }
58
+ def register_attrs(object, name)
59
+ # Create the virtual method in our current scope
60
+ write = statement.method_name(true) == :prop ? object : nil
61
+ namespace.attributes[scope][name] ||= SymbolHash[read: object, write: write]
62
+ end
63
+
64
+ # Store the prop for use in the constructor definition
65
+ sig { params(prop: TStructProp).void }
66
+ def update_state(prop)
67
+ extra_state.prop_docs ||= Hash.new { |h, k| h[k] = [] }
68
+ extra_state.prop_docs[namespace] << prop
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,62 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module YARDSorbet
5
+ # Helper methods for working with `YARD` AST Nodes
6
+ module NodeUtils
7
+ extend T::Sig
8
+
9
+ # Command node types that can have type signatures
10
+ ATTRIBUTE_METHODS = T.let(%i[attr attr_accessor attr_reader attr_writer].freeze, T::Array[Symbol])
11
+ # Node types that can have type signatures
12
+ SIGABLE_NODE = T.type_alias do
13
+ T.any(YARD::Parser::Ruby::MethodDefinitionNode, YARD::Parser::Ruby::MethodCallNode)
14
+ end
15
+ # Skip these method contents during BFS node traversal, they can have their own nested types via `T.Proc`
16
+ SKIP_METHOD_CONTENTS = T.let(%i[params returns], T::Array[Symbol])
17
+
18
+ private_constant :ATTRIBUTE_METHODS, :SIGABLE_NODE
19
+
20
+ # Traverese AST nodes in breadth-first order
21
+ # @note This will skip over some node types.
22
+ # @yield [YARD::Parser::Ruby::AstNode]
23
+ sig do
24
+ params(
25
+ node: YARD::Parser::Ruby::AstNode,
26
+ _blk: T.proc.params(n: YARD::Parser::Ruby::AstNode).void
27
+ ).void
28
+ end
29
+ def self.bfs_traverse(node, &_blk)
30
+ queue = [node]
31
+ until queue.empty?
32
+ n = T.must(queue.shift)
33
+ yield n
34
+ n.children.each { |c| queue.push(c) }
35
+ queue.pop if n.is_a?(YARD::Parser::Ruby::MethodCallNode) && SKIP_METHOD_CONTENTS.include?(n.method_name(true))
36
+ end
37
+ end
38
+
39
+ # Gets the node that a sorbet `sig` can be attached do, bypassing visisbility modifiers and the like
40
+ sig { params(node: YARD::Parser::Ruby::AstNode).returns(SIGABLE_NODE) }
41
+ def self.get_method_node(node)
42
+ case node
43
+ when YARD::Parser::Ruby::MethodDefinitionNode
44
+ return node
45
+ when YARD::Parser::Ruby::MethodCallNode
46
+ return node if ATTRIBUTE_METHODS.include?(node.method_name(true))
47
+ end
48
+
49
+ node.jump(:def, :defs)
50
+ end
51
+
52
+ # Find and return the adjacent node (ascending)
53
+ # @raise [IndexError] if the node does not have an adjacent sibling (ascending)
54
+ sig { params(node: YARD::Parser::Ruby::AstNode).returns(YARD::Parser::Ruby::AstNode) }
55
+ def self.sibling_node(node)
56
+ siblings = node.parent.children
57
+ siblings.each_with_index.find do |sibling, i|
58
+ return siblings.fetch(i + 1) if sibling.equal?(node)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -1,111 +1,130 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- # Translate sig type syntax to YARD type syntax.
5
- module YARDSorbet::SigToYARD
6
- extend T::Sig
7
-
8
- IS_LEGACY_RUBY_VERSION = T.let(RUBY_VERSION.start_with?('2.5.'), T::Boolean)
9
-
10
- # @see https://yardoc.org/types.html
11
- sig { params(node: YARD::Parser::Ruby::AstNode).returns(T::Array[String]) }
12
- def self.convert(node)
13
- types = convert_type(node)
14
- # scrub newlines, as they break the YARD parser
15
- types.map { |type| type.gsub(/\n\s*/, ' ') }
16
- end
4
+ module YARDSorbet
5
+ # Translate `sig` type syntax to `YARD` type syntax.
6
+ module SigToYARD
7
+ extend T::Sig
8
+
9
+ # @see https://yardoc.org/types.html
10
+ sig { params(node: YARD::Parser::Ruby::AstNode).returns(T::Array[String]) }
11
+ def self.convert(node)
12
+ # scrub newlines, as they break the YARD parser
13
+ convert_node(node).map { |type| type.gsub(/\n\s*/, ' ') }
14
+ end
15
+
16
+ sig { params(node: YARD::Parser::Ruby::AstNode).returns(T::Array[String]) }
17
+ private_class_method def self.convert_node(node)
18
+ case node
19
+ when YARD::Parser::Ruby::MethodCallNode then convert_call(node)
20
+ when YARD::Parser::Ruby::ReferenceNode then convert_ref(node)
21
+ else convert_node_type(node)
22
+ end
23
+ end
24
+
25
+ sig { params(node: YARD::Parser::Ruby::AstNode).returns(T::Array[String]) }
26
+ private_class_method def self.convert_node_type(node)
27
+ case node.type
28
+ when :aref then convert_aref(node)
29
+ when :arg_paren then convert_node(node.first)
30
+ when :array then convert_array(node)
31
+ # Fixed hashes as return values are unsupported:
32
+ # https://github.com/lsegal/yard/issues/425
33
+ #
34
+ # Hash key params can be individually documented with `@option`, but
35
+ # sig translation is currently unsupported.
36
+ when :hash then ['Hash']
37
+ # seen when sig methods omit parentheses
38
+ when :list then convert_list(node)
39
+ else convert_unknown(node)
40
+ end
41
+ end
42
+
43
+ sig { params(node: YARD::Parser::Ruby::AstNode).returns(String) }
44
+ private_class_method def self.build_generic_type(node)
45
+ return node.source if node.empty? || node.type != :aref
46
+
47
+ collection_type = node.first.source
48
+ member_type = node.last.children.map { |child| build_generic_type(child) }.join(', ')
17
49
 
18
- sig { params(node: YARD::Parser::Ruby::AstNode).returns(T::Array[String]) }
19
- def self.convert_type(node)
20
- children = node.children
21
- case node.type
22
- when :aref
50
+ "#{collection_type}[#{member_type}]"
51
+ end
52
+
53
+ sig { params(node: YARD::Parser::Ruby::AstNode).returns(T::Array[String]) }
54
+ private_class_method def self.convert_aref(node)
23
55
  # https://www.rubydoc.info/gems/yard/file/docs/Tags.md#Parametrized_Types
24
- case children.first.source
25
- when 'T::Array', 'T::Enumerable', 'T::Range', 'T::Set'
26
- collection_type = children.first.source.split('::').last
27
- member_type = convert(children.last.children.first).join(', ')
28
- ["#{collection_type}<#{member_type}>"]
29
- when 'T::Hash'
30
- key_type = convert(children.last.children.first).join(', ')
31
- value_type = convert(children.last.children.last).join(', ')
32
- ["Hash{#{key_type} => #{value_type}}"]
56
+ case node.first.source
57
+ when 'T::Array', 'T::Enumerable', 'T::Range', 'T::Set' then convert_collection(node)
58
+ when 'T::Hash' then convert_hash(node)
33
59
  else
34
60
  log.info("Unsupported sig aref node #{node.source}")
35
61
  [build_generic_type(node)]
36
62
  end
37
- when :arg_paren
38
- convert(children.first.children.first)
39
- when :array
63
+ end
64
+
65
+ sig { params(node: YARD::Parser::Ruby::AstNode).returns(T::Array[String]) }
66
+ private_class_method def self.convert_array(node)
40
67
  # https://www.rubydoc.info/gems/yard/file/docs/Tags.md#Order-Dependent_Lists
41
- member_types = children.first.children.map { |n| convert(n) }.join(', ')
42
- ["Array(#{member_types})"]
43
- when :call
44
- if children[0].source == 'T'
45
- t_method = IS_LEGACY_RUBY_VERSION ? children[1].source : children[2].source
46
- case t_method
47
- when 'all', 'attached_class', 'class_of', 'enum', 'noreturn', 'self_type', 'type_parameter', 'untyped'
48
- # YARD doesn't have equivalent notions, so we just use the raw source
49
- [node.source]
50
- when 'any'
51
- children.last.children.first.children.map { |n| convert(n) }.flatten
52
- when 'nilable'
53
- # Order matters here, putting `nil` last results in a more concise
54
- # return syntax in the UI (superscripted `?`)
55
- convert(children.last) + ['nil']
56
- else
57
- log.warn("Unsupported T method #{node.source}")
58
- [node.source]
59
- end
60
- else
61
- [node.source]
62
- end
63
- when :const_path_ref, :const
64
- case node.source
65
- when 'T::Boolean'
66
- ['Boolean'] # YARD convention for booleans
67
- else
68
- [node.source]
69
- end
70
- when :hash, :list
71
- # Fixed hashes as return values are unsupported:
72
- # https://github.com/lsegal/yard/issues/425
73
- #
74
- # Hash key params can be individually documented with `@option`, but
75
- # sig translation is unsupported.
76
- ['Hash']
77
- when :var_ref
68
+ member_types = node.first.children.map { |n| convert_node(n) }
69
+ sequence = member_types.map { |mt| mt.size == 1 ? mt[0] : mt.to_s.tr('"', '') }.join(', ')
70
+ ["Array(#{sequence})"]
71
+ end
72
+
73
+ sig { params(node: YARD::Parser::Ruby::MethodCallNode).returns(T::Array[String]) }
74
+ private_class_method def self.convert_call(node)
75
+ node.namespace.source == 'T' ? convert_t_method(node) : [node.source]
76
+ end
77
+
78
+ sig { params(node: YARD::Parser::Ruby::AstNode).returns(T::Array[String]) }
79
+ private_class_method def self.convert_collection(node)
80
+ collection_type = node.first.source.split('::').last
81
+ member_type = convert_node(node.last.first).join(', ')
82
+ ["#{collection_type}<#{member_type}>"]
83
+ end
84
+
85
+ sig { params(node: YARD::Parser::Ruby::AstNode).returns(T::Array[String]) }
86
+ private_class_method def self.convert_hash(node)
87
+ kv = node.last.children
88
+ key_type = convert_node(kv.first).join(', ')
89
+ value_type = convert_node(kv.last).join(', ')
90
+ ["Hash{#{key_type} => #{value_type}}"]
91
+ end
92
+
93
+ sig { params(node: YARD::Parser::Ruby::AstNode).returns(T::Array[String]) }
94
+ private_class_method def self.convert_list(node)
95
+ node.children.size == 1 ? convert_node(node.children.first) : [node.source]
96
+ end
97
+
98
+ sig { params(node: YARD::Parser::Ruby::AstNode).returns(T::Array[String]) }
99
+ private_class_method def self.convert_ref(node)
100
+ source = node.source
101
+ case source
102
+ when 'T::Boolean' then ['Boolean'] # YARD convention for booleans
78
103
  # YARD convention is use singleton objects when applicable:
79
104
  # https://www.rubydoc.info/gems/yard/file/docs/Tags.md#Literals
80
- case node.source
81
- when 'FalseClass'
82
- ['false']
83
- when 'NilClass'
84
- ['nil']
85
- when 'TrueClass'
86
- ['true']
87
- else
88
- [node.source]
105
+ when 'FalseClass' then ['false']
106
+ when 'NilClass' then ['nil']
107
+ when 'TrueClass' then ['true']
108
+ else [source]
89
109
  end
90
- when :top_const_ref
91
- # A top-level constant reference, such as ::Klass
92
- # It contains a child node of type :const
93
- convert(children.first)
94
- else
95
- log.warn("Unsupported sig #{node.type} node #{node.source}")
96
- [node.source]
97
110
  end
98
- end
99
-
100
- sig { params(node: YARD::Parser::Ruby::AstNode).returns(String) }
101
- def self.build_generic_type(node)
102
- return node.source if node.children.empty? || node.type != :aref
103
111
 
104
- collection_type = node.children.first.source
105
- member_type = node.children.last.children
106
- .map { |child| build_generic_type(child) }
107
- .join(', ')
112
+ sig { params(node: YARD::Parser::Ruby::MethodCallNode).returns(T::Array[String]) }
113
+ private_class_method def self.convert_t_method(node)
114
+ case node.method_name(true)
115
+ when :any then node.last.first.children.map { |n| convert_node(n) }.flatten
116
+ # Order matters here, putting `nil` last results in a more concise
117
+ # return syntax in the UI (superscripted `?`)
118
+ # https://github.com/lsegal/yard/blob/cfa62ae/lib/yard/templates/helpers/html_helper.rb#L499-L500
119
+ when :nilable then convert_node(node.last) + ['nil']
120
+ else [node.source]
121
+ end
122
+ end
108
123
 
109
- "#{collection_type}[#{member_type}]"
124
+ sig { params(node: YARD::Parser::Ruby::AstNode).returns(T::Array[String]) }
125
+ private_class_method def self.convert_unknown(node)
126
+ log.warn("Unsupported sig #{node.type} node #{node.source}")
127
+ [node.source]
128
+ end
110
129
  end
111
130
  end
@@ -0,0 +1,13 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module YARDSorbet
5
+ # Used to store the details of a `T::Struct` `prop` definition
6
+ class TStructProp < T::Struct
7
+ const :default, T.nilable(String)
8
+ const :doc, String
9
+ const :prop_name, String
10
+ const :source, String
11
+ const :types, T::Array[String]
12
+ end
13
+ end
@@ -0,0 +1,44 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module YARDSorbet
5
+ # Helper methods for working with `YARD` tags
6
+ module TagUtils
7
+ extend T::Sig
8
+
9
+ # @return the tag with the matching `tag_name` and `name`, or `nil`
10
+ sig do
11
+ params(docstring: YARD::Docstring, tag_name: String, name: T.nilable(String))
12
+ .returns(T.nilable(YARD::Tags::Tag))
13
+ end
14
+ def self.find_tag(docstring, tag_name, name)
15
+ docstring.tags.find { |t| t.tag_name == tag_name && t.name == name }
16
+ end
17
+
18
+ # Create or update a `YARD` tag with type information
19
+ sig do
20
+ params(
21
+ docstring: YARD::Docstring,
22
+ tag_name: String,
23
+ types: T.nilable(T::Array[String]),
24
+ name: T.nilable(String),
25
+ text: String
26
+ ).void
27
+ end
28
+ def self.upsert_tag(docstring, tag_name, types = nil, name = nil, text = '')
29
+ tag = find_tag(docstring, tag_name, name)
30
+ if tag
31
+ return unless types
32
+
33
+ # Updating a tag in place doesn't seem to work, so we'll delete it, add the types, and re-add it
34
+ docstring.delete_tag_if { |t| t == tag }
35
+ # overwrite any existing type annotation (sigs should win)
36
+ tag.types = types
37
+ tag.text = text unless text.empty?
38
+ else
39
+ tag = YARD::Tags::Tag.new(tag_name, text, types, name)
40
+ end
41
+ docstring.add_tag(tag)
42
+ end
43
+ end
44
+ end
@@ -1,6 +1,8 @@
1
1
  # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
+ # Types are documentation
4
5
  module YARDSorbet
5
- VERSION = '0.4.1'
6
+ # {https://rubygems.org/gems/yard-sorbet Version history}
7
+ VERSION = '0.5.3'
6
8
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yard-sorbet
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Douglas Eichelberger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-04-28 00:00:00.000000000 Z
11
+ date: 2021-08-01 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler-audit
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: codecov
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -38,6 +52,20 @@ dependencies:
38
52
  - - "~>"
39
53
  - !ruby/object:Gem::Version
40
54
  version: '13.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: redcarpet
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.5'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.5'
41
69
  - !ruby/object:Gem::Dependency
42
70
  name: rspec
43
71
  requirement: !ruby/object:Gem::Requirement
@@ -58,14 +86,14 @@ dependencies:
58
86
  requirements:
59
87
  - - "~>"
60
88
  - !ruby/object:Gem::Version
61
- version: 1.13.0
89
+ version: 1.18.0
62
90
  type: :development
63
91
  prerelease: false
64
92
  version_requirements: !ruby/object:Gem::Requirement
65
93
  requirements:
66
94
  - - "~>"
67
95
  - !ruby/object:Gem::Version
68
- version: 1.13.0
96
+ version: 1.18.0
69
97
  - !ruby/object:Gem::Dependency
70
98
  name: rubocop-performance
71
99
  requirement: !ruby/object:Gem::Requirement
@@ -86,28 +114,28 @@ dependencies:
86
114
  requirements:
87
115
  - - "~>"
88
116
  - !ruby/object:Gem::Version
89
- version: 0.5.1
117
+ version: 0.6.0
90
118
  type: :development
91
119
  prerelease: false
92
120
  version_requirements: !ruby/object:Gem::Requirement
93
121
  requirements:
94
122
  - - "~>"
95
123
  - !ruby/object:Gem::Version
96
- version: 0.5.1
124
+ version: 0.6.0
97
125
  - !ruby/object:Gem::Dependency
98
126
  name: rubocop-rspec
99
127
  requirement: !ruby/object:Gem::Requirement
100
128
  requirements:
101
129
  - - "~>"
102
130
  - !ruby/object:Gem::Version
103
- version: 2.3.0
131
+ version: 2.4.0
104
132
  type: :development
105
133
  prerelease: false
106
134
  version_requirements: !ruby/object:Gem::Requirement
107
135
  requirements:
108
136
  - - "~>"
109
137
  - !ruby/object:Gem::Version
110
- version: 2.3.0
138
+ version: 2.4.0
111
139
  - !ruby/object:Gem::Dependency
112
140
  name: rubocop-sorbet
113
141
  requirement: !ruby/object:Gem::Requirement
@@ -142,42 +170,42 @@ dependencies:
142
170
  requirements:
143
171
  - - "~>"
144
172
  - !ruby/object:Gem::Version
145
- version: 0.5.6193
173
+ version: 0.5.6289
146
174
  type: :development
147
175
  prerelease: false
148
176
  version_requirements: !ruby/object:Gem::Requirement
149
177
  requirements:
150
178
  - - "~>"
151
179
  - !ruby/object:Gem::Version
152
- version: 0.5.6193
180
+ version: 0.5.6289
153
181
  - !ruby/object:Gem::Dependency
154
182
  name: sorbet-runtime
155
183
  requirement: !ruby/object:Gem::Requirement
156
184
  requirements:
157
185
  - - ">="
158
186
  - !ruby/object:Gem::Version
159
- version: 0.5.5845
187
+ version: '0.5'
160
188
  type: :runtime
161
189
  prerelease: false
162
190
  version_requirements: !ruby/object:Gem::Requirement
163
191
  requirements:
164
192
  - - ">="
165
193
  - !ruby/object:Gem::Version
166
- version: 0.5.5845
194
+ version: '0.5'
167
195
  - !ruby/object:Gem::Dependency
168
196
  name: yard
169
197
  requirement: !ruby/object:Gem::Requirement
170
198
  requirements:
171
199
  - - ">="
172
200
  - !ruby/object:Gem::Version
173
- version: 0.9.16
201
+ version: '0.9'
174
202
  type: :runtime
175
203
  prerelease: false
176
204
  version_requirements: !ruby/object:Gem::Requirement
177
205
  requirements:
178
206
  - - ">="
179
207
  - !ruby/object:Gem::Version
180
- version: 0.9.16
208
+ version: '0.9'
181
209
  description: 'A YARD plugin that incorporates Sorbet type information
182
210
 
183
211
  '
@@ -189,17 +217,28 @@ files:
189
217
  - LICENSE
190
218
  - lib/yard-sorbet.rb
191
219
  - lib/yard-sorbet/directives.rb
192
- - lib/yard-sorbet/sig_handler.rb
220
+ - lib/yard-sorbet/handlers.rb
221
+ - lib/yard-sorbet/handlers/abstract_dsl_handler.rb
222
+ - lib/yard-sorbet/handlers/enums_handler.rb
223
+ - lib/yard-sorbet/handlers/sig_handler.rb
224
+ - lib/yard-sorbet/handlers/struct_class_handler.rb
225
+ - lib/yard-sorbet/handlers/struct_class_handler.rbi
226
+ - lib/yard-sorbet/handlers/struct_prop_handler.rb
227
+ - lib/yard-sorbet/node_utils.rb
193
228
  - lib/yard-sorbet/sig_to_yard.rb
194
- - lib/yard-sorbet/struct_handler.rb
229
+ - lib/yard-sorbet/t_struct_prop.rb
230
+ - lib/yard-sorbet/tag_utils.rb
195
231
  - lib/yard-sorbet/version.rb
196
232
  homepage: https://github.com/dduugg/yard-sorbet
197
233
  licenses:
198
234
  - Apache-2.0
199
235
  metadata:
236
+ bug_tracker_uri: https://github.com/dduugg/yard-sorbet/issues
237
+ changelog_uri: https://github.com/dduugg/yard-sorbet/blob/main/CHANGELOG.md
238
+ documentation_uri: https://dduugg.github.io/yard-sorbet/
200
239
  homepage_uri: https://github.com/dduugg/yard-sorbet
201
240
  source_code_uri: https://github.com/dduugg/yard-sorbet
202
- changelog_uri: https://github.com/dduugg/yard-sorbet/blob/master/CHANGELOG.md
241
+ wiki_uri: https://github.com/dduugg/yard-sorbet/wiki
203
242
  post_install_message:
204
243
  rdoc_options: []
205
244
  require_paths:
@@ -215,7 +254,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
215
254
  - !ruby/object:Gem::Version
216
255
  version: '0'
217
256
  requirements: []
218
- rubygems_version: 3.1.4
257
+ rubygems_version: 3.1.6
219
258
  signing_key:
220
259
  specification_version: 4
221
260
  summary: Create YARD docs from Sorbet type signatures
@@ -1,180 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- # A YARD Handler for Sorbet type declarations
5
- class YARDSorbet::SigHandler < YARD::Handlers::Ruby::Base
6
- extend T::Sig
7
- handles :class, :module, :singleton_class?
8
-
9
- # A struct that holds the parsed contents of a Sorbet type signature
10
- class ParsedSig < T::Struct
11
- prop :abstract, T::Boolean, default: false
12
- prop :params, T::Hash[String, T::Array[String]], default: {}
13
- prop :return, T.nilable(T::Array[String])
14
- end
15
-
16
- PARAM_EXCLUDES = T.let(%i[array call hash].freeze, T::Array[Symbol])
17
- PROCESSABLE_NODES = T.let(%i[def defs command].freeze, T::Array[Symbol])
18
- SIG_EXCLUDES = T.let(%i[array hash].freeze, T::Array[Symbol])
19
- SIG_NODE_TYPES = T.let(%i[call fcall vcall].freeze, T::Array[Symbol])
20
-
21
- private_constant :ParsedSig, :PARAM_EXCLUDES, :PROCESSABLE_NODES, :SIG_EXCLUDES, :SIG_NODE_TYPES
22
-
23
- sig { void }
24
- def process
25
- # Find the list of declarations inside the class
26
- class_def = statement.children.find { |c| c.type == :list }
27
- class_contents = class_def.children
28
-
29
- process_class_contents(class_contents)
30
- end
31
-
32
- sig { params(class_contents: T::Array[YARD::Parser::Ruby::MethodCallNode]).void }
33
- private def process_class_contents(class_contents)
34
- class_contents.each_with_index do |child, i|
35
- if child.type == :sclass && child.children.size == 2 && child.children[1].type == :list
36
- singleton_class_contents = child.children[1]
37
- process_class_contents(singleton_class_contents)
38
- end
39
- next unless type_signature?(child)
40
-
41
- next_statement = class_contents[i + 1]
42
- next unless processable_method?(next_statement)
43
-
44
- process_method_definition(T.must(next_statement), child)
45
- end
46
- end
47
-
48
- sig { params(next_statement: T.nilable(YARD::Parser::Ruby::AstNode)).returns(T::Boolean) }
49
- private def processable_method?(next_statement)
50
- PROCESSABLE_NODES.include?(next_statement&.type)
51
- end
52
-
53
- sig do
54
- params(
55
- method_node: YARD::Parser::Ruby::AstNode,
56
- sig_node: YARD::Parser::Ruby::MethodCallNode
57
- ).void
58
- end
59
- private def process_method_definition(method_node, sig_node)
60
- # Swap the method definition docstring and the sig docstring.
61
- # Parse relevant parts of the `sig` and include them as well.
62
- docstring, directives = YARDSorbet::Directives.extract_directives(sig_node.docstring)
63
- parsed_sig = parse_sig(sig_node)
64
- enhance_tag(docstring, :abstract, parsed_sig)
65
- enhance_tag(docstring, :return, parsed_sig)
66
- if method_node.type != :command
67
- parsed_sig.params.each do |name, types|
68
- enhance_param(docstring, name, types)
69
- end
70
- end
71
- method_node.docstring = docstring.to_raw
72
- YARDSorbet::Directives.add_directives(method_node.docstring, directives)
73
- sig_node.docstring = nil
74
- end
75
-
76
- sig { params(docstring: YARD::Docstring, name: String, types: T::Array[String]).void }
77
- private def enhance_param(docstring, name, types)
78
- tag = docstring.tags.find { |t| t.tag_name == 'param' && t.name == name }
79
- if tag
80
- docstring.delete_tag_if { |t| t == tag }
81
- tag.types = types
82
- else
83
- tag = YARD::Tags::Tag.new(:param, '', types, name)
84
- end
85
- docstring.add_tag(tag)
86
- end
87
-
88
- sig { params(docstring: YARD::Docstring, type: Symbol, parsed_sig: ParsedSig).void }
89
- private def enhance_tag(docstring, type, parsed_sig)
90
- type_value = parsed_sig.public_send(type)
91
- return if !type_value
92
-
93
- tag = docstring.tags.find { |t| t.tag_name == type.to_s }
94
- if tag
95
- docstring.delete_tags(type)
96
- else
97
- tag = YARD::Tags::Tag.new(type, '')
98
- end
99
- if type_value.is_a?(Array)
100
- tag.types = type_value
101
- end
102
- docstring.add_tag(tag)
103
- end
104
-
105
- sig { params(sig_node: YARD::Parser::Ruby::MethodCallNode).returns(ParsedSig) }
106
- private def parse_sig(sig_node)
107
- parsed = ParsedSig.new
108
- found_params = T.let(false, T::Boolean)
109
- found_return = T.let(false, T::Boolean)
110
- bfs_traverse(sig_node, exclude: SIG_EXCLUDES) do |n|
111
- if n.source == 'abstract'
112
- parsed.abstract = true
113
- elsif n.source == 'params' && !found_params
114
- found_params = true
115
- sibling = T.must(sibling_node(n))
116
- bfs_traverse(sibling, exclude: PARAM_EXCLUDES) do |p|
117
- if p.type == :assoc
118
- param_name = p.children.first.source[0...-1]
119
- types = YARDSorbet::SigToYARD.convert(p.children.last)
120
- parsed.params[param_name] = types
121
- end
122
- end
123
- elsif n.source == 'returns' && !found_return
124
- found_return = true
125
- parsed.return = YARDSorbet::SigToYARD.convert(T.must(sibling_node(n)))
126
- elsif n.source == 'void'
127
- parsed.return ||= ['void']
128
- end
129
- end
130
- parsed
131
- end
132
-
133
- # Returns true if the given node is part of a type signature.
134
- sig { params(node: T.nilable(YARD::Parser::Ruby::AstNode)).returns(T::Boolean) }
135
- private def type_signature?(node)
136
- loop do
137
- return false if node.nil?
138
- return false unless SIG_NODE_TYPES.include?(node.type)
139
- return true if T.unsafe(node).method_name(true) == :sig
140
-
141
- node = T.let(node.children.first, T.nilable(YARD::Parser::Ruby::AstNode))
142
- end
143
- end
144
-
145
- sig { params(node: YARD::Parser::Ruby::AstNode).returns(T.nilable(YARD::Parser::Ruby::AstNode)) }
146
- private def sibling_node(node)
147
- found_sibling = T.let(false, T::Boolean)
148
- node.parent.children.each do |n|
149
- if found_sibling
150
- return n
151
- end
152
-
153
- if n == node
154
- found_sibling = true
155
- end
156
- end
157
- nil
158
- end
159
-
160
- # @yield [YARD::Parser::Ruby::AstNode]
161
- sig do
162
- params(
163
- node: YARD::Parser::Ruby::AstNode,
164
- exclude: T::Array[Symbol],
165
- _blk: T.proc.params(n: YARD::Parser::Ruby::AstNode).void
166
- ).void
167
- end
168
- private def bfs_traverse(node, exclude: [], &_blk)
169
- queue = [node]
170
- while !queue.empty?
171
- n = T.must(queue.shift)
172
- yield n
173
- n.children.each do |c|
174
- if !exclude.include?(c.type)
175
- queue.push(c)
176
- end
177
- end
178
- end
179
- end
180
- end
@@ -1,110 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- # Handles all `const`/`prop` calls, creating accessor methods, and compiles them for later usage at the class level
5
- # in creating a constructor
6
- class YARDSorbet::StructHandler < YARD::Handlers::Ruby::Base
7
- extend T::Sig
8
-
9
- handles method_call(:const), method_call(:prop)
10
- namespace_only
11
-
12
- sig { void }
13
- def process
14
- # Store the property for use in the constructor definition
15
- name = statement.parameters[0].jump(
16
- :ident, # handles most "normal" identifiers
17
- :kw, # handles prop names using reserved words like `end` or `def`
18
- :const # handles capitalized prop names like Foo
19
- ).source
20
-
21
- doc = statement.docstring.to_s
22
- source = statement.source
23
- types = YARDSorbet::SigToYARD.convert(statement.parameters[1])
24
- default_node = statement.traverse { |n| break n if n.source == 'default:' && n.type == :label }
25
- default = default_node.parent[1].source if default_node
26
-
27
- extra_state.prop_docs ||= Hash.new { |h, k| h[k] = [] }
28
- extra_state.prop_docs[namespace] << {
29
- doc: doc,
30
- prop_name: name,
31
- types: types,
32
- source: source,
33
- default: default
34
- }
35
-
36
- # Create the virtual method in our current scope
37
- namespace.attributes[scope][name] ||= SymbolHash[read: nil, write: nil]
38
-
39
- object = MethodObject.new(namespace, name, scope)
40
- object.source = source
41
-
42
- reader_docstring = doc.empty? ? "Returns the value of attribute +#{name}+." : doc
43
- docstring = YARD::DocstringParser.new.parse(reader_docstring).to_docstring
44
- docstring.add_tag(YARD::Tags::Tag.new(:return, '', types))
45
- object.docstring = docstring.to_raw
46
-
47
- # Register the object explicitly as an attribute.
48
- # While `const` attributes are immutable, `prop` attributes may be reassigned.
49
- if statement.method_name.source == 'prop'
50
- namespace.attributes[scope][name][:write] = object
51
- end
52
- namespace.attributes[scope][name][:read] = object
53
- end
54
- end
55
-
56
- # Class-level handler that folds all `const` and `prop` declarations into the constructor documentation
57
- # this needs to be injected as a module otherwise the default Class handler will overwrite documentation
58
- module YARDSorbet::StructClassHandler
59
- extend T::Sig
60
-
61
- sig { void }
62
- def process
63
- ret = super
64
-
65
- return ret if T.unsafe(self).extra_state.prop_docs.nil?
66
-
67
- # lookup the full YARD path for the current class
68
- class_ns = YARD::CodeObjects::ClassObject.new(
69
- T.unsafe(self).namespace, T.unsafe(self).statement[0].source.gsub(/\s/, '')
70
- )
71
- props = T.unsafe(self).extra_state.prop_docs[class_ns]
72
-
73
- return ret if props.empty?
74
-
75
- # Create a virtual `initialize` method with all the `prop`/`const` arguments
76
- # having the name :initialize & the scope :instance marks this as the constructor.
77
- # There is a chance that there is a custom initializer, so make sure we steal the existing docstring
78
- # and source
79
- object = YARD::CodeObjects::MethodObject.new(class_ns, :initialize, :instance)
80
-
81
- docstring, directives = YARDSorbet::Directives.extract_directives(object.docstring || '')
82
-
83
- # Annotate the parameters of the constructor with the prop docs
84
- props.each do |prop|
85
- docstring.add_tag(YARD::Tags::Tag.new(:param, prop[:doc], prop[:types], prop[:prop_name]))
86
- end
87
-
88
- docstring.add_tag(YARD::Tags::Tag.new(:return, '', class_ns))
89
-
90
- # Use kwarg style arguments, with optionals being marked with a default (unless an actual default was specified)
91
- object.parameters = props.map do |prop|
92
- default = prop[:default] || (prop[:types].include?('nil') ? 'nil' : nil)
93
- ["#{prop[:prop_name]}:", default]
94
- end
95
-
96
- # The "source" of our constructor is compromised with the props/consts
97
- object.source ||= props.map { |p| p[:source] }.join("\n")
98
- object.explicit ||= false # not strictly necessary
99
-
100
- T.unsafe(self).register(object)
101
-
102
- object.docstring = docstring.to_raw
103
-
104
- YARDSorbet::Directives.add_directives(object.docstring, directives)
105
-
106
- ret
107
- end
108
- end
109
-
110
- YARD::Handlers::Ruby::ClassHandler.include YARDSorbet::StructClassHandler