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