Skip to content

Commit 6521988

Browse files
committed
Add "&" intersection types, "|" union operator, and "[...]" precedence override
Adds three related type-tag syntax elements: - `&` (intersection, closes #1644): `Foo & Bar` means a value must satisfy both `Foo` and `Bar`, matching Solargraph's syntax (castwide/solargraph#1231). Legal in every position a type can appear, and always binds tighter than the union or slot separator around it, matching RBS's documented precedence. Renders as "both a Foo and a Bar" (or "all of a Foo, a Bar, and a Baz" for 3+), to avoid reading like two separate values. - `|` (union, closes #1699): marks a union - a value matching any of the listed types. Some type lists already mean a union without it (the top level, a hash's key/value lists, `[...]`, and `Array<...>`/`Set<...>`), so `,` and `|` land on the same result there. Elsewhere, each comma-separated item is a distinct, positional type parameter instead (a fixed-order list like `Array(...)`, or `<...>` for a name other than `Array`/`Set`) - there, `|` groups alternatives within a single one of them: `Array(Foo | Bar, Baz)` is a 2-element Array whose first element is a Foo or a Bar, and `Result<Success | Failure, Other>` is a Result whose first type parameter is a Success or a Failure. - `[...]` (closes #1699): used the same way parentheses are in algebra, to override the default order of operations - e.g. to use a union as one conjunct of an intersection, which otherwise has no way to mark where the union ends: `[Foo | Bar] & Baz`. Also documents three pre-existing but previously undocumented anonymous shorthand forms - `<A>`, `(A)`, `{A=>B}` - where the leading type name can be omitted and defaults to `Array`/`Hash` (see #1701), and stops `Foo<A, B>` from always being read as a union: `<...>`'s type parameters are conventionally used both ways - a homogeneous collection's implicit union of element type(s) (`Array<String, Symbol>`), or a class's distinct, positional type parameters (`Result<Success, Failure>`). `Array`/`Set` (and any name with a single type parameter) keep the union reading; `Hash<K, V>` gets its own dedicated key/value rendering matching `Hash{K=>V}`; anything else with 2+ parameters reads neutrally ("a Result with type parameters (a Success, a Failure)"). This choice is made entirely by `CollectionType#to_s` at render time - the parser always treats `<...>` the same way it already treats `(...)` (`,` separates positional type parameters, `|` groups alternatives within one of them), with no name-specific knowledge at all. Full rules and examples are in the new "Operator Precedence" and "Overriding the Order of Operations" sections of `docs/Tags.md`, and the rewritten "Parameterized Types"/"Union Operator" sections. Test plan: - `bundle exec rspec spec/tags/types_explainer_spec.rb` - specs for `IntersectionType`/`GroupType`/`CollectionType#to_s`, parser-level precedence/error cases, and end-to-end `.explain` examples. - `bundle exec rspec` - full suite green (2830 examples, 0 failures).
1 parent 1f6bac2 commit 6521988

4 files changed

Lines changed: 704 additions & 24 deletions

File tree

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
11
# main
22

3+
- Add support for intersection (`Foo & Bar`) types: a value must satisfy
4+
every type listed, rather than any one of them, reading as "both a Foo
5+
and a Bar" (or "all of a Foo, a Bar, and a Baz" for 3+) to avoid reading
6+
like two separate values (closes #1644)
7+
- Add `|` as a union operator (`Foo | Bar` means an object that's either a
8+
Foo or a Bar). Type lists that already mean a union - the top level, a
9+
hash's key/value lists, `Array<...>`/`Set<...>` - read `,` and `|` the
10+
same way; elsewhere, where a comma-separated list means distinct type
11+
parameters or tuple slots (`Array(...)`, or `<...>` for a name other
12+
than `Array`/`Set`), `|` groups a union within a single one of them
13+
(closes #1699)
14+
- Add `[...]`, used the same way parentheses are in algebra: to override
15+
the default order of operations, e.g. to use a union as one conjunct of
16+
an intersection (closes #1699)
17+
- Document the existing anonymous `<A>`, `(A)`, and `{A=>B}` shorthand forms
18+
- Stop assuming `Foo<A, B>` always means "A or B": only `Array`/`Set` (known
19+
homogeneous collections) keep that implicit-union reading; any other
20+
name with 2+ type parameters now reads neutrally as "with type
21+
parameters (A, B)", since `<...>` is also conventionally used for a
22+
class's distinct type parameters (e.g. `Result<Success, Failure>`).
23+
`Hash<KeyType, ValueType>` gets its own dedicated positional rendering,
24+
matching `Hash{KeyType=>ValueType}`
325
- Fix duplicate "View source" links after client-side navigation in default HTML template
426

527
# [0.9.45] - July 14th, 2026

docs/Tags.md

Lines changed: 96 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -169,18 +169,29 @@ Note that one extra type that is accepted by convention is the `Boolean` type,
169169
which represents both the `TrueClass` and `FalseClass` types. This type does not
170170
exist in Ruby, however.
171171

172-
#### Parametrized Types
172+
#### Parameterized Types
173173

174174
In addition to basic types (like String or Array), YARD conventions allow for
175-
a "generics" like syntax to specify container objects or other parametrized types.
175+
a "generics" like syntax to specify container objects or other parameterized types.
176176
The syntax is `Type<SubType, OtherSubType, ...>`. For instance, an Array might
177177
contain only String objects, in which case the type specification would be
178-
`Array<String>`. Multiple parametrized types can be listed, separated by commas.
179-
180-
Note that parametrized types are typically not order-dependent, in other words,
181-
a list of parametrized types can occur in any order inside of a type. An array
182-
specified as `Array<String, Fixnum>` can contain any amount of Strings or Fixnums,
183-
in any order. When the order matters, use "order-dependent lists", described below.
178+
`Array<String>`. Multiple type parameters can be listed, separated by commas:
179+
`Array<String, Fixnum>` can contain any amount of Strings or Fixnums, in any
180+
order - `Array` and `Set` both treat their type parameters as an implicit
181+
union, meaning "any of these".
182+
183+
The type name before `<...>` can be omitted, in which case it defaults to
184+
`Array`: `<String, Fixnum>` means the same thing as `Array<String, Fixnum>`.
185+
186+
Not every type uses its parameters this way, though: `Result<Success, Failure>`
187+
uses `Success` and `Failure` as two distinct type parameters, not "either of
188+
these". A type with a single parameter is unambiguous either way. For a name
189+
that isn't specifically known to mean a union, YARD describes its
190+
parameters neutrally, without asserting either reading:
191+
`Result<Success, Failure>` reads as "a Result with type parameters (a
192+
Success, a Failure)". `Hash<KeyType, ValueType>` is a special case with its
193+
own dedicated positional rendering (slot 1 is the key type, slot 2 is the
194+
value type), matching the `Hash{KeyType=>ValueType}` syntax described below.
184195

185196
#### Duck-Types
186197

@@ -194,9 +205,24 @@ that responds to the "read" method:
194205
# @param io [#read] the input object to read from
195206
def read(io) io.read end
196207

208+
#### Intersection Types
209+
210+
Types joined with `&` describe an intersection: a value that satisfies every
211+
type listed, rather than any one of them (which is what a union - the `|`
212+
operator - already means). For instance, an argument that must both
213+
inherit from `Foo` and respond to `#bar` would be listed as `Foo & #bar`.
214+
215+
# Accepts any Comparable string.
216+
# @param value [String & Comparable] the value to accept
217+
def accept(value) end
218+
219+
`&` is legal in every position a type can appear, and always binds tighter
220+
than any union or slot separator around it - see
221+
[Operator Precedence](#Operator_Precedence) below.
222+
197223
#### Hashes
198224

199-
Hashes can be specified either via the parametrized type discussed above,
225+
Hashes can be specified either via the parameterized type discussed above,
200226
in the form `Hash<KeyType, ValueType>`, or using the hash specific syntax:
201227
`Hash{KeyTypes=>ValueTypes}`. In the latter case, KeyTypes or ValueTypes can
202228
also be a list of types separated by commas.
@@ -222,13 +248,74 @@ Keys in the hash-specific syntax are commonly [literal values](#Literals) such
222248
as symbols (`:key`) or strings (`'key'`, `"key"`), but any type listed in the
223249
[type conventions](#Type_List_Conventions) is allowed.
224250

251+
The type name before `{...}` can be omitted, in which case it defaults to
252+
`Hash`: `{K=>V}` means the same thing as `Hash{K=>V}`.
253+
225254
#### Order-Dependent Lists
226255

227256
An order dependent list is a set of types surrounded by "()" and separated by
228257
commas. This list must contain exactly those types in exactly the order specified.
229258
For instance, an Array containing a String, Fixnum and Hash in that order (and
230259
having exactly those 3 elements) would be listed as: `Array(String, Fixnum, Hash)`.
231260

261+
The type name before `(...)` can be omitted, in which case it defaults to
262+
`Array`: `(String, Fixnum, Hash)` means the same thing as
263+
`Array(String, Fixnum, Hash)`.
264+
265+
A single slot can itself be a union: `Array(Integer | String, Symbol)` is a
266+
2-element Array whose first element is an Integer or a String - see
267+
[Union Operator](#Union_Operator) below.
268+
269+
#### Union Operator
270+
271+
`|` marks a union: a value matching any of the listed types.
272+
`Integer | String` means an Integer or a String.
273+
274+
Some type lists already mean a union without `|` - a plain comma-separated
275+
list at the top level, a hash's key or value list, inside `[...]`
276+
(described below), and inside `Array<...>`/`Set<...>`. In those places `,`
277+
and `|` come to the same thing, so use whichever reads better:
278+
`Integer, String` and `Integer | String` describe the same type.
279+
280+
Elsewhere, each comma-separated item is a distinct, positional type
281+
parameter instead - a fixed-order list like `Array(...)`, or `<...>` for a
282+
type other than `Array`/`Set` (see
283+
[Parameterized Types](#Parameterized_Types) above). There, use `|` within a
284+
single item to say it can be any of several types:
285+
`Array(Integer | String, Symbol)` is a 2-element Array whose first element
286+
is an Integer or a String, and `Result<Success | Failure, Other>` is a
287+
Result whose first type parameter is a Success or a Failure.
288+
289+
#### Operator Precedence
290+
291+
`&`, `,`, and `|` can all appear in the same type, and `&` always binds
292+
tighter than the union or slot separator around it: `Foo & Bar, Baz` means
293+
either both a Foo and a Bar, or a Baz - not `Foo`, unioned with `Bar & Baz`.
294+
295+
#### Overriding the Order of Operations
296+
297+
Square brackets `[...]` are used the same way parentheses are in algebra:
298+
to override the order of operations described above. Types inside `[...]`
299+
are combined first; hence `[Foo | Bar] & Baz` describes a value that's
300+
either a Foo or a Bar, and also a Baz - not `Foo`, unioned with
301+
`Bar & Baz`. Without the brackets, `&` binds tighter than the union
302+
around it, so `Foo | Bar & Baz` means the latter.
303+
304+
Inside `[...]`, `,` and `|` both mean a union, so `[Foo, Bar]` and
305+
`[Foo | Bar]` describe the same type.
306+
307+
<p class="note">
308+
While you can treat them the same in practice, this <code>[...]</code> is
309+
not the same thing as the <code>[Types]</code> brackets that delimit a
310+
tag's whole <a href="#Types_Specifier_List">types specifier list</a> -
311+
that outer bracket is tag punctuation, not part of any individual type.
312+
</p>
313+
314+
`[...]` never takes a preceding type name - `[Integer | String]` alone
315+
just means "an Integer or a String," identical in meaning to the plain
316+
top-level list `Integer, String`, just usable in more places. It can nest
317+
inside itself (`[[Foo | Bar] | Baz]`).
318+
232319
#### Literals
233320

234321
Some literals are accepted by virtue of being Ruby literals, but also by YARD

0 commit comments

Comments
 (0)