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 d87657665..b6ce2f660 100644 --- a/lib/yard/tags/types_explainer.rb +++ b/lib/yard/tags/types_explainer.rb @@ -31,16 +31,14 @@ def initialize(name) end def to_s(singular = true) - if name[0, 1] == "#" - singular ? "an object that responds to #{name}" : "objects that respond to #{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) index = 0 @@ -54,6 +52,20 @@ def list_join(list) 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 #{name}" : "objects that respond to #{name}" + end + end + # @private class CollectionType < Type attr_accessor :types @@ -101,7 +113,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 => /\{/, @@ -135,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 @@ -148,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 @@ -156,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/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 8075790de..4d76d3149 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 @@ -32,12 +34,6 @@ def parse_fail(types) expect(@t.to_s(false)).to eq "Arrays" end - 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" - end - it "works for a constant value" do ['false', 'true', 'nil', '4'].each do |name| @t.name = name @@ -47,6 +43,24 @@ def parse_fail(types) end end + describe DuckType, '#to_s' do + it "works for a method (ducktype)" do + 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 + end + + describe LiteralType, '#to_s' do + it "works for literal values" do + [':symbol', "'5'"].each do |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 + describe CollectionType, '#to_s' do before { @t = CollectionType.new("Array", nil) } @@ -85,7 +99,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 @@ -131,6 +145,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) @@ -192,7 +217,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)