Skip to content

Commit b28ce92

Browse files
committed
Clarify the new annotation mechanism
1 parent 662ec29 commit b28ce92

File tree

6 files changed

+177
-37
lines changed

6 files changed

+177
-37
lines changed

src/System.CommandLine.Subsystems/HelpAnnotationExtensions.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,48 @@
11
// Copyright (c) .NET Foundation and contributors. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

4+
using System.CommandLine.Subsystems;
45
using System.CommandLine.Subsystems.Annotations;
56

67
namespace System.CommandLine;
78

89
public static class HelpAnnotationExtensions
910
{
11+
/// <summary>
12+
/// Sets the help description on the <paramref name="symbol"/>
13+
/// </summary>
14+
/// <typeparam name="TSymbol">The type of the symbol</typeparam>
15+
/// <param name="symbol">The symbol</param>
16+
/// <param name="description">The help description for the symbol</param>
17+
/// <returns>The <paramref name="symbol">, to enable fluent construction of symbols with annotations.</returns>
1018
public static TSymbol WithDescription<TSymbol> (this TSymbol symbol, string description) where TSymbol : CliSymbol
1119
{
1220
symbol.SetDescription(description);
1321
return symbol;
1422
}
1523

24+
25+
/// <summary>
26+
/// Sets the help description on the <paramref name="symbol"/>
27+
/// </summary>
28+
/// <typeparam name="TSymbol">The type of the symbol</typeparam>
29+
/// <param name="symbol">The symbol</param>
30+
/// <param name="description">The help description for the symbol</param>
1631
public static void SetDescription<TSymbol>(this TSymbol symbol, string description) where TSymbol : CliSymbol
1732
{
1833
symbol.SetAnnotation(HelpAnnotations.Description, description);
1934
}
2035

36+
/// <summary>
37+
/// Get the help description on the <paramref name="symbol"/>
38+
/// </summary>
39+
/// <typeparam name="TSymbol">The type of the symbol</typeparam>
40+
/// <param name="symbol">The symbol</param>
41+
/// <returns>The symbol description if any, otherwise <see langword="null"/></returns>
42+
/// <remarks>
43+
/// This is intended to be called by CLI authors. Subsystems should instead call <see cref="HelpSubsystem.TryGetDescription(CliSymbol, out string?)"/>,
44+
/// values from the subsystem's <see cref="IAnnotationProvider"/>.
45+
/// </remarks>
2146
public static string? GetDescription<TSymbol>(this TSymbol symbol) where TSymbol : CliSymbol
2247
{
2348
return symbol.GetAnnotationOrDefault(HelpAnnotations.Description);

src/System.CommandLine.Subsystems/HelpSubsystem.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,7 @@ protected internal override CliExit Execute(PipelineContext pipelineContext)
4040
pipelineContext.ConsoleHack.WriteLine("Help me!");
4141
return CliExit.SuccessfullyHandled(pipelineContext.ParseResult);
4242
}
43+
44+
public bool TryGetDescription (CliSymbol symbol, out string? description)
45+
=> TryGetAnnotation (symbol, HelpAnnotations.Description, out description);
4346
}

