Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch to storing annotations in a static field #2426

Merged
merged 2 commits into from May 17, 2024

Conversation

mhutch
Copy link
Member

@mhutch mhutch commented May 11, 2024

Although it's kind of icky to store instance data on a static field, it is implemented in a robust manner that should prevent any surprises, and the details are hidden from authors of CLIs and authors of subsystems.

A subsystem reference is no longer needed when annotating the CliSymbol objects in the grammar, which makes construction of a pipleline much simpler.

The fluent grammar construction API now looks like this:

new Option<bool>("--greeting").WithDescription("The greeting")

Eventually we will be able to use extension properties:

new Option<bool>("--greeting") {
  Description = "The greeting"
}

Note that we still support the IAnnotationProvider model that allows advanced CLI authors to lazily or dynamically provide annotations.

I'm still trying to figure out ways to enforce type safety when using DefaultValue/DefaultValueCalculation with the underlying annotation storage methods SetAnnotation/TryGetAnnotation. However, the SetDefaultValue and SetDefaultValueCalculation specialized setters will at least ensure safety for CLI authors.

@mhutch mhutch added the Powderhouse Work to isolate parser and features label May 11, 2024
@mhutch mhutch force-pushed the static-annotation-storage branch 3 times, most recently from d81fa05 to 7ae4784 Compare May 11, 2024 06:47
Although it's kind of icky to store instance data on a static
field, it is implemented in a robust manner that should prevent
any surprises, and the details are hidden from authors of CLIs
and authors of subsystems.

A subsystem reference is no longer needed when annotating the
CliSymbol objects in the grammar, which makes construction
of a pipleline much simpler.

The fluent grammar construction API now looks like this:

```csharp
new Option<bool>("--greeting").WithDescription("The greeting")
```

Eventually we will be able to use extension properties:

```csharp
new Option<bool>("--greeting") {
  Description = "The greeting"
}
```

We still support the `IAnnotationProvider` model that allows
advanced CLI authors to lazily or dynamically provide annotations.
@mhutch mhutch force-pushed the static-annotation-storage branch from 7ae4784 to 662ec29 Compare May 11, 2024 06:53
Copy link
Contributor

@KathleenDollard KathleenDollard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great.

I have several comments for consideration, and some nits.

My vote is to merge this as soon as possible so I can accommodate it in some outstanding work (currently in a draft PR that needs rebasing and will be merge fun).

If you need me to re-approve after changes, ping me in teams or text so I can update your approval.

{
class AnnotationStorage : IAnnotationProvider
{
record struct AnnotationKey(CliSymbol symbol, string annotationId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit

annotationId is used here for a parameter that is not of AnnotatonId type. Consider different naming, especially since the next method calls an AnnotationId instance an id.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, this was existing code (renamed from DefaultAnnotationProvider), but it turns out it was ignore the Prefix qualification. I restructured it to be clearer.


namespace System.CommandLine.Subsystems.Annotations;

partial class AnnotationStorageExtensions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit

Extensions is used in a type name that is not an extension or a static class for extension methods. It is probable that the Extension suffix will be widely used for extension types in C# 13.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a static class for extension methods. It's just a partial class so the nested class can be split into a separate file.

//
// This is fine, as we will have the following well-defined threading behavior: an annotated grammar and pipeline may
// only be constructed/modified from a single thread. Once the grammar/pipeline instance is fully constructed, it may
// be safely used from multiple threads.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to add that once constructed, it is not safe to modify the grammar if it is in use.

/// <summary>
/// Handles storage of annotations associated with <see cref="CliSymbol"/> instances.
/// </summary>
public static partial class AnnotationStorageExtensions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this class need to be public?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Implementors of subsystems need to use it to store any newly defined annotations. However, it's in the System.CommandLine.Subsystems.Annotations so it's not something most users will see.

/// provider take precedence over those stored in internal annotation storage.
/// </param>
/// <returns>The annotation value, if successful, otherwise <c>default</c></returns>
public static TValue? GetAnnotationOrDefault<TValue>(this CliSymbol symbol, AnnotationId<TValue> annotationId, IAnnotationProvider? provider = null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a bit worried about this name as default has additional meaning in System.CommandLine

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but I'm not sure of a better name. This naming pattern is used by LINQ for SingleorDefault etc.

/// <param name="id">The id for the value to be retrieved. For example, an annotation ID for help is description</param>
/// <param name="id">
/// The identifier for the annotation value to be retrieved.
/// For example, the annotation identifier for the help description is <see cref="HelpAnnotations.Description">.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the type name HelpAnnotation used by the CLI author? IOW, do CLI authors ever have to type HelpAnnotations.Description? If they do, I think we should consider simplifying the name.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's used IAnnotationProvider authors and subsystem authors. What would you simplify it to?

protected internal void SetAnnotation<TValue>(CliSymbol symbol, AnnotationId<Func<TValue>> id, Func<TValue> value)
{
(_defaultProvider ??= new DefaultAnnotationProvider()).Set<Func<TValue>>(symbol, id, value);
return symbol.TryGetAnnotation(id, out value, _annotationProvider);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we still need this method. As a convenience, perhaps wait until we understand the calling code better.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will revise the comment. It's not just a convenience, it's the way that subsystem authors must access annotations if they want to respect the subsystem's IAnnotationProvider, as the provider is private.

@mhutch mhutch merged commit cfb184a into dotnet:main-powderhouse May 17, 2024
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Powderhouse Work to isolate parser and features
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants