123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452 |
- using System;
- using System.Diagnostics.CodeAnalysis;
- using System.Text;
- using System.Threading;
- using Microsoft.CodeAnalysis;
- using Microsoft.CodeAnalysis.Text;
- using Terminal.Gui.Analyzers.Internal.Attributes;
- using Terminal.Gui.Analyzers.Internal.Constants;
- namespace Terminal.Gui.Analyzers.Internal.Generators.EnumExtensions;
- /// <summary>
- /// Incremental code generator for enums decorated with <see cref="GenerateEnumExtensionMethodsAttribute"/>.
- /// </summary>
- [SuppressMessage ("CodeQuality", "IDE0079", Justification = "Suppressions here are intentional and the warnings they disable are just noise.")]
- [Generator (LanguageNames.CSharp)]
- public sealed class EnumExtensionMethodsIncrementalGenerator : IIncrementalGenerator
- {
- private const string ExtensionsForEnumTypeAttributeFullyQualifiedName = $"{Strings.AnalyzersAttributesNamespace}.{ExtensionsForEnumTypeAttributeName}";
- private const string ExtensionsForEnumTypeAttributeName = "ExtensionsForEnumTypeAttribute";
- private const string GeneratorAttributeFullyQualifiedName = $"{Strings.AnalyzersAttributesNamespace}.{GeneratorAttributeName}";
- private const string GeneratorAttributeName = nameof (GenerateEnumExtensionMethodsAttribute);
- /// <summary>Fully-qualified symbol name format without the "global::" prefix.</summary>
- private static readonly SymbolDisplayFormat _fullyQualifiedSymbolDisplayFormatWithoutGlobal =
- SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle (SymbolDisplayGlobalNamespaceStyle.Omitted);
- /// <inheritdoc/>
- /// <remarks>
- /// <para>
- /// Basically, this method is called once by the compiler, and is responsible for wiring up
- /// everything important about how source generation works.
- /// </para>
- /// <para>
- /// See in-line comments for specifics of what's going on.
- /// </para>
- /// <para>
- /// Note that <paramref name="context"/> is everything in the compilation,
- /// except for code generated by this generator or generators which have not yet executed.<br/>
- /// The methods registered to perform generation get called on-demand by the host (the IDE,
- /// compiler, etc), sometimes as often as every single keystroke.
- /// </para>
- /// </remarks>
- public void Initialize (IncrementalGeneratorInitializationContext context)
- {
- // Write out namespaces that may be used later. Harmless to declare them now and will avoid
- // additional processing and potential omissions later on.
- context.RegisterPostInitializationOutput (GenerateDummyNamespaces);
- // This executes the delegate given to it immediately after Roslyn gets all set up.
- //
- // As written, this will result in the GenerateEnumExtensionMethodsAttribute code
- // being added to the environment, so that it can be used without having to actually
- // be declared explicitly in the target project.
- // This is important, as it guarantees the type will exist and also guarantees it is
- // defined exactly as the generator expects it to be defined.
- context.RegisterPostInitializationOutput (GenerateAttributeSources);
- // Next up, we define our pipeline.
- // To do so, we create one or more IncrementalValuesProvider<T> objects, each of which
- // defines on stage of analysis or generation as needed.
- //
- // Critically, these must be as fast and efficient as reasonably possible because,
- // once the pipeline is registered, this stuff can get called A LOT.
- //
- // Note that declaring these doesn't really do much of anything unless they are given to the
- // RegisterSourceOutput method at the end of this method.
- //
- // The delegates are not actually evaluated right here. That is triggered by changes being
- // made to the source code.
- // This provider grabs attributes that pass our filter and then creates lightweight
- // metadata objects to be used in the final code generation step.
- // It also preemptively removes any nulls from the collection before handing things off
- // to the code generation logic.
- IncrementalValuesProvider<EnumExtensionMethodsGenerationInfo?> enumGenerationInfos =
- context
- .SyntaxProvider
- // This method is a highly-optimized (and highly-recommended) filter on the incoming
- // code elements that only bothers to present code that is annotated with the specified
- // attribute, by its fully-qualified name, as a string, which is the first parameter.
- //
- // Two delegates are passed to it, in the second and third parameters.
- //
- // The second parameter is a filter predicate taking each SyntaxNode that passes the
- // name filter above, and then refines that result.
- //
- // It is critical that the filter predicate be as simple and fast as possible, as it
- // will be called a ton, triggered by keystrokes or anything else that modifies code
- // in or even related to (in either direction) the pre-filtered code.
- // It should collect metadata only and not actually generate any code.
- // It must return a boolean indicating whether the supplied SyntaxNode should be
- // given to the transform delegate at all.
- //
- // The third parameter is the "transform" delegate.
- // That one only runs when code is changed that passed both the attribute name filter
- // and the filter predicate in the second parameter.
- // It will be called for everything that passes both of those, so it can still happen
- // a lot, but should at least be pretty close.
- // In our case, it should be 100% accurate, since we're using OUR attribute, which can
- // only be applied to enum types in the first place.
- //
- // That delegate is responsible for creating some sort of lightweight data structure
- // which can later be used to generate the actual source code for output.
- //
- // THIS DELEGATE DOES NOT GENERATE CODE!
- // However, it does need to return instances of the metadata class in use that are either
- // null or complete enough to generate meaningful code from, later on.
- //
- // We then filter out any that were null with the .Where call at the end, so that we don't
- // know or care about them when it's time to generate code.
- //
- // While the syntax of that .Where call is the same as LINQ, that is actually a
- // highly-optimized implementation specifically for this use.
- .ForAttributeWithMetadataName (
- GeneratorAttributeFullyQualifiedName,
- IsPotentiallyInterestingDeclaration,
- GatherMetadataForCodeGeneration
- )
- .WithTrackingName ("CollectEnumMetadata")
- .Where (static eInfo => eInfo is { });
- // Finally, we wire up any IncrementalValuesProvider<T> instances above to the appropriate
- // delegate that takes the SourceProductionContext that is current at run-time and an instance of
- // our metadata type and takes appropriate action.
- // Typically that means generating code from that metadata and adding it to the compilation via
- // the received context object.
- //
- // As with everything else , the delegate will be invoked once for each item that passed
- // all of the filters above, so we get to write that method from the perspective of a single
- // enum type declaration.
- context.RegisterSourceOutput (enumGenerationInfos, GenerateSourceFromGenerationInfo);
- }
- private static EnumExtensionMethodsGenerationInfo? GatherMetadataForCodeGeneration (
- GeneratorAttributeSyntaxContext context,
- CancellationToken cancellationToken
- )
- {
- var cts = CancellationTokenSource.CreateLinkedTokenSource (cancellationToken);
- cancellationToken.ThrowIfCancellationRequested ();
- // If it's not an enum symbol, we don't care.
- // EnumUnderlyingType is null for non-enums, so this validates it's an enum declaration.
- if (context.TargetSymbol is not INamedTypeSymbol { EnumUnderlyingType: { } } namedSymbol)
- {
- return null;
- }
- INamespaceSymbol? enumNamespaceSymbol = namedSymbol.ContainingNamespace;
- if (enumNamespaceSymbol is null or { IsGlobalNamespace: true })
- {
- // Explicitly choosing not to support enums in the global namespace.
- // The corresponding analyzer will report this.
- return null;
- }
- string enumName = namedSymbol.Name;
- string enumNamespace = enumNamespaceSymbol.ToDisplayString (_fullyQualifiedSymbolDisplayFormatWithoutGlobal);
- TypeCode enumTypeCode = namedSymbol.EnumUnderlyingType.Name switch
- {
- "UInt32" => TypeCode.UInt32,
- "Int32" => TypeCode.Int32,
- _ => TypeCode.Empty
- };
- EnumExtensionMethodsGenerationInfo info = new (
- enumNamespace,
- enumName,
- enumTypeCode
- );
- if (!info.TryConfigure (namedSymbol, cts.Token))
- {
- cts.Cancel ();
- cts.Token.ThrowIfCancellationRequested ();
- }
- return info;
- }
- private static void GenerateAttributeSources (IncrementalGeneratorPostInitializationContext postInitializationContext)
- {
- postInitializationContext
- .AddSource (
- $"{nameof (IExtensionsForEnumTypeAttributes)}.g.cs",
- SourceText.From (
- $$"""
- // ReSharper disable All
- {{Strings.Templates.AutoGeneratedCommentBlock}}
- using System;
- namespace {{Strings.AnalyzersAttributesNamespace}};
- /// <summary>
- /// Interface to simplify general enumeration of constructed generic types for
- /// <see cref="ExtensionsForEnumTypeAttribute{TEnum}"/>
- /// </summary>
- {{Strings.Templates.AttributesForGeneratedInterfaces}}
- public interface IExtensionsForEnumTypeAttributes
- {
- System.Type EnumType { get; }
- }
- """,
- Encoding.UTF8));
- postInitializationContext
- .AddSource (
- $"{nameof (AssemblyExtendedEnumTypeAttribute)}.g.cs",
- SourceText.From (
- $$"""
- // ReSharper disable All
- #nullable enable
- {{Strings.Templates.AutoGeneratedCommentBlock}}
- namespace {{Strings.AnalyzersAttributesNamespace}};
- /// <summary>Assembly attribute declaring a known pairing of an <see langword="enum" /> type to an extension class.</summary>
- /// <remarks>This attribute should only be written by internal source generators for Terminal.Gui. No other usage of any kind is supported.</remarks>
- {{Strings.Templates.AttributesForGeneratedTypes}}
- [System.AttributeUsageAttribute(System.AttributeTargets.Assembly, AllowMultiple = true)]
- public sealed class {{nameof(AssemblyExtendedEnumTypeAttribute)}} : System.Attribute
- {
- /// <summary>Creates a new instance of <see cref="AssemblyExtendedEnumTypeAttribute" /> from the provided parameters.</summary>
- /// <param name="enumType">The <see cref="System.Type" /> of an <see langword="enum" /> decorated with a <see cref="GenerateEnumExtensionMethodsAttribute" />.</param>
- /// <param name="extensionClass">The <see cref="System.Type" /> of the <see langword="class" /> decorated with an <see cref="ExtensionsForEnumTypeAttribute{TEnum}" /> referring to the same type as <paramref name="enumType" />.</param>
- public AssemblyExtendedEnumTypeAttribute (System.Type enumType, System.Type extensionClass)
- {
- EnumType = enumType;
- ExtensionClass = extensionClass;
- }
- /// <summary>An <see langword="enum" /> type that has been extended by Terminal.Gui source generators.</summary>
- public System.Type EnumType { get; init; }
- /// <summary>A class containing extension methods for <see cref="EnumType"/>.</summary>
- public System.Type ExtensionClass { get; init; }
- /// <inheritdoc />
- public override string ToString () => $"{EnumType.Name},{ExtensionClass.Name}";
- }
- """,
- Encoding.UTF8));
- postInitializationContext
- .AddSource (
- $"{GeneratorAttributeFullyQualifiedName}.g.cs",
- SourceText.From (
- $$"""
- {{Strings.Templates.StandardHeader}}
-
- namespace {{Strings.AnalyzersAttributesNamespace}};
- /// <summary>
- /// Used to enable source generation of a common set of extension methods for enum types.
- /// </summary>
- {{Strings.Templates.AttributesForGeneratedTypes}}
- [{{Strings.DotnetNames.Types.AttributeUsageAttribute}} ({{Strings.DotnetNames.Types.AttributeTargets}}.Enum)]
- public sealed class {{GeneratorAttributeName}} : {{Strings.DotnetNames.Types.Attribute}}
- {
- /// <summary>
- /// The name of the generated static class.
- /// </summary>
- /// <remarks>
- /// If unspecified, null, empty, or only whitespace, defaults to the name of the enum plus "Extensions".<br/>
- /// No other validation is performed, so illegal values will simply result in compiler errors.
- /// <para>
- /// Explicitly specifying a default value is unnecessary and will result in unnecessary processing.
- /// </para>
- /// </remarks>
- public string? ClassName { get; set; }
-
- /// <summary>
- /// The namespace in which to place the generated static class containing the extension methods.
- /// </summary>
- /// <remarks>
- /// If unspecified, null, empty, or only whitespace, defaults to the namespace of the enum.<br/>
- /// No other validation is performed, so illegal values will simply result in compiler errors.
- /// <para>
- /// Explicitly specifying a default value is unnecessary and will result in unnecessary processing.
- /// </para>
- /// </remarks>
- public string? ClassNamespace { get; set; }
-
- /// <summary>
- /// Whether to generate a fast, zero-allocation, non-boxing, and reflection-free alternative to the built-in
- /// <see cref="Enum.HasFlag"/> method.
- /// </summary>
- /// <remarks>
- /// <para>
- /// Default: false
- /// </para>
- /// <para>
- /// If the enum is not decorated with <see cref="Flags"/>, this option has no effect.
- /// </para>
- /// <para>
- /// If multiple members have the same value, the first member with that value will be used and subsequent members
- /// with the same value will be skipped.
- /// </para>
- /// <para>
- /// Overloads taking the enum type itself as well as the underlying type of the enum will be generated, enabling
- /// avoidance of implicit or explicit cast overhead.
- /// </para>
- /// <para>
- /// Explicitly specifying a default value is unnecessary and will result in unnecessary processing.
- /// </para>
- /// </remarks>
- public bool FastHasFlags { get; set; }
-
- /// <summary>
- /// Whether to generate a fast, zero-allocation, and reflection-free alternative to the built-in
- /// <see cref="Enum.IsDefined"/> method,
- /// using a switch expression as a hard-coded reverse mapping of numeric values to explicitly-named members.
- /// </summary>
- /// <remarks>
- /// <para>
- /// Default: true
- /// </para>
- /// <para>
- /// If multiple members have the same value, the first member with that value will be used and subsequent members
- /// with the same value will be skipped.
- /// </para>
- /// <para>
- /// As with <see cref="Enum.IsDefined"/> the source generator only considers explicitly-named members.<br/>
- /// Generation of values which represent valid bitwise combinations of members of enums decorated with
- /// <see cref="Flags"/> is not affected by this property.
- /// </para>
- /// </remarks>
- public bool FastIsDefined { get; init; } = true;
-
- /// <summary>
- /// Gets a <see langword="bool"/> value indicating if this <see cref="GenerateEnumExtensionMethodsAttribute"/> instance
- /// contains default values only. See <see href="#remarks">remarks</see> of this method or documentation on properties of this type for details.
- /// </summary>
- /// <returns>
- /// A <see langword="bool"/> value indicating if all property values are default for this
- /// <see cref="GenerateEnumExtensionMethodsAttribute"/> instance.
- /// </returns>
- /// <remarks>
- /// Default values that will result in a <see langword="true"/> return value are:<br/>
- /// <see cref="FastIsDefined"/> && !<see cref="FastHasFlags"/> && <see cref="ClassName"/>
- /// <see langword="is"/> <see langword="null"/> && <see cref="ClassNamespace"/> <see langword="is"/>
- /// <see langword="null"/>
- /// </remarks>
- public override bool IsDefaultAttribute ()
- {
- return FastIsDefined
- && !FastHasFlags
- && ClassName is null
- && ClassNamespace is null;
- }
- }
-
- """,
- Encoding.UTF8));
- postInitializationContext
- .AddSource (
- $"{ExtensionsForEnumTypeAttributeFullyQualifiedName}.g.cs",
- SourceText.From (
- $$"""
- // ReSharper disable RedundantNameQualifier
- // ReSharper disable RedundantNullableDirective
- // ReSharper disable UnusedType.Global
- {{Strings.Templates.AutoGeneratedCommentBlock}}
- #nullable enable
- namespace {{Strings.AnalyzersAttributesNamespace}};
- /// <summary>
- /// Attribute written by the source generator for enum extension classes, for easier analysis and reflection.
- /// </summary>
- /// <remarks>
- /// Properties are just convenient shortcuts to properties of <typeparamref name="TEnum"/>.
- /// </remarks>
- {{Strings.Templates.AttributesForGeneratedTypes}}
- [System.AttributeUsageAttribute (System.AttributeTargets.Class | System.AttributeTargets.Interface)]
- public sealed class {{ExtensionsForEnumTypeAttributeName}}<TEnum>: System.Attribute, IExtensionsForEnumTypeAttributes where TEnum : struct, Enum
- {
- /// <summary>
- /// The namespace-qualified name of <typeparamref name="TEnum"/>.
- /// </summary>
- public string EnumFullName => EnumType.FullName!;
-
- /// <summary>
- /// The unqualified name of <typeparamref name="TEnum"/>.
- /// </summary>
- public string EnumName => EnumType.Name;
-
- /// <summary>
- /// The namespace containing <typeparamref name="TEnum"/>.
- /// </summary>
- public string EnumNamespace => EnumType.Namespace!;
-
- /// <summary>
- /// The <see cref="Type"/> given by <see langword="typeof"/>(<typeparamref name="TEnum"/>).
- /// </summary>
- public Type EnumType => typeof (TEnum);
- }
-
- """,
- Encoding.UTF8));
- }
- [SuppressMessage ("Roslynator", "RCS1267", Justification = "Intentionally used so that Spans are used.")]
- private static void GenerateDummyNamespaces (IncrementalGeneratorPostInitializationContext postInitializeContext)
- {
- postInitializeContext.AddSource (
- string.Concat (Strings.InternalAnalyzersNamespace, "Namespaces.g.cs"),
- SourceText.From (Strings.Templates.DummyNamespaceDeclarations, Encoding.UTF8));
- }
- private static void GenerateSourceFromGenerationInfo (SourceProductionContext context, EnumExtensionMethodsGenerationInfo? enumInfo)
- {
- // Just in case we still made it this far with a null...
- if (enumInfo is not { })
- {
- return;
- }
- CodeWriter writer = new (enumInfo);
- context.AddSource ($"{enumInfo.FullyQualifiedClassName}.g.cs", writer.GenerateSourceText ());
- }
- /// <summary>
- /// Returns true if <paramref name="syntaxNode"/> is an EnumDeclarationSyntax
- /// whose parent is a NamespaceDeclarationSyntax, FileScopedNamespaceDeclarationSyntax, or a
- /// (Class|Struct)DeclarationSyntax.<br/>
- /// Additional filtering is performed in later stages.
- /// </summary>
- private static bool IsPotentiallyInterestingDeclaration (SyntaxNode syntaxNode, CancellationToken cancellationToken)
- {
- cancellationToken.ThrowIfCancellationRequested ();
- return syntaxNode is
- {
- RawKind: 8858, //(int)SyntaxKind.EnumDeclaration,
- Parent.RawKind: 8845 //(int)SyntaxKind.FileScopedNamespaceDeclaration
- or 8842 //(int)SyntaxKind.NamespaceDeclaration
- or 8855 //(int)SyntaxKind.ClassDeclaration
- or 8856 //(int)SyntaxKind.StructDeclaration
- or 9068 //(int)SyntaxKind.RecordStructDeclaration
- or 9063 //(int)SyntaxKind.RecordDeclaration
- };
- }
- }
|