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
};
}
}