From 1d2530f1c8477a99e6d331fdd069fc41ba276396 Mon Sep 17 00:00:00 2001 From: Beakona Date: Thu, 14 Mar 2024 13:47:43 +0100 Subject: [PATCH] 1. method parameter attributes was not generated 2. primary constructor properties --- AutoInterfaceSample/Program.cs | 163 ++++++---------- .../AutoInterfaceTemplateAttribute.cs | 9 +- .../AutoInterfaceRecord.cs | 33 +--- .../AutoInterfaceSourceGenerator.cs | 181 +++++++++++++----- .../BeaKona.AutoInterfaceGenerator.csproj | 6 +- .../CSharpCodeTextWriter.cs | 109 ++++++++--- .../ComparerBySignature.cs | 9 +- .../ICodeTextWriter.cs | 2 + BeaKona.AutoInterfaceGenerator/ScopeInfo.cs | 2 +- .../SourceBuilder.cs | 18 +- .../Templates/PartialTemplate.cs | 15 +- .../Templates/TemplateDefinition.cs | 12 +- .../Templates/TemplatedSourceGenerator.cs | 31 ++- 13 files changed, 314 insertions(+), 276 deletions(-) diff --git a/AutoInterfaceSample/Program.cs b/AutoInterfaceSample/Program.cs index 081c39b..31fdde1 100644 --- a/AutoInterfaceSample/Program.cs +++ b/AutoInterfaceSample/Program.cs @@ -1,139 +1,88 @@ using System; +using System.Diagnostics.CodeAnalysis; namespace AutoInterfaceSample.Test { - public class Program - { - public static void Main() - { - //System.Diagnostics.Debug.WriteLine(BeaKona.Output.Debug_TestClass_1.Info); - //IArbitrary p = new Person(); - //int f; - //int g = 1; - //p.Method(1, out f, ref g, "t", 1, 2, 3); - - IPrintable p = new MyPrinter(); - //var c = p.Count; - //Count = 3; - p.Print(); - //p.PrintComplex(); - } - } - - public interface IPrintable - { - public void Print(); - } - - public class SimplePrinter : IPrintable - { - public void Print() { Console.WriteLine("OK"); } - } - - public partial class MyPrinter : IPrintable + public partial record TestRecord([property: BeaKona.AutoInterface] ILogger Logger) { - [BeaKona.AutoInterface] - private readonly IPrintable _simplePrinter = new SimplePrinter(); - } - - public partial class Person2 /*: IPrintableComplex*/ - { - //[BeaKona.AutoInterface(typeof(IPrintableComplex), AllowMissingMembers = true, MemberMatch = BeaKona.MemberMatchTypes.Public)] - //private readonly SimplePrinter aspect1 = new SimplePrinter(); - - //[BeaKona.AutoInterface(typeof(IPrintableComplex), AllowMissingMembers = true, MemberMatch = BeaKona.MemberMatchTypes.Explicit)] - //private readonly SimplePrinter aspect2 = new SimplePrinter(); - - //[BeaKona.AutoInterface(typeof(IPrintableComplex), AllowMissingMembers = true)] - //private readonly SimplePrinter aspect3 = new SimplePrinter(); + //[field: BeaKona.AutoInterface] + //public ILogger Logger { get; } - //[BeaKona.AutoInterface(typeof(IPrintableComplex), AllowMissingMembers = true, MemberMatch = BeaKona.MemberMatchTypes.Any)] - //private readonly SimplePrinter aspect4 = new SimplePrinter(); - - //void IPrintableComplex.PrintComplex() => Console.WriteLine("OKC2"); - - public void PrintComplex() { Console.WriteLine("Oh, K."); } - //void IPrintableComplex.PrintComplex() { Console.WriteLine("Oh, K."); } - } - - - public class SignalPlotXYConst where TX : struct, IComparable - { + //[BeaKona.AutoInterface] + //private readonly ILogger logger2 = new SimpleLogger(); } - public interface ISome + public sealed class LogEventProperty { } - public interface ISome2 + [AttributeUsage(AttributeTargets.ReturnValue | AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.GenericParameter, AllowMultiple = true)] + public class TestAttribute : Attribute { } - public struct Heatmap + public sealed class SimpleLogger : ILogger { - } - - public class Colormap - { - } - - public interface IArbitrary - { - T Length { get; } - int? Method(int a, out int b, ref int c, dynamic d, params int[] p); - - SignalPlotXYConst? Method2(TXi x, TYi[] ys, in int s) where TXi : struct, IComparable where TYi : struct, IComparable, ISome, ISome2; - - Heatmap AddHeatmap(double?[,] intensities, Colormap? colormap = null, bool? lockScales = true); - Heatmap AddHeatmap(double[,] intensities, Colormap? colormap = null, bool? lockScales = null); - } - - public class PrinterV1 - { - public void Print() + public bool BindProperty<[Test] T>(string? propertyName, object? value, bool destructureObjects, [NotNullWhen(true)] out LogEventProperty? property, [Test] params int[] values) { + property = default; + return false; } - public int Length => 1; - - public int? Method(int a, out int b, ref int c, dynamic d, params int[] p) + [Test] + public int Count { - b = a; - return a; + [return: Test] + [Test] + get => 1; + [return: Test] + [Test] + set + { + } } - - public SignalPlotXYConst? Method2(TXc x, TYc[] ys, in int s) where TXc : struct, IComparable where TYc : struct, IComparable, ISome, ISome2 + public int Length { - return null; + get => 1; + set { } } + } - public Heatmap AddHeatmap(double?[,] intensities, Colormap? colormap = null, bool? lockScales = true) + public class Program + { + public static void Main() { - return new Heatmap(); - } + //System.Diagnostics.Debug.WriteLine(BeaKona.Output.Debug_TestClass_1.Info); - public Heatmap AddHeatmap(double[,] intensities, Colormap? colormap = null, bool? lockScales = null) - { - return new Heatmap(); + ILogger p = new TestRecord(new SimpleLogger()); + var result = p.BindProperty("test", 1, false, out var property, 1, 2, 3); } } - public partial class Person + public interface ILogger { - [BeaKona.AutoInterface(typeof(IArbitrary), PreferCoalesce = true)] - //[BeaKona.AutoInterface(typeof(ITestable))] - //[BeaKona.AutoInterface(typeof(IPrintable), true)] - //[BeaKona.AutoInterface(typeof(IPrintable), false)] - //[BeaKona.AutoInterface(typeof(IPrintable))]//, TemplateBody = "void TestB1() {}" - //[BeaKona.AutoInterface(typeof(IPrintable2))]//, TemplateBody = "void TestB2() {}" - //[BeaKona.AutoInterfaceTemplate(BeaKona.AutoInterfaceTargets.PropertyGetter, Filter = "Length", Language = "scriban", Body = "return 1;")] - //[BeaKona.AutoInterfaceTemplate(BeaKona.AutoInterfaceTargets.Method, Filter = "Print(\\d)?", Body = "LogDebug(nameof({{interface}}.{{name}})); {{expression}};")] - private readonly PrinterV1? aspect1 = new PrinterV1(); + [Test] + [return: Test] + [return: Test] + bool BindProperty<[Test] T>(string? propertyName, object? value, bool destructureObjects, [NotNullWhen(true)] out LogEventProperty? property, [Test] params int[] values); + + [Test] + int Count + { + [return: Test] + [Test] + get; + [return: Test] + [Test] + set; + } - //[BeaKona.AutoInterface(typeof(IPrintable), IncludeBaseInterfaces = true)] - //[BeaKona.AutoInterfaceTemplate(BeaKona.AutoInterfaceTargets.Method, Filter = "Print2", Body = "/* */")] - //[BeaKona.AutoInterfaceTemplate(BeaKona.AutoInterfaceTargets.Method, Filter = "Print2", Body = "/* */")] - //[BeaKona.AutoInterface(typeof(ITestable))] - //private readonly PrinterV1? aspect2 = new PrinterV1(); + int Length + { + [return: Test] + get; + [return: Test] + set; + } } } diff --git a/BeaKona.AutoInterfaceAttributes/AutoInterfaceTemplateAttribute.cs b/BeaKona.AutoInterfaceAttributes/AutoInterfaceTemplateAttribute.cs index 3805a66..fb153ad 100644 --- a/BeaKona.AutoInterfaceAttributes/AutoInterfaceTemplateAttribute.cs +++ b/BeaKona.AutoInterfaceAttributes/AutoInterfaceTemplateAttribute.cs @@ -5,14 +5,9 @@ namespace BeaKona; [Conditional("CodeGeneration")] [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] -public sealed class AutoInterfaceTemplateAttribute : Attribute +public sealed class AutoInterfaceTemplateAttribute(AutoInterfaceTargets targets) : Attribute { - public AutoInterfaceTemplateAttribute(AutoInterfaceTargets targets) - { - this.Targets = targets; - } - - public AutoInterfaceTargets Targets { get; } + public AutoInterfaceTargets Targets { get; } = targets; public string? Filter { get; set; } public string? Language { get; set; } public string? Body { get; set; } diff --git a/BeaKona.AutoInterfaceGenerator/AutoInterfaceRecord.cs b/BeaKona.AutoInterfaceGenerator/AutoInterfaceRecord.cs index 9cfb309..48e5b67 100644 --- a/BeaKona.AutoInterfaceGenerator/AutoInterfaceRecord.cs +++ b/BeaKona.AutoInterfaceGenerator/AutoInterfaceRecord.cs @@ -2,28 +2,15 @@ namespace BeaKona.AutoInterfaceGenerator; -internal sealed class AutoInterfaceRecord : IMemberInfo +internal sealed class AutoInterfaceRecord(ISymbol member, ITypeSymbol receiverType, INamedTypeSymbol interfaceType, TemplateDefinition? template, IEnumerable templateParts, bool bySignature, bool preferCoalesce, bool allowMissingMembers, MemberMatchTypes memberMatch) : IMemberInfo { - public AutoInterfaceRecord(ISymbol member, ITypeSymbol receiverType, INamedTypeSymbol interfaceType, TemplateDefinition? template, List templateParts, bool bySignature, bool preferCoalesce, bool allowMissingMembers, MemberMatchTypes memberMatch) - { - this.Member = member; - this.ReceiverType = receiverType; - this.InterfaceType = interfaceType; - this.Template = template; - this.TemplateParts = templateParts?.ToArray() ?? []; - this.BySignature = bySignature; - this.PreferCoalesce = preferCoalesce; - this.AllowMissingMembers = allowMissingMembers; - this.MemberMatch = memberMatch; - } - - public ISymbol Member { get; } - public ITypeSymbol ReceiverType { get; } - public INamedTypeSymbol InterfaceType { get; } - public TemplateDefinition? Template { get; } - public PartialTemplate[] TemplateParts { get; } - public bool BySignature { get; } - public bool PreferCoalesce { get; } - public bool AllowMissingMembers { get; } - public MemberMatchTypes MemberMatch { get; } + public ISymbol Member { get; } = member; + public ITypeSymbol ReceiverType { get; } = receiverType; + public INamedTypeSymbol InterfaceType { get; } = interfaceType; + public TemplateDefinition? Template { get; } = template; + public PartialTemplate[] TemplateParts { get; } = templateParts?.ToArray() ?? []; + public bool BySignature { get; } = bySignature; + public bool PreferCoalesce { get; } = preferCoalesce; + public bool AllowMissingMembers { get; } = allowMissingMembers; + public MemberMatchTypes MemberMatch { get; } = memberMatch; } diff --git a/BeaKona.AutoInterfaceGenerator/AutoInterfaceSourceGenerator.cs b/BeaKona.AutoInterfaceGenerator/AutoInterfaceSourceGenerator.cs index e41967f..7d99cfe 100644 --- a/BeaKona.AutoInterfaceGenerator/AutoInterfaceSourceGenerator.cs +++ b/BeaKona.AutoInterfaceGenerator/AutoInterfaceSourceGenerator.cs @@ -33,37 +33,61 @@ public void Execute(GeneratorExecutionContext context) // loop over the candidates, and keep the ones that are actually annotated List records = []; - foreach (MemberDeclarationSyntax candidate in receiver.Candidates) + HashSet fields = []; + HashSet properties = []; + + foreach (var fieldSyntax in receiver.Fields) { - SemanticModel model = compilation.GetSemanticModel(candidate.SyntaxTree); - if (candidate is FieldDeclarationSyntax fieldSyntax) + SemanticModel model = compilation.GetSemanticModel(fieldSyntax.SyntaxTree); + + foreach (VariableDeclaratorSyntax variableSyntax in fieldSyntax.Declaration.Variables) { - foreach (VariableDeclaratorSyntax variableSyntax in fieldSyntax.Declaration.Variables) + if (model.GetDeclaredSymbol(variableSyntax) is IFieldSymbol field) { - // get symbol being declared by the member, and keep it if its annotated - if (model.GetDeclaredSymbol(variableSyntax) is IFieldSymbol field) - { - records.AddRange(CollectRecords(context, field, field.Type, autoInterfaceAttributeSymbol, autoInterfaceTemplateAttributeSymbol)); - } + fields.Add(field); } } - else if (candidate is PropertyDeclarationSyntax propertySyntax) + } + + foreach (var propertySyntax in receiver.Properties) + { + SemanticModel model = compilation.GetSemanticModel(propertySyntax.SyntaxTree); + + if (model.GetDeclaredSymbol(propertySyntax) is IPropertySymbol property) { - // get symbol being declared by the member, and keep it if its annotated - if (model.GetDeclaredSymbol(propertySyntax) is IPropertySymbol property) - { - if (property.IsWriteOnly) - { - Helpers.ReportDiagnostic(context, "BKAG06", nameof(AutoInterfaceResource.AG06_title), nameof(AutoInterfaceResource.AG06_message), nameof(AutoInterfaceResource.AG06_description), DiagnosticSeverity.Error, property, - property.Name); - continue; - } + properties.Add(property); + } + } - records.AddRange(CollectRecords(context, property, property.Type, autoInterfaceAttributeSymbol, autoInterfaceTemplateAttributeSymbol)); + foreach (var parameterSyntax in receiver.Parameters) + { + SemanticModel model = compilation.GetSemanticModel(parameterSyntax.SyntaxTree); + if (model.GetDeclaredSymbol(parameterSyntax) is IParameterSymbol parameter) + { + if (parameter.ContainingType.GetMembers(parameter.Name).FirstOrDefault() is IPropertySymbol property) + { + properties.Add(property); } } } + foreach (var field in fields) + { + records.AddRange(GetRecords(context, field, field.Type, autoInterfaceAttributeSymbol, autoInterfaceTemplateAttributeSymbol)); + } + + foreach (var property in properties) + { + if (property.IsWriteOnly) + { + Helpers.ReportDiagnostic(context, "BKAG06", nameof(AutoInterfaceResource.AG06_title), nameof(AutoInterfaceResource.AG06_message), nameof(AutoInterfaceResource.AG06_description), DiagnosticSeverity.Error, property, + property.Name); + continue; + } + + records.AddRange(GetRecords(context, property, property.Type, autoInterfaceAttributeSymbol, autoInterfaceTemplateAttributeSymbol)); + } + // group elements by the containing class, and generate the source foreach (IGrouping recordsByContainingType in records.GroupBy(i => i.Member.ContainingType, SymbolEqualityComparer.Default)) { @@ -127,19 +151,18 @@ private static void GeneratePreview(GeneratorExecutionContext context, string na } #endif - private static List CollectRecords(GeneratorExecutionContext context, ISymbol symbol, ITypeSymbol receiverType, INamedTypeSymbol autoInterfaceAttributeSymbol, INamedTypeSymbol autoInterfaceTemplateAttributeSymbol) + private static List GetTemplateRecords(GeneratorExecutionContext context, ISymbol directSymbol, ITypeSymbol receiverType, INamedTypeSymbol autoInterfaceTemplateAttributeSymbol, Action registerInterface) { List templateParts = []; - var danglingInterfaceTypesBySymbols = new Dictionary>(SymbolEqualityComparer.Default); - foreach (AttributeData attribute in symbol.GetAttributes()) + foreach (AttributeData attribute in directSymbol.GetAttributes()) { if (attribute.AttributeClass != null && attribute.AttributeClass.Equals(autoInterfaceTemplateAttributeSymbol, SymbolEqualityComparer.Default)) { - ITypeSymbol? type = receiverType.WithNullableAnnotation(NullableAnnotation.NotAnnotated); - AutoInterfaceTargets memberTargets = (AutoInterfaceTargets)Convert.ToInt32(attribute.ConstructorArguments[0].Value); + ITypeSymbol? type = receiverType.WithNullableAnnotation(NullableAnnotation.NotAnnotated); + if (type.TypeKind == TypeKind.Interface) { if (type is INamedTypeSymbol interfaceType) @@ -219,12 +242,7 @@ private static List CollectRecords(GeneratorExecutionContex } templateParts.Add(new PartialTemplate(memberTargets, rxMemberFilter, new TemplateDefinition(templateLanguage ?? "scriban", templateBody.Trim()))); - if (danglingInterfaceTypesBySymbols.TryGetValue(symbol, out HashSet interfaceTypes) == false) - { - danglingInterfaceTypesBySymbols[symbol] = interfaceTypes = new HashSet(SymbolEqualityComparer.Default); - } - - interfaceTypes.Add(interfaceType); + registerInterface(directSymbol, interfaceType); } else if (templateFileName != null && templateFileName.Trim().Length > 0) { @@ -268,12 +286,7 @@ private static List CollectRecords(GeneratorExecutionContex } templateParts.Add(new PartialTemplate(memberTargets, rxMemberFilter, new TemplateDefinition(templateLanguage ?? "scriban", content))); - if (danglingInterfaceTypesBySymbols.TryGetValue(symbol, out HashSet interfaceTypes) == false) - { - danglingInterfaceTypesBySymbols[symbol] = interfaceTypes = new HashSet(SymbolEqualityComparer.Default); - } - - interfaceTypes.Add(interfaceType); + registerInterface(directSymbol, interfaceType); } else { @@ -305,9 +318,14 @@ private static List CollectRecords(GeneratorExecutionContex } } + return templateParts; + } + + private static List GetEmbeddedRecords(GeneratorExecutionContext context, ISymbol directSymbol, ITypeSymbol receiverType, INamedTypeSymbol autoInterfaceAttributeSymbol, IEnumerable templateParts, Action unregisterInterface) + { List records = []; - foreach (AttributeData attribute in symbol.GetAttributes()) + foreach (AttributeData attribute in directSymbol.GetAttributes()) { if (attribute.AttributeClass != null && attribute.AttributeClass.Equals(autoInterfaceAttributeSymbol, SymbolEqualityComparer.Default)) { @@ -447,7 +465,7 @@ private static List CollectRecords(GeneratorExecutionContex templateLanguage = extension; } - if (templateParts.Count > 0) + if (templateParts.Any()) { Helpers.ReportDiagnostic(context, "BKAG13", nameof(AutoInterfaceResource.AG13_title), nameof(AutoInterfaceResource.AG13_message), nameof(AutoInterfaceResource.AG13_description), DiagnosticSeverity.Error, attribute.ApplicationSyntaxReference?.GetSyntax() ); @@ -464,26 +482,26 @@ private static List CollectRecords(GeneratorExecutionContex if (IsImplementedDirectly(receiverType, type)) { - danglingInterfaceTypesBySymbols.Remove(symbol); - records.Add(new AutoInterfaceRecord(symbol, receiverType, interfaceType, template, templateParts, false, preferCoalesce.Value, allowMissingMembers.Value, memberMatch.Value)); + unregisterInterface(directSymbol); + records.Add(new AutoInterfaceRecord(directSymbol, receiverType, interfaceType, template, templateParts, false, preferCoalesce.Value, allowMissingMembers.Value, memberMatch.Value)); if (includeBaseInterfaces.Value) { foreach (INamedTypeSymbol baseInterfaceType in interfaceType.AllInterfaces) { - records.Add(new AutoInterfaceRecord(symbol, receiverType, baseInterfaceType, template, templateParts, false, preferCoalesce.Value, allowMissingMembers.Value, memberMatch.Value)); + records.Add(new AutoInterfaceRecord(directSymbol, receiverType, baseInterfaceType, template, templateParts, false, preferCoalesce.Value, allowMissingMembers.Value, memberMatch.Value)); } } } else if (IsDuckImplementation(receiverType, type, includeBaseInterfaces.Value, allowMissingMembers.Value)) { - danglingInterfaceTypesBySymbols.Remove(symbol); - records.Add(new AutoInterfaceRecord(symbol, receiverType, interfaceType, template, templateParts, true, preferCoalesce.Value, allowMissingMembers.Value, memberMatch.Value)); + unregisterInterface(directSymbol); + records.Add(new AutoInterfaceRecord(directSymbol, receiverType, interfaceType, template, templateParts, true, preferCoalesce.Value, allowMissingMembers.Value, memberMatch.Value)); if (includeBaseInterfaces.Value) { foreach (INamedTypeSymbol baseInterfaceType in interfaceType.AllInterfaces) { bool byType = receiverType.IsMatchByTypeOrImplementsInterface(baseInterfaceType); - records.Add(new AutoInterfaceRecord(symbol, receiverType, baseInterfaceType, template, templateParts, !byType, preferCoalesce.Value, allowMissingMembers.Value, memberMatch.Value)); + records.Add(new AutoInterfaceRecord(directSymbol, receiverType, baseInterfaceType, template, templateParts, !byType, preferCoalesce.Value, allowMissingMembers.Value, memberMatch.Value)); } } } @@ -510,6 +528,31 @@ private static List CollectRecords(GeneratorExecutionContex } } + return records; + } + + private static List GetRecords(GeneratorExecutionContext context, ISymbol directSymbol, ITypeSymbol receiverType, INamedTypeSymbol autoInterfaceAttributeSymbol, INamedTypeSymbol autoInterfaceTemplateAttributeSymbol) + { + var danglingInterfaceTypesBySymbols = new Dictionary>(SymbolEqualityComparer.Default); + void RegisterPossibleDanglingInterface(ISymbol symbol, INamedTypeSymbol interfaceType) + { + if (danglingInterfaceTypesBySymbols.TryGetValue(symbol, out HashSet interfaceTypes) == false) + { + danglingInterfaceTypesBySymbols[symbol] = interfaceTypes = new HashSet(SymbolEqualityComparer.Default); + } + + interfaceTypes.Add(interfaceType); + } + + void UnregisterDanglingInterface(ISymbol symbol) + { + danglingInterfaceTypesBySymbols.Remove(symbol); + } + + var templateParts = GetTemplateRecords(context, directSymbol, receiverType, autoInterfaceTemplateAttributeSymbol, RegisterPossibleDanglingInterface); + + var records = GetEmbeddedRecords(context, directSymbol, receiverType, autoInterfaceAttributeSymbol, templateParts, UnregisterDanglingInterface).ToList(); + foreach (KeyValuePair> danglingInterfaceTypes in danglingInterfaceTypesBySymbols) { foreach (INamedTypeSymbol interfaceType in danglingInterfaceTypes.Value) @@ -570,6 +613,7 @@ private static bool IsDuckImplementation(ITypeSymbol receiverType, ITypeSymbol i builder.AppendIndentation(); writer.WriteTypeDeclarationBeginning(builder, type, scope); + { bool first = true; foreach (INamedTypeSymbol missingInterfaceType in members.Select(i => i.InterfaceType).Where(i => type.AllInterfaces.Contains(i, SymbolEqualityComparer.Default) == false).ToHashSet()) @@ -581,6 +625,7 @@ private static bool IsDuckImplementation(ITypeSymbol receiverType, ITypeSymbol i } } builder.AppendLine(); + builder.AppendIndentation(); builder.AppendLine('{'); builder.IncrementIndentation(); @@ -801,24 +846,58 @@ EventModel CreateEvent(IEventSymbol @event) /// private sealed class SyntaxReceiver : ISyntaxReceiver { - public List Candidates { get; } = []; + public List Fields { get; } = []; + public List Properties { get; } = []; + public List Parameters { get; } = []; /// /// Called for every syntax node in the compilation, we can inspect the nodes and save any information useful for generation /// public void OnVisitSyntaxNode(SyntaxNode syntaxNode) { - if (syntaxNode is MemberDeclarationSyntax memberDeclarationSyntax && memberDeclarationSyntax.AttributeLists.Count > 0) + if (syntaxNode is MemberDeclarationSyntax memberDeclarationSyntax) { - if (memberDeclarationSyntax.Parent is TypeDeclarationSyntax) + // any field or property with at least one attribute is a candidate for source generation + if (memberDeclarationSyntax.AttributeLists.Count > 0 && memberDeclarationSyntax.Parent is TypeDeclarationSyntax) { - // any field or property with at least one attribute is a candidate for source generation - if (memberDeclarationSyntax is FieldDeclarationSyntax || memberDeclarationSyntax is PropertyDeclarationSyntax) + if (memberDeclarationSyntax is FieldDeclarationSyntax fieldSyntax) { - this.Candidates.Add(memberDeclarationSyntax); + this.Fields.Add(fieldSyntax); + } + else if (memberDeclarationSyntax is PropertyDeclarationSyntax propertySyntax) + { + this.Properties.Add(propertySyntax); } } } + else if (syntaxNode is AttributeTargetSpecifierSyntax attributeTargetSyntax) + { + //any primary constructor parameter that has attribute with property target + if (attributeTargetSyntax.Identifier.Text is string attributeTarget) + { + if (string.Equals(attributeTarget, "property", StringComparison.InvariantCulture)) + { + if (ResolveParameter(attributeTargetSyntax) is ParameterSyntax parameterSyntax) + { + if (ResolveTypeDeclaration(parameterSyntax) is RecordDeclarationSyntax) + { + this.Parameters.Add(parameterSyntax); + } + } + } + } + + } + } + + private static TypeDeclarationSyntax? ResolveTypeDeclaration(ParameterSyntax parameterSyntax) + { + return parameterSyntax?.Parent?.Parent as TypeDeclarationSyntax; + } + + private static ParameterSyntax? ResolveParameter(AttributeTargetSpecifierSyntax attributeTargetSpecifierSyntax) + { + return attributeTargetSpecifierSyntax?.Parent?.Parent as ParameterSyntax; } } } diff --git a/BeaKona.AutoInterfaceGenerator/BeaKona.AutoInterfaceGenerator.csproj b/BeaKona.AutoInterfaceGenerator/BeaKona.AutoInterfaceGenerator.csproj index a8cf1d8..21642d3 100644 --- a/BeaKona.AutoInterfaceGenerator/BeaKona.AutoInterfaceGenerator.csproj +++ b/BeaKona.AutoInterfaceGenerator/BeaKona.AutoInterfaceGenerator.csproj @@ -23,10 +23,10 @@ - - + + - + diff --git a/BeaKona.AutoInterfaceGenerator/CSharpCodeTextWriter.cs b/BeaKona.AutoInterfaceGenerator/CSharpCodeTextWriter.cs index 42bce74..8b0d25d 100644 --- a/BeaKona.AutoInterfaceGenerator/CSharpCodeTextWriter.cs +++ b/BeaKona.AutoInterfaceGenerator/CSharpCodeTextWriter.cs @@ -205,6 +205,15 @@ public void WriteTypeArgumentsDefinition(SourceBuilder builder, IEnumerable parameters) { bool first = true; @@ -219,6 +228,8 @@ public void WriteParameterDefinition(SourceBuilder builder, ScopeInfo scope, IEn builder.Append(", "); } + this.WriteParameterAttributes(builder, parameter); + if (parameter.IsParams) { builder.Append("params"); @@ -262,28 +273,67 @@ public void WriteCallParameters(SourceBuilder builder, IEnumerable attributes) { - if (this.obsoleteAttributeSymbol != null) + if (attributes != null) { - foreach (var attribute in this.GetForwardAttributes(member)) + foreach (var attribute in attributes) { builder.AppendIndentation(); - builder.Append('['); - builder.Append(attribute.ToString()); - builder.AppendLine(']'); + this.WriteAttribute(builder, attribute, true); + builder.AppendLine(); } } } - private IEnumerable GetForwardAttributes(ISymbol member) + private void WriteAttribute(SourceBuilder builder, AttributeData attribute, bool isReturn = false) + { + builder.Append('['); + if (isReturn) + { + builder.Append("return: "); + } + builder.Append(attribute.ToString()); + builder.Append(']'); + } + + private IEnumerable GetForwardAttributes(IEnumerable attributes) + { + if (this.obsoleteAttributeSymbol != null && attributes.FirstOrDefault(i => i.AttributeClass != null && i.AttributeClass.Equals(this.obsoleteAttributeSymbol, SymbolEqualityComparer.Default)) is AttributeData obsoleteAttribute) + { + yield return obsoleteAttribute; + } + } + + private bool HasAttributes(params ISymbol?[] members) { - if (this.obsoleteAttributeSymbol != null) + if (this.HasForwardAttributes(members)) { - return member.GetAttributes().Where(i => i.AttributeClass != null && i.AttributeClass.Equals(obsoleteAttributeSymbol, SymbolEqualityComparer.Default)); + return true; } - return new AttributeData[0]; + foreach (var member in members) + { + if (member is IMethodSymbol method) + { + if (method.GetReturnTypeAttributes().Any()) + { + return true; + } + } + } + + return false; } private bool HasForwardAttributes(params ISymbol?[] members) @@ -294,7 +344,7 @@ private bool HasForwardAttributes(params ISymbol?[] members) { if (member != null) { - if (GetForwardAttributes(member).Any()) + if (this.GetForwardAttributes(member.GetAttributes()).Any()) { return true; } @@ -309,7 +359,8 @@ public void WriteMethodDefinition(SourceBuilder builder, IMethodSymbol method, S { var methodScope = new ScopeInfo(scope); - this.WriteForwardedAttributes(builder, method); + this.WriteForwardAttributes(builder, method); + this.WriteReturnAttributes(builder, method.GetReturnTypeAttributes()); if (method.IsGenericMethod) { @@ -466,7 +517,8 @@ public void WritePropertyDefinition(SourceBuilder builder, IPropertySymbol prope PartialTemplate? getterTemplate = this.GetMatchedTemplates(references, getterTarget, property.IsIndexer ? "this" : property.Name); PartialTemplate? setterTemplate = this.GetMatchedTemplates(references, setterTarget, property.IsIndexer ? "this" : property.Name); - this.WriteForwardedAttributes(builder, property); + this.WriteForwardAttributes(builder, property); + this.WriteReturnAttributes(builder, property.Type.GetAttributes()); builder.AppendIndentation(); this.WriteTypeReference(builder, property.Type, scope); @@ -485,9 +537,9 @@ public void WritePropertyDefinition(SourceBuilder builder, IPropertySymbol prope this.WriteIdentifier(builder, property); } - bool noForwardedAttributes = this.HasForwardAttributes(property.GetMethod, property.SetMethod) == false; + bool noAttributes = this.HasAttributes(property.GetMethod, property.SetMethod) == false; - if (property.SetMethod == null && getterTemplate == null && noForwardedAttributes) + if (property.SetMethod == null && getterTemplate == null && noAttributes) { builder.Append(" => "); this.WritePropertyCall(builder, references.First(), property, scope, SemanticFacts.IsNullable(this.Compilation, property.Type), references.First().PreferCoalesce); @@ -501,13 +553,14 @@ public void WritePropertyDefinition(SourceBuilder builder, IPropertySymbol prope builder.IncrementIndentation(); try { - if (references.Count() == 1 && getterTemplate == null && setterTemplate == null && noForwardedAttributes) + if (references.Count() == 1 && getterTemplate == null && setterTemplate == null && noAttributes) { IMemberInfo reference = references.First(); if (property.GetMethod is not null) { - this.WriteForwardedAttributes(builder, property.GetMethod); + this.WriteForwardAttributes(builder, property.GetMethod); + this.WriteReturnAttributes(builder, property.GetMethod.GetReturnTypeAttributes()); builder.AppendIndentation(); builder.Append("get => "); @@ -516,7 +569,8 @@ public void WritePropertyDefinition(SourceBuilder builder, IPropertySymbol prope } if (property.SetMethod is not null) { - this.WriteForwardedAttributes(builder, property.SetMethod); + this.WriteForwardAttributes(builder, property.SetMethod); + this.WriteReturnAttributes(builder, property.SetMethod.GetReturnTypeAttributes()); builder.AppendIndentation(); builder.Append("set => "); @@ -528,7 +582,8 @@ public void WritePropertyDefinition(SourceBuilder builder, IPropertySymbol prope { if (property.GetMethod is not null) { - this.WriteForwardedAttributes(builder, property.GetMethod); + this.WriteForwardAttributes(builder, property.GetMethod); + this.WriteReturnAttributes(builder, property.GetMethod.GetReturnTypeAttributes()); builder.AppendIndentation(); if (getterTemplate != null) @@ -571,7 +626,8 @@ public void WritePropertyDefinition(SourceBuilder builder, IPropertySymbol prope } if (property.SetMethod is not null) { - this.WriteForwardedAttributes(builder, property.SetMethod); + this.WriteForwardAttributes(builder, property.SetMethod); + this.WriteReturnAttributes(builder, property.SetMethod.GetReturnTypeAttributes()); builder.AppendIndentation(); builder.AppendLine("set"); @@ -630,7 +686,8 @@ public void WriteEventDefinition(SourceBuilder builder, IEventSymbol @event, Sco PartialTemplate? adderTemplate = this.GetMatchedTemplates(references, AutoInterfaceTargets.EventAdder, @event.Name); PartialTemplate? removerTemplate = this.GetMatchedTemplates(references, AutoInterfaceTargets.EventRemover, @event.Name); - this.WriteForwardedAttributes(builder, @event); + this.WriteForwardAttributes(builder, @event); + this.WriteReturnAttributes(builder, @event.Type.GetAttributes()); builder.AppendIndentation(); builder.Append("event"); @@ -646,9 +703,9 @@ public void WriteEventDefinition(SourceBuilder builder, IEventSymbol @event, Sco builder.IncrementIndentation(); try { - bool noForwardedAttributes = this.HasForwardAttributes(@event.AddMethod, @event.RemoveMethod) == false; + bool noAttributes = this.HasAttributes(@event.AddMethod, @event.RemoveMethod) == false; - if (references.Count() == 1 && adderTemplate == null && removerTemplate == null && noForwardedAttributes) + if (references.Count() == 1 && adderTemplate == null && removerTemplate == null && noAttributes) { IMemberInfo reference = references.First(); builder.AppendIndentation(); @@ -668,7 +725,8 @@ public void WriteEventDefinition(SourceBuilder builder, IEventSymbol @event, Sco { if (@event.AddMethod != null) { - this.WriteForwardedAttributes(builder, @event.AddMethod); + this.WriteForwardAttributes(builder, @event.AddMethod); + this.WriteReturnAttributes(builder, @event.AddMethod.GetReturnTypeAttributes()); builder.AppendIndentation(); builder.AppendLine("add"); @@ -712,7 +770,8 @@ public void WriteEventDefinition(SourceBuilder builder, IEventSymbol @event, Sco } if (@event.RemoveMethod != null) { - this.WriteForwardedAttributes(builder, @event.RemoveMethod); + this.WriteForwardAttributes(builder, @event.RemoveMethod); + this.WriteReturnAttributes(builder, @event.RemoveMethod.GetReturnTypeAttributes()); builder.AppendIndentation(); builder.AppendLine("remove"); diff --git a/BeaKona.AutoInterfaceGenerator/ComparerBySignature.cs b/BeaKona.AutoInterfaceGenerator/ComparerBySignature.cs index 88d61a6..926dd2b 100644 --- a/BeaKona.AutoInterfaceGenerator/ComparerBySignature.cs +++ b/BeaKona.AutoInterfaceGenerator/ComparerBySignature.cs @@ -1,13 +1,8 @@ namespace BeaKona.AutoInterfaceGenerator; -internal sealed class ComparerBySignature : IEqualityComparer +internal sealed class ComparerBySignature(bool strict) : IEqualityComparer { - public ComparerBySignature(bool strict) - { - this.strict = strict; - } - - private readonly bool strict; + private readonly bool strict = strict; private readonly Dictionary> aliasesByKey = new(SymbolEqualityComparer.Default); public int GetHashCode(ISymbol obj) => throw new NotSupportedException();//SymbolEqualityComparer.Default.GetHashCode(obj); diff --git a/BeaKona.AutoInterfaceGenerator/ICodeTextWriter.cs b/BeaKona.AutoInterfaceGenerator/ICodeTextWriter.cs index 1e308d5..b9274af 100644 --- a/BeaKona.AutoInterfaceGenerator/ICodeTextWriter.cs +++ b/BeaKona.AutoInterfaceGenerator/ICodeTextWriter.cs @@ -10,6 +10,8 @@ internal interface ICodeTextWriter void WriteTypeArgumentsDefinition(SourceBuilder builder, IEnumerable typeArguments, ScopeInfo scope); + void WriteParameterAttributes(SourceBuilder builder, IParameterSymbol parameter); + void WriteParameterDefinition(SourceBuilder builder, ScopeInfo scope, IEnumerable parameters); void WriteCallParameters(SourceBuilder builder, IEnumerable parameters); diff --git a/BeaKona.AutoInterfaceGenerator/ScopeInfo.cs b/BeaKona.AutoInterfaceGenerator/ScopeInfo.cs index e4684c6..9b8124f 100644 --- a/BeaKona.AutoInterfaceGenerator/ScopeInfo.cs +++ b/BeaKona.AutoInterfaceGenerator/ScopeInfo.cs @@ -39,7 +39,7 @@ private static ImmutableList AllTypeArguments(ISymbol symbol) } } - return types.ToImmutableList(); + return [.. types]; } public void CreateAliases(ImmutableArray typeArguments) diff --git a/BeaKona.AutoInterfaceGenerator/SourceBuilder.cs b/BeaKona.AutoInterfaceGenerator/SourceBuilder.cs index 26a330b..243a2cb 100644 --- a/BeaKona.AutoInterfaceGenerator/SourceBuilder.cs +++ b/BeaKona.AutoInterfaceGenerator/SourceBuilder.cs @@ -2,13 +2,8 @@ namespace BeaKona.AutoInterfaceGenerator; -internal sealed class SourceBuilder +internal sealed class SourceBuilder(SourceBuilderOptions options) { - public SourceBuilder(SourceBuilderOptions options) - { - this.Options = options; - } - private SourceBuilder(SourceBuilder owner, SourceBuilderOptions options) : this(options) { this.owner = owner; @@ -16,7 +11,7 @@ private SourceBuilder(SourceBuilder owner, SourceBuilderOptions options) : this( private readonly SourceBuilder? owner; - public SourceBuilderOptions Options { get; } + public SourceBuilderOptions Options { get; } = options; private readonly List elements = []; private readonly HashSet aliases = []; @@ -211,14 +206,9 @@ private sealed class LineSeparatorMarker { } - private sealed class IndentationMarker + private sealed class IndentationMarker(int depth) { - public IndentationMarker(int depth) - { - this.Depth = depth; - } - - public readonly int Depth; + public readonly int Depth = depth; } private sealed class FlexibleSpaceMarker diff --git a/BeaKona.AutoInterfaceGenerator/Templates/PartialTemplate.cs b/BeaKona.AutoInterfaceGenerator/Templates/PartialTemplate.cs index be7e08d..9588644 100644 --- a/BeaKona.AutoInterfaceGenerator/Templates/PartialTemplate.cs +++ b/BeaKona.AutoInterfaceGenerator/Templates/PartialTemplate.cs @@ -2,18 +2,11 @@ namespace BeaKona.AutoInterfaceGenerator.Templates; -internal class PartialTemplate +internal class PartialTemplate(AutoInterfaceTargets memberTargets, Regex? memberFilter, TemplateDefinition template) { - public PartialTemplate(AutoInterfaceTargets memberTargets, Regex? memberFilter, TemplateDefinition template) - { - this.MemberTargets = memberTargets; - this.MemberFilter = memberFilter; - this.Template = template; - } - - public AutoInterfaceTargets MemberTargets; - public Regex? MemberFilter; - public TemplateDefinition Template; + public AutoInterfaceTargets MemberTargets = memberTargets; + public Regex? MemberFilter = memberFilter; + public TemplateDefinition Template = template; public bool FilterMatch(string name) => this.MemberFilter == null || this.MemberFilter.IsMatch(name); diff --git a/BeaKona.AutoInterfaceGenerator/Templates/TemplateDefinition.cs b/BeaKona.AutoInterfaceGenerator/Templates/TemplateDefinition.cs index 5615493..f2503c0 100644 --- a/BeaKona.AutoInterfaceGenerator/Templates/TemplateDefinition.cs +++ b/BeaKona.AutoInterfaceGenerator/Templates/TemplateDefinition.cs @@ -1,15 +1,9 @@ namespace BeaKona.AutoInterfaceGenerator.Templates; -internal class TemplateDefinition : IEquatable +internal class TemplateDefinition(string language, string body) : IEquatable { - public TemplateDefinition(string language, string body) - { - this.Language = language ?? ""; - this.Body = body ?? ""; - } - - public string Language { get; } - public string Body { get; } + public string Language { get; } = language ?? ""; + public string Body { get; } = body ?? ""; public bool Equals(TemplateDefinition other) { diff --git a/BeaKona.AutoInterfaceGenerator/Templates/TemplatedSourceGenerator.cs b/BeaKona.AutoInterfaceGenerator/Templates/TemplatedSourceGenerator.cs index fbe44a2..f24ebbd 100644 --- a/BeaKona.AutoInterfaceGenerator/Templates/TemplatedSourceGenerator.cs +++ b/BeaKona.AutoInterfaceGenerator/Templates/TemplatedSourceGenerator.cs @@ -1,27 +1,12 @@ namespace BeaKona.AutoInterfaceGenerator.Templates; -internal class TemplatedSourceTextGenerator : ISourceTextGenerator +internal class TemplatedSourceTextGenerator(TemplateDefinition template) : ISourceTextGenerator { - public TemplatedSourceTextGenerator(TemplateDefinition template) - { - this.Template = template; - } - - public TemplateDefinition Template { get; } + public TemplateDefinition Template { get; } = template; public void Emit(ICodeTextWriter writer, SourceBuilder builder, object? model, ref bool separatorRequired) { - Scriban.Template? template = (this.Template.Language ?? "").ToLowerInvariant() switch - { - "scriban" => Scriban.Template.Parse(this.Template.Body), - "liquid" => Scriban.Template.ParseLiquid(this.Template.Body), - _ => null, - }; - - if (template == null) - { - throw new NotSupportedException($"Template language '{this.Template.Language}' is not supported."); - } + var template = this.ResolveTemplate(this.Template.Language ?? "") ?? throw new NotSupportedException($"Template language '{this.Template.Language}' is not supported."); string text = template.Render(model); @@ -49,4 +34,14 @@ public void Emit(ICodeTextWriter writer, SourceBuilder builder, object? model, r } } } + + private Scriban.Template? ResolveTemplate(string language) + { + return language.ToLowerInvariant() switch + { + "scriban" => Scriban.Template.Parse(this.Template.Body), + "liquid" => Scriban.Template.ParseLiquid(this.Template.Body), + _ => null, + }; + } }