Skip to content

Commit d78cc24

Browse files
committed
Move UNION_COLLECTION_NAMES out of the parser entirely
The parser's node-class selection for <...> still referenced UNION_COLLECTION_NAMES to choose between CollectionType and ParameterizedType, even after slot_pipe stopped depending on it. Per review, that's still type-specific knowledge living in parsing code. Merge ParameterizedType into CollectionType: the parser now always builds a CollectionType for <...> (aside from the pre-existing Hash<K, V> and fixed-tuple special cases), and CollectionType#to_s decides per-instance, via a private #union? check, whether to render its type parameters as a flat union (Array/Set, or any single parameter) or neutrally. UNION_COLLECTION_NAMES now lives only on CollectionType, read only during rendering. Output is unchanged for every existing case.
1 parent 1471edb commit d78cc24

2 files changed

Lines changed: 45 additions & 52 deletions

File tree

lib/yard/tags/types_explainer.rb

Lines changed: 34 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -123,20 +123,48 @@ def disambiguate(type, singular)
123123
end
124124

125125
# @private
126+
#
127+
# `<...>`'s type parameters are conventionally used two ways - as a
128+
# homogeneous collection's implicit union of element type(s), or as
129+
# a class's distinct, positional type parameters, e.g.
130+
# `Result<Success, Failure>` - and this class renders either way,
131+
# chosen per instance by {#union?}. This keeps that choice entirely
132+
# out of the parser: every `<...>` becomes one of these, regardless
133+
# of name.
126134
class CollectionType < Type
127135
attr_accessor :types
128136

137+
# Type names known in advance to be genuinely homogeneous
138+
# collections, where `<...>`'s type parameters really do mean
139+
# "any one of these". Fixed and not user-configurable - YARD has
140+
# no syntax for a class to declare its own `<...>` convention, so
141+
# this can only ever be a hardcoded, conservative list.
142+
UNION_COLLECTION_NAMES = %w[Array Set].freeze
143+
129144
def initialize(name, types)
130145
@name = name
131146
@types = types
132147
end
133148

134149
def to_s(_singular = true)
135-
"#{indefinite_article(name)} #{name} of (" + list_join(flattened_types.map {|t| t.to_s(false) }) + ")"
150+
if union?
151+
"#{indefinite_article(name)} #{name} of (" + list_join(flattened_types.map {|t| t.to_s(false) }) + ")"
152+
else
153+
"#{indefinite_article(name)} #{name} with type parameters (" +
154+
types.map {|t| t.to_s(true) }.join(", ") + ")"
155+
end
136156
end
137157

138158
private
139159

160+
# A single type parameter is never ambiguous (there's nothing to
161+
# distinguish a union from a positional type parameter when
162+
# there's only one), so it's read as a union regardless of name;
163+
# otherwise, only the known homogeneous-collection names are.
164+
def union?
165+
types.size <= 1 || UNION_COLLECTION_NAMES.include?(name)
166+
end
167+
140168
# A parsed type parameter that's itself a union (from `Foo | Bar`
141169
# in e.g. `Array<Foo | Bar, Baz>`) is a {GroupType}. Since this
142170
# whole parameter list is already read as one flat union, absorb
@@ -148,29 +176,6 @@ def flattened_types
148176
end
149177
end
150178

151-
# @private
152-
#
153-
# Unlike {CollectionType}, this doesn't assert that its type
154-
# parameters are alternatives ("of (A's or B's)") - `<...>` is
155-
# conventionally used both ways (a homogeneous collection's element
156-
# type(s), or a class's distinct positional type-parameter roles,
157-
# e.g. `Result<Success, Failure>`), and there's no way for YARD to
158-
# know which one a given class means. This is the honest fallback
159-
# for any name not specifically known to mean the former.
160-
class ParameterizedType < Type
161-
attr_accessor :types
162-
163-
def initialize(name, types)
164-
@name = name
165-
@types = types
166-
end
167-
168-
def to_s(_singular = true)
169-
"#{indefinite_article(name)} #{name} with type parameters (" +
170-
types.map {|t| t.to_s(true) }.join(", ") + ")"
171-
end
172-
end
173-
174179
# @private
175180
class FixedCollectionType < CollectionType
176181
def to_s(_singular = true)
@@ -258,13 +263,6 @@ class Parser
258263
:parse_end => nil
259264
}
260265

