yard 0.9.43 → 0.9.45

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: 92d411c4d39cc74df916cbac8719ebd7ac6ed1fb17b37022148f584440bda554
4
- data.tar.gz: 9582c48d6c1949c055e22035f325abc957ec31900613f5b04307f7f27fc011a4
3
+ metadata.gz: 8e6dbc08e061218c5f0736d4b07923de3407ef37427f48307c01917e84c904e2
4
+ data.tar.gz: 7fc6e859d2f4a385eb7a956796782b3d6f45b0b480aadb566ed31aa862c5191b
5
5
  SHA512:
6
- metadata.gz: d566d6989e868b3bc4959037309c9c07a89608f07a39ad2719ead8765fca6ab3ae6240bfe7d56394ad47c8d75c5999d02313f6f7eaedb056a2ea584750d15cc1
7
- data.tar.gz: 641e462842732b47a67ff95eda6cfdb0373de7b2f487ceb7aa827815c0f8201fa55639979f7df17ec4d12864de60c4cc631fc7921cc787b20a04f5493cd86033
6
+ metadata.gz: 0ef54ea2c613b5b67200f0770ca5accb0c592511087efb5b66cb8c8847e89277247030befc947838eae3ef1f3f911acc66dfbaa0691e9119168e0d76d546ca76
7
+ data.tar.gz: 1d459a1eed141b9183ed4323e16cf0802b42c3043bd7fecc8aa6cc234e4c73a4b4fe90a39ad9bf063f6bc8b42ed911ab16ae56437ac7e4deaa595ef9829485aa
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # main
2
2
 
