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