261-
# Type names known in advance to be genuinely homogeneous
262-
# collections, where `<...>`'s comma-separated slots really do mean
263-
# "any one of these". Fixed and not user-configurable - YARD has no
264-
# syntax for a class to declare its own `<...>` convention, so this
265-
# can only ever be a hardcoded, conservative list.
266-
UNION_COLLECTION_NAMES = %w[Array Set].freeze
267-
268266
def self.parse(string)
269267
new(string).parse
270268
end
@@ -377,18 +375,12 @@ def parse_until(until_tokens, slot_pipe: false)
377375
# (slot 0 = key type, slot 1 = value type), matching the
378376
# dedicated `Hash{K=>V}` syntax - not an implicit union.
379377
HashCollectionType.new(name, [nested_types[0]], [nested_types[1]])
380-
elsif nested_types.size <= 1 || UNION_COLLECTION_NAMES.include?(name)
381-
# A single slot is never ambiguous (nothing to distinguish
382-
# union from positional with only one type), and these
383-
# names are known, genuinely homogeneous collections.
384-
CollectionType.new(name, nested_types)
385378
else
386-
# `<...>` is conventionally used both ways - a homogeneous
387-
# collection's element type(s), or a class's distinct
388-
# positional type-parameter roles - and YARD has no way to
389-
# know which one an arbitrary class means. Don't assert
390-
# union for a name we don't specifically know means that.
391-
ParameterizedType.new(name, nested_types)
379+
# Whether these type parameters get described as a union
380+
# (Array, Set) or neutrally (everything else) doesn't
381+
# affect parsing, so it's not decided here - see
382+
# {CollectionType#union?}.
383+
CollectionType.new(name, nested_types)
392384
end
393385
when :group_start
394386
raise SyntaxError, "'[' cannot follow a type name" if name

spec/tags/types_explainer_spec.rb

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -149,12 +149,10 @@ def type(name)
149149
@t.types = [group, type("Baz")]
150150
expect(@t.to_s).to eq "an Array of (Foos, Bars or Bazs)"
151151
end
152-
end
153152

154-
describe YARD::Tags::TypesExplainer::ParameterizedType, '#to_s' do
155-
it "lists type parameters without pluralizing or asserting union/order" do
156-
parameterized = described_class.new("Result", [type("Success"), type("Failure")])
157-
expect(parameterized.to_s).to eq "a Result with type parameters (a Success, a Failure)"
153+
it "lists a non-allow-listed name's 2+ type parameters without pluralizing or asserting union/order" do
154+
result = described_class.new("Result", [type("Success"), type("Failure")])
155+
expect(result.to_s).to eq "a Result with type parameters (a Success, a Failure)"
158156
end
159157
end
160158

@@ -262,11 +260,12 @@ def parse_fail(types)
262260
expect(type.first).to be_a(YARD::Tags::TypesExplainer::CollectionType)
263261
end
264262

265-
it "uses ParameterizedType for a non-allow-listed name with 2+ parameters" do
263+
it "renders a non-allow-listed name's 2+ parameters neutrally" do
266264
type = parse("Result<Success, Failure>")
267-
expect(type.first).to be_a(YARD::Tags::TypesExplainer::ParameterizedType)
265+
expect(type.first).to be_a(YARD::Tags::TypesExplainer::CollectionType)
268266
expect(type.first.name).to eq "Result"
269267
expect(type.first.types.map(&:name)).to eq ["Success", "Failure"]
268+
expect(type.first.to_s).to eq "a Result with type parameters (a Success, a Failure)"
270269
end
271270

272271
it "special-cases Hash<KeyType, ValueType> to match Hash{K=>V}'s key/value rendering" do
@@ -277,16 +276,18 @@ def parse_fail(types)
277276
expect(by_angle_brackets.first.value_types.map(&:name)).to eq by_braces.first.value_types.map(&:name)
278277
end
279278

280-
it "falls back to ParameterizedType for Hash<...> with the wrong number of parameters" do
279+
it "falls back to a neutral CollectionType for Hash<...> with the wrong number of parameters" do
281280
type = parse("Hash<A, B, C>")
282-
expect(type.first).to be_a(YARD::Tags::TypesExplainer::ParameterizedType)
281+
expect(type.first).to be_a(YARD::Tags::TypesExplainer::CollectionType)
282+
expect(type.first.to_s).to eq "a Hash with type parameters (an A, a B, a C)"
283283
type = parse("Hash<A>")
284284
expect(type.first).to be_a(YARD::Tags::TypesExplainer::CollectionType)
285+
expect(type.first.to_s).to eq "a Hash of (A's)"
285286
end
286287

287288
it "groups '|' within a single slot of a non-allow-listed <...>, since its slots are positional" do
288289
type = parse("Result<Success | Failure, Other>")
289-
expect(type.first).to be_a(YARD::Tags::TypesExplainer::ParameterizedType)
290+
expect(type.first).to be_a(YARD::Tags::TypesExplainer::CollectionType)
290291
expect(type.first.types.size).to eq 2
291292
expect(type.first.types.first).to be_a(YARD::Tags::TypesExplainer::GroupType)
292293
expect(type.first.types.first.types.map(&:name)).to eq ["Success", "Failure"]

0 commit comments

Comments
 (0)