123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443 |
- 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;
- /// <summary>
- /// 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.
- /// </summary>
- /// <remarks>
- /// Minimal validation is performed by this type.<br/>
- /// Errors in analyzed source code will result in generation failure or broken output.<br/>
- /// This type is not intended for use outside of Terminal.Gui library development.
- /// </remarks>
- internal sealed record EnumExtensionMethodsGenerationInfo : IGeneratedTypeMetadata<EnumExtensionMethodsGenerationInfo>,
- IEqualityOperators<EnumExtensionMethodsGenerationInfo, EnumExtensionMethodsGenerationInfo, bool>
- {
- 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;
- /// <summary>
- /// 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.
- /// </summary>
- /// <param name="enumNamespace">The fully-qualified namespace of the enum type, without assembly name.</param>
- /// <param name="enumTypeName">
- /// The name of the enum type, as would be given by <see langword="nameof"/> on the enum's type
- /// declaration.
- /// </param>
- /// <param name="typeNamespace">
- /// The fully-qualified namespace in which to place the generated code, without assembly name. If omitted or explicitly
- /// null, uses the value provided in <paramref name="enumNamespace"/>.
- /// </param>
- /// <param name="typeName">
- /// The name of the generated class. If omitted or explicitly null, appends "Extensions" to the value of
- /// <paramref name="enumTypeName"/>.
- /// </param>
- /// <param name="enumBackingTypeCode">The backing type of the enum. Defaults to <see cref="int"/>.</param>
- /// <param name="generateFastHasFlags">
- /// Whether to generate a fast HasFlag alternative. (Default: true) Ignored if the enum does not also have
- /// <see cref="FlagsAttribute"/>.
- /// </param>
- /// <param name="generateFastIsDefined">Whether to generate a fast IsDefined alternative. (Default: true)</param>
- /// <remarks>
- /// Minimal validation is performed by this type.<br/>
- /// Errors in analyzed source code will result in generation failure or broken output.<br/>
- /// This type is not intended for use outside of Terminal.Gui library development.
- /// </remarks>
- 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);
- /// <summary>The name of the extension class.</summary>
- public string? GeneratedTypeName
- {
- get => _generatedTypeName ?? string.Concat (TargetTypeName, Strings.DefaultTypeNameSuffix);
- set => _generatedTypeName = value ?? string.Concat (TargetTypeName, Strings.DefaultTypeNameSuffix);
- }
- /// <summary>The namespace for the extension class.</summary>
- /// <remarks>
- /// Value is not validated by the set accessor.<br/>
- /// Get accessor will never return null and is thus marked [NotNull] for static analysis, even though the property is
- /// declared as a nullable <see langword="string?"/>.<br/>If the backing field for this property is null, the get
- /// accessor will return <see cref="TargetTypeNamespace"/> instead.
- /// </remarks>
- public string? GeneratedTypeNamespace
- {
- get => _generatedTypeNamespace ?? TargetTypeNamespace;
- set => _generatedTypeNamespace = value ?? TargetTypeNamespace;
- }
- /// <inheritdoc/>
- public string TargetTypeFullName => string.Concat (TargetTypeNamespace, ".", TargetTypeName);
- /// <inheritdoc/>
- public Accessibility Accessibility
- {
- get;
- [UsedImplicitly]
- internal set;
- } = Accessibility.Public;
- /// <inheritdoc/>
- public TypeKind TypeKind => TypeKind.Class;
- /// <inheritdoc/>
- public bool IsRecord => false;
- /// <inheritdoc/>
- public bool IsClass => true;
- /// <inheritdoc/>
- public bool IsStruct => false;
- /// <inheritdoc/>
- public bool IsByRefLike => false;
- /// <inheritdoc/>
- public bool IsSealed => false;
- /// <inheritdoc/>
- public bool IsAbstract => false;
- /// <inheritdoc/>
- public bool IsEnum => false;
- /// <inheritdoc/>
- public bool IsStatic => true;
- /// <inheritdoc/>
- public bool IncludeInterface => false;
- public string GeneratedTypeFullName => $"{GeneratedTypeNamespace}.{GeneratedTypeName}";
- /// <summary>Whether to generate the extension class as partial (Default: true)</summary>
- public bool IsPartial => true;
- /// <summary>The fully-qualified namespace of the source enum type.</summary>
- public string TargetTypeNamespace
- {
- get;
- [UsedImplicitly]
- set;
- }
- /// <summary>The UNQUALIFIED name of the source enum type.</summary>
- public string TargetTypeName
- {
- get;
- [UsedImplicitly]
- set;
- }
- /// <summary>
- /// The backing type for the enum.
- /// </summary>
- /// <remarks>For simplicity and formality, only System.Int32 and System.UInt32 are supported at this time.</remarks>
- 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;
- }
- }
- /// <summary>
- /// Whether a fast alternative to the built-in Enum.HasFlag method will be generated (Default: false)
- /// </summary>
- public bool GenerateFastHasFlags { [UsedImplicitly] get; set; }
- /// <summary>Whether a switch-based IsDefined replacement will be generated (Default: true)</summary>
- public bool GenerateFastIsDefined { [UsedImplicitly]get; set; } = true;
- internal ImmutableHashSet<int>? _intMembers;
- internal ImmutableHashSet<uint>? _uIntMembers;
- /// <summary>
- /// Fully-qualified name of the extension class
- /// </summary>
- internal string FullyQualifiedClassName => $"{GeneratedTypeNamespace}.{GeneratedTypeName}";
- /// <summary>
- /// Whether a Flags was found on the enum type.
- /// </summary>
- 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<AttributeData> 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<ISymbol> enumMembers = enumSymbol.GetMembers ();
- IEnumerable<IFieldSymbol> fieldSymbols = enumMembers.OfType<IFieldSymbol> ();
- _intMembers = fieldSymbols.Select (static m => m.HasConstantValue ? (int)m.ConstantValue : 0).ToImmutableHashSet ();
- }
- private void PopulateUIntMembersHashSet (INamedTypeSymbol enumSymbol)
- {
- _uIntMembers = enumSymbol.GetMembers ().OfType<IFieldSymbol> ().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<string, TypedConstant> 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);
- }
- }
|