From b72b6ffa55ce26aa987026575bfa58b81af59102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lek=C3=AB=20Mula?= Date: Sat, 13 Sep 2025 19:15:51 +0200 Subject: [PATCH 1/4] Add support for symbol and string literals in TypesExplainer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add LITERALMATCH regex constant to match symbol (:symbol) and string ('string', "string") literals - Update TypesExplainer parser to recognize symbol and string literal types - Format literal values as "a literal value :symbol" for better readability - Add comprehensive test coverage for LITERALMATCH constant and literal type parsing - Fixes parsing inconsistency between YARD TypesExplainer and online parser Resolves #1627 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- lib/yard/code_objects/base.rb | 3 +++ lib/yard/tags/types_explainer.rb | 4 +++- spec/code_objects/constants_spec.rb | 20 ++++++++++++++++++++ spec/tags/types_explainer_spec.rb | 22 +++++++++++++++++++++- 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/lib/yard/code_objects/base.rb b/lib/yard/code_objects/base.rb index 561481317..9794f660c 100644 --- a/lib/yard/code_objects/base.rb +++ b/lib/yard/code_objects/base.rb @@ -63,6 +63,9 @@ def push(value) # Regular expression to match a fully qualified method def (self.foo, Class.foo). METHODMATCH = /(?:(?:#{NAMESPACEMATCH}|[a-z]\w*)\s*(?:#{CSEPQ}|#{NSEPQ})\s*)?#{METHODNAMEMATCH}/ + # Regular expression to match symbol and string literals + LITERALMATCH = /:\w+|'[^']*'|"[^"]*"/ + # All builtin Ruby exception classes for inheritance tree. BUILTIN_EXCEPTIONS = ["ArgumentError", "ClosedQueueError", "EncodingError", "EOFError", "Exception", "FiberError", "FloatDomainError", "IndexError", diff --git a/lib/yard/tags/types_explainer.rb b/lib/yard/tags/types_explainer.rb index d101ed68b..6533d3ddd 100644 --- a/lib/yard/tags/types_explainer.rb +++ b/lib/yard/tags/types_explainer.rb @@ -33,6 +33,8 @@ def initialize(name) def to_s(singular = true) if name[0, 1] == "#" (singular ? "an object that responds to " : "objects that respond to ") + list_join(name.split(/ *& */), with: "and") + elsif name[0, 1] == ":" || (name[0, 1] =~ /['"]/ && name[-1, 1] =~ /['"]/) + "a literal value #{name}" elsif name[0, 1] =~ /[A-Z]/ singular ? "a#{name[0, 1] =~ /[aeiou]/i ? 'n' : ''} " + name : "#{name}#{name[-1, 1] =~ /[A-Z]/ ? "'" : ''}s" else @@ -101,7 +103,7 @@ class Parser :collection_end => />/, :fixed_collection_start => /\(/, :fixed_collection_end => /\)/, - :type_name => /#{ISEP}#{METHODNAMEMATCH}|#{NAMESPACEMATCH}|\w+/, + :type_name => /#{ISEP}#{METHODNAMEMATCH}|#{NAMESPACEMATCH}|#{LITERALMATCH}|\w+/, :type_next => /[,;]/, :whitespace => /\s+/, :hash_collection_start => /\{/, diff --git a/spec/code_objects/constants_spec.rb b/spec/code_objects/constants_spec.rb index 3c6b9180d..e9ce5f316 100644 --- a/spec/code_objects/constants_spec.rb +++ b/spec/code_objects/constants_spec.rb @@ -56,6 +56,26 @@ def silence_warnings end end + describe :LITERALMATCH do + it "matches symbol literals" do + expect(":symbol"[CodeObjects::LITERALMATCH]).to eq ":symbol" + expect(":some_symbol"[CodeObjects::LITERALMATCH]).to eq ":some_symbol" + expect("not_a_symbol"[CodeObjects::LITERALMATCH]).to be nil + end + + it "matches single-quoted string literals" do + expect("'string'"[CodeObjects::LITERALMATCH]).to eq "'string'" + expect("'some string with spaces'"[CodeObjects::LITERALMATCH]).to eq "'some string with spaces'" + expect("not_quoted"[CodeObjects::LITERALMATCH]).to be nil + end + + it "matches double-quoted string literals" do + expect('"string"'[CodeObjects::LITERALMATCH]).to eq '"string"' + expect('"some string with spaces"'[CodeObjects::LITERALMATCH]).to eq '"some string with spaces"' + expect("not_quoted"[CodeObjects::LITERALMATCH]).to be nil + end + end + describe :BUILTIN_EXCEPTIONS do it "includes all base exceptions" do bad_names = [] diff --git a/spec/tags/types_explainer_spec.rb b/spec/tags/types_explainer_spec.rb index de7454d3c..f174cdfe4 100644 --- a/spec/tags/types_explainer_spec.rb +++ b/spec/tags/types_explainer_spec.rb @@ -57,6 +57,14 @@ def parse_fail(types) expect(@t.to_s(false)).to eq name end end + + it "works for literal values" do + [':symbol', "'5'"].each do |name| + @t.name = name + expect(@t.to_s).to eq "a literal value #{name}" + expect(@t.to_s(false)).to eq "a literal value #{name}" + end + end end describe CollectionType, '#to_s' do @@ -143,6 +151,17 @@ def parse_fail(types) expect(type[3].name).to eq "E" end + it 'parses a list of literal values' do + type = parse("true, false, nil, 4, :symbol, '5'") + expect(type.size).to eq 6 + expect(type[0].name).to eq "true" + expect(type[1].name).to eq "false" + expect(type[2].name).to eq "nil" + expect(type[3].name).to eq "4" + expect(type[4].name).to eq ":symbol" + expect(type[5].name).to eq "'5'" + end + it "parses a collection type" do type = parse("MyList") expect(type.first).to be_a(CollectionType) @@ -204,7 +223,8 @@ def parse_fail(types) a Hash with keys made of (Foos or Bars) and values of (Symbols or Numbers)", "#weird_method?, #<=>, #!=" => "an object that responds to #weird_method?; an object that responds to #<=>; - an object that responds to #!=" + an object that responds to #!=", + ":symbol, 'string'" => "a literal value :symbol; a literal value 'string'" } expect.each do |input, expected| explain = YARD::Tags::TypesExplainer.explain(input) From d51391e386e20bf30f5446b63d8985118d3164cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lek=C3=AB=20Mula?= Date: Sun, 28 Sep 2025 16:27:18 +0200 Subject: [PATCH 2/4] Refactor: Extract Litral & Duck type classes This could be useful for solargraph gem --- lib/yard/tags/types_explainer.rb | 38 +++++++++++++++++++----- spec/tags/types_explainer_spec.rb | 49 ++++++++++++++++++------------- 2 files changed, 58 insertions(+), 29 deletions(-) diff --git a/lib/yard/tags/types_explainer.rb b/lib/yard/tags/types_explainer.rb index 6533d3ddd..83d56c7b1 100644 --- a/lib/yard/tags/types_explainer.rb +++ b/lib/yard/tags/types_explainer.rb @@ -31,18 +31,14 @@ def initialize(name) end def to_s(singular = true) - if name[0, 1] == "#" - (singular ? "an object that responds to " : "objects that respond to ") + list_join(name.split(/ *& */), with: "and") - elsif name[0, 1] == ":" || (name[0, 1] =~ /['"]/ && name[-1, 1] =~ /['"]/) - "a literal value #{name}" - elsif name[0, 1] =~ /[A-Z]/ + if name[0, 1] =~ /[A-Z]/ singular ? "a#{name[0, 1] =~ /[aeiou]/i ? 'n' : ''} " + name : "#{name}#{name[-1, 1] =~ /[A-Z]/ ? "'" : ''}s" else name end end - private + protected def list_join(list, with: "or") index = 0 @@ -56,6 +52,20 @@ def list_join(list, with: "or") end end + # @private + class LiteralType < Type + def to_s(_singular = true) + "a literal value #{name}" + end + end + + # @private + class DuckType < Type + def to_s(singular = true) + (singular ? "an object that responds to " : "objects that respond to ") + list_join(name.split(/ *& */), with: "and") + end + end + # @private class CollectionType < Type attr_accessor :types @@ -137,7 +147,7 @@ def parse name = token when :type_next raise SyntaxError, "expecting name, got '#{token}' at #{@scanner.pos}" if name.nil? - type = Type.new(name) unless type + type = create_type(name) unless type types << type type = nil name = nil @@ -150,7 +160,7 @@ def parse type = HashCollectionType.new(name, parse, parse) when :hash_collection_next, :hash_collection_end, :fixed_collection_end, :collection_end, :parse_end raise SyntaxError, "expecting name, got '#{token}'" if name.nil? - type = Type.new(name) unless type + type = create_type(name) unless type types << type return types end @@ -158,6 +168,18 @@ def parse raise SyntaxError, "invalid character at #{@scanner.peek(1)}" unless found end end + + private + + def create_type(name) + if name[0, 1] == ":" || (name[0, 1] =~ /['"]/ && name[-1, 1] =~ /['"]/) + LiteralType.new(name) + elsif name[0, 1] == "#" + DuckType.new(name) + else + Type.new(name) + end + end end end end diff --git a/spec/tags/types_explainer_spec.rb b/spec/tags/types_explainer_spec.rb index f174cdfe4..f9fd5914d 100644 --- a/spec/tags/types_explainer_spec.rb +++ b/spec/tags/types_explainer_spec.rb @@ -2,6 +2,8 @@ RSpec.describe YARD::Tags::TypesExplainer do Type = YARD::Tags::TypesExplainer::Type + LiteralType = YARD::Tags::TypesExplainer::LiteralType + DuckType = YARD::Tags::TypesExplainer::DuckType CollectionType = YARD::Tags::TypesExplainer::CollectionType FixedCollectionType = YARD::Tags::TypesExplainer::FixedCollectionType HashCollectionType = YARD::Tags::TypesExplainer::HashCollectionType @@ -31,38 +33,43 @@ def parse_fail(types) expect(@t.to_s).to eq "an Array" expect(@t.to_s(false)).to eq "Arrays" end + + it "works for a constant value" do + ['false', 'true', 'nil', '4'].each do |name| + @t.name = name + expect(@t.to_s).to eq name + expect(@t.to_s(false)).to eq name + end + end + end + describe DuckType, '#to_s' do it "works for a method (ducktype)" do - @t.name = "#mymethod" - expect(@t.to_s).to eq "an object that responds to #mymethod" - expect(@t.to_s(false)).to eq "objects that respond to #mymethod" + duck_type = DuckType.new("#mymethod") + expect(duck_type.to_s).to eq "an object that responds to #mymethod" + expect(duck_type.to_s(false)).to eq "objects that respond to #mymethod" end it "works for multiple methods joined with '&' (ducktype)" do - @t.name = "#mymethod&#myothermethod&#mythirdmethod" - expect(@t.to_s).to eq "an object that responds to #mymethod, #myothermethod and #mythirdmethod" - expect(@t.to_s(false)).to eq "objects that respond to #mymethod, #myothermethod and #mythirdmethod" + duck_type = DuckType.new("#mymethod&#myothermethod&#mythirdmethod") + duck_type.name = "#mymethod&#myothermethod&#mythirdmethod" + expect(duck_type.to_s).to eq "an object that responds to #mymethod, #myothermethod and #mythirdmethod" + expect(duck_type.to_s(false)).to eq "objects that respond to #mymethod, #myothermethod and #mythirdmethod" end it "works for multiple methods joined with ' & ' (ducktype)" do - @t.name = "#mymethod & #myothermethod & #mythirdmethod" - expect(@t.to_s).to eq "an object that responds to #mymethod, #myothermethod and #mythirdmethod" - expect(@t.to_s(false)).to eq "objects that respond to #mymethod, #myothermethod and #mythirdmethod" - end - - it "works for a constant value" do - ['false', 'true', 'nil', '4'].each do |name| - @t.name = name - expect(@t.to_s).to eq name - expect(@t.to_s(false)).to eq name - end + duck_type = DuckType.new("#mymethod & #myothermethod & #mythirdmethod") + expect(duck_type.to_s).to eq "an object that responds to #mymethod, #myothermethod and #mythirdmethod" + expect(duck_type.to_s(false)).to eq "objects that respond to #mymethod, #myothermethod and #mythirdmethod" end + end + describe LiteralType, '#to_s' do it "works for literal values" do [':symbol', "'5'"].each do |name| - @t.name = name - expect(@t.to_s).to eq "a literal value #{name}" - expect(@t.to_s(false)).to eq "a literal value #{name}" + literal_type = LiteralType.new(name) + expect(literal_type.to_s).to eq "a literal value #{name}" + expect(literal_type.to_s(false)).to eq "a literal value #{name}" end end end @@ -105,7 +112,7 @@ def parse_fail(types) end end - describe FixedCollectionType, '#to_s' do + describe HashCollectionType, '#to_s' do before { @t = HashCollectionType.new("Hash", nil, nil) } it "can contain a single key type and value type" do From 871c974750ebcb919d61fc98eddf08bc6f118b4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lek=C3=AB=20Mula?= Date: Tue, 24 Feb 2026 22:54:22 +0100 Subject: [PATCH 3/4] Move LITERALMATCH consant out of CodeObjects --- lib/yard/code_objects/base.rb | 3 --- lib/yard/tags/types_explainer.rb | 3 +++ spec/code_objects/constants_spec.rb | 20 -------------------- spec/tags/types_explainer_spec.rb | 20 ++++++++++++++++++++ 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/lib/yard/code_objects/base.rb b/lib/yard/code_objects/base.rb index 9794f660c..561481317 100644 --- a/lib/yard/code_objects/base.rb +++ b/lib/yard/code_objects/base.rb @@ -63,9 +63,6 @@ def push(value) # Regular expression to match a fully qualified method def (self.foo, Class.foo). METHODMATCH = /(?:(?:#{NAMESPACEMATCH}|[a-z]\w*)\s*(?:#{CSEPQ}|#{NSEPQ})\s*)?#{METHODNAMEMATCH}/ - # Regular expression to match symbol and string literals - LITERALMATCH = /:\w+|'[^']*'|"[^"]*"/ - # All builtin Ruby exception classes for inheritance tree. BUILTIN_EXCEPTIONS = ["ArgumentError", "ClosedQueueError", "EncodingError", "EOFError", "Exception", "FiberError", "FloatDomainError", "IndexError", diff --git a/lib/yard/tags/types_explainer.rb b/lib/yard/tags/types_explainer.rb index 83d56c7b1..3011269f1 100644 --- a/lib/yard/tags/types_explainer.rb +++ b/lib/yard/tags/types_explainer.rb @@ -4,6 +4,9 @@ module YARD module Tags class TypesExplainer + # Regular expression to match symbol and string literals + LITERALMATCH = /:\w+|'[^']*'|"[^"]*"/ + # (see Tag#explain_types) # @param types [Array] a list of types to parse and summarize def self.explain(*types) diff --git a/spec/code_objects/constants_spec.rb b/spec/code_objects/constants_spec.rb index e9ce5f316..3c6b9180d 100644 --- a/spec/code_objects/constants_spec.rb +++ b/spec/code_objects/constants_spec.rb @@ -56,26 +56,6 @@ def silence_warnings end end - describe :LITERALMATCH do - it "matches symbol literals" do - expect(":symbol"[CodeObjects::LITERALMATCH]).to eq ":symbol" - expect(":some_symbol"[CodeObjects::LITERALMATCH]).to eq ":some_symbol" - expect("not_a_symbol"[CodeObjects::LITERALMATCH]).to be nil - end - - it "matches single-quoted string literals" do - expect("'string'"[CodeObjects::LITERALMATCH]).to eq "'string'" - expect("'some string with spaces'"[CodeObjects::LITERALMATCH]).to eq "'some string with spaces'" - expect("not_quoted"[CodeObjects::LITERALMATCH]).to be nil - end - - it "matches double-quoted string literals" do - expect('"string"'[CodeObjects::LITERALMATCH]).to eq '"string"' - expect('"some string with spaces"'[CodeObjects::LITERALMATCH]).to eq '"some string with spaces"' - expect("not_quoted"[CodeObjects::LITERALMATCH]).to be nil - end - end - describe :BUILTIN_EXCEPTIONS do it "includes all base exceptions" do bad_names = [] diff --git a/spec/tags/types_explainer_spec.rb b/spec/tags/types_explainer_spec.rb index f9fd5914d..b9871024d 100644 --- a/spec/tags/types_explainer_spec.rb +++ b/spec/tags/types_explainer_spec.rb @@ -17,6 +17,26 @@ def parse_fail(types) expect { parse(types) }.to raise_error(SyntaxError) end + describe "LITERALMATCH" do + it "matches symbol literals" do + expect(":symbol"[described_class::LITERALMATCH]).to eq ":symbol" + expect(":some_symbol"[described_class::LITERALMATCH]).to eq ":some_symbol" + expect("not_a_symbol"[described_class::LITERALMATCH]).to be nil + end + + it "matches single-quoted string literals" do + expect("'string'"[described_class::LITERALMATCH]).to eq "'string'" + expect("'some string with spaces'"[described_class::LITERALMATCH]).to eq "'some string with spaces'" + expect("not_quoted"[described_class::LITERALMATCH]).to be nil + end + + it "matches double-quoted string literals" do + expect('"string"'[described_class::LITERALMATCH]).to eq '"string"' + expect('"some string with spaces"'[described_class::LITERALMATCH]).to eq '"some string with spaces"' + expect("not_quoted"[described_class::LITERALMATCH]).to be nil + end + end + describe Type, '#to_s' do before { @t = Type.new(nil) } From bcf20c90768a87c788a520e294becdc736da838b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lek=C3=AB=20Mula?= Date: Sat, 27 Sep 2025 13:55:13 +0200 Subject: [PATCH 4/4] Add support for multiple & nested Hash keys definition --- lib/yard/tags/types_explainer.rb | 154 +++++++++++++++++++++++------- spec/tags/types_explainer_spec.rb | 5 +- 2 files changed, 124 insertions(+), 35 deletions(-) diff --git a/lib/yard/tags/types_explainer.rb b/lib/yard/tags/types_explainer.rb index 3011269f1..420b9fb09 100644 --- a/lib/yard/tags/types_explainer.rb +++ b/lib/yard/tags/types_explainer.rb @@ -92,18 +92,56 @@ def to_s(_singular = true) # @private class HashCollectionType < Type - attr_accessor :key_types, :value_types + attr_accessor :key_value_pairs - def initialize(name, key_types, value_types) + def initialize(name, key_types_or_pairs, value_types = nil) @name = name - @key_types = key_types - @value_types = value_types + + if value_types.nil? + # New signature: (name, key_value_pairs) + @key_value_pairs = key_types_or_pairs || [] + else + # Old signature: (name, key_types, value_types) + @key_value_pairs = [[key_types_or_pairs, value_types]] + end + end + + # Backward compatibility accessors + def key_types + return [] if @key_value_pairs.empty? + @key_value_pairs.first[0] || [] + end + + def key_types=(types) + if @key_value_pairs.empty? + @key_value_pairs = [[types, []]] + else + @key_value_pairs[0][0] = types + end + end + + def value_types + return [] if @key_value_pairs.empty? + @key_value_pairs.first[1] || [] + end + + def value_types=(types) + if @key_value_pairs.empty? + @key_value_pairs = [[[], types]] + else + @key_value_pairs[0][1] = types + end end def to_s(_singular = true) - "a#{name[0, 1] =~ /[aeiou]/i ? 'n' : ''} #{name} with keys made of (" + - list_join(key_types.map {|t| t.to_s(false) }) + - ") and values of (" + list_join(value_types.map {|t| t.to_s(false) }) + ")" + return "a#{name[0, 1] =~ /[aeiou]/i ? 'n' : ''} #{name}" if @key_value_pairs.empty? + + result = "a#{name[0, 1] =~ /[aeiou]/i ? 'n' : ''} #{name} with " + parts = @key_value_pairs.map do |keys, values| + "keys made of (" + list_join(keys.map {|t| t.to_s(false) }) + + ") and values of (" + list_join(values.map {|t| t.to_s(false) }) + ")" + end + result + parts.join(" and ") end end @@ -117,11 +155,13 @@ class Parser :fixed_collection_start => /\(/, :fixed_collection_end => /\)/, :type_name => /#{ISEP}#{METHODNAMEMATCH}|#{NAMESPACEMATCH}|#{LITERALMATCH}|\w+/, - :type_next => /[,;]/, + :type_next => /[,]/, :whitespace => /\s+/, :hash_collection_start => /\{/, - :hash_collection_next => /=>/, + :hash_collection_value => /=>/, + :hash_collection_value_end => /;/, :hash_collection_end => /\}/, + # :symbol_start => /:/, :parse_end => nil } @@ -133,10 +173,45 @@ def initialize(string) @scanner = StringScanner.new(string) end - def parse - types = [] + # @return [Array(Boolean, Array)] - finished, types + def parse(until_tokens: [:parse_end]) + current_parsed_types = [] type = nil name = nil + finished = false + parse_with_handlers do |token_type, token| + case token_type + when *until_tokens + raise SyntaxError, "expecting name, got '#{token}'" if name.nil? + type = create_type(name) unless type + current_parsed_types << type + finished = true + when :type_name + raise SyntaxError, "expecting END, got name '#{token}'" if name + name = token + when :type_next + raise SyntaxError, "expecting name, got '#{token}' at #{@scanner.pos}" if name.nil? + type = create_type(name) unless type + current_parsed_types << type + name = nil + type = nil + when :fixed_collection_start, :collection_start + name ||= "Array" + klass = token_type == :collection_start ? CollectionType : FixedCollectionType + type = klass.new(name, parse(until_tokens: [:fixed_collection_end, :collection_end, :parse_end])) + when :hash_collection_start + name ||= "Hash" + type = parse_hash_collection(name) + end + + [finished, current_parsed_types] + end + end + + private + + # @return [Array] + def parse_with_handlers loop do found = false TOKENS.each do |token_type, match| @@ -144,32 +219,43 @@ def parse # rubocop:disable Lint/AssignmentInCondition next unless (match.nil? && @scanner.eos?) || (match && token = @scanner.scan(match)) found = true - case token_type - when :type_name - raise SyntaxError, "expecting END, got name '#{token}'" if name - name = token - when :type_next - raise SyntaxError, "expecting name, got '#{token}' at #{@scanner.pos}" if name.nil? - type = create_type(name) unless type - types << type - type = nil - name = nil - when :fixed_collection_start, :collection_start - name ||= "Array" - klass = token_type == :collection_start ? CollectionType : FixedCollectionType - type = klass.new(name, parse) - when :hash_collection_start - name ||= "Hash" - type = HashCollectionType.new(name, parse, parse) - when :hash_collection_next, :hash_collection_end, :fixed_collection_end, :collection_end, :parse_end - raise SyntaxError, "expecting name, got '#{token}'" if name.nil? - type = create_type(name) unless type - types << type - return types - end + # @type [Array] + finished, types = yield(token_type, token) + return types if finished + break end raise SyntaxError, "invalid character at #{@scanner.peek(1)}" unless found end + nil + end + + def parse_hash_collection(name) + key_value_pairs = [] + current_keys = [] + finished = false + + parse_with_handlers do |token_type, token| + case token_type + when :type_name + current_keys << create_type(token) + when :type_next + # Comma - continue collecting keys unless we just processed a value + # In that case, start a new key group + when :hash_collection_value + # => - current keys map to the next value(s) + raise SyntaxError, "no keys before =>" if current_keys.empty? + values = parse(until_tokens: [:hash_collection_value_end, :parse_end]) + key_value_pairs << [current_keys, values] + current_keys = [] + when :hash_collection_end, :parse_end + # End of hash + finished = true + when :whitespace + # Ignore whitespace + end + + [finished, HashCollectionType.new(name, key_value_pairs)] + end end private diff --git a/spec/tags/types_explainer_spec.rb b/spec/tags/types_explainer_spec.rb index b9871024d..1a5087422 100644 --- a/spec/tags/types_explainer_spec.rb +++ b/spec/tags/types_explainer_spec.rb @@ -251,7 +251,10 @@ def parse_fail(types) "#weird_method?, #<=>, #!=" => "an object that responds to #weird_method?; an object that responds to #<=>; an object that responds to #!=", - ":symbol, 'string'" => "a literal value :symbol; a literal value 'string'" + ":symbol, 'string'" => "a literal value :symbol; a literal value 'string'", + "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)", + "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))", + "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)" } expect.each do |input, expected| explain = YARD::Tags::TypesExplainer.explain(input)