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 = 0b_0100; private const int ExplicitFastIsDefinedMask = 0b_1000; private const int ExplicitNameMask = 0b_0010; private const int ExplicitNamespaceMask = 0b_0001; private const string GeneratorAttributeFullyQualifiedName = $"{GeneratorAttributeNamespace}.{GeneratorAttributeName}"; private const string GeneratorAttributeName = nameof (GenerateEnumExtensionMethodsAttribute); private const string GeneratorAttributeNamespace = 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 readonly 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 => false; 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; init { 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 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); } }