From 71eee3bea12d3978ff7cceef531ef227e0d1929d 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/2] 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 d87657665..1aa9a50a9 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 #{name}" : "objects that respond to #{name}" + 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 8075790de..87d28005e 100644 --- a/spec/tags/types_explainer_spec.rb +++ b/spec/tags/types_explainer_spec.rb @@ -45,6 +45,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 @@ -131,6 +139,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 +211,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 2d79fb8aef716a1b339495f9d16f41cd7b8e00a8 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/2] 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 | 26 +++++++++++++-------- 2 files changed, 46 insertions(+), 18 deletions(-) diff --git a/lib/yard/tags/types_explainer.rb b/lib/yard/tags/types_explainer.rb index 1aa9a50a9..b6ce2f660 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 #{name}" : "objects that respond to #{name}" - 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) index = 0 @@ -56,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 @@ -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 87d28005e..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 @@ -45,12 +41,22 @@ def parse_fail(types) expect(@t.to_s(false)).to eq name end 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| - @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 @@ -93,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