diff --git a/src/AttributeSourceGenerator/AttributeIncrementalGeneratorBase.cs b/src/AttributeSourceGenerator/AttributeIncrementalGeneratorBase.cs index cb6b674..f70e61c 100644 --- a/src/AttributeSourceGenerator/AttributeIncrementalGeneratorBase.cs +++ b/src/AttributeSourceGenerator/AttributeIncrementalGeneratorBase.cs @@ -1,125 +1,129 @@ using System.Text; using AttributeSourceGenerator.Common; +using AttributeSourceGenerator.Models; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; +// ReSharper disable CheckNamespace + namespace AttributeSourceGenerator; +/// Provides a base class for incremental source generators that generate source using marker attributes. public abstract class AttributeIncrementalGeneratorBase : IIncrementalGenerator { - protected abstract string AttributeFullName { get; } - protected abstract string AttributeSource { get; } - protected abstract FilterType AttributeFilter { get; } - protected abstract Func GenerateSourceForSymbol { get; } + private readonly AttributeIncrementalGeneratorConfiguration _configuration; - public void Initialize(IncrementalGeneratorInitializationContext context) + /// Initializes a new instance of the class with the given configuration initializer. + /// The configuration for the generator. + protected AttributeIncrementalGeneratorBase(AttributeIncrementalGeneratorConfiguration configuration) { - context.RegisterPostInitializationOutput(AddSource); - - var pipeline = context.SyntaxProvider.ForAttributeWithMetadataName(AttributeFullName, Filter, Transform); - - context.RegisterSourceOutput(pipeline, GenerateSource); + _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); } - private void AddSource(IncrementalGeneratorPostInitializationContext ctx) + /// Initializes a new instance of the class with the given configuration initializer. + /// A function that provides the configuration for the generator. + protected AttributeIncrementalGeneratorBase(Func initializer) { - ctx.AddSource($"{AttributeFullName}.g.cs", SourceText.From(AttributeSource, Encoding.UTF8)); - } + if (initializer is null) + throw new ArgumentNullException(nameof(initializer)); - private bool Filter(SyntaxNode syntaxNode, CancellationToken _) - { - return AttributeFilter switch - { - FilterType.Interface => syntaxNode is InterfaceDeclarationSyntax, - FilterType.Class => syntaxNode is ClassDeclarationSyntax, - FilterType.Record => syntaxNode is RecordDeclarationSyntax syntax && syntax.Kind() == SyntaxKind.ClassDeclaration, - FilterType.Struct => syntaxNode is StructDeclarationSyntax, - FilterType.RecordStruct => syntaxNode is RecordDeclarationSyntax syntax && syntax.Kind() == SyntaxKind.StructDeclaration, - FilterType.Method => syntaxNode is MethodDeclarationSyntax, - _ => true, - }; + _configuration = initializer(); } - private static Symbol Transform(GeneratorAttributeSyntaxContext context, CancellationToken _) + /// Initializes the incremental generator. + /// The initialization context. + public void Initialize(IncrementalGeneratorInitializationContext context) { - var symbol = context.TargetSymbol; - var containingDeclarations = BuildHierarchy(symbol); - var symbolName = symbol.Name; - return new Symbol(symbolName, containingDeclarations); - } + context.RegisterPostInitializationOutput(initializationContext => AddSource(initializationContext, _configuration.AttributeFullyQualifiedName, _configuration.AttributeSource)); - private void GenerateSource(SourceProductionContext context, Symbol symbol) - { - var sourceText = GenerateSourceForSymbol(symbol); - context.AddSource($"{symbol.FullName}.g.cs", sourceText); + var pipeline = context.SyntaxProvider.ForAttributeWithMetadataName(_configuration.AttributeFullyQualifiedName, (syntaxNode, _) => Filter(syntaxNode, _configuration.SymbolFilter), (syntaxContext, _) => Transform(syntaxContext)); + + context.RegisterSourceOutput(pipeline, (productionContext, symbol) => GenerateSourceForSymbol(productionContext, symbol, _configuration.SourceGenerator)); } - private static EquatableReadOnlyList BuildHierarchy(ISymbol symbol) + /// Adds a source file to the output. + /// The post-initialization context. + /// The name of the source file. + /// The source code for the file. + private static void AddSource(IncrementalGeneratorPostInitializationContext context, string name, string? source) { - var declarations = new Stack(); - BuildContainingSymbolHierarchy(symbol, in declarations); - return declarations.ToEquatableReadOnlyList(); + if (source?.Length > 0) + context.AddSource($"{name}.g.cs", SourceText.From(source, Encoding.UTF8)); } - private static void BuildContainingSymbolHierarchy(ISymbol symbol, in Stack declarations) + /// Determines whether a syntax node should be included based on the filter settings. + /// The syntax node to filter. + /// The filter configuration. + /// if the syntax node should be included, otherwise . + private static bool Filter(SyntaxNode syntaxNode, FilterType filterType) { - if (symbol.ContainingType is not null) - BuildTypeHierarchy(symbol.ContainingType, in declarations); - else if (symbol.ContainingNamespace is not null) - BuildNamespaceHierarchy(symbol.ContainingNamespace, declarations); + var filter = filterType == FilterType.None ? FilterType.All : filterType; + + if (filter.HasFlag(FilterType.Interface) && syntaxNode is InterfaceDeclarationSyntax) + return true; + if (filter.HasFlag(FilterType.Class) && syntaxNode is ClassDeclarationSyntax) + return true; + if (filter.HasFlag(FilterType.Record) && syntaxNode is RecordDeclarationSyntax recordDeclaration && recordDeclaration.Kind() == SyntaxKind.ClassDeclaration) + return true; + if (filter.HasFlag(FilterType.Struct) && syntaxNode is StructDeclarationSyntax) + return true; + if (filter.HasFlag(FilterType.RecordStruct) && syntaxNode is RecordDeclarationSyntax recordStructDeclaration && recordStructDeclaration.Kind() == SyntaxKind.StructDeclaration) + return true; + if (filter.HasFlag(FilterType.Method) && syntaxNode is MethodDeclarationSyntax) + return true; + + return false; } - private static void BuildTypeHierarchy(INamedTypeSymbol symbol, in Stack declarations) + /// Transforms a generator attribute syntax context into a symbol for source generation. + /// The generator attribute syntax context. + /// The transformed symbol. + private static Symbol Transform(GeneratorAttributeSyntaxContext context) { - DeclarationType? declarationType = null; - - - if (symbol.IsReferenceType) - { - if (symbol.TypeKind == TypeKind.Interface) - declarationType = DeclarationType.Interface; - else if (symbol.IsRecord) - declarationType = DeclarationType.Record; - else - declarationType = DeclarationType.Class; - } - else if (symbol.IsValueType) + var targetSymbol = context.TargetSymbol; + if (targetSymbol is not INamedTypeSymbol && targetSymbol is not IMethodSymbol) + throw new InvalidOperationException($"{nameof(AttributeIncrementalGeneratorBase)} unexpectedly tried to transform a {nameof(context.TargetSymbol)} that was not an {nameof(INamedTypeSymbol)} or a {nameof(IMethodSymbol)}."); + + var markerAttribute = context.GetMarkerAttribute(); + var containingDeclarations = targetSymbol.GetContainingDeclarations(); + var symbolType = targetSymbol.GetSymbolType(); + var symbolName = targetSymbol.Name; + EquatableReadOnlyList genericTypeParameters; + EquatableReadOnlyList constructorParameters; + string returnType; + switch (targetSymbol) { - if (symbol.IsRecord) - declarationType = DeclarationType.RecordStruct; - else - declarationType = DeclarationType.Struct; + case INamedTypeSymbol namedTypeSymbol: + genericTypeParameters = namedTypeSymbol.GetGenericTypeParameters(); + constructorParameters = EquatableReadOnlyList.Empty; + returnType = ""; + break; + case IMethodSymbol methodSymbol: + genericTypeParameters = methodSymbol.GetGenericTypeParameters(); + constructorParameters = methodSymbol.GetConstructorParameters(); + returnType = methodSymbol.ReturnType.ToDisplayString(); + break; + default: + genericTypeParameters = EquatableReadOnlyList.Empty; + constructorParameters = EquatableReadOnlyList.Empty; + returnType = ""; + break; } - if (declarationType is null) - return; + var symbol = new Symbol(markerAttribute, containingDeclarations, symbolType, symbolName, genericTypeParameters, constructorParameters, returnType); - var genericParameters = EquatableReadOnlyList.Empty; - if (symbol.IsGenericType) - { - var typeParameters = new List(); - foreach (var typeParameter in symbol.TypeParameters) - typeParameters.Add(typeParameter.Name); - genericParameters = new EquatableReadOnlyList(typeParameters); - } - - var typeDeclaration = new Declaration(declarationType.Value, symbol.Name, genericParameters); - declarations.Push(typeDeclaration); - - BuildContainingSymbolHierarchy(symbol, declarations); + return symbol; } - private static void BuildNamespaceHierarchy(INamespaceSymbol symbol, in Stack declarations) + /// Generates source code for a given symbol. + /// The source production context. + /// The symbol to generate source for. + /// A function that generates the source code for a symbol. + private static void GenerateSourceForSymbol(SourceProductionContext context, Symbol symbol, Func generate) { - if (!symbol.IsGlobalNamespace) - { - var namespaceDeclaration = new Declaration(DeclarationType.Namespace, symbol.Name, EquatableReadOnlyList.Empty); - declarations.Push(namespaceDeclaration); - } - - if (symbol.ContainingNamespace is not null && !symbol.ContainingNamespace.IsGlobalNamespace) - BuildNamespaceHierarchy(symbol.ContainingNamespace, declarations); + var sourceText = generate(symbol); + context.AddSource($"{symbol.FullyQualifiedName}.g.cs", sourceText); } -} +} \ No newline at end of file diff --git a/src/AttributeSourceGenerator/AttributeIncrementalGeneratorConfiguration.cs b/src/AttributeSourceGenerator/AttributeIncrementalGeneratorConfiguration.cs new file mode 100644 index 0000000..a808fe7 --- /dev/null +++ b/src/AttributeSourceGenerator/AttributeIncrementalGeneratorConfiguration.cs @@ -0,0 +1,24 @@ +// ReSharper disable CheckNamespace + +namespace AttributeSourceGenerator; + +/// Defines the configuration for an incremental attribute generator. +public sealed class AttributeIncrementalGeneratorConfiguration +{ + /// The fully qualified name of the attribute. + public required string AttributeFullyQualifiedName { get; init; } + + /// The source for the attribute. + public string? AttributeSource { get; init; } + + /// The filter to apply to symbols. + public FilterType SymbolFilter { get; init; } = FilterType.All; + + /// The function that generates the source code for the attribute. + public required Func SourceGenerator { get; init; } + + /// Initializes a new instance of the class + public AttributeIncrementalGeneratorConfiguration() + { + } +} diff --git a/src/AttributeSourceGenerator/AttributeSourceGenerator.csproj b/src/AttributeSourceGenerator/AttributeSourceGenerator.csproj index c97af2f..452b571 100644 --- a/src/AttributeSourceGenerator/AttributeSourceGenerator.csproj +++ b/src/AttributeSourceGenerator/AttributeSourceGenerator.csproj @@ -6,7 +6,7 @@ enable AttributeSourceGenerator latest - 8.0.1 + 8.0.2 AttributeSourceGenerator Jean-Sebastien Carle A simple attribute-based Roslyn incremental source generator base class for .NET. @@ -18,8 +18,8 @@ https://github.com/jscarle/AttributeSourceGenerator git attribute source-generator attribute-based source-generators - 8.0.1.0 - 8.0.1.0 + 8.0.2.0 + 8.0.2.0 en-US true snupkg @@ -31,12 +31,12 @@ - - + + - + True \ diff --git a/src/AttributeSourceGenerator/Common/DeclarationExtensions.cs b/src/AttributeSourceGenerator/Common/DeclarationExtensions.cs new file mode 100644 index 0000000..8505b9d --- /dev/null +++ b/src/AttributeSourceGenerator/Common/DeclarationExtensions.cs @@ -0,0 +1,58 @@ +using System.Text; +using AttributeSourceGenerator.Models; + +// ReSharper disable CheckNamespace + +namespace AttributeSourceGenerator.Common; + +/// Provides extension methods for working with declarations. +internal static class DeclarationExtensions +{ + /// Converts a list of declarations to their corresponding namespace. + /// The list of declarations to convert. + /// The namespace represented by the declarations. + public static string ToNamespace(this EquatableReadOnlyList declarations) + { + var builder = new StringBuilder(); + + // ReSharper disable once ForCanBeConvertedToForeach + for (var index = 0; index < declarations.Count; index++) + { + var declaration = declarations[index]; + if (declaration.DeclarationType != DeclarationType.Namespace) + continue; + + if (builder.Length > 0) + builder.Append('.'); + builder.Append(declaration.Name); + } + + return builder.ToString(); + } + + /// Converts a list of declarations to their fully qualified name. + /// The list of declarations to convert. + /// The fully qualified name represented by the declarations. + public static string ToFullyQualifiedName(this EquatableReadOnlyList declarations) + { + var builder = new StringBuilder(); + + // ReSharper disable once ForCanBeConvertedToForeach + for (var index = 0; index < declarations.Count; index++) + { + var declaration = declarations[index]; + + if (builder.Length > 0) + builder.Append('.'); + builder.Append(declaration.Name); + + if (declaration.GenericParameters.Count <= 0) + continue; + + builder.Append('`'); + builder.Append(declaration.GenericParameters.Count); + } + + return builder.ToString(); + } +} diff --git a/src/AttributeSourceGenerator/Common/EquatableReadOnlyList.cs b/src/AttributeSourceGenerator/Common/EquatableReadOnlyList.cs index 7a2cff2..0f4f242 100644 --- a/src/AttributeSourceGenerator/Common/EquatableReadOnlyList.cs +++ b/src/AttributeSourceGenerator/Common/EquatableReadOnlyList.cs @@ -1,7 +1,9 @@ -namespace AttributeSourceGenerator.Common; +// ReSharper disable CheckNamespace + +namespace AttributeSourceGenerator.Common; /// Provides extension methods to convert various collections to an . -public static class EquatableReadOnlyList +internal static class EquatableReadOnlyList { /// Converts an to an . /// The to convert. diff --git a/src/AttributeSourceGenerator/Common/EquatableReadOnlyList`1.cs b/src/AttributeSourceGenerator/Common/EquatableReadOnlyList`1.cs index ac08110..71ed855 100644 --- a/src/AttributeSourceGenerator/Common/EquatableReadOnlyList`1.cs +++ b/src/AttributeSourceGenerator/Common/EquatableReadOnlyList`1.cs @@ -1,5 +1,7 @@ using System.Collections; +// ReSharper disable CheckNamespace + namespace AttributeSourceGenerator.Common; /// A read-only list that implements for value-based equality comparisons. @@ -7,16 +9,17 @@ namespace AttributeSourceGenerator.Common; public readonly struct EquatableReadOnlyList : IEquatable>, IReadOnlyList { /// Gets an empty . - public static EquatableReadOnlyList Empty { get; } = new([]); + internal static EquatableReadOnlyList Empty { get; } = new([]); + + /// Gets the number of elements in the list. + public int Count => Collection.Count; /// Gets the element at the specified index. /// The index of the element to get. /// The element at the specified index. public T this[int index] => Collection[index]; - /// Gets the number of elements in the list. - public int Count => Collection.Count; - + private IReadOnlyList Collection => _collection ?? []; private readonly IReadOnlyList? _collection; /// Creates a new from an existing . @@ -26,8 +29,6 @@ internal EquatableReadOnlyList(IReadOnlyList? collection) _collection = collection; } - private IReadOnlyList Collection => _collection ?? []; - /// Determines whether this instance and another object are equal. /// The object to compare with this instance. /// True if the objects are equal, false otherwise. @@ -68,7 +69,8 @@ public override int GetHashCode() { var hashCode = new HashCode(); - foreach (var item in Collection) hashCode.Add(item); + foreach (var item in Collection) + hashCode.Add(item); return hashCode.ToHashCode(); } diff --git a/src/AttributeSourceGenerator/Common/GeneratorAttributeSyntaxContextExtensions.cs b/src/AttributeSourceGenerator/Common/GeneratorAttributeSyntaxContextExtensions.cs new file mode 100644 index 0000000..f9fdcb5 --- /dev/null +++ b/src/AttributeSourceGenerator/Common/GeneratorAttributeSyntaxContextExtensions.cs @@ -0,0 +1,120 @@ +using AttributeSourceGenerator.Models; +using Microsoft.CodeAnalysis; + +// ReSharper disable CheckNamespace + +namespace AttributeSourceGenerator.Common; + +/// Provides extension methods for working with GeneratorAttributeSyntaxContext. +internal static class GeneratorAttributeSyntaxContextExtensions +{ + /// Gets the marker attribute data for the given context. + /// The to get the marker attribute for. + /// The representing the marker attribute. + public static MarkerAttributeData GetMarkerAttribute(this GeneratorAttributeSyntaxContext context) + { + var attribute = context.Attributes.First(); + + var attributeClass = attribute.AttributeClass; + if (attributeClass is null) + throw new InvalidOperationException( + $"{nameof(AttributeIncrementalGeneratorBase)} unexpectedly found that {nameof(AttributeData.AttributeClass)} was null while transforming a {nameof(GeneratorAttributeSyntaxContext.TargetSymbol)}."); + + var attributeName = attributeClass.Name; + var genericTypeArguments = attribute.GetGenericTypeArguments(); + var constructorArguments = attribute.GetConstructorArguments(); + var namedArguments = attribute.GetNamedArguments(); + var markerAttributeData = new MarkerAttributeData(attributeName, genericTypeArguments, constructorArguments, namedArguments); + + return markerAttributeData; + } + + + /// Gets a list of generic type arguments for the given attribute. + /// The to get the generic type arguments for. + /// A list of representing the generic type arguments. + private static EquatableReadOnlyList GetGenericTypeArguments(this AttributeData attribute) + { + var attributeClass = attribute.AttributeClass; + if (attributeClass is null) + throw new InvalidOperationException( + $"{nameof(AttributeIncrementalGeneratorBase)} unexpectedly found that {nameof(AttributeData.AttributeClass)} was null while transforming a {nameof(GeneratorAttributeSyntaxContext.TargetSymbol)}."); + + if (!attributeClass.IsGenericType) + return EquatableReadOnlyList.Empty; + + var genericTypeArguments = new List(); + + // ReSharper disable once ForCanBeConvertedToForeach + // ReSharper disable once LoopCanBeConvertedToQuery + for (var index = 0; index < attributeClass.TypeParameters.Length; index++) + { + var typeParameterSymbol = attributeClass.TypeParameters[index]; + var name = typeParameterSymbol.Name; + var value = attributeClass.TypeArguments[index].ToDisplayString(); + var genericTypeArgument = new GenericTypeArgument(name, value); + + genericTypeArguments.Add(genericTypeArgument); + } + + return genericTypeArguments.ToEquatableReadOnlyList(); + } + + /// Gets a list of constructor arguments for the given attribute. + /// The to get the constructor arguments for. + /// A list of representing the constructor arguments. + private static EquatableReadOnlyList GetConstructorArguments(this AttributeData attribute) + { + var attributeConstructor = attribute.AttributeConstructor; + if (attributeConstructor is null) + throw new InvalidOperationException( + $"{nameof(AttributeIncrementalGeneratorBase)} unexpectedly found that {nameof(AttributeData.AttributeConstructor)} was null while transforming a {nameof(GeneratorAttributeSyntaxContext.TargetSymbol)}."); + + if (attributeConstructor.Parameters.Length <= 0) + return EquatableReadOnlyList.Empty; + + var constructorArguments = new List(); + + // ReSharper disable once ForCanBeConvertedToForeach + // ReSharper disable once LoopCanBeConvertedToQuery + for (var index = 0; index < attributeConstructor.Parameters.Length; index++) + { + var constructorParameterSymbol = attributeConstructor.Parameters[index]; + var constructorArgumentSymbol = attribute.ConstructorArguments[index]; + var type = constructorParameterSymbol.Type.ToDisplayString(); + var name = constructorParameterSymbol.Name; + var value = constructorArgumentSymbol.Value?.ToString(); + var constructorArgument = new ConstructorArgument(type, name, value); + + constructorArguments.Add(constructorArgument); + } + + return constructorArguments.ToEquatableReadOnlyList(); + } + + /// Gets a list of named arguments for the given attribute. + /// The to get the named arguments for. + /// A list of representing the named arguments. + private static EquatableReadOnlyList GetNamedArguments(this AttributeData attribute) + { + if (attribute.NamedArguments.Length <= 0) + return EquatableReadOnlyList.Empty; + + var namedArguments = new List(); + + // ReSharper disable once ForCanBeConvertedToForeach + // ReSharper disable once LoopCanBeConvertedToQuery + for (var index = 0; index < attribute.NamedArguments.Length; index++) + { + var namedArgumentSymbol = attribute.NamedArguments[index].Value; + var type = namedArgumentSymbol.Type?.ToDisplayString() ?? ""; + var name = attribute.NamedArguments[index].Key; + var value = namedArgumentSymbol.Value?.ToString(); + var namedArgument = new NamedArgument(type, name, value); + + namedArguments.Add(namedArgument); + } + + return namedArguments.ToEquatableReadOnlyList(); + } +} \ No newline at end of file diff --git a/src/AttributeSourceGenerator/Common/MethodSymbolExtensions.cs b/src/AttributeSourceGenerator/Common/MethodSymbolExtensions.cs new file mode 100644 index 0000000..23bc87a --- /dev/null +++ b/src/AttributeSourceGenerator/Common/MethodSymbolExtensions.cs @@ -0,0 +1,56 @@ +using AttributeSourceGenerator.Models; +using Microsoft.CodeAnalysis; + +// ReSharper disable CheckNamespace + +namespace AttributeSourceGenerator.Common; + +/// Provides extension methods for working with method symbols. +internal static class MethodSymbolExtensions +{ + /// Gets the generic type parameters for the given method symbol. + /// The to get the type parameters for. + /// An of strings representing the generic type parameter names. + public static EquatableReadOnlyList GetGenericTypeParameters(this IMethodSymbol symbol) + { + if (!symbol.IsGenericMethod) + return EquatableReadOnlyList.Empty; + + var genericTypeParameters = new List(); + + // ReSharper disable once ForCanBeConvertedToForeach + // ReSharper disable once LoopCanBeConvertedToQuery + for (var index = 0; index < symbol.TypeParameters.Length; index++) + { + var typeParameter = symbol.TypeParameters[index]; + genericTypeParameters.Add(typeParameter.Name); + } + + return genericTypeParameters.ToEquatableReadOnlyList(); + } + + /// Gets a list of constructor parameters for the given method symbol. + /// The to get the type parameters for. + /// An of strings representing the constructor parameters. + public static EquatableReadOnlyList GetConstructorParameters(this IMethodSymbol symbol) + { + if (symbol.Parameters.Length <= 0) + return EquatableReadOnlyList.Empty; + + var constructorParameters = new List(); + + // ReSharper disable once ForCanBeConvertedToForeach + // ReSharper disable once LoopCanBeConvertedToQuery + for (var index = 0; index < symbol.Parameters.Length; index++) + { + var constructorParameterSymbol = symbol.Parameters[index]; + var type = constructorParameterSymbol.Type.ToDisplayString(); + var name = constructorParameterSymbol.Name; + var constructorParameter = new ConstructorParameter(type, name); + + constructorParameters.Add(constructorParameter); + } + + return constructorParameters.ToEquatableReadOnlyList(); + } +} \ No newline at end of file diff --git a/src/AttributeSourceGenerator/Common/NamedSymbolExtensions.cs b/src/AttributeSourceGenerator/Common/NamedSymbolExtensions.cs new file mode 100644 index 0000000..a711ee8 --- /dev/null +++ b/src/AttributeSourceGenerator/Common/NamedSymbolExtensions.cs @@ -0,0 +1,30 @@ +using Microsoft.CodeAnalysis; + +// ReSharper disable CheckNamespace + +namespace AttributeSourceGenerator.Common; + +/// Provides extension methods for working with named symbols. +internal static class NamedSymbolExtensions +{ + /// Gets the generic type parameters for the given named type symbol. + /// The to get the type parameters for. + /// An of strings representing the generic type parameter names. + public static EquatableReadOnlyList GetGenericTypeParameters(this INamedTypeSymbol symbol) + { + if (!symbol.IsGenericType) + return EquatableReadOnlyList.Empty; + + var genericTypeParameters = new List(); + + // ReSharper disable once ForCanBeConvertedToForeach + // ReSharper disable once LoopCanBeConvertedToQuery + for (var index = 0; index < symbol.TypeParameters.Length; index++) + { + var typeParameter = symbol.TypeParameters[index]; + genericTypeParameters.Add(typeParameter.Name); + } + + return genericTypeParameters.ToEquatableReadOnlyList(); + } +} \ No newline at end of file diff --git a/src/AttributeSourceGenerator/Common/SymbolExtensions.cs b/src/AttributeSourceGenerator/Common/SymbolExtensions.cs new file mode 100644 index 0000000..e441f9b --- /dev/null +++ b/src/AttributeSourceGenerator/Common/SymbolExtensions.cs @@ -0,0 +1,98 @@ +using AttributeSourceGenerator.Models; +using Microsoft.CodeAnalysis; + +// ReSharper disable CheckNamespace + +namespace AttributeSourceGenerator.Common; + +/// Provides extension methods for working with symbols. +internal static class SymbolExtensions +{ + /// Gets the for the given based on its type kind. + /// The to get the for. + /// A if the symbol can be mapped to a symbol type, otherwise null. + public static SymbolType GetSymbolType(this ISymbol symbol) + { + switch (symbol) + { + case ITypeSymbol { IsReferenceType: true } typeSymbol: + { + if (typeSymbol.TypeKind == TypeKind.Interface) + return SymbolType.Interface; + if (typeSymbol.IsRecord) + return SymbolType.Record; + return SymbolType.Class; + } + case ITypeSymbol { IsValueType: true } typeSymbol: + { + if (typeSymbol.IsRecord) + return SymbolType.RecordStruct; + return SymbolType.Struct; + } + case IMethodSymbol: + return SymbolType.Method; + default: + throw new InvalidOperationException($"{nameof(AttributeIncrementalGeneratorBase)} unexpectedly received an {nameof(ISymbol)} that was unsupported."); + } + } + + /// Gets a list of declarations representing the hierarchy containing the given symbol. + /// The to get the containing declarations for. + /// An of objects representing the hierarchy. + public static EquatableReadOnlyList GetContainingDeclarations(this ISymbol symbol) + { + var declarations = new Stack(); + + BuildContainingSymbolHierarchy(symbol, in declarations); + + return declarations.ToEquatableReadOnlyList(); + } + + /// Builds the hierarchy of containing symbols starting from the given symbol. + /// The to start building the hierarchy from. + /// A of objects to store the hierarchy. + private static void BuildContainingSymbolHierarchy(ISymbol symbol, in Stack declarations) + { + switch (symbol.ContainingSymbol) + { + case INamespaceSymbol namespaceSymbol: + BuildNamespaceHierarchy(namespaceSymbol, declarations); + break; + case INamedTypeSymbol namedTypeSymbol: + BuildTypeHierarchy(namedTypeSymbol, in declarations); + break; + } + } + + /// Builds the hierarchy of containing namespaces starting from the given namespace symbol. + /// The to start building the hierarchy from. + /// A of objects to store the hierarchy. + private static void BuildNamespaceHierarchy(INamespaceSymbol symbol, in Stack declarations) + { + if (!symbol.IsGlobalNamespace) + { + var namespaceDeclaration = new Declaration(DeclarationType.Namespace, symbol.Name, EquatableReadOnlyList.Empty); + declarations.Push(namespaceDeclaration); + } + + if (symbol.ContainingNamespace is not null && !symbol.ContainingNamespace.IsGlobalNamespace) + BuildNamespaceHierarchy(symbol.ContainingNamespace, declarations); + } + + /// Builds the hierarchy of containing types starting from the given type symbol. + /// The to start building the hierarchy from. + /// A of objects to store the hierarchy. + private static void BuildTypeHierarchy(INamedTypeSymbol symbol, in Stack declarations) + { + var declarationType = symbol.GetDeclarationType(); + if (declarationType is null) + return; + + var genericTypeParameters = symbol.GetGenericTypeParameters(); + + var typeDeclaration = new Declaration(declarationType.Value, symbol.Name, genericTypeParameters); + declarations.Push(typeDeclaration); + + BuildContainingSymbolHierarchy(symbol, declarations); + } +} \ No newline at end of file diff --git a/src/AttributeSourceGenerator/Common/TypeSymbolExtensions.cs b/src/AttributeSourceGenerator/Common/TypeSymbolExtensions.cs new file mode 100644 index 0000000..e14b6ae --- /dev/null +++ b/src/AttributeSourceGenerator/Common/TypeSymbolExtensions.cs @@ -0,0 +1,33 @@ +using Microsoft.CodeAnalysis; + +// ReSharper disable CheckNamespace + +namespace AttributeSourceGenerator.Common; + +/// Provides extension methods for working with type symbols. +internal static class TypeSymbolExtensions +{ + /// Gets the for the given based on its type kind. + /// The to get the for. + /// A if the symbol can be mapped to a declaration type, otherwise null. + public static DeclarationType? GetDeclarationType(this ITypeSymbol symbol) + { + if (symbol.IsReferenceType) + { + if (symbol.TypeKind == TypeKind.Interface) + return DeclarationType.Interface; + if (symbol.IsRecord) + return DeclarationType.Record; + return DeclarationType.Class; + } + + if (symbol.IsValueType) + { + if (symbol.IsRecord) + return DeclarationType.RecordStruct; + return DeclarationType.Struct; + } + + return null; + } +} diff --git a/src/AttributeSourceGenerator/DeclarationType.cs b/src/AttributeSourceGenerator/DeclarationType.cs index b5c00d5..d9462a7 100644 --- a/src/AttributeSourceGenerator/DeclarationType.cs +++ b/src/AttributeSourceGenerator/DeclarationType.cs @@ -1,4 +1,6 @@ -namespace AttributeSourceGenerator; +// ReSharper disable CheckNamespace + +namespace AttributeSourceGenerator; /// Specifies the kind of declaration. public enum DeclarationType @@ -20,4 +22,4 @@ public enum DeclarationType /// Represents a record struct declaration. RecordStruct = 5 -} +} \ No newline at end of file diff --git a/src/AttributeSourceGenerator/FilterType.cs b/src/AttributeSourceGenerator/FilterType.cs index 35b037b..78d5f3e 100644 --- a/src/AttributeSourceGenerator/FilterType.cs +++ b/src/AttributeSourceGenerator/FilterType.cs @@ -1,26 +1,32 @@ -namespace AttributeSourceGenerator; +// ReSharper disable CheckNamespace + +namespace AttributeSourceGenerator; /// Specifies the kind of filter. +[Flags] public enum FilterType { /// Do not filter. None = 0, - /// Filter for interfaces only. + /// Only filter for attributes that are attached to interfaces. Interface = 1, - /// Filter for classes only. + /// Only filter for attributes that are attached to classes. Class = 2, - /// Filter for records only. - Record = 3, + /// Only filter for attributes that are attached to records. + Record = 4, - /// Filter for structs only. - Struct = 4, + /// Only filter for attributes that are attached to structs. + Struct = 8, - /// Filter for record structs only. - RecordStruct = 5, + /// Only filter for attributes that are attached to record structs. + RecordStruct = 16, - /// Filter for methods only. - Method = 6 + /// Only filter for attributes that are attached to methods. + Method = 32, + + /// Filter for all supported attributes. + All = Interface | Class | Record | Struct | RecordStruct | Method } diff --git a/src/AttributeSourceGenerator/Models/ConstructorArgument.cs b/src/AttributeSourceGenerator/Models/ConstructorArgument.cs new file mode 100644 index 0000000..b1903f5 --- /dev/null +++ b/src/AttributeSourceGenerator/Models/ConstructorArgument.cs @@ -0,0 +1,27 @@ +// ReSharper disable CheckNamespace + +namespace AttributeSourceGenerator.Models; + +/// Represents a constructor argument. +public readonly record struct ConstructorArgument +{ + /// Gets the type of the argument. + public string Type { get; } + + /// Gets the name of the argument. + public string Name { get; } + + /// Gets the value of the argument. + public string? Value { get; } + + /// Initializes a new instance of the record with the specified type, name, and value. + /// The type of the argument. + /// The name of the argument. + /// The value of the argument. + internal ConstructorArgument(string type, string name, string? value) + { + Type = type; + Name = name; + Value = value; + } +} \ No newline at end of file diff --git a/src/AttributeSourceGenerator/Models/ConstructorParameter.cs b/src/AttributeSourceGenerator/Models/ConstructorParameter.cs new file mode 100644 index 0000000..e6bc3b6 --- /dev/null +++ b/src/AttributeSourceGenerator/Models/ConstructorParameter.cs @@ -0,0 +1,22 @@ +// ReSharper disable CheckNamespace + +namespace AttributeSourceGenerator.Models; + +/// Represents a constructor parameter +public readonly record struct ConstructorParameter +{ + /// Gets the type of the parameter. + public string Type { get; } + + /// Gets the name of the parameter. + public string Name { get; } + + /// Initializes a new instance of the record with the specified type and name. + /// The type of the argument. + /// The name of the argument. + internal ConstructorParameter(string type, string name) + { + Type = type; + Name = name; + } +} \ No newline at end of file diff --git a/src/AttributeSourceGenerator/Declaration.cs b/src/AttributeSourceGenerator/Models/Declaration.cs similarity index 94% rename from src/AttributeSourceGenerator/Declaration.cs rename to src/AttributeSourceGenerator/Models/Declaration.cs index b954a5c..23ffa13 100644 --- a/src/AttributeSourceGenerator/Declaration.cs +++ b/src/AttributeSourceGenerator/Models/Declaration.cs @@ -1,8 +1,10 @@ using AttributeSourceGenerator.Common; -namespace AttributeSourceGenerator; +// ReSharper disable CheckNamespace -/// Represents a declaration with information about its type, name, and generic parameters. +namespace AttributeSourceGenerator.Models; + +/// Represents a declaration. public readonly record struct Declaration { /// Gets the type of declaration. @@ -56,4 +58,4 @@ public void Deconstruct(out DeclarationType declarationType, out string name, ou name = Name; genericParameters = GenericParameters; } -} +} \ No newline at end of file diff --git a/src/AttributeSourceGenerator/Models/GenericTypeArgument.cs b/src/AttributeSourceGenerator/Models/GenericTypeArgument.cs new file mode 100644 index 0000000..6a50892 --- /dev/null +++ b/src/AttributeSourceGenerator/Models/GenericTypeArgument.cs @@ -0,0 +1,22 @@ +// ReSharper disable CheckNamespace + +namespace AttributeSourceGenerator.Models; + +/// Represents a generic type argument. +public readonly record struct GenericTypeArgument +{ + /// Gets the name of the argument. + public string Name { get; } + + /// Gets the value of the argument. + public string? Value { get; } + + /// Initializes a new instance of the record with the specified name and value. + /// The name of the argument. + /// The value of the argument. + internal GenericTypeArgument(string name, string? value) + { + Name = name; + Value = value; + } +} \ No newline at end of file diff --git a/src/AttributeSourceGenerator/Models/MarkerAttributeData.cs b/src/AttributeSourceGenerator/Models/MarkerAttributeData.cs new file mode 100644 index 0000000..73440f8 --- /dev/null +++ b/src/AttributeSourceGenerator/Models/MarkerAttributeData.cs @@ -0,0 +1,47 @@ +using AttributeSourceGenerator.Common; + +// ReSharper disable CheckNamespace + +namespace AttributeSourceGenerator.Models; + +/// Represents a marker attribute. +public readonly record struct MarkerAttributeData +{ + /// Gets the name of the attribute. + public string Name { get; } + + /// Gets a read-only list of the generic type arguments for the attribute, or an empty list otherwise. + public EquatableReadOnlyList GenericTypeArguments { get; } + + /// Gets a read-only list of the constructor arguments for the attribute, or an empty list otherwise. + public EquatableReadOnlyList ConstructorArguments { get; } + + /// Gets a read-only list of the named arguments for the attribute, or an empty list otherwise. + public EquatableReadOnlyList NamedArguments { get; } + + /// Initializes a new instance of the record with the specified name, generic type arguments, constructor arguments, and named arguments. + /// The name of the attribute. + /// The list of generic type arguments for the attribute. + /// The list of constructor arguments for the attribute. + /// The list of named arguments for the attribute. + internal MarkerAttributeData(string name, EquatableReadOnlyList genericTypeArguments, EquatableReadOnlyList constructorArguments, EquatableReadOnlyList namedArguments) + { + Name = name; + GenericTypeArguments = genericTypeArguments; + ConstructorArguments = constructorArguments; + NamedArguments = namedArguments; + } + + /// Deconstructs the attribute into its constituent parts. + /// Receives the name of the attribute. + /// The list of generic type arguments for the attribute. + /// The list of constructor arguments for the attribute. + /// The list of named arguments for the attribute. + public void Deconstruct(out string name, out EquatableReadOnlyList genericTypeArguments, out EquatableReadOnlyList constructorArguments, out EquatableReadOnlyList namedArguments) + { + name = Name; + genericTypeArguments = GenericTypeArguments; + constructorArguments = ConstructorArguments; + namedArguments = NamedArguments; + } +} \ No newline at end of file diff --git a/src/AttributeSourceGenerator/Models/NamedArgument.cs b/src/AttributeSourceGenerator/Models/NamedArgument.cs new file mode 100644 index 0000000..f7f6d58 --- /dev/null +++ b/src/AttributeSourceGenerator/Models/NamedArgument.cs @@ -0,0 +1,27 @@ +// ReSharper disable CheckNamespace + +namespace AttributeSourceGenerator.Models; + +/// Represents a named argument. +public readonly record struct NamedArgument +{ + /// Gets the type of the argument. + public string Type { get; } + + /// Gets the name of the argument. + public string Name { get; } + + /// Gets the value of the argument. + public string? Value { get; } + + /// Initializes a new instance of the record with the specified type, name, and value. + /// The type of the argument. + /// The name of the argument. + /// The value of the argument. + internal NamedArgument(string type, string name, string? value) + { + Type = type; + Name = name; + Value = value; + } +} \ No newline at end of file diff --git a/src/AttributeSourceGenerator/Source.cs b/src/AttributeSourceGenerator/Source.cs deleted file mode 100644 index 6dc66f2..0000000 --- a/src/AttributeSourceGenerator/Source.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace AttributeSourceGenerator; - -/// Represents source. -public readonly record struct Source -{ - /// Gets the name of the source. - /// '.g.cs' will be appended to this name. - /// - public string Name { get; } - - /// Gets the text of the source. - public string Text { get; } - - /// Initializes a new instance of the record with the specified name and text. - /// The name of the source. - /// The text of the source. - public Source(string name, string text) - { - Name = name; - Text = text; - } -} diff --git a/src/AttributeSourceGenerator/Symbol.cs b/src/AttributeSourceGenerator/Symbol.cs index 9b3b283..748d0f1 100644 --- a/src/AttributeSourceGenerator/Symbol.cs +++ b/src/AttributeSourceGenerator/Symbol.cs @@ -1,46 +1,101 @@ using AttributeSourceGenerator.Common; +using AttributeSourceGenerator.Models; + +// ReSharper disable CheckNamespace namespace AttributeSourceGenerator; -/// Represents a symbol in a codebase. +/// Represents a symbol. public readonly record struct Symbol { + /// Gets the marker attribute attached to this symbol. + public MarkerAttributeData MarkerAttribute { get; } + + /// Gets a read-only list of the declarations that contain this symbol. + /// The list is populated in order, from the outer declaration furthest away to the symbol towards the inner declaration closest to the symbol. + /// + public EquatableReadOnlyList ContainingDeclarations { get; init; } + + /// Gets the type of symbol. + public SymbolType SymbolType { get; } + /// Gets the name of the symbol. public string Name { get; } + /// Gets a read-only list of generic parameters for generic symbols, or an empty list otherwise. + public EquatableReadOnlyList GenericTypeParameters { get; } + + /// Gets a read-only list of constructor parameters for method symbols, or an empty list otherwise. + public EquatableReadOnlyList ConstructorParameters { get; } + + /// Gets the return type for method symbols, or an empty string otherwise. + public string ReturnType { get; } + /// Gets the full name of the symbol. - public string FullName + public string FullyQualifiedName { get { + var name = GenericTypeParameters.Count <= 0 ? Name : $"{Name}`{GenericTypeParameters.Count}"; + if (ContainingDeclarations.Count <= 0) - return Name; + return name; - var names = ContainingDeclarations.Select(declaration => declaration.Name).ToArray(); - return $"{string.Join(".", names)}.{Name}"; + var path = ContainingDeclarations.ToFullyQualifiedName(); + return $"{path}.{name}"; } } - /// Gets a read-only list of the declarations that contain this symbol. - /// The list is populated in order, from the outer declaration furthest away to the symbol towards the inner declaration closest to the symbol. - /// - public EquatableReadOnlyList ContainingDeclarations { get; init; } + /// Gets the namespace of the symbol. + public string Namespace + { + get + { + if (ContainingDeclarations.Count <= 0) + return ""; + + var ns = ContainingDeclarations.ToNamespace(); + return ns; + } + } - /// Initializes a new instance of the record with the specified name and containing declarations. + /// Initializes a new instance of the record with the specified marker attribute, containing declarations, symbol type, name, and generic parameters. + /// The marker attribute attached to this symbol. + /// The read-only list of the declarations that contain this symbol. + /// The type of symbol. /// The name of the symbol. - /// The declarations that contain this symbol. - internal Symbol(string name, EquatableReadOnlyList containingDeclarations) + /// The list of the generic parameters for the symbol. + /// The list of the constructor parameters for the symbol. + /// The return type for the symbol. + internal Symbol(MarkerAttributeData markerAttribute, EquatableReadOnlyList containingDeclarations, SymbolType symbolType, string name, EquatableReadOnlyList genericTypeParameters, + EquatableReadOnlyList constructorParameters, string returnType) { - Name = name; + MarkerAttribute = markerAttribute; ContainingDeclarations = containingDeclarations; + SymbolType = symbolType; + Name = name; + GenericTypeParameters = genericTypeParameters; + ConstructorParameters = constructorParameters; + ReturnType = returnType; } /// Deconstructs the symbol into its constituent parts. + /// Receives the marker attribute attached to this symbol. + /// Receives the read-only list of the declarations that contain this symbol. + /// The type of symbol. /// Receives the name of the symbol. - /// Receives the read-only list of the containing declarations. - public void Deconstruct(out string name, out EquatableReadOnlyList containingDeclarations) + /// Receives the read-only list of the generic parameters for the symbol. + /// Receives the read-only list of the constructor parameters for the symbol. + /// Receives the return type for the symbol. + public void Deconstruct(out MarkerAttributeData markerAttribute, out EquatableReadOnlyList containingDeclarations, out SymbolType symbolType, out string name, out EquatableReadOnlyList genericParameters, + out EquatableReadOnlyList constructorParameters, out string returnType) { + markerAttribute = MarkerAttribute; containingDeclarations = ContainingDeclarations; + symbolType = SymbolType; name = Name; + genericParameters = GenericTypeParameters; + constructorParameters = ConstructorParameters; + returnType = ReturnType; } -} +} \ No newline at end of file diff --git a/src/AttributeSourceGenerator/SymbolType.cs b/src/AttributeSourceGenerator/SymbolType.cs new file mode 100644 index 0000000..1a5b5eb --- /dev/null +++ b/src/AttributeSourceGenerator/SymbolType.cs @@ -0,0 +1,25 @@ +// ReSharper disable CheckNamespace + +namespace AttributeSourceGenerator; + +/// Specifies the type of symbol. +public enum SymbolType +{ + /// Represents an interface symbol. + Interface = 0, + + /// Represents a class symbol. + Class = 1, + + /// Represents a record symbol. + Record = 2, + + /// Represents a struct symbol. + Struct = 3, + + /// Represents a record struct symbol. + RecordStruct = 4, + + /// Represents a method symbol. + Method = 5 +} \ No newline at end of file