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; /// /// Incremental code generator for enums decorated with . /// [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); /// Fully-qualified symbol name format without the "global::" prefix. private static readonly SymbolDisplayFormat _fullyQualifiedSymbolDisplayFormatWithoutGlobal = SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle (SymbolDisplayGlobalNamespaceStyle.Omitted); /// /// /// /// Basically, this method is called once by the compiler, and is responsible for wiring up /// everything important about how source generation works. /// /// /// See in-line comments for specifics of what's going on. /// /// /// Note that is everything in the compilation, /// except for code generated by this generator or generators which have not yet executed.
/// The methods registered to perform generation get called on-demand by the host (the IDE, /// compiler, etc), sometimes as often as every single keystroke. ///
///
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 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 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 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}}; /// /// Interface to simplify general enumeration of constructed generic types for /// /// {{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}}; /// Assembly attribute declaring a known pairing of an type to an extension class. /// This attribute should only be written by internal source generators for Terminal.Gui. No other usage of any kind is supported. {{Strings.Templates.AttributesForGeneratedTypes}} [System.AttributeUsageAttribute(System.AttributeTargets.Assembly, AllowMultiple = true)] public sealed class {{nameof(AssemblyExtendedEnumTypeAttribute)}} : System.Attribute { /// Creates a new instance of from the provided parameters. /// The of an decorated with a . /// The of the decorated with an referring to the same type as . public AssemblyExtendedEnumTypeAttribute (System.Type enumType, System.Type extensionClass) { EnumType = enumType; ExtensionClass = extensionClass; } /// An type that has been extended by Terminal.Gui source generators. public System.Type EnumType { get; init; } /// A class containing extension methods for . public System.Type ExtensionClass { get; init; } /// public override string ToString () => $"{EnumType.Name},{ExtensionClass.Name}"; } """, Encoding.UTF8)); postInitializationContext .AddSource ( $"{GeneratorAttributeFullyQualifiedName}.g.cs", SourceText.From ( $$""" {{Strings.Templates.StandardHeader}} namespace {{Strings.AnalyzersAttributesNamespace}}; /// /// Used to enable source generation of a common set of extension methods for enum types. /// {{Strings.Templates.AttributesForGeneratedTypes}} [{{Strings.DotnetNames.Types.AttributeUsageAttribute}} ({{Strings.DotnetNames.Types.AttributeTargets}}.Enum)] public sealed class {{GeneratorAttributeName}} : {{Strings.DotnetNames.Types.Attribute}} { /// /// The name of the generated static class. /// /// /// If unspecified, null, empty, or only whitespace, defaults to the name of the enum plus "Extensions".
/// No other validation is performed, so illegal values will simply result in compiler errors. /// /// Explicitly specifying a default value is unnecessary and will result in unnecessary processing. /// ///
public string? ClassName { get; set; } /// /// The namespace in which to place the generated static class containing the extension methods. /// /// /// If unspecified, null, empty, or only whitespace, defaults to the namespace of the enum.
/// No other validation is performed, so illegal values will simply result in compiler errors. /// /// Explicitly specifying a default value is unnecessary and will result in unnecessary processing. /// ///
public string? ClassNamespace { get; set; } /// /// Whether to generate a fast, zero-allocation, non-boxing, and reflection-free alternative to the built-in /// method. /// /// /// /// Default: false /// /// /// If the enum is not decorated with , this option has no effect. /// /// /// 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. /// /// /// 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. /// /// /// Explicitly specifying a default value is unnecessary and will result in unnecessary processing. /// /// public bool FastHasFlags { get; set; } /// /// Whether to generate a fast, zero-allocation, and reflection-free alternative to the built-in /// method, /// using a switch expression as a hard-coded reverse mapping of numeric values to explicitly-named members. /// /// /// /// Default: true /// /// /// 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. /// /// /// As with the source generator only considers explicitly-named members.
/// Generation of values which represent valid bitwise combinations of members of enums decorated with /// is not affected by this property. ///
///
public bool FastIsDefined { get; init; } = true; /// /// Gets a value indicating if this instance /// contains default values only. See remarks of this method or documentation on properties of this type for details. /// /// /// A value indicating if all property values are default for this /// instance. /// /// /// Default values that will result in a return value are:
/// && ! && /// && /// ///
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}}; /// /// Attribute written by the source generator for enum extension classes, for easier analysis and reflection. /// /// /// Properties are just convenient shortcuts to properties of . /// {{Strings.Templates.AttributesForGeneratedTypes}} [System.AttributeUsageAttribute (System.AttributeTargets.Class | System.AttributeTargets.Interface)] public sealed class {{ExtensionsForEnumTypeAttributeName}}: System.Attribute, IExtensionsForEnumTypeAttributes where TEnum : struct, Enum { /// /// The namespace-qualified name of . /// public string EnumFullName => EnumType.FullName!; /// /// The unqualified name of . /// public string EnumName => EnumType.Name; /// /// The namespace containing . /// public string EnumNamespace => EnumType.Namespace!; /// /// The given by (). /// 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 ()); } /// /// Returns true if is an EnumDeclarationSyntax /// whose parent is a NamespaceDeclarationSyntax, FileScopedNamespaceDeclarationSyntax, or a /// (Class|Struct)DeclarationSyntax.
/// Additional filtering is performed in later stages. ///
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 }; } }