3
+ # [0.9.45] - July 14th, 2026
4
+
5
+ [0.9.45]: https://github.com/lsegal/yard/compare/v0.9.44...v0.9.45
6
+
7
+ - Use `@param` types to document generated `Struct.new` and `Data.define` members (#1684)
8
+ - Add compatibility with RDoc 8
9
+ - Fix TypesExplainer parsing of types following Hash collections (#1688)
10
+ - Fix HTML generation for RBS constants without source values (#1686)
11
+ - Fix method redefinition warnings when loading YARD with Ruby warnings enabled (#1687)
12
+ - Improve sanitization of `yard server` request paths
13
+
14
+ # [0.9.44] - May 25th, 2026
15
+
16
+ [0.9.44]: https://github.com/lsegal/yard/compare/v0.9.43...v0.9.44
17
+
18
+ - Fix possible path traversal with document_root (`--docroot`) and disk caching (`--cache`) set in `yard server` ([GHSA-pxcc-8665-phx8](https://github.com/lsegal/yard/security/advisories/GHSA-pxcc-8665-phx8))
19
+ - Fix support for HTML entities in HybridMarkup (#1680, #1681)
20
+ - Add support for string literals in TypesExplainer (#1628)
21
+ - Add support for multiple & nested Hash keys definition (#1630)
22
+
3
23
  # [0.9.43] - April 17th, 2026
4
24
 
5
25
  [0.9.43]: https://github.com/lsegal/yard/compare/v0.9.42...v0.9.43
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2007-2022 Loren Segal
1
+ Copyright (c) 2007-2026 Loren Segal
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person
4
4
  obtaining a copy of this software and associated documentation
data/README.md CHANGED
@@ -323,7 +323,7 @@ See {file:CHANGELOG.md} for a list of changes.
323
323
 
324
324
  ## License
325
325
 
326
- YARD © 2007-2020 by [Loren Segal](mailto:lsegal@soen.ca). YARD is licensed
326
+ YARD © by [Loren Segal](mailto:lsegal@soen.ca). YARD is licensed
327
327
  under the MIT license except for some files which come from the RDoc/Ruby
328
328
  distributions. Please see the {file:LICENSE} and {file:LEGAL} documents for more
329
329
  information.
data/docs/Tags.md CHANGED
@@ -201,6 +201,27 @@ in the form `Hash<KeyType, ValueType>`, or using the hash specific syntax:
201
201
  `Hash{KeyTypes=>ValueTypes}`. In the latter case, KeyTypes or ValueTypes can
202
202
  also be a list of types separated by commas.
203
203
 
204
+ For more precise type signatures, the hash-specific syntax also supports
205
+ multiple keys mapping to the same value type(s), multiple key/value groups,
206
+ and nested hashes:
207
+
208
+ * **Multiple keys for the same value type(s)** &mdash; A comma-separated list of
209
+ keys on the left of `=>` all share the value types on the right. For example,
210
+ `Hash{:name, :title => String}` describes a Hash where both `:name` and
211
+ `:title` keys map to String values.
212
+ * **Multiple key/value groups** &mdash; Separate distinct key/value groups with
213
+ a semicolon (`;`). For example,
214
+ `Hash{:name => String; :age => Integer}` describes a Hash with a `:name` key
215
+ that maps to a String and an `:age` key that maps to an Integer.
216
+ * **Nested hashes** &mdash; A value type can itself be a hash, allowing nested
217
+ structures. For example,
218
+ `Hash{:user => Hash{:name => String, :age => Integer}}` describes a Hash with
219
+ a `:user` key whose value is another Hash with `:name` and `:age` keys.
220
+
221
+ Keys in the hash-specific syntax are commonly [literal values](#Literals) such
222
+ as symbols (`:key`) or strings (`'key'`, `"key"`), but any type listed in the
223
+ [type conventions](#Type_List_Conventions) is allowed.
224
+
204
225
  #### Order-Dependent Lists
205
226
 
206
227
  An order dependent list is a set of types surrounded by "()" and separated by
@@ -213,7 +234,7 @@ having exactly those 3 elements) would be listed as: `Array(String, Fixnum, Hash
213
234
  Some literals are accepted by virtue of being Ruby literals, but also by YARD
214
235
  conventions. Here is a non-exhaustive list of certain accepted literal values:
215
236
 
216
- * `true`, `false`, `nil`, `:foo` &mdash; used when a method returns
237
+ * `true`, `false`, `nil`, `:foo`, `"bar"` &mdash; used when a method returns
217
238
  these explicit literal values. Note that if your method returns both
218
239
  `true` or `false`, you should use the `Boolean` conventional type
219
240
  instead.
data/lib/yard/autoload.rb CHANGED
@@ -287,6 +287,7 @@ module YARD
287
287
  module Templates
288
288
  module Helpers # Namespace for template helpers
289
289
  module Markup # Namespace for markup providers
290
+ autoload :HtmlEntities, __p('templates/helpers/markup/html_entities')
290
291
  autoload :HybridMarkdown, __p('templates/helpers/markup/hybrid_markdown')
291
292
  autoload :RDocMarkup, __p('templates/helpers/markup/rdoc_markup')
292
293
  autoload :RDocMarkdown, __p('templates/helpers/markup/rdoc_markdown')
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- # Helper methods to parse @attr_* tags on a class.
2
+ # Helper methods to document generated Struct and Data members.
3
3
  #
4
4
  # @deprecated The use of +@attr+ tags are deprecated since 0.8.0 in favour of
5
5
  # the +@!attribute+ directive. This module should not be relied on.
@@ -19,6 +19,26 @@ module YARD::Handlers::Ruby::StructHandlerMethods
19
19
  (klass.tags(specific_tag) + klass.tags(:attr)).find {|tag| tag.name == member }
20
20
  end
21
21
 
22
+ # Extracts the user's defined @param tag for a given generated member.
23
+ #
24
+ # @param [ClassObject] klass the class whose tags we're searching
25
+ # @param [String] member the name of the struct or data member we need
26
+ # @return [Tags::Tag, nil] the matching tag, or nil if not found
27
+ def parameter_tag_for_member(klass, member)
28
+ klass.tags(:param).find {|tag| tag.name == member }
29
+ end
30
+
31
+ # Returns the tag that supplies a generated member's type. Existing @attr*
32
+ # tags take precedence over the more concise @param form.
33
+ #
34
+ # @param [ClassObject] klass the class whose tags we're searching
35
+ # @param [String] member the name of the struct or data member we need
36
+ # @param [Symbol] type reader method, or writer method?
37
+ # @return [Tags::Tag, nil] the tag supplying the type, or nil if not found
38
+ def type_tag_for_member(klass, member, type = :read)
39
+ member_tag_for_member(klass, member, type) || parameter_tag_for_member(klass, member)
40
+ end
41
+
22
42
  # Retrieves all members defined in @attr* tags
23
43
  #
24
44
  # @param [ClassObject] klass the class with the attributes
@@ -61,7 +81,7 @@ module YARD::Handlers::Ruby::StructHandlerMethods
61
81
  # @return [String] a docstring to be attached to the getter method for this member
62
82
  def add_reader_tags(klass, new_method, member)
63
83
  member_tag = member_tag_for_member(klass, member, :read)
64
- return_type = return_type_from_tag(member_tag)
84
+ return_type = return_type_from_tag(type_tag_for_member(klass, member, :read))
65
85
  getter_doc_text = member_tag ? member_tag.text : "Returns the value of attribute #{member}"
66
86
  new_method.docstring.replace(getter_doc_text)
67
87
  new_method.add_tag YARD::Tags::Tag.new(:return, "the current value of #{member}", return_type)
@@ -76,7 +96,7 @@ module YARD::Handlers::Ruby::StructHandlerMethods
76
96
  # @return [String] a docstring to be attached to the setter method for this member
77
97
  def add_writer_tags(klass, new_method, member)
78
98
  member_tag = member_tag_for_member(klass, member, :write)
79
- return_type = return_type_from_tag(member_tag)
99
+ return_type = return_type_from_tag(type_tag_for_member(klass, member, :write))
80
100
  setter_doc_text = member_tag ? member_tag.text : "Sets the attribute #{member}"
81
101
  new_method.docstring.replace(setter_doc_text)
82
102
  new_method.add_tag YARD::Tags::Tag.new(:param, "the value to set the attribute #{member} to.", return_type, "value")
@@ -104,7 +124,7 @@ module YARD::Handlers::Ruby::StructHandlerMethods
104
124
  def create_writer(klass, member)
105
125
  # We want to convert these members into attributes just like
106
126
  # as if they were declared using attr_accessor.
107
- new_meth = register MethodObject.new(klass, "#{member}=", :instance) do |o|
127
+ new_meth = register_struct_member_method MethodObject.new(klass, "#{member}=", :instance) do |o|
108
128
  o.parameters = [['value', nil]]
109
129
  o.signature ||= "def #{member}=(value)"
110
130
  o.source ||= "#{o.signature}\n @#{member} = value\nend"
@@ -119,7 +139,7 @@ module YARD::Handlers::Ruby::StructHandlerMethods
119
139
  # @param [ClassObject] klass the class to attach the method to
120
140
  # @param [String] member the name of the member we're generating a method for
121
141
  def create_reader(klass, member)
122
- new_meth = register MethodObject.new(klass, member, :instance) do |o|
142
+ new_meth = register_struct_member_method MethodObject.new(klass, member, :instance) do |o|
123
143
  o.signature ||= "def #{member}"
124
144
  o.source ||= "#{o.signature}\n @#{member}\nend"
125
145
  end
@@ -140,4 +160,20 @@ module YARD::Handlers::Ruby::StructHandlerMethods
140
160
  create_reader klass, member if create_member_method?(klass, member, :read)
141
161
  end
142
162
  end
163
+
164
+ # Registers an auto-generated member method without reapplying the class's
165
+ # docstring to it. The generated reader or writer receives its own docstring
166
+ # and tags immediately after registration.
167
+ def register_struct_member_method(method, &block)
168
+ previous = @registering_struct_member_method
169
+ @registering_struct_member_method = true
170
+ register(method, &block)
171
+ ensure
172
+ @registering_struct_member_method = previous
173
+ end
174
+
175
+ def register_docstring(object, docstring = statement.comments, stmt = statement)
176
+ docstring = nil if @registering_struct_member_method
177
+ super(object, docstring, stmt)
178
+ end
143
179
  end
@@ -379,6 +379,7 @@ module YARD
379
379
  # entries from corrupting source ranges of later hash literals and brace blocks.
380
380
  # Bare hash patterns (key: val without braces) fire no brace scanner events, so
381
381
  # we only clean up when @map[:rbrace] confirms a closing brace was scanned.
382
+ begin; undef on_hshptn; rescue NameError; end
382
383
  def on_hshptn(*args)
383
384
  if (@map[:rbrace] ||= []).any?
384
385
  (@map[:lbrace] ||= []).pop
@@ -443,6 +444,7 @@ module YARD
443
444
  # on_rbracket scanner events. The corresponding parser events are on_aryptn/on_fndptn
444
445
  # (not on_aref), so we must clean up the bracket maps to prevent stale entries from
445
446
  # corrupting source ranges of later array indexing expressions.
447
+ begin; undef on_aryptn; rescue NameError; end
446
448
  def on_aryptn(*args)
447
449
  (@map[:lbracket] ||= []).pop
448
450
  (@map[:aref] ||= []).shift
@@ -451,6 +453,7 @@ module YARD
451
453
  AstNode.new(:aryptn, args)
452
454
  end
453
455
 
456
+ begin; undef on_fndptn; rescue NameError; end
454
457
  def on_fndptn(*args)
455
458
  (@map[:lbracket] ||= []).pop
456
459
  (@map[:aref] ||= []).shift
@@ -1,6 +1,4 @@
1
1
  # frozen_string_literal: true
2
- require 'fileutils'
3
-
4
2
  module YARD
5
3
  module Server
6
4
  module Commands
@@ -32,6 +30,8 @@ module YARD
32
30
  # @abstract
33
31
  # @see #run
34
32
  class Base
33
+ include StaticCaching
34
+
35
35
  # @group Basic Command and Adapter Options
36
36
 
37
37
  # @return [Hash] the options passed to the command's constructor
@@ -88,7 +88,7 @@ module YARD
88
88
  # of status, headers, and body wrapped in an array.
89
89
  def call(request)
90
90
  self.request = request
91
- self.path ||= File.cleanpath(request.path_info[1..-1])
91
+ self.path ||= Server.clean_path(request.path_info[1..-1])
92
92
  self.headers = {'Content-Type' => 'text/html'}
93
93
  self.body = ''
94
94
  self.status = 200
@@ -163,13 +163,7 @@ module YARD
163
163
  # @return [String] the same cached data (for chaining)
164
164
  # @see StaticCaching
165
165
  def cache(data)
166
- if caching && adapter.document_root
167
- path = File.join(adapter.document_root, request.path_info.sub(/\.html$/, '') + '.html')
168
- path = path.sub(%r{/\.html$}, '.html')
169
- FileUtils.mkdir_p(File.dirname(path))
170
- log.debug "Caching data to #{path}"
171
- File.open(path, 'wb') {|f| f.write(data) }
172
- end
166
+ super if caching
173
167
  self.body = data
174
168
  end
175
169
 
@@ -179,7 +179,7 @@ module YARD
179
179
  # @param (see #route_docs)
180
180
  # @return [Hash] finalized options
181
181
  def final_options(library, paths)
182
- path = File.cleanpath(paths.join('/')).gsub(%r{^(\.\./)+}, '')
182
+ path = Server.clean_path(paths.join('/'))
183
183
  adapter.options.merge(:library => library, :path => path)
184
184
  end
185
185
  end
@@ -1,4 +1,6 @@
1
1
  # frozen_string_literal: true
2
+ require 'fileutils'
3
+
2
4
  module YARD
3
5
  module Server
4
6
  # Implements static caching for requests.
@@ -10,9 +12,8 @@ module YARD
10
12
  # implement your own +#check_static_cache+ method and mix the module into
11
13
  # the Router class.
12
14
  #
13
- # Note that caching does not occur here. This method simply checks for
14
- # the existence of cached data. To actually cache a response, see
15
- # {Commands::Base#cache}.
15
+ # This method checks for the existence of cached data. To actually cache
16
+ # a response, see {#cache}.
16
17
  #
17
18
  # @example Implementing In-Memory Cache Checking
18
19
  # module MemoryCaching
@@ -33,14 +34,44 @@ module YARD
33
34
  # @see Commands::Base#cache
34
35
  def check_static_cache
35
36
  return nil unless adapter.document_root
36
- cache_path = File.join(adapter.document_root, request.path.sub(/\.html$/, '') + '.html')
37
- cache_path = cache_path.sub(%r{/\.html$}, '.html')
37
+ cache_path = cache_path(request.path)
38
+ return nil unless cache_path
39
+
38
40
  if File.file?(cache_path)
39
41
  log.debug "Loading cache from disk: #{cache_path}"
40
42
  return [200, {'Content-Type' => 'text/html'}, [File.read_binary(cache_path)]]
41
43
  end
42
44
  nil
43
45
  end
46
+
47
+ # Caches rendered HTML response data to disk.
48
+ #
49
+ # @param [String] data the data to cache
50
+ # @return [void]
51
+ # @since 0.9.44
52
+ def cache(data)
53
+ return unless adapter.document_root
54
+
55
+ path = cache_path(request.path_info)
56
+ return unless path
57
+
58
+ FileUtils.mkdir_p(File.dirname(path))
59
+ log.debug "Caching data to #{path}"
60
+ File.open(path, 'wb') {|f| f.write(data) }
61
+ end
62
+
63
+ private
64
+
65
+ def cache_path(request_path)
66
+ return nil if request_path.split(/[\/\\]/).include?('..')
67
+
68
+ path = request_path.sub(/\.html$/, '') + '.html'
69
+ path = path.sub(%r{\A/+}, '')
70
+ return nil if path =~ /\A[A-Za-z]:/
71
+
72
+ path = File.cleanpath(path)
73
+ File.join(adapter.document_root, path)
74
+ end
44
75
  end
45
76
  end
46
77
  end
data/lib/yard/server.rb CHANGED
@@ -1,6 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
  module YARD
3
3
  module Server
4
+ # Normalizes an HTTP request path into a relative command path.
5
+ # @api private
6
+ # @param [String] path the request path to normalize
7
+ # @return [String] a relative path using forward slashes
8
+ def self.clean_path(path)
9
+ path = File.cleanpath(path.tr('\\', '/'))
10
+ path.sub(/\A[A-Za-z]:(?!:)/, '').sub(%r{\A/+}, '')
11
+ end
12
+
4
13
  # Registers a static path to be used in static asset lookup.
5
14
  # @param [String] path the pathname to register
6
15
  # @return [void]
@@ -4,6 +4,9 @@ require 'strscan'
4
4
  module YARD
5
5
  module Tags
6
6
  class TypesExplainer
7
+ # Regular expression to match symbol and string literals
8
+ LITERALMATCH = /:\w+|'[^']*'|"[^"]*"/
9
+
7
10
  # (see Tag#explain_types)
8
11
  # @param types [Array<String>] a list of types to parse and summarize
9
12
  def self.explain(*types)
@@ -31,16 +34,14 @@ module YARD
31
34
  end
32
35
 
33
36
  def to_s(singular = true)
34
- if name[0, 1] == "#"
35
- (singular ? "an object that responds to " : "objects that respond to ") + list_join(name.split(/ *& */), with: "and")
36
- elsif name[0, 1] =~ /[A-Z]/
37
+ if name[0, 1] =~ /[A-Z]/
37
38
  singular ? "a#{name[0, 1] =~ /[aeiou]/i ? 'n' : ''} " + name : "#{name}#{name[-1, 1] =~ /[A-Z]/ ? "'" : ''}s"
38
39
  else
39
40
  name
40
41
  end
41
42
  end
42
43
 
43
- private
44
+ protected
44
45
 
45
46
  def list_join(list, with: "or")
46
47
  index = 0
@@ -54,6 +55,20 @@ module YARD
54
55
  end
55
56
  end
56
57
 
58
+ # @private
59
+ class LiteralType < Type
60
+ def to_s(_singular = true)
61
+ "a literal value #{name}"
62
+ end
63
+ end
64
+
65
+ # @private
66
+ class DuckType < Type
67
+ def to_s(singular = true)
68
+ (singular ? "an object that responds to " : "objects that respond to ") + list_join(name.split(/ *& */), with: "and")
69
+ end
70
+ end
71
+
57
72
  # @private
58
73
  class CollectionType < Type
59
74
  attr_accessor :types
@@ -77,18 +92,56 @@ module YARD
77
92
 
78
93
  # @private
79
94
  class HashCollectionType < Type
80
- attr_accessor :key_types, :value_types
95
+ attr_accessor :key_value_pairs
81
96
 
82
- def initialize(name, key_types, value_types)
97
+ def initialize(name, key_types_or_pairs, value_types = nil)
83
98
  @name = name
84
- @key_types = key_types
85
- @value_types = value_types
99
+
100
+ if value_types.nil?
101
+ # New signature: (name, key_value_pairs)
102
+ @key_value_pairs = key_types_or_pairs || []
103
+ else
104
+ # Old signature: (name, key_types, value_types)
105
+ @key_value_pairs = [[key_types_or_pairs, value_types]]
106
+ end
107
+ end
108
+
109
+ # Backward compatibility accessors
110
+ def key_types
111
+ return [] if @key_value_pairs.empty?
112
+ @key_value_pairs.first[0] || []
113
+ end
114
+
115
+ def key_types=(types)
116
+ if @key_value_pairs.empty?
117
+ @key_value_pairs = [[types, []]]
118
+ else
119
+ @key_value_pairs[0][0] = types
120
+ end
121
+ end
122
+
123
+ def value_types
124
+ return [] if @key_value_pairs.empty?
125
+ @key_value_pairs.first[1] || []
126
+ end
127
+
128
+ def value_types=(types)
129
+ if @key_value_pairs.empty?
130
+ @key_value_pairs = [[[], types]]
131
+ else
132
+ @key_value_pairs[0][1] = types
133
+ end
86
134
  end
87
135
 
88
136
  def to_s(_singular = true)
89
- "a#{name[0, 1] =~ /[aeiou]/i ? 'n' : ''} #{name} with keys made of (" +
90
- list_join(key_types.map {|t| t.to_s(false) }) +
91
- ") and values of (" + list_join(value_types.map {|t| t.to_s(false) }) + ")"
137
+ return "a#{name[0, 1] =~ /[aeiou]/i ? 'n' : ''} #{name}" if @key_value_pairs.empty?
138
+
139
+ result = "a#{name[0, 1] =~ /[aeiou]/i ? 'n' : ''} #{name} with "
140
+ parts = @key_value_pairs.map do |keys, values|
141
+ "keys made of (" + list_join(keys.map {|t| t.to_s(false) }) +
142
+ ") and values of (" + list_join(values.map {|t| t.to_s(false) }) + ")"
143
+ end
144
+ result + parts.join(" and ")
92
145
  end
93
146
  end
94
147
 
@@ -101,13 +154,15 @@ module YARD
101
154
  :collection_end => />/,
102
155
  :fixed_collection_start => /\(/,
103
156
  :fixed_collection_end => /\)/,
104
- :type_name => /#{ISEP}#{METHODNAMEMATCH}|#{NAMESPACEMATCH}|\w+/,
157
+ :type_name => /#{ISEP}#{METHODNAMEMATCH}|#{NAMESPACEMATCH}|#{LITERALMATCH}|\w+/,
105
158
  :symbol => /:#{METHODNAMEMATCH}/,
106
- :type_next => /[,;]/,
159
+ :type_next => /[,]/,
107
160
  :whitespace => /\s+/,
108
161
  :hash_collection_start => /\{/,
109
- :hash_collection_next => /=>/,
162
+ :hash_collection_value => /=>/,
163
+ :hash_collection_value_end => /;/,
110
164
  :hash_collection_end => /\}/,
165
+ # :symbol_start => /:/,
111
166
  :parse_end => nil
112
167
  }
113
168
 
@@ -119,10 +174,53 @@ module YARD
119
174
  @scanner = StringScanner.new(string)
120
175
  end
121
176
 
122
- def parse
123
- types = []
177
+ # @return [Array(Boolean, Array<Type>)] - finished, types
178
+ def parse(until_tokens: [:parse_end])
179
+ parse_until(until_tokens).first
180
+ end
181
+
182
+ private
183
+
184
+ def parse_until(until_tokens)
185
+ current_parsed_types = []
124
186
  type = nil
125
187
  name = nil
188
+ finished = false
189
+ end_token = nil
190
+ types = parse_with_handlers do |token_type, token|
191
+ case token_type
192
+ when *until_tokens
193
+ raise SyntaxError, "expecting name, got '#{token}'" if name.nil?
194
+ type = create_type(name) unless type
195
+ current_parsed_types << type
196
+ finished = true
197
+ end_token = token_type
198
+ when :type_name
199
+ raise SyntaxError, "expecting END, got name '#{token}'" if name
200
+ name = token
201
+ when :type_next
202
+ raise SyntaxError, "expecting name, got '#{token}' at #{@scanner.pos}" if name.nil?
203
+ type = create_type(name) unless type
204
+ current_parsed_types << type
205
+ name = nil
206
+ type = nil
207
+ when :fixed_collection_start, :collection_start
208
+ name ||= "Array"
209
+ klass = token_type == :collection_start ? CollectionType : FixedCollectionType
210
+ nested_types, = parse_until([:fixed_collection_end, :collection_end, :parse_end])
211
+ type = klass.new(name, nested_types)
212
+ when :hash_collection_start
213
+ name ||= "Hash"
214
+ type = parse_hash_collection(name)
215
+ end
216
+
217
+ [finished, current_parsed_types]
218
+ end
219
+ [types, end_token]
220
+ end
221
+
222
+ # @return [Array<Type>]
223
+ def parse_with_handlers
126
224
  loop do
127
225
  found = false
128
226
  TOKENS.each do |token_type, match|
@@ -130,32 +228,68 @@ module YARD
130
228
  # rubocop:disable Lint/AssignmentInCondition
131
229
  next unless (match.nil? && @scanner.eos?) || (match && token = @scanner.scan(match))
132
230
  found = true
133
- case token_type
134
- when :type_name, :symbol
135
- raise SyntaxError, "expecting END, got name '#{token}'" if name
136
- name = token
137
- when :type_next
138
- raise SyntaxError, "expecting name, got '#{token}' at #{@scanner.pos}" if name.nil?
139
- type = Type.new(name) unless type
140
- types << type
141
- type = nil
142
- name = nil
143
- when :fixed_collection_start, :collection_start
144
- name ||= "Array"
145
- klass = token_type == :collection_start ? CollectionType : FixedCollectionType
146
- type = klass.new(name, parse)
147
- when :hash_collection_start
148
- name ||= "Hash"
149
- type = HashCollectionType.new(name, parse, parse)
150
- when :hash_collection_next, :hash_collection_end, :fixed_collection_end, :collection_end, :parse_end
151
- raise SyntaxError, "expecting name, got '#{token}'" if name.nil?
152
- type = Type.new(name) unless type
153
- types << type
154
- return types
155
- end
231
+ # @type [Array<Type>]
232
+ finished, types = yield(token_type, token)
233
+ return types if finished
234
+ break
156
235
  end
157
236
  raise SyntaxError, "invalid character at #{@scanner.peek(1)}" unless found
158
237
  end
238
+ nil
239
+ end
240
+
241
+ def parse_hash_collection(name)
242
+ key_value_pairs = []
243
+ current_keys = []
244
+ finished = false
245
+
246
+ parse_with_handlers do |token_type, token|
247
+ case token_type
248
+ when :type_name
249
+ current_keys << create_type(token)
250
+ when :type_next
251
+ # Comma - continue collecting keys unless we just processed a value
252
+ # In that case, start a new key group
253
+ when :hash_collection_value
254
+ # => - current keys map to the next value(s)
255
+ raise SyntaxError, "no keys before =>" if current_keys.empty?
256
+ values, end_token = parse_until([:hash_collection_value_end, :hash_collection_end, :parse_end])
257
+ key_value_pairs << [current_keys, values]
258
+ current_keys = []
259
+ finished = end_token != :hash_collection_value_end
260
+ when :hash_collection_end, :parse_end
261
+ # End of hash
262
+ finished = true
263
+ when :whitespace
264
+ # Ignore whitespace
265
+ end
266
+
267
+ [finished, HashCollectionType.new(name, key_value_pairs)]
268
+ end
269
+ end
270
+
271
+ private
272
+
273
+ def create_type(name)
274
+ if name[0, 1] == ":" || (name[0, 1] =~ /['"]/ && name[-1, 1] =~ /['"]/)
275
+ LiteralType.new(name)
276
+ elsif name[0, 1] == "#"
277
+ DuckType.new(name)
278
+ else
279
+ Type.new(name)
280
+ end
281
+ end
282
+
283
+ private
284
+
285
+ def create_type(name)
286
+ if name[0, 1] == ":" || (name[0, 1] =~ /['"]/ && name[-1, 1] =~ /['"]/)
287
+ LiteralType.new(name)
288
+ elsif name[0, 1] == "#"
289
+ DuckType.new(name)
290
+ else
291
+ Type.new(name)
292
+ end
159
293
  end
160
294
  end
161
295
  end