Skip to content

Commit 71f3ca4

Browse files
committed
Implement multivalued annotations
Annotations may be defined as having multiple values, in which case multiple values may be added to the internal storage on the symbol, and providers may return enumerations of values. New annotation enumeration APIs allow enumerating the values from the symbol and the providers.
1 parent 0914d56 commit 71f3ca4

6 files changed

+269
-101
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
namespace System.CommandLine.Subsystems.Annotations;
5+
6+
/// <summary>
7+
/// Thrown when an annotation collection value does not match the expected type for that <see cref="AnnotationId"/>.
8+
/// </summary>
9+
public class AnnotationCollectionTypeException(AnnotationId annotationId, Type expectedType, Type? actualType, IAnnotationProvider? provider = null)
10+
: AnnotationTypeException(annotationId, expectedType, actualType, provider)
11+
{
12+
public override string Message
13+
{
14+
get
15+
{
16+
if (Provider is not null)
17+
{
18+
return
19+
$"Typed accessor for annotation '${AnnotationId}' expected collection of values of type " +
20+
$"'{ExpectedType}' but the annotation provider returned an annotation value of type " +
21+
$"'{ActualType?.ToString() ?? "[null]"}'. " +
22+
$"This may be an authoring error in in the annotation provider '{Provider.GetType()}' or in a " +
23+
"typed annotation accessor.";
24+
25+
}
26+
27+
return
28+
$"Typed accessor for annotation '${AnnotationId}' expected collection of values of type '{ExpectedType}' " +
29+
$"but the stored annotation value is of type '{ActualType?.ToString() ?? "[null]"}'. " +
30+
$"This may be an authoring error in a typed annotation accessor, or the annotation may have been stored " +
31+
$"directly with the incorrect type, bypassing the typed accessors.";
32+
}
33+
}
34+
}

src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationResolver.cs

+69-6
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,14 @@ public AnnotationResolver(PipelineResult pipelineResult)
2525

