Skip to content

Commit bcf20c9

Browse files
committed
Add support for multiple & nested Hash keys definition
1 parent 871c974 commit bcf20c9

File tree

2 files changed

+124
-35
lines changed

2 files changed

+124
-35
lines changed

lib/yard/tags/types_explainer.rb

Lines changed: 120 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -92,18 +92,56 @@ def to_s(_singular = true)
9292

9393
# @private
9494
class HashCollectionType < Type
95-
attr_accessor :key_types, :value_types
95+
attr_accessor :key_value_pairs
9696

97-
def initialize(name, key_types, value_types)
97+
def initialize(name, key_types_or_pairs, value_types = nil)
9898
@name = name
99-
@key_types = key_types
100-
@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
101134
end
102135

103136
def to_s(_singular = true)
104-
"a#{name[0, 1] =~ /[aeiou]/i ? 'n' : ''} #{name} with keys made of (" +
105-
list_join(key_types.map {|t| t.to_s(false) }) +
106-
") 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 ")
107145
end
108146
end
109147

@@ -117,11 +155,13 @@ class Parser
117155
:fixed_collection_start => /\(/,
118156
:fixed_collection_end => /\)/,
119157
:type_name => /#{ISEP}#{METHODNAMEMATCH}|#{NAMESPACEMATCH}|#{LITERALMATCH}|\w+/,
120-
:type_next => /[,;]/,
158+
:type_next => /[,]/,
121159
:whitespace => /\s+/,
122160
:hash_collection_start => /\{/,
123-
:hash_collection_next => /=>/,
161+
:hash_collection_value => /=>/,
162+
:hash_collection_value_end => /;/,
124163
:hash_collection_end => /\}/,
164+
# :symbol_start => /:/,
125165
:parse_end => nil
126166
}
127167

@@ -133,43 +173,89 @@ def initialize(string)
133173
@scanner = StringScanner.new(string)
134174
end
135175

136-
def parse
137-
types = []
176+
# @return [Array(Boolean, Array<Type>)] - finished, types
177+
def parse(until_tokens: [:parse_end])
178+
current_parsed_types = []
138179
type = nil
139180
name = nil
181+
finished = false
182+
parse_with_handlers do |token_type, token|
183+
case token_type
184+
when *until_tokens
185+
raise SyntaxError, "expecting name, got '#{token}'" if name.nil?
186+
type = create_type(name) unless type
187+
current_parsed_types << type
188+
finished = true
189+
when :type_name
190+
raise SyntaxError, "expecting END, got name '#{token}'" if name
191+
name = token
192+
when :type_next
193+
raise SyntaxError, "expecting name, got '#{token}' at #{@scanner.pos}" if name.nil?
194+
type = create_type(name) unless type
195+
current_parsed_types << type
196+
name = nil
197+
type = nil
198+
when :fixed_collection_start, :collection_start
199+
name ||= "Array"
200+
klass = token_type == :collection_start ? CollectionType : FixedCollectionType
201+
type = klass.new(name, parse(until_tokens: [:fixed_collection_end, :collection_end, :parse_end]))
202+
when :hash_collection_start
203+
name ||= "Hash"
204+
type = parse_hash_collection(name)
205+
end
206+
207+
[finished, current_parsed_types]
208+
end
209+
end
210+
211+
private
212+
213+
# @return [Array<Type>]
214+
def parse_with_handlers
140215
loop do
141216
found = false
142217
TOKENS.each do |token_type, match|
143218
# TODO: cleanup this code.
144219
# rubocop:disable Lint/AssignmentInCondition
145220
next unless (match.nil? && @scanner.eos?) || (match && token = @scanner.scan(match))
146221
found = true
147-
case token_type
148-
when :type_name
149-
raise SyntaxError, "expecting END, got name '#{token}'" if name
150-
name = token
151-
when :type_next
152-
raise SyntaxError, "expecting name, got '#{token}' at #{@scanner.pos}" if name.nil?
153-
type = create_type(name) unless type
154-
types << type
155-
type = nil
156-
name = nil
157-
when :fixed_collection_start, :collection_start
158-
name ||= "Array"
159-
klass = token_type == :collection_start ? CollectionType : FixedCollectionType
160-
type = klass.new(name, parse)
161-
when :hash_collection_start
162-
name ||= "Hash"
163-
type = HashCollectionType.new(name, parse, parse)
164-
when :hash_collection_next, :hash_collection_end, :fixed_collection_end, :collection_end, :parse_end
165-
raise SyntaxError, "expecting name, got '#{token}'" if name.nil?
166-
type = create_type(name) unless type
167-
types << type
168-
return types
169-
end
222+
# @type [Array<Type>]
223+
finished, types = yield(token_type, token)
224+
return types if finished
225+
break
170226
end
171227
raise SyntaxError, "invalid character at #{@scanner.peek(1)}" unless found
172228
end
229+
nil
230+
end
231+
232+
def parse_hash_collection(name)
233+
key_value_pairs = []
234+
current_keys = []
235+
finished = false
236+
237+
parse_with_handlers do |token_type, token|
238+
case token_type
239+
when :type_name
240+
current_keys << create_type(token)
241+
when :type_next
242+
# Comma - continue collecting keys unless we just processed a value
243+
# In that case, start a new key group
244+
when :hash_collection_value
245+
# => - current keys map to the next value(s)
246+
raise SyntaxError, "no keys before =>" if current_keys.empty?
247+
values = parse(until_tokens: [:hash_collection_value_end, :parse_end])
248+
key_value_pairs << [current_keys, values]
249+
current_keys = []
250+
when :hash_collection_end, :parse_end
251+
# End of hash
252+
finished = true
253+
when :whitespace
254+
# Ignore whitespace
255+
end
256+
257+
[finished, HashCollectionType.new(name, key_value_pairs)]
258+
end
173259
end
174260

175261
private

spec/tags/types_explainer_spec.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,10 @@ def parse_fail(types)
251251
"#weird_method?, #<=>, #!=" => "an object that responds to #weird_method?;
252252
an object that responds to #<=>;
253253
an object that responds to #!=",
254-
":symbol, 'string'" => "a literal value :symbol; a literal value 'string'"
254+
":symbol, 'string'" => "a literal value :symbol; a literal value 'string'",
255+
"Hash{:key_one, :key_two => String; :key_three => Symbol}" => "a Hash with keys made of (a literal value :key_one or a literal value :key_two) and values of (Strings) and keys made of (a literal value :key_three) and values of (Symbols)",
256+
"Hash{:key_one, :key_two => String; :key_three => Symbol; :key_four => Hash{:sub_key_one => String}}" => "a Hash with keys made of (a literal value :key_one or a literal value :key_two) and values of (Strings) and keys made of (a literal value :key_three) and values of (Symbols) and keys made of (a literal value :key_four) and values of (a Hash with keys made of (a literal value :sub_key_one) and values of (Strings))",
257+
"Hash{:key_one => String, Number; :key_two => String}" => "a Hash with keys made of (a literal value :key_one) and values of (Strings or Numbers) and keys made of (a literal value :key_two) and values of (Strings)"
255258
}
256259
expect.each do |input, expected|
257260
explain = YARD::Tags::TypesExplainer.explain(input)

0 commit comments

Comments
 (0)