diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/Analyzers/GenerateEnumExtensionMethodsAttributeAnalyzer.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/Analyzers/GenerateEnumExtensionMethodsAttributeAnalyzer.cs
new file mode 100644
index 000000000..d49fd37d1
--- /dev/null
+++ b/Analyzers/Terminal.Gui.Analyzers.Internal/Analyzers/GenerateEnumExtensionMethodsAttributeAnalyzer.cs
@@ -0,0 +1,117 @@
+#define JETBRAINS_ANNOTATIONS
+using System.Collections.Immutable;
+using System.Linq;
+using JetBrains.Annotations;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Terminal.Gui.Analyzers.Internal.Attributes;
+using Terminal.Gui.Analyzers.Internal.Generators.EnumExtensions;
+
+namespace Terminal.Gui.Analyzers.Internal.Analyzers;
+
+///
+/// Design-time analyzer that checks for proper use of .
+///
+[DiagnosticAnalyzer (LanguageNames.CSharp)]
+[UsedImplicitly]
+internal sealed class GenerateEnumExtensionMethodsAttributeAnalyzer : DiagnosticAnalyzer
+{
+ // ReSharper disable once InconsistentNaming
+ private static readonly DiagnosticDescriptor TG0001_GlobalNamespaceNotSupported = new (
+ // ReSharper restore InconsistentNaming
+ "TG0001",
+ $"{nameof (GenerateEnumExtensionMethodsAttribute)} not supported on global enums",
+ "{0} is in the global namespace, which is not supported by the source generator ({1}) used by {2}. Move the enum to a namespace or remove the attribute.",
+ "Usage",
+ DiagnosticSeverity.Error,
+ true,
+ null,
+ null,
+ WellKnownDiagnosticTags.NotConfigurable,
+ WellKnownDiagnosticTags.Compiler);
+
+ // ReSharper disable once InconsistentNaming
+ private static readonly DiagnosticDescriptor TG0002_UnderlyingTypeNotSupported = new (
+ "TG0002",
+ $"{nameof (GenerateEnumExtensionMethodsAttribute)} not supported for this enum type",
+ "{0} has an underlying type of {1}, which is not supported by the source generator ({2}) used by {3}. Only enums backed by int or uint are supported.",
+ "Usage",
+ DiagnosticSeverity.Error,
+ true,
+ null,
+ null,
+ WellKnownDiagnosticTags.NotConfigurable,
+ WellKnownDiagnosticTags.Compiler);
+
+ ///
+ public override ImmutableArray SupportedDiagnostics { get; } =
+ [
+ TG0001_GlobalNamespaceNotSupported,
+ TG0002_UnderlyingTypeNotSupported
+ ];
+
+ ///
+ public override void Initialize (AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis (GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution ();
+
+ context.RegisterSyntaxNodeAction (CheckAttributeLocations, SyntaxKind.EnumDeclaration);
+
+ return;
+
+ static void CheckAttributeLocations (SyntaxNodeAnalysisContext analysisContext)
+ {
+ ISymbol? symbol = analysisContext.SemanticModel.GetDeclaredSymbol (analysisContext.Node) as INamedTypeSymbol;
+
+ if (symbol is not INamedTypeSymbol { EnumUnderlyingType: { } } enumSymbol)
+ {
+ // Somehow not even an enum declaration.
+ // Skip it.
+ return;
+ }
+
+ // Check attributes for those we care about and react accordingly.
+ foreach (AttributeData attributeData in enumSymbol.GetAttributes ())
+ {
+ if (attributeData.AttributeClass?.Name != nameof (GenerateEnumExtensionMethodsAttribute))
+ {
+ // Just skip - not an interesting attribute.
+ continue;
+ }
+
+ // Check enum underlying type for supported types (int and uint, currently)
+ // Report TG0002 if unsupported underlying type.
+ if (enumSymbol.EnumUnderlyingType is not { SpecialType: SpecialType.System_Int32 or SpecialType.System_UInt32 })
+ {
+ analysisContext.ReportDiagnostic (
+ Diagnostic.Create (
+ TG0002_UnderlyingTypeNotSupported,
+ enumSymbol.Locations.FirstOrDefault (),
+ enumSymbol.Name,
+ enumSymbol.EnumUnderlyingType.Name,
+ nameof (EnumExtensionMethodsIncrementalGenerator),
+ nameof (GenerateEnumExtensionMethodsAttribute)
+ )
+ );
+ }
+
+ // Check enum namespace (only non-global supported, currently)
+ // Report TG0001 if in the global namespace.
+ if (enumSymbol.ContainingSymbol is not INamespaceSymbol { IsGlobalNamespace: false })
+ {
+ analysisContext.ReportDiagnostic (
+ Diagnostic.Create (
+ TG0001_GlobalNamespaceNotSupported,
+ enumSymbol.Locations.FirstOrDefault (),
+ enumSymbol.Name,
+ nameof (EnumExtensionMethodsIncrementalGenerator),
+ nameof (GenerateEnumExtensionMethodsAttribute)
+ )
+ );
+ }
+ }
+ }
+ }
+}
diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/AssemblyExtendedEnumTypeAttribute.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/AssemblyExtendedEnumTypeAttribute.cs
new file mode 100644
index 000000000..6115fdc46
--- /dev/null
+++ b/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/AssemblyExtendedEnumTypeAttribute.cs
@@ -0,0 +1,26 @@
+// ReSharper disable ClassNeverInstantiated.Global
+#nullable enable
+
+namespace Terminal.Gui.Analyzers.Internal.Attributes;
+
+/// 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.
+[System.AttributeUsage(System.AttributeTargets.Assembly, AllowMultiple = true)]
+internal sealed class 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}";
+}
diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/CombinationGroupingAttribute.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/CombinationGroupingAttribute.cs
new file mode 100644
index 000000000..22d8eafd3
--- /dev/null
+++ b/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/CombinationGroupingAttribute.cs
@@ -0,0 +1,22 @@
+using System;
+using JetBrains.Annotations;
+
+namespace Terminal.Gui.Analyzers.Internal.Attributes;
+
+///
+/// Designates an enum member for inclusion in generation of bitwise combinations with other members decorated with
+/// this attribute which have the same value.
+///
+///
+/// This attribute is only considered for members of enum types which have the
+/// .
+///
+[AttributeUsage (AttributeTargets.Field)]
+[UsedImplicitly]
+internal sealed class CombinationGroupingAttribute : Attribute
+{
+ ///
+ /// Name of a group this member participates in, for FastHasFlags.
+ ///
+ public string GroupTag { get; set; }
+}
diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/ExtensionsForEnumTypeAttribute.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/ExtensionsForEnumTypeAttribute.cs
new file mode 100644
index 000000000..be4b6eef4
--- /dev/null
+++ b/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/ExtensionsForEnumTypeAttribute.cs
@@ -0,0 +1,37 @@
+// ReSharper disable RedundantNameQualifier
+// ReSharper disable RedundantNullableDirective
+// ReSharper disable UnusedType.Global
+#pragma warning disable IDE0001, IDE0240
+#nullable enable
+
+namespace Terminal.Gui.Analyzers.Internal.Attributes;
+
+///
+/// Attribute written by the source generator for extension classes, for easier analysis and reflection.
+///
+///
+/// Properties are just convenient shortcuts to properties of .
+///
+[System.AttributeUsage (System.AttributeTargets.Class | System.AttributeTargets.Interface)]
+internal sealed class ExtensionsForEnumTypeAttribute: System.Attribute, IExtensionsForEnumTypeAttributes where TEnum : struct, System.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 System.Type EnumType => typeof (TEnum);
+}
diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/GenerateEnumExtensionMethodsAttribute.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/GenerateEnumExtensionMethodsAttribute.cs
new file mode 100644
index 000000000..507c45102
--- /dev/null
+++ b/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/GenerateEnumExtensionMethodsAttribute.cs
@@ -0,0 +1,110 @@
+// ReSharper disable RedundantNullableDirective
+// ReSharper disable RedundantUsingDirective
+// ReSharper disable ClassNeverInstantiated.Global
+
+#nullable enable
+using System;
+using Attribute = System.Attribute;
+using AttributeUsageAttribute = System.AttributeUsageAttribute;
+using AttributeTargets = System.AttributeTargets;
+
+namespace Terminal.Gui.Analyzers.Internal.Attributes;
+
+///
+/// Used to enable source generation of a common set of extension methods for enum types.
+///
+[AttributeUsage (AttributeTargets.Enum)]
+internal sealed class GenerateEnumExtensionMethodsAttribute : 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;
+ }
+}
diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/GenerateEnumMemberCombinationsAttribute.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/GenerateEnumMemberCombinationsAttribute.cs
new file mode 100644
index 000000000..77d0c13a2
--- /dev/null
+++ b/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/GenerateEnumMemberCombinationsAttribute.cs
@@ -0,0 +1,110 @@
+// ReSharper disable RedundantUsingDirective
+
+using System;
+using JetBrains.Annotations;
+using Terminal.Gui.Analyzers.Internal.Compatibility;
+
+namespace Terminal.Gui.Analyzers.Internal.Attributes;
+
+///
+/// Designates an enum member for inclusion in generation of bitwise combinations with other members decorated with
+/// this attribute which have the same value.
+///
+///
+///
+/// This attribute is only considered for enum types with the .
+///
+///
+[AttributeUsage (AttributeTargets.Enum)]
+[UsedImplicitly]
+public sealed class GenerateEnumMemberCombinationsAttribute : System.Attribute
+{
+ private const byte MaximumPopCountLimit = 14;
+ private uint _mask;
+ private uint _maskPopCount;
+ private byte _popCountLimit = 8;
+ ///
+ public string GroupTag { get; set; }
+
+ ///
+ /// The mask for the group defined in
+ ///
+ public uint Mask
+ {
+ get => _mask;
+ set
+ {
+#if NET8_0_OR_GREATER
+ _maskPopCount = uint.PopCount (value);
+#else
+ _maskPopCount = value.GetPopCount ();
+#endif
+ PopCountLimitExceeded = _maskPopCount > PopCountLimit;
+ MaximumPopCountLimitExceeded = _maskPopCount > MaximumPopCountLimit;
+
+ if (PopCountLimitExceeded || MaximumPopCountLimitExceeded)
+ {
+ return;
+ }
+
+ _mask = value;
+ }
+ }
+
+ ///
+ /// The maximum number of bits allowed to be set to 1 in .
+ ///
+ ///
+ ///
+ /// Default: 8 (256 possible combinations)
+ ///
+ ///
+ /// Increasing this value is not recommended!
+ /// Decreasing this value is pointless unless you want to limit maximum possible generated combinations even
+ /// further.
+ ///
+ ///
+ /// If the result of () exceeds 2 ^
+ /// , no
+ /// combinations will be generated for the members which otherwise would have been included by .
+ /// Values exceeding the actual population count of have no effect.
+ ///
+ ///
+ /// This option is set to a sane default of 8, but also has a hard-coded limit of 14 (16384 combinations), as a
+ /// protection against generation of extremely large files.
+ ///
+ ///
+ /// CAUTION: The maximum number of possible combinations possible is equal to 1 <<
+ /// ().
+ /// See for hard-coded limit,
+ ///
+ ///
+ public byte PopCountLimit
+ {
+ get => _popCountLimit;
+ set
+ {
+#if NET8_0_OR_GREATER
+ _maskPopCount = uint.PopCount (_mask);
+#else
+ _maskPopCount = _mask.GetPopCount ();
+#endif
+
+ PopCountLimitExceeded = _maskPopCount > value;
+ MaximumPopCountLimitExceeded = _maskPopCount > MaximumPopCountLimit;
+
+ if (PopCountLimitExceeded || MaximumPopCountLimitExceeded)
+ {
+ return;
+ }
+
+ _mask = value;
+ _popCountLimit = value;
+ }
+ }
+
+ [UsedImplicitly]
+ internal bool MaximumPopCountLimitExceeded { get; private set; }
+ [UsedImplicitly]
+ internal bool PopCountLimitExceeded { get; private set; }
+}
diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/IExtensionsForEnumTypeAttribute.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/IExtensionsForEnumTypeAttribute.cs
new file mode 100644
index 000000000..4ae8875b7
--- /dev/null
+++ b/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/IExtensionsForEnumTypeAttribute.cs
@@ -0,0 +1,14 @@
+// ReSharper disable All
+
+using System;
+
+namespace Terminal.Gui.Analyzers.Internal.Attributes;
+
+///
+/// Interface to simplify general enumeration of constructed generic types for
+///
+///
+internal interface IExtensionsForEnumTypeAttributes
+{
+ Type EnumType { get; }
+}
diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/Generators/EnumExtensions/CodeWriter.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/Generators/EnumExtensions/CodeWriter.cs
new file mode 100644
index 000000000..230d6293b
--- /dev/null
+++ b/Analyzers/Terminal.Gui.Analyzers.Internal/Generators/EnumExtensions/CodeWriter.cs
@@ -0,0 +1,235 @@
+using System;
+using System.CodeDom.Compiler;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Text;
+using Microsoft.CodeAnalysis.Text;
+using Terminal.Gui.Analyzers.Internal.Constants;
+
+namespace Terminal.Gui.Analyzers.Internal.Generators.EnumExtensions;
+
+///
+/// The class responsible for turning an
+/// into actual C# code.
+///
+/// Try to use this type as infrequently as possible.
+///
+/// A reference to an which will be used
+/// to generate the extension class code. The object will not be validated,
+/// so it is critical that it be correct and remain unchanged while in use
+/// by an instance of this class. Behavior if those rules are not followed
+/// is undefined.
+///
+[SuppressMessage ("CodeQuality", "IDE0079", Justification = "Suppressions here are intentional and the warnings they disable are just noise.")]
+internal sealed class CodeWriter (in EnumExtensionMethodsGenerationInfo metadata) : IStandardCSharpCodeGenerator
+{
+ // Using the null suppression operator here because this will always be
+ // initialized to non-null before a reference to it is returned.
+ private SourceText _sourceText = null!;
+
+ ///
+ public EnumExtensionMethodsGenerationInfo Metadata
+ {
+ [MethodImpl (MethodImplOptions.AggressiveInlining)]
+ [return: NotNull]
+ get;
+ [param: DisallowNull]
+ set;
+ } = metadata;
+
+ ///
+ public ref readonly SourceText GenerateSourceText (Encoding? encoding = null)
+ {
+ encoding ??= Encoding.UTF8;
+ _sourceText = SourceText.From (GetFullSourceText (), encoding);
+
+ return ref _sourceText;
+ }
+
+ ///
+ /// Gets the using directive for the namespace containing the enum,
+ /// if different from the extension class namespace, or an empty string, if they are the same.
+ ///
+ private string EnumNamespaceUsingDirective => Metadata.TargetTypeNamespace != Metadata.GeneratedTypeNamespace
+
+ // ReSharper disable once HeapView.ObjectAllocation
+ ? $"using {Metadata.TargetTypeNamespace};"
+ : string.Empty;
+
+ private string EnumTypeKeyword => Metadata.EnumBackingTypeCode switch
+ {
+ TypeCode.Int32 => "int",
+ TypeCode.UInt32 => "uint",
+ _ => string.Empty
+ };
+
+ /// Gets the class declaration line.
+ private string ExtensionClassDeclarationLine => $"public static partial class {Metadata.GeneratedTypeName}";
+
+ // ReSharper disable once HeapView.ObjectAllocation
+ /// Gets the XmlDoc for the extension class declaration.
+ private string ExtensionClassDeclarationXmlDoc =>
+ $"/// Extension methods for the type.";
+
+ // ReSharper disable once HeapView.ObjectAllocation
+ /// Gets the extension class file-scoped namespace directive.
+ private string ExtensionClassNamespaceDirective => $"namespace {Metadata.GeneratedTypeNamespace};";
+
+ ///
+ /// An attribute to decorate the extension class with for easy mapping back to the target enum type, for reflection and
+ /// analysis.
+ ///
+ private string ExtensionsForTypeAttributeLine => $"[ExtensionsForEnumType<{Metadata.TargetTypeFullName}>]";
+
+ ///
+ /// Creates the code for the FastHasFlags method.
+ ///
+ ///
+ /// Since the generator already only writes code for enums backed by and ,
+ /// this method is safe, as we'll always be using a DWORD.
+ ///
+ /// An instance of an to write to.
+ private void GetFastHasFlagsMethods (IndentedTextWriter w)
+ {
+ // The version taking the same enum type as the check value.
+ w.WriteLine (
+ $"/// Determines if the specified flags are set in the current value of this .");
+ w.WriteLine ("/// NO VALIDATION IS PERFORMED!");
+
+ w.WriteLine (
+ $"/// True, if all flags present in are also present in the current value of the .
Otherwise false.");
+ w.WriteLine (Strings.DotnetNames.Attributes.Applications.AggressiveInlining);
+
+ w.Push (
+ $"{Metadata.Accessibility.ToCSharpString ()} static bool FastHasFlags (this {Metadata.TargetTypeFullName} e, {Metadata.TargetTypeFullName} checkFlags)");
+ w.WriteLine ($"ref uint enumCurrentValueRef = ref Unsafe.As<{Metadata.TargetTypeFullName},uint> (ref e);");
+ w.WriteLine ($"ref uint checkFlagsValueRef = ref Unsafe.As<{Metadata.TargetTypeFullName},uint> (ref checkFlags);");
+ w.WriteLine ("return (enumCurrentValueRef & checkFlagsValueRef) == checkFlagsValueRef;");
+ w.Pop ();
+
+ // The version taking the underlying type of the enum as the check value.
+ w.WriteLine (
+ $"/// Determines if the specified mask bits are set in the current value of this .");
+
+ w.WriteLine (
+ $"/// The value to check against the value.");
+ w.WriteLine ("/// A mask to apply to the current value.");
+
+ w.WriteLine (
+ $"/// True, if all bits set to 1 in the mask are also set to 1 in the current value of the .
Otherwise false.");
+ w.WriteLine ("/// NO VALIDATION IS PERFORMED!");
+ w.WriteLine (Strings.DotnetNames.Attributes.Applications.AggressiveInlining);
+
+ w.Push (
+ $"{Metadata.Accessibility.ToCSharpString ()} static bool FastHasFlags (this {Metadata.TargetTypeFullName} e, {EnumTypeKeyword} mask)");
+ w.WriteLine ($"ref {EnumTypeKeyword} enumCurrentValueRef = ref Unsafe.As<{Metadata.TargetTypeFullName},{EnumTypeKeyword}> (ref e);");
+ w.WriteLine ("return (enumCurrentValueRef & mask) == mask;");
+ w.Pop ();
+ }
+
+ ///
+ /// Creates the code for the FastIsDefined method.
+ ///
+ [SuppressMessage ("ReSharper", "SwitchStatementHandlesSomeKnownEnumValuesWithDefault", Justification = "Only need to handle int and uint.")]
+ [SuppressMessage ("ReSharper", "SwitchStatementMissingSomeEnumCasesNoDefault", Justification = "Only need to handle int and uint.")]
+ private void GetFastIsDefinedMethod (IndentedTextWriter w)
+ {
+ w.WriteLine (
+ $"/// Determines if the specified value is explicitly defined as a named value of the type.");
+
+ w.WriteLine (
+ "/// Only explicitly named values return true, as with IsDefined. Combined valid flag values of flags enums which are not explicitly named will return false.");
+
+ w.Push (
+ $"{Metadata.Accessibility.ToCSharpString ()} static bool FastIsDefined (this {Metadata.TargetTypeFullName} e, {EnumTypeKeyword} value)");
+ w.Push ("return value switch");
+
+ switch (Metadata.EnumBackingTypeCode)
+ {
+ case TypeCode.Int32:
+ foreach (int definedValue in Metadata.IntMembers)
+ {
+ w.WriteLine ($"{definedValue:D} => true,");
+ }
+
+ break;
+ case TypeCode.UInt32:
+ foreach (uint definedValue in Metadata.UIntMembers)
+ {
+ w.WriteLine ($"{definedValue:D} => true,");
+ }
+
+ break;
+ }
+
+ w.WriteLine ("_ => false");
+
+ w.Pop ("};");
+ w.Pop ();
+ }
+
+ private string GetFullSourceText ()
+ {
+ StringBuilder sb = new (
+ $"""
+ {Strings.Templates.StandardHeader}
+
+ [assembly: {Strings.AssemblyExtendedEnumTypeAttributeFullName} (typeof({Metadata.TargetTypeFullName}), typeof({Metadata.GeneratedTypeFullName}))]
+
+ {EnumNamespaceUsingDirective}
+ {ExtensionClassNamespaceDirective}
+ {ExtensionClassDeclarationXmlDoc}
+ {Strings.Templates.AttributesForGeneratedTypes}
+ {ExtensionsForTypeAttributeLine}
+ {ExtensionClassDeclarationLine}
+
+ """,
+ 4096);
+
+ using IndentedTextWriter w = new (new StringWriter (sb));
+ w.Push ();
+
+ GetNamedValuesToInt32Method (w);
+ GetNamedValuesToUInt32Method (w);
+
+ if (Metadata.GenerateFastIsDefined)
+ {
+ GetFastIsDefinedMethod (w);
+ }
+
+ if (Metadata.GenerateFastHasFlags)
+ {
+ GetFastHasFlagsMethods (w);
+ }
+
+ w.Pop ();
+
+ w.Flush ();
+
+ return sb.ToString ();
+ }
+
+ [MethodImpl (MethodImplOptions.AggressiveInlining)]
+ private void GetNamedValuesToInt32Method (IndentedTextWriter w)
+ {
+ w.WriteLine (
+ $"/// Directly converts this value to an value with the same binary representation.");
+ w.WriteLine ("/// NO VALIDATION IS PERFORMED!");
+ w.WriteLine (Strings.DotnetNames.Attributes.Applications.AggressiveInlining);
+ w.Push ($"{Metadata.Accessibility.ToCSharpString ()} static int AsInt32 (this {Metadata.TargetTypeFullName} e)");
+ w.WriteLine ($"return Unsafe.As<{Metadata.TargetTypeFullName},int> (ref e);");
+ w.Pop ();
+ }
+
+ [MethodImpl (MethodImplOptions.AggressiveInlining)]
+ private void GetNamedValuesToUInt32Method (IndentedTextWriter w)
+ {
+ w.WriteLine (
+ $"/// Directly converts this value to a value with the same binary representation.");
+ w.WriteLine ("/// NO VALIDATION IS PERFORMED!");
+ w.WriteLine (Strings.DotnetNames.Attributes.Applications.AggressiveInlining);
+ w.Push ($"{Metadata.Accessibility.ToCSharpString ()} static uint AsUInt32 (this {Metadata.TargetTypeFullName} e)");
+ w.WriteLine ($"return Unsafe.As<{Metadata.TargetTypeFullName},uint> (ref e);");
+ w.Pop ();
+ }
+}
diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/Generators/EnumExtensions/EnumExtensionMethodsGenerationInfo.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/Generators/EnumExtensions/EnumExtensionMethodsGenerationInfo.cs
new file mode 100644
index 000000000..970c51c35
--- /dev/null
+++ b/Analyzers/Terminal.Gui.Analyzers.Internal/Generators/EnumExtensions/EnumExtensionMethodsGenerationInfo.cs
@@ -0,0 +1,457 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Threading;
+using JetBrains.Annotations;
+using Microsoft.CodeAnalysis;
+using Terminal.Gui.Analyzers.Internal.Attributes;
+using Terminal.Gui.Analyzers.Internal.Constants;
+
+namespace Terminal.Gui.Analyzers.Internal.Generators.EnumExtensions;
+
+///
+/// Type containing the information necessary to generate code according to the declared attribute values,
+/// as well as the actual code to create the corresponding source code text, to be used in the
+/// source generator pipeline.
+///
+///
+/// Minimal validation is performed by this type.
+/// Errors in analyzed source code will result in generation failure or broken output.
+/// This type is not intended for use outside of Terminal.Gui library development.
+///
+internal sealed record EnumExtensionMethodsGenerationInfo : IGeneratedTypeMetadata,
+ IEqualityOperators
+{
+ private const int ExplicitFastHasFlagsMask = 0b1000;
+ private const int ExplicitFastIsDefinedMask = 0b1_0000;
+ private const int ExplicitIncludeInterfaceMask = 0b10_0000;
+ private const int ExplicitNameMask = 0b10;
+ private const int ExplicitNamespaceMask = 0b1;
+ private const int ExplicitPartialMask = 0b100;
+ private const string GeneratorAttributeFullyQualifiedName = $"{GeneratorAttributeNamespace}.{GeneratorAttributeName}";
+ private const string GeneratorAttributeName = nameof (GenerateEnumExtensionMethodsAttribute);
+ private const string GeneratorAttributeNamespace = Constants.Strings.AnalyzersAttributesNamespace;
+
+ ///
+ /// Type containing the information necessary to generate code according to the declared attribute values,
+ /// as well as the actual code to create the corresponding source code text, to be used in the
+ /// source generator pipeline.
+ ///
+ /// The fully-qualified namespace of the enum type, without assembly name.
+ ///
+ /// The name of the enum type, as would be given by on the enum's type
+ /// declaration.
+ ///
+ ///
+ /// The fully-qualified namespace in which to place the generated code, without assembly name. If omitted or explicitly
+ /// null, uses the value provided in .
+ ///
+ ///
+ /// The name of the generated class. If omitted or explicitly null, appends "Extensions" to the value of
+ /// .
+ ///
+ /// The backing type of the enum. Defaults to .
+ ///
+ /// Whether to generate a fast HasFlag alternative. (Default: true) Ignored if the enum does not also have
+ /// .
+ ///
+ /// Whether to generate a fast IsDefined alternative. (Default: true)
+ ///
+ /// Minimal validation is performed by this type.
+ /// Errors in analyzed source code will result in generation failure or broken output.
+ /// This type is not intended for use outside of Terminal.Gui library development.
+ ///
+ public EnumExtensionMethodsGenerationInfo (
+ string enumNamespace,
+ string enumTypeName,
+ string? typeNamespace = null,
+ string? typeName = null,
+ TypeCode enumBackingTypeCode = TypeCode.Int32,
+ bool generateFastHasFlags = true,
+ bool generateFastIsDefined = true
+ ) : this (enumNamespace, enumTypeName, enumBackingTypeCode)
+ {
+ GeneratedTypeNamespace = typeNamespace ?? enumNamespace;
+ GeneratedTypeName = typeName ?? string.Concat (enumTypeName, Strings.DefaultTypeNameSuffix);
+ GenerateFastHasFlags = generateFastHasFlags;
+ GenerateFastIsDefined = generateFastIsDefined;
+ }
+
+ public EnumExtensionMethodsGenerationInfo (string enumNamespace, string enumTypeName, TypeCode enumBackingType)
+ {
+ // Interning these since they're rather unlikely to change.
+ string enumInternedNamespace = string.Intern (enumNamespace);
+ string enumInternedName = string.Intern (enumTypeName);
+ TargetTypeNamespace = enumInternedNamespace;
+ TargetTypeName = enumInternedName;
+ EnumBackingTypeCode = enumBackingType;
+ }
+
+ [AccessedThroughProperty (nameof (EnumBackingTypeCode))]
+ private TypeCode _enumBackingTypeCode;
+
+ [AccessedThroughProperty (nameof (GeneratedTypeName))]
+ private string? _generatedTypeName;
+
+ [AccessedThroughProperty (nameof (GeneratedTypeNamespace))]
+ private string? _generatedTypeNamespace;
+
+ private BitVector32 _discoveredProperties = new (0);
+
+ /// The name of the extension class.
+ public string? GeneratedTypeName
+ {
+ get => _generatedTypeName ?? string.Concat (TargetTypeName, Strings.DefaultTypeNameSuffix);
+ set => _generatedTypeName = value ?? string.Concat (TargetTypeName, Strings.DefaultTypeNameSuffix);
+ }
+
+ /// The namespace for the extension class.
+ ///
+ /// Value is not validated by the set accessor.
+ /// Get accessor will never return null and is thus marked [NotNull] for static analysis, even though the property is
+ /// declared as a nullable .
If the backing field for this property is null, the get
+ /// accessor will return instead.
+ ///
+ public string? GeneratedTypeNamespace
+ {
+ get => _generatedTypeNamespace ?? TargetTypeNamespace;
+ set => _generatedTypeNamespace = value ?? TargetTypeNamespace;
+ }
+
+ ///
+ public string TargetTypeFullName => string.Concat (TargetTypeNamespace, ".", TargetTypeName);
+
+ ///
+ public Accessibility Accessibility
+ {
+ get;
+ [UsedImplicitly]
+ internal set;
+ } = Accessibility.Public;
+
+ ///
+ public TypeKind TypeKind => TypeKind.Class;
+
+ ///
+ public bool IsRecord => false;
+
+ ///
+ public bool IsClass => true;
+
+ ///
+ public bool IsStruct => false;
+
+ ///
+ public bool IsByRefLike => false;
+
+ ///
+ public bool IsSealed => false;
+
+ ///
+ public bool IsAbstract => false;
+
+ ///
+ public bool IsEnum => false;
+
+ ///
+ public bool IsStatic => true;
+
+ ///
+ public bool IncludeInterface { get; private set; }
+
+ public string GeneratedTypeFullName => $"{GeneratedTypeNamespace}.{GeneratedTypeName}";
+
+ /// Whether to generate the extension class as partial (Default: true)
+ public bool IsPartial => true;
+
+ /// The fully-qualified namespace of the source enum type.
+ public string TargetTypeNamespace
+ {
+ get;
+ [UsedImplicitly]
+ set;
+ }
+
+ /// The UNQUALIFIED name of the source enum type.
+ public string TargetTypeName
+ {
+ get;
+ [UsedImplicitly]
+ set;
+ }
+
+ ///
+ /// The backing type for the enum.
+ ///
+ /// For simplicity and formality, only System.Int32 and System.UInt32 are supported at this time.
+ public TypeCode EnumBackingTypeCode
+ {
+ get => _enumBackingTypeCode;
+ set
+ {
+ if (value is not TypeCode.Int32 and not TypeCode.UInt32)
+ {
+ throw new NotSupportedException ("Only System.Int32 and System.UInt32 are supported at this time.");
+ }
+
+ _enumBackingTypeCode = value;
+ }
+ }
+
+ ///
+ /// Whether a fast alternative to the built-in Enum.HasFlag method will be generated (Default: false)
+ ///
+ public bool GenerateFastHasFlags { [UsedImplicitly] get; set; }
+
+ /// Whether a switch-based IsDefined replacement will be generated (Default: true)
+ public bool GenerateFastIsDefined { [UsedImplicitly]get; set; } = true;
+
+ internal ImmutableHashSet? IntMembers;
+ internal ImmutableHashSet? UIntMembers;
+
+ ///
+ /// Fully-qualified name of the extension class
+ ///
+ internal string FullyQualifiedClassName => $"{GeneratedTypeNamespace}.{GeneratedTypeName}";
+
+ ///
+ /// Whether a Flags was found on the enum type.
+ ///
+ internal bool HasFlagsAttribute {[UsedImplicitly] get; set; }
+
+ private static readonly SymbolDisplayFormat FullyQualifiedSymbolDisplayFormatWithoutGlobal =
+ SymbolDisplayFormat.FullyQualifiedFormat
+ .WithGlobalNamespaceStyle (
+ SymbolDisplayGlobalNamespaceStyle.Omitted);
+
+
+ internal bool TryConfigure (INamedTypeSymbol enumSymbol, CancellationToken cancellationToken)
+ {
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource (cancellationToken);
+ cts.Token.ThrowIfCancellationRequested ();
+
+ ImmutableArray attributes = enumSymbol.GetAttributes ();
+
+ // This is theoretically impossible, but guarding just in case and canceling if it does happen.
+ if (attributes.Length == 0)
+ {
+ cts.Cancel (true);
+
+ return false;
+ }
+
+ // Check all attributes provided for anything interesting.
+ // Attributes can be in any order, so just check them all and adjust at the end if necessary.
+ // Note that we do not perform as strict validation on actual usage of the attribute, at this stage,
+ // because the analyzer should have already thrown errors for invalid uses like global namespace
+ // or unsupported enum underlying types.
+ foreach (AttributeData attr in attributes)
+ {
+ cts.Token.ThrowIfCancellationRequested ();
+ string? attributeFullyQualifiedName = attr.AttributeClass?.ToDisplayString (FullyQualifiedSymbolDisplayFormatWithoutGlobal);
+
+ // Skip if null or not possibly an attribute we care about
+ if (attributeFullyQualifiedName is null or not { Length: >= 5 })
+ {
+ continue;
+ }
+
+ switch (attributeFullyQualifiedName)
+ {
+ // For Flags enums
+ case Strings.DotnetNames.Attributes.Flags:
+ {
+ HasFlagsAttribute = true;
+ }
+
+ continue;
+
+ // For the attribute that started this whole thing
+ case GeneratorAttributeFullyQualifiedName:
+
+ {
+ // If we can't successfully complete this method,
+ // something is wrong enough that we may as well just stop now.
+ if (!TryConfigure (attr, cts.Token))
+ {
+ if (cts.Token.CanBeCanceled)
+ {
+ cts.Cancel ();
+ }
+
+ return false;
+ }
+ }
+
+ continue;
+ }
+ }
+
+ // Now get the members, if we know we'll need them.
+ if (GenerateFastIsDefined || GenerateFastHasFlags)
+ {
+ if (EnumBackingTypeCode == TypeCode.Int32)
+ {
+ PopulateIntMembersHashSet (enumSymbol);
+ }
+ else if (EnumBackingTypeCode == TypeCode.UInt32)
+ {
+ PopulateUIntMembersHashSet (enumSymbol);
+ }
+ }
+
+ return true;
+ }
+
+ private void PopulateIntMembersHashSet (INamedTypeSymbol enumSymbol)
+ {
+ ImmutableArray enumMembers = enumSymbol.GetMembers ();
+ IEnumerable fieldSymbols = enumMembers.OfType ();
+ IntMembers = fieldSymbols.Select (static m => m.HasConstantValue ? (int)m.ConstantValue : 0).ToImmutableHashSet ();
+ }
+ private void PopulateUIntMembersHashSet (INamedTypeSymbol enumSymbol)
+ {
+ UIntMembers = enumSymbol.GetMembers ().OfType ().Select (static m => (uint)m.ConstantValue).ToImmutableHashSet ();
+ }
+
+ private bool HasExplicitFastHasFlags
+ {
+ [UsedImplicitly]get => _discoveredProperties [ExplicitFastHasFlagsMask];
+ set => _discoveredProperties [ExplicitFastHasFlagsMask] = value;
+ }
+
+ private bool HasExplicitFastIsDefined
+ {
+ [UsedImplicitly]get => _discoveredProperties [ExplicitFastIsDefinedMask];
+ set => _discoveredProperties [ExplicitFastIsDefinedMask] = value;
+ }
+
+ private bool HasExplicitIncludeInterface
+ {
+ [UsedImplicitly]get => _discoveredProperties [ExplicitIncludeInterfaceMask];
+ set => _discoveredProperties [ExplicitIncludeInterfaceMask] = value;
+ }
+
+ private bool HasExplicitPartial
+ {
+ [UsedImplicitly]get => _discoveredProperties [ExplicitPartialMask];
+ set => _discoveredProperties [ExplicitPartialMask] = value;
+ }
+
+ private bool HasExplicitTypeName
+ {
+ get => _discoveredProperties [ExplicitNameMask];
+ set => _discoveredProperties [ExplicitNameMask] = value;
+ }
+
+ private bool HasExplicitTypeNamespace
+ {
+ get => _discoveredProperties [ExplicitNamespaceMask];
+ set => _discoveredProperties [ExplicitNamespaceMask] = value;
+ }
+
+ [MemberNotNullWhen (true, nameof (_generatedTypeName), nameof (_generatedTypeNamespace))]
+ private bool TryConfigure (AttributeData attr, CancellationToken cancellationToken)
+ {
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource (cancellationToken);
+ cts.Token.ThrowIfCancellationRequested ();
+
+ if (attr is not { NamedArguments.Length: > 0 })
+ {
+ // Just a naked attribute, so configure with appropriate defaults.
+ GeneratedTypeNamespace = TargetTypeNamespace;
+ GeneratedTypeName = string.Concat (TargetTypeName, Strings.DefaultTypeNameSuffix);
+
+ return true;
+ }
+
+ cts.Token.ThrowIfCancellationRequested ();
+
+ foreach (KeyValuePair kvp in attr.NamedArguments)
+ {
+ string? propName = kvp.Key;
+ TypedConstant propValue = kvp.Value;
+
+ cts.Token.ThrowIfCancellationRequested ();
+
+ // For every property name and value pair, set associated metadata
+ // property, if understood.
+ switch (propName, propValue)
+ {
+ // Null or empty string doesn't make sense, so skip if it happens.
+ case (null, _):
+ case ("", _):
+ continue;
+
+ // ClassName is specified, not explicitly null, and at least 1 character long.
+ case (AttributeProperties.TypeNamePropertyName, { IsNull: false, Value: string { Length: > 1 } classNameProvidedValue }):
+ if (string.IsNullOrWhiteSpace (classNameProvidedValue))
+ {
+ return false;
+ }
+
+ GeneratedTypeName = classNameProvidedValue;
+ HasExplicitTypeName = true;
+
+ continue;
+
+ // Class namespace is specified, not explicitly null, and at least 1 character long.
+ case (AttributeProperties.TypeNamespacePropertyName, { IsNull: false, Value: string { Length: > 1 } classNamespaceProvidedValue }):
+
+ if (string.IsNullOrWhiteSpace (classNamespaceProvidedValue))
+ {
+ return false;
+ }
+
+ GeneratedTypeNamespace = classNamespaceProvidedValue;
+ HasExplicitTypeNamespace = true;
+
+ continue;
+
+ // FastHasFlags is specified
+ case (AttributeProperties.FastHasFlagsPropertyName, { IsNull: false } fastHasFlagsConstant):
+ GenerateFastHasFlags = fastHasFlagsConstant.Value is true;
+ HasExplicitFastHasFlags = true;
+
+ continue;
+
+ // FastIsDefined is specified
+ case (AttributeProperties.FastIsDefinedPropertyName, { IsNull: false } fastIsDefinedConstant):
+ GenerateFastIsDefined = fastIsDefinedConstant.Value is true;
+ HasExplicitFastIsDefined = true;
+
+ continue;
+ }
+ }
+
+ // The rest is simple enough it's not really worth worrying about cancellation, so don't bother from here on...
+
+ // Configure anything that wasn't specified that doesn't have an implicitly safe default
+ if (!HasExplicitTypeName || _generatedTypeName is null)
+ {
+ _generatedTypeName = string.Concat (TargetTypeName, Strings.DefaultTypeNameSuffix);
+ }
+
+ if (!HasExplicitTypeNamespace || _generatedTypeNamespace is null)
+ {
+ _generatedTypeNamespace = TargetTypeNamespace;
+ }
+
+ if (!HasFlagsAttribute)
+ {
+ GenerateFastHasFlags = false;
+ }
+
+ return true;
+ }
+
+ private static class AttributeProperties
+ {
+ internal const string FastHasFlagsPropertyName = nameof (GenerateEnumExtensionMethodsAttribute.FastHasFlags);
+ internal const string FastIsDefinedPropertyName = nameof (GenerateEnumExtensionMethodsAttribute.FastIsDefined);
+ internal const string TypeNamePropertyName = nameof (GenerateEnumExtensionMethodsAttribute.ClassName);
+ internal const string TypeNamespacePropertyName = nameof (GenerateEnumExtensionMethodsAttribute.ClassNamespace);
+ }
+}
diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/Generators/EnumExtensions/EnumExtensionMethodsIncrementalGenerator.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/Generators/EnumExtensions/EnumExtensionMethodsIncrementalGenerator.cs
new file mode 100644
index 000000000..66bfd3e81
--- /dev/null
+++ b/Analyzers/Terminal.Gui.Analyzers.Internal/Generators/EnumExtensions/EnumExtensionMethodsIncrementalGenerator.cs
@@ -0,0 +1,452 @@
+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}}
+ [System.AttributeUsageAttribute (System.AttributeTargets.Enum)]
+ public sealed class {{GeneratorAttributeName}} : 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
+ };
+ }
+}
diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/Generators/EnumExtensions/EnumMemberCombinationsGenerator.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/Generators/EnumExtensions/EnumMemberCombinationsGenerator.cs
new file mode 100644
index 000000000..82753d078
--- /dev/null
+++ b/Analyzers/Terminal.Gui.Analyzers.Internal/Generators/EnumExtensions/EnumMemberCombinationsGenerator.cs
@@ -0,0 +1,130 @@
+using System.Text;
+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;
+
+///
+/// Implementation of for types decorated with .
+///
+[Generator]
+internal sealed class EnumMemberCombinationsGenerator : IIncrementalGenerator
+{
+ private const string AttributeCodeText = $$"""
+ {{Strings.Templates.StandardHeader}}
+
+ namespace {{Strings.AnalyzersAttributesNamespace}};
+
+ ///
+ /// Designates an enum member for inclusion in generation of bitwise combinations with other members decorated with
+ /// this attribute which have the same value.
+ ///
+ ///
+ ///
+ /// This attribute is only considered for enum types with the .
+ ///
+ ///
+ /// Masks with more than 8 bits set will
+ ///
+ ///
+ [AttributeUsageAttribute(AttributeTargets.Enum)]
+ internal sealed class {{nameof (GenerateEnumMemberCombinationsAttribute)}} : System.Attribute
+ {
+ public const byte MaximumPopCountLimit = 14;
+ private uint _mask;
+ private uint _maskPopCount;
+ private byte _popCountLimit = 8;
+ public required string GroupTag { get; set; }
+
+ public required uint Mask
+ {
+ get => _mask;
+ set
+ {
+ _maskPopCount = uint.PopCount(value);
+
+ PopCountLimitExceeded = _maskPopCount > PopCountLimit;
+ MaximumPopCountLimitExceeded = _maskPopCount > MaximumPopCountLimit;
+
+ if (PopCountLimitExceeded || MaximumPopCountLimitExceeded)
+ {
+ return;
+ }
+
+ _mask = value;
+ }
+ }
+
+ ///
+ /// The maximum number of bits allowed to be set to 1 in .
+ ///
+ ///
+ ///
+ /// Default: 8 (256 possible combinations)
+ ///
+ ///
+ /// Increasing this value is not recommended!
+ /// Decreasing this value is pointless unless you want to limit maximum possible generated combinations even
+ /// further.
+ ///
+ ///
+ /// If the result of () exceeds 2 ^ , no
+ /// combinations will be generated for the members which otherwise would have been included by .
+ /// Values exceeding the actual population count of have no effect.
+ ///
+ ///
+ /// This option is set to a sane default of 8, but also has a hard-coded limit of 14 (16384 combinations), as a
+ /// protection against generation of extremely large files.
+ ///
+ ///
+ /// CAUTION: The maximum number of possible combinations possible is equal to 1 <<
+ /// ().
+ /// See for hard-coded limit,
+ ///
+ ///
+ public byte PopCountLimit
+ {
+ get => _popCountLimit;
+ set
+ {
+ _maskPopCount = uint.PopCount(_mask);
+
+ PopCountLimitExceeded = _maskPopCount > value;
+ MaximumPopCountLimitExceeded = _maskPopCount > MaximumPopCountLimit;
+
+ if (PopCountLimitExceeded || MaximumPopCountLimitExceeded)
+ {
+ return;
+ }
+
+ _mask = value;
+ _popCountLimit = value;
+ }
+ }
+
+ internal bool MaximumPopCountLimitExceeded { get; private set; }
+ internal bool PopCountLimitExceeded { get; private set; }
+ }
+
+ """;
+
+ private const string AttributeFullyQualifiedName = $"{Strings.AnalyzersAttributesNamespace}.{AttributeName}";
+ private const string AttributeName = "GenerateEnumMemberCombinationsAttribute";
+
+ ///
+ public void Initialize (IncrementalGeneratorInitializationContext context)
+ {
+ context.RegisterPostInitializationOutput (GenerateAttributeCode);
+
+ return;
+
+ static void GenerateAttributeCode (IncrementalGeneratorPostInitializationContext initContext)
+ {
+#pragma warning disable IDE0061 // Use expression body for local function
+ initContext.AddSource ($"{AttributeFullyQualifiedName}.g.cs", SourceText.From (AttributeCodeText, Encoding.UTF8));
+#pragma warning restore IDE0061 // Use expression body for local function
+ }
+ }
+}
diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/IGeneratedTypeMetadata.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/IGeneratedTypeMetadata.cs
new file mode 100644
index 000000000..c72a8cc44
--- /dev/null
+++ b/Analyzers/Terminal.Gui.Analyzers.Internal/IGeneratedTypeMetadata.cs
@@ -0,0 +1,38 @@
+using JetBrains.Annotations;
+using Microsoft.CodeAnalysis;
+
+namespace Terminal.Gui.Analyzers.Internal;
+
+///
+/// Interface for all generators to use for their metadata classes.
+///
+/// The type implementing this interface.
+internal interface IGeneratedTypeMetadata where TSelf : IGeneratedTypeMetadata
+{
+ [UsedImplicitly]
+ string GeneratedTypeNamespace { get; }
+ [UsedImplicitly]
+ string? GeneratedTypeName { get; }
+ [UsedImplicitly]
+ string GeneratedTypeFullName { get; }
+ [UsedImplicitly]
+ string TargetTypeNamespace { get; }
+ [UsedImplicitly]
+ string TargetTypeName { get; }
+ string TargetTypeFullName { get; }
+ [UsedImplicitly]
+ Accessibility Accessibility { get; }
+ TypeKind TypeKind { get; }
+ bool IsRecord { get; }
+ bool IsClass { get; }
+ bool IsStruct { get; }
+ [UsedImplicitly]
+ bool IsPartial { get; }
+ bool IsByRefLike { get; }
+ bool IsSealed { get; }
+ bool IsAbstract { get; }
+ bool IsEnum { get; }
+ bool IsStatic { get; }
+ [UsedImplicitly]
+ bool IncludeInterface { get; }
+}
\ No newline at end of file
diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/IStandardCSharpCodeGenerator.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/IStandardCSharpCodeGenerator.cs
new file mode 100644
index 000000000..a0e3d584d
--- /dev/null
+++ b/Analyzers/Terminal.Gui.Analyzers.Internal/IStandardCSharpCodeGenerator.cs
@@ -0,0 +1,28 @@
+using System.Text;
+using JetBrains.Annotations;
+using Microsoft.CodeAnalysis.Text;
+
+namespace Terminal.Gui.Analyzers.Internal;
+
+internal interface IStandardCSharpCodeGenerator where T : IGeneratedTypeMetadata
+{
+ ///
+ /// Generates and returns the full source text corresponding to ,
+ /// in the requested or if not provided.
+ ///
+ ///
+ /// The of the generated source text or if not
+ /// provided.
+ ///
+ ///
+ [UsedImplicitly]
+ [SkipLocalsInit]
+ ref readonly SourceText GenerateSourceText (Encoding? encoding = null);
+
+ ///
+ /// A type implementing which
+ /// will be used for source generation.
+ ///
+ [UsedImplicitly]
+ T Metadata { get; set; }
+}