src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.AnnotationStorage.cs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@ partial class AnnotationStorageExtensions
99
{
1010
class AnnotationStorage : IAnnotationProvider
1111
{
12-
record struct AnnotationKey(CliSymbol symbol, string annotationId);
12+
record struct AnnotationKey(CliSymbol symbol, string prefix, string id)
13+
{
14+
public static AnnotationKey Create<TAnnotation> (CliSymbol symbol, AnnotationId<TAnnotation> annotationId)
15+
=> new (symbol, annotationId.Prefix, annotationId.Id);
16+
}
1317

1418
readonly Dictionary<AnnotationKey, object> annotations = [];
1519

16-
public bool TryGet<TValue>(CliSymbol symbol, AnnotationId<TValue> id, [NotNullWhen(true)] out TValue? value)
20+
public bool TryGet<TValue>(CliSymbol symbol, AnnotationId<TValue> annotationId, [NotNullWhen(true)] out TValue? value)
1721
{
18-
if (annotations.TryGetValue(new AnnotationKey(symbol, id.Id), out var obj))
22+
if (annotations.TryGetValue(AnnotationKey.Create(symbol, annotationId), out var obj))
1923
{
2024
value = (TValue)obj;
2125
return true;
@@ -25,15 +29,16 @@ public bool TryGet<TValue>(CliSymbol symbol, AnnotationId<TValue> id, [NotNullWh
2529
return false;
2630
}
2731

28-
public void Set<TValue>(CliSymbol symbol, AnnotationId<TValue> id, TValue value)
32+
public void Set<TValue>(CliSymbol symbol, AnnotationId<TValue> annotationId, TValue value)
2933
{
34+
var key = AnnotationKey.Create(symbol, annotationId);
3035
if (value is not null)
3136
{
32-
annotations[new AnnotationKey(symbol, id.Id)] = value;
37+
annotations[key] = value;
3338
}
3439
else
3540
{
36-
annotations.Remove(new AnnotationKey(symbol, id.Id));
41+
annotations.Remove(key);
3742
}
3843
}
3944
}

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

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public static partial class AnnotationStorageExtensions
5050
//
5151
// This is fine, as we will have the following well-defined threading behavior: an annotated grammar and pipeline may
5252
// only be constructed/modified from a single thread. Once the grammar/pipeline instance is fully constructed, it may
53-
// be safely used from multiple threads.
53+
// be safely used from multiple threads, but is not safe to further modify once in use.
5454

5555
static readonly ConditionalWeakTable<CliSymbol, AnnotationStorage> symbolToAnnotationStorage = new();
5656

@@ -68,7 +68,7 @@ public static void SetAnnotation<TValue>(this CliSymbol symbol, AnnotationId<TVa
6868
var storage = symbolToAnnotationStorage.GetValue(symbol, static (CliSymbol _) => new AnnotationStorage());
6969
storage.Set(symbol, annotationId, value);
7070
}
71-
71+
7272
/// <summary>
7373
/// Sets the value for the annotation <paramref name="id"/> associated with the <paramref name="symbol"/> in the internal annotation storage,
7474
/// and returns the <paramref name="symbol"> to enable fluent construction of symbols with annotations.
@@ -79,35 +79,34 @@ public static void SetAnnotation<TValue>(this CliSymbol symbol, AnnotationId<TVa
7979
/// The identifier for the annotation. For example, the annotation identifier for the help description is <see cref="HelpAnnotations.Description">.
8080
/// </param>
8181
/// <param name="value">The annotation value</param>
82+
/// <returns>
83+
/// The <paramref name="symbol">, to enable fluent construction of symbols with annotations.
84+
/// </returns>
8285
public static TSymbol WithAnnotation<TSymbol, TValue>(this TSymbol symbol, AnnotationId<TValue> annotationId, TValue value) where TSymbol : CliSymbol
8386
{
8487
symbol.SetAnnotation(annotationId, value);
8588
return symbol;
8689
}
8790

8891
/// <summary>
89-
/// Attempts to get the value for the annotation <paramref name="id"/> associated with the <paramref name="symbol"/>,
90-
/// first from the optional <paramref name="provider"/>, and falling back to the internal annotation storage used to
91-
/// store values set via <see cref="SetAnnotation{TValue}(CliSymbol, AnnotationId{TValue}, TValue)"/>.
92+
/// Attempts to get the value for the annotation <paramref name="annotationId"/> associated with the <paramref name="symbol"/> in the internal annotation
93+
/// storage used to store values set via <see cref="SetAnnotation{TValue}(CliSymbol, AnnotationId{TValue}, TValue)"/>.
9294
/// </summary>
9395
/// <typeparam name="TValue">The type of the annotation value</typeparam>
9496
/// <param name="symbol">The symbol that is annotated</param>
95-
/// <param name="id">
97+
/// <param name="annotationId">
9698
/// The identifier for the annotation. For example, the annotation identifier for the help description is <see cref="HelpAnnotations.Description">.
9799
/// </param>
98100
/// <param name="value">The annotation value, if successful, otherwise <c>default</c></param>
99-
/// <param name="provider">
100-
/// An optional annotation provider that may implement custom or lazy construction of annotation values. Annotation returned by an annotation
101-
/// provider take precedence over those stored in internal annotation storage.
102-
/// </param>
103101
/// <returns>True if successful</returns>
104-
public static bool TryGetAnnotation<TValue>(this CliSymbol symbol, AnnotationId<TValue> annotationId, [NotNullWhen(true)] out TValue? value, IAnnotationProvider? provider = null)
102+
/// <remarks>
103+
/// This is intended to be called by specialized ID-specific accessors for CLI authors such as <see cref="HelpAnnotationExtensions.GetDescription{TSymbol}(TSymbol)"/>.
104+
/// Subsystems should not call it, as it does not account for values from the subsystem's <see cref="IAnnotationProvider"/>. They should instead call
105+
/// <see cref="CliSubsystem.TryGetAnnotation{TValue}(CliSymbol, AnnotationId{TValue}, out TValue?)"/> or an ID-specific accessor on the subsystem such
106+
/// <see cref="HelpSubsystem.TryGetDescription(CliSymbol, out string?)"/>.
107+
/// </remarks>
108+
public static bool TryGetAnnotation<TValue>(this CliSymbol symbol, AnnotationId<TValue> annotationId, [NotNullWhen(true)] out TValue? value)
105109
{
106-
if (provider is not null && provider.TryGet(symbol, annotationId, out value))
107-
{
108-
return true;
109-
}
110-
111110
if (symbolToAnnotationStorage.TryGetValue(symbol, out var storage) && storage.TryGet (symbol, annotationId, out value))
112111
{
113112
return true;
@@ -116,27 +115,30 @@ public static bool TryGetAnnotation<TValue>(this CliSymbol symbol, AnnotationId<
116115
value = default;
117116
return false;
118117
}
118+
119119
/// <summary>
120-
/// Attempt to retrieve the <paramref name="symbol"/>'s value for the annotation <paramref name="id"/>
121-
/// from the optional <paramref name="provider"/> and the internal annotation storage.
120+
/// Attempts to get the value for the annotation <paramref name="annotationId"/> associated with the <paramref name="symbol"/> in the internal annotation
121+
/// storage used to store values set via <see cref="SetAnnotation{TValue}(CliSymbol, AnnotationId{TValue}, TValue)"/>.
122122
/// </summary>
123123
/// <typeparam name="TValue">The type of the annotation value</typeparam>
124124
/// <param name="symbol">The symbol that is annotated</param>
125-
/// <param name="id">
125+
/// <param name="annotationId">
126126
/// The identifier for the annotation. For example, the annotation identifier for the help description is <see cref="HelpAnnotations.Description">.
127127
/// </param>
128-
/// <param name="provider">
129-
/// An optional annotation provider that may implement custom or lazy construction of annotation values. Annotation returned by an annotation
130-
/// provider take precedence over those stored in internal annotation storage.
131-
/// </param>
132128
/// <returns>The annotation value, if successful, otherwise <c>default</c></returns>
133-
public static TValue? GetAnnotationOrDefault<TValue>(this CliSymbol symbol, AnnotationId<TValue> annotationId, IAnnotationProvider? provider = null)
129+
/// <remarks>
130+
/// This is intended to be called by specialized ID-specific accessors for CLI authors such as <see cref="HelpAnnotationExtensions.GetDescription{TSymbol}(TSymbol)"/>.
131+
/// Subsystems should not call it, as it does not account for values from the subsystem's <see cref="IAnnotationProvider"/>. They should instead call
132+
/// <see cref="CliSubsystem.TryGetAnnotation{TValue}(CliSymbol, AnnotationId{TValue}, out TValue?)"/> or an ID-specific accessor on the subsystem such
133+
/// <see cref="HelpSubsystem.TryGetDescription(CliSymbol, out string?)"/>.
134+
/// </remarks>
135+
public static TValue? GetAnnotationOrDefault<TValue>(this CliSymbol symbol, AnnotationId<TValue> annotationId)
134136
{
135-
if (symbol.TryGetAnnotation(annotationId, out TValue? value, provider))
137+
if (symbol.TryGetAnnotation(annotationId, out TValue? value))
136138
{
137139
return value;
138140
}
139141

140142
return default;
141143
}
142-
}
144+
}

src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,17 @@ protected CliSubsystem(string name, SubsystemKind subsystemKind, IAnnotationProv
4444
/// <param name="value">An out parameter to contain the result</param>
4545
/// <returns>True if successful</returns>
4646
/// <remarks>
47-
/// This value is protected because it is a convenience for subsystem authors. It calls <see cref="SymbolAnnotationStorageExtensions"/>
47+
/// Subsystem authors must use this to access annotation values, as it respects the subsystem's <see cref="IAnnotationProvider"/> if it has one.
48+
/// This value is protected because it is intended for use only by subsystem authors. It calls <see cref="AnnotationStorageExtensions"/>
4849
/// </remarks>
49-
protected internal bool TryGetAnnotation<TValue>(CliSymbol symbol, AnnotationId<TValue> id, [NotNullWhen(true)] out TValue? value)
50+
protected internal bool TryGetAnnotation<TValue>(CliSymbol symbol, AnnotationId<TValue> annotationId, [NotNullWhen(true)] out TValue? value)
5051
{
51-
return symbol.TryGetAnnotation(id, out value, _annotationProvider);
52+
if (_annotationProvider is not null && _annotationProvider.TryGet(symbol, annotationId, out value))
53+
{
54+
return true;
55+
}
56+
57+
return symbol.TryGetAnnotation(annotationId, out value);
5258
}
5359

5460
/// <summary>

0 commit comments

Comments
 (0)