2626
/// <summary>
2727
/// Attempt to retrieve the <paramref name="symbol"/>'s value for the annotation <paramref name="id"/>. This will check any
28-
/// annotation providers that were passed to the constructor, and the internal per-symbol annotation storage.
28+
/// annotation providers that were passed to the resolver, and the internal per-symbol annotation storage.
2929
/// </summary>
3030
/// <typeparam name="TValue">
3131
/// The expected type of the annotation value. If the type does not match, a <see cref="AnnotationTypeException"/> will be thrown.
3232
/// If the annotation allows multiple types for its values, and a type parameter cannot be determined statically,
33-
/// use <see cref="TryGetAnnotation(CliSymbol, AnnotationId, out object?)"/> to access the annotation value without checking its type.
33+
/// use <see cref="TryGet(CliSymbol, AnnotationId, out object?)"/> to access the annotation value without checking its type.
3434
/// </typeparam>
35-
/// <param name="symbol">The symbol the value is attached to</param>
35+
/// <param name="symbol">The symbol that is annotated</param>
3636
/// <param name="id">
3737
/// The identifier for the annotation value to be retrieved.
3838
/// For example, the annotation identifier for the help description is <see cref="HelpAnnotations.Description">.
@@ -69,9 +69,9 @@ public bool TryGet<TValue>(CliSymbol symbol, AnnotationId annotationId, [NotNull
6969

7070
/// <summary>
7171
/// Attempt to retrieve the <paramref name="symbol"/>'s value for the annotation <paramref name="id"/>. This will check any
72-
/// annotation providers that were passed to the constructor, and the internal per-symbol annotation storage.
72+
/// annotation providers that were passed to the resolver, and the internal per-symbol annotation storage.
7373
/// </summary>
74-
/// <param name="symbol">The symbol the value is attached to</param>
74+
/// <param name="symbol">The symbol that is annotated</param>
7575
/// <param name="id">
7676
/// The identifier for the annotation value to be retrieved.
7777
/// For example, the annotation identifier for the help description is <see cref="HelpAnnotations.Description">.
@@ -90,6 +90,13 @@ public bool TryGet<TValue>(CliSymbol symbol, AnnotationId annotationId, [NotNull
9090
/// </remarks>
9191
public bool TryGet(CliSymbol symbol, AnnotationId annotationId, [NotNullWhen(true)] out object? value)
9292
{
93+
// a value set directly on the symbol takes precedence over values returned by providers.
94+
if (symbol.TryGetAnnotation(annotationId, out value))
95+
{
96+
return true;
97+
}
98+
99+
// Providers are given precedence in the order they were provided to the resolver.
93100
foreach (var provider in providers)
94101
{
95102
if (provider.TryGet(symbol, annotationId, context, out value))
@@ -98,7 +105,7 @@ public bool TryGet(CliSymbol symbol, AnnotationId annotationId, [NotNullWhen(tru
98105
}
99106
}
100107

101-
return symbol.TryGetAnnotation(annotationId, out value);
108+
return false;
102109
}
103110

104111
/// <summary>
@@ -128,4 +135,60 @@ public bool TryGet(CliSymbol symbol, AnnotationId annotationId, [NotNullWhen(tru
128135

129136
return default;
130137
}
138+
139+
/// <summary>
140+
/// For an annotation <paramref name="id"/> that permits multiple values, enumerate the values associated with
141+
/// the <paramref name="symbol"/>. If the annotation is not set, an empty enumerable will be returned. This will
142+
/// check any annotation providers that were passed to the resolver, and the internal per-symbol annotation storage.
143+
/// </summary>
144+
/// <typeparam name="TValue">
145+
/// The expected type of the annotation value. If the type does not match, a <see cref="AnnotationCollectionTypeException"/>
146+
/// will be thrown.
147+
/// </typeparam>
148+
/// <param name="symbol">The symbol that is annotated</param>
149+
/// <param name="id">
150+
/// The identifier for the annotation value to be retrieved.
151+
/// For example, the annotation identifier for the help description is <see cref="HelpAnnotations.Description">.
152+
/// </param>
153+
/// <param name="value">An out parameter to contain the result</param>
154+
/// <returns>True if successful</returns>
155+
/// <remarks>
156+
/// This is intended for use by developers defining custom annotation IDs. Anyone defining an annotation
157+
/// ID should also define an accessor extension method on <see cref="AnnotationResolver"/> extension method
158+
/// on <see cref="CliSymbol"/> that subsystem authors can use to access the annotation value, such as
159+
/// <see cref="HelpAnnotationExtensions.GetDescription{TSymbol}(AnnotationResolver, TSymbol)"/>.
160+
/// </remarks>
161+
public IEnumerable<TValue> Enumerate<TValue>(CliSymbol symbol, AnnotationId annotationId)
162+
{
163+
// Values added directly on the symbol take precedence over values returned by providers,
164+
// so they are returned first.
165+
// NOTE: EnumerateAnnotations returns these in the reverse order they were added, which means that
166+
// callers that take the first value of a given subtype will get the most recently added value of
167+
// that subtype that the CLI author added to the symbol.
168+
foreach (var value in symbol.EnumerateAnnotations<TValue>(annotationId))
169+
{
170+
yield return value;
171+
}
172+
173+
// Providers are given precedence in the order they were provided to the resolver.
174+
foreach (var provider in providers)
175+
{
176+
if (!provider.TryGet(symbol, annotationId, context, out object? rawValue))
177+
{
178+
continue;
179+
}
180+
181+
if (rawValue is IEnumerable<TValue> expectedTypeValue)
182+
{
183+
foreach (var value in expectedTypeValue)
184+
{
185+
yield return value;
186+
}
187+
}
188+
else
189+
{
190+
throw new AnnotationTypeException(annotationId, typeof(IEnumerable<TValue>), rawValue?.GetType(), provider);
191+
}
192+
}
193+
}
131194
}

0 commit comments

Comments
 (0)