mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-28 08:47:59 +01:00
453 lines
29 KiB
C#
453 lines
29 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Incremental code generator for enums decorated with <see cref="GenerateEnumExtensionMethodsAttribute"/>.
|
|
/// </summary>
|
|
[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);
|
|
|
|
/// <summary>Fully-qualified symbol name format without the "global::" prefix.</summary>
|
|
private static readonly SymbolDisplayFormat _fullyQualifiedSymbolDisplayFormatWithoutGlobal =
|
|
SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle (SymbolDisplayGlobalNamespaceStyle.Omitted);
|
|
|
|
/// <inheritdoc/>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// Basically, this method is called once by the compiler, and is responsible for wiring up
|
|
/// everything important about how source generation works.
|
|
/// </para>
|
|
/// <para>
|
|
/// See in-line comments for specifics of what's going on.
|
|
/// </para>
|
|
/// <para>
|
|
/// Note that <paramref name="context"/> is everything in the compilation,
|
|
/// except for code generated by this generator or generators which have not yet executed.<br/>
|
|
/// The methods registered to perform generation get called on-demand by the host (the IDE,
|
|
/// compiler, etc), sometimes as often as every single keystroke.
|
|
/// </para>
|
|
/// </remarks>
|
|
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<T> 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<EnumExtensionMethodsGenerationInfo?> 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<T> 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}};
|
|
|
|
/// <summary>
|
|
/// Interface to simplify general enumeration of constructed generic types for
|
|
/// <see cref="ExtensionsForEnumTypeAttribute{TEnum}"/>
|
|
/// </summary>
|
|
{{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}};
|
|
|
|
/// <summary>Assembly attribute declaring a known pairing of an <see langword="enum" /> type to an extension class.</summary>
|
|
/// <remarks>This attribute should only be written by internal source generators for Terminal.Gui. No other usage of any kind is supported.</remarks>
|
|
{{Strings.Templates.AttributesForGeneratedTypes}}
|
|
[System.AttributeUsageAttribute(System.AttributeTargets.Assembly, AllowMultiple = true)]
|
|
public sealed class {{nameof(AssemblyExtendedEnumTypeAttribute)}} : System.Attribute
|
|
{
|
|
/// <summary>Creates a new instance of <see cref="AssemblyExtendedEnumTypeAttribute" /> from the provided parameters.</summary>
|
|
/// <param name="enumType">The <see cref="System.Type" /> of an <see langword="enum" /> decorated with a <see cref="GenerateEnumExtensionMethodsAttribute" />.</param>
|
|
/// <param name="extensionClass">The <see cref="System.Type" /> of the <see langword="class" /> decorated with an <see cref="ExtensionsForEnumTypeAttribute{TEnum}" /> referring to the same type as <paramref name="enumType" />.</param>
|
|
public AssemblyExtendedEnumTypeAttribute (System.Type enumType, System.Type extensionClass)
|
|
{
|
|
EnumType = enumType;
|
|
ExtensionClass = extensionClass;
|
|
}
|
|
/// <summary>An <see langword="enum" /> type that has been extended by Terminal.Gui source generators.</summary>
|
|
public System.Type EnumType { get; init; }
|
|
/// <summary>A class containing extension methods for <see cref="EnumType"/>.</summary>
|
|
public System.Type ExtensionClass { get; init; }
|
|
/// <inheritdoc />
|
|
public override string ToString () => $"{EnumType.Name},{ExtensionClass.Name}";
|
|
}
|
|
|
|
""",
|
|
Encoding.UTF8));
|
|
|
|
postInitializationContext
|
|
.AddSource (
|
|
$"{GeneratorAttributeFullyQualifiedName}.g.cs",
|
|
SourceText.From (
|
|
$$"""
|
|
{{Strings.Templates.StandardHeader}}
|
|
|
|
namespace {{Strings.AnalyzersAttributesNamespace}};
|
|
|
|
/// <summary>
|
|
/// Used to enable source generation of a common set of extension methods for enum types.
|
|
/// </summary>
|
|
{{Strings.Templates.AttributesForGeneratedTypes}}
|
|
[{{Strings.DotnetNames.Types.AttributeUsageAttribute}} ({{Strings.DotnetNames.Types.AttributeTargets}}.Enum)]
|
|
public sealed class {{GeneratorAttributeName}} : {{Strings.DotnetNames.Types.Attribute}}
|
|
{
|
|
/// <summary>
|
|
/// The name of the generated static class.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If unspecified, null, empty, or only whitespace, defaults to the name of the enum plus "Extensions".<br/>
|
|
/// No other validation is performed, so illegal values will simply result in compiler errors.
|
|
/// <para>
|
|
/// Explicitly specifying a default value is unnecessary and will result in unnecessary processing.
|
|
/// </para>
|
|
/// </remarks>
|
|
public string? ClassName { get; set; }
|
|
|
|
/// <summary>
|
|
/// The namespace in which to place the generated static class containing the extension methods.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If unspecified, null, empty, or only whitespace, defaults to the namespace of the enum.<br/>
|
|
/// No other validation is performed, so illegal values will simply result in compiler errors.
|
|
/// <para>
|
|
/// Explicitly specifying a default value is unnecessary and will result in unnecessary processing.
|
|
/// </para>
|
|
/// </remarks>
|
|
public string? ClassNamespace { get; set; }
|
|
|
|
/// <summary>
|
|
/// Whether to generate a fast, zero-allocation, non-boxing, and reflection-free alternative to the built-in
|
|
/// <see cref="Enum.HasFlag"/> method.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// Default: false
|
|
/// </para>
|
|
/// <para>
|
|
/// If the enum is not decorated with <see cref="Flags"/>, this option has no effect.
|
|
/// </para>
|
|
/// <para>
|
|
/// 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.
|
|
/// </para>
|
|
/// <para>
|
|
/// 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.
|
|
/// </para>
|
|
/// <para>
|
|
/// Explicitly specifying a default value is unnecessary and will result in unnecessary processing.
|
|
/// </para>
|
|
/// </remarks>
|
|
public bool FastHasFlags { get; set; }
|
|
|
|
/// <summary>
|
|
/// Whether to generate a fast, zero-allocation, and reflection-free alternative to the built-in
|
|
/// <see cref="Enum.IsDefined"/> method,
|
|
/// using a switch expression as a hard-coded reverse mapping of numeric values to explicitly-named members.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// Default: true
|
|
/// </para>
|
|
/// <para>
|
|
/// 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.
|
|
/// </para>
|
|
/// <para>
|
|
/// As with <see cref="Enum.IsDefined"/> the source generator only considers explicitly-named members.<br/>
|
|
/// Generation of values which represent valid bitwise combinations of members of enums decorated with
|
|
/// <see cref="Flags"/> is not affected by this property.
|
|
/// </para>
|
|
/// </remarks>
|
|
public bool FastIsDefined { get; init; } = true;
|
|
|
|
/// <summary>
|
|
/// Gets a <see langword="bool"/> value indicating if this <see cref="GenerateEnumExtensionMethodsAttribute"/> instance
|
|
/// contains default values only. See <see href="#remarks">remarks</see> of this method or documentation on properties of this type for details.
|
|
/// </summary>
|
|
/// <returns>
|
|
/// A <see langword="bool"/> value indicating if all property values are default for this
|
|
/// <see cref="GenerateEnumExtensionMethodsAttribute"/> instance.
|
|
/// </returns>
|
|
/// <remarks>
|
|
/// Default values that will result in a <see langword="true"/> return value are:<br/>
|
|
/// <see cref="FastIsDefined"/> && !<see cref="FastHasFlags"/> && <see cref="ClassName"/>
|
|
/// <see langword="is"/> <see langword="null"/> && <see cref="ClassNamespace"/> <see langword="is"/>
|
|
/// <see langword="null"/>
|
|
/// </remarks>
|
|
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}};
|
|
|
|
/// <summary>
|
|
/// Attribute written by the source generator for enum extension classes, for easier analysis and reflection.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Properties are just convenient shortcuts to properties of <typeparamref name="TEnum"/>.
|
|
/// </remarks>
|
|
{{Strings.Templates.AttributesForGeneratedTypes}}
|
|
[System.AttributeUsageAttribute (System.AttributeTargets.Class | System.AttributeTargets.Interface)]
|
|
public sealed class {{ExtensionsForEnumTypeAttributeName}}<TEnum>: System.Attribute, IExtensionsForEnumTypeAttributes where TEnum : struct, Enum
|
|
{
|
|
/// <summary>
|
|
/// The namespace-qualified name of <typeparamref name="TEnum"/>.
|
|
/// </summary>
|
|
public string EnumFullName => EnumType.FullName!;
|
|
|
|
/// <summary>
|
|
/// The unqualified name of <typeparamref name="TEnum"/>.
|
|
/// </summary>
|
|
public string EnumName => EnumType.Name;
|
|
|
|
/// <summary>
|
|
/// The namespace containing <typeparamref name="TEnum"/>.
|
|
/// </summary>
|
|
public string EnumNamespace => EnumType.Namespace!;
|
|
|
|
/// <summary>
|
|
/// The <see cref="Type"/> given by <see langword="typeof"/>(<typeparamref name="TEnum"/>).
|
|
/// </summary>
|
|
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 ());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if <paramref name="syntaxNode"/> is an EnumDeclarationSyntax
|
|
/// whose parent is a NamespaceDeclarationSyntax, FileScopedNamespaceDeclarationSyntax, or a
|
|
/// (Class|Struct)DeclarationSyntax.<br/>
|
|
/// Additional filtering is performed in later stages.
|
|
/// </summary>
|
|
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
|
|
};
|
|
}
|
|
}
|