From 5ccf37d425748d9c2631bb6802cc35ad184da9de Mon Sep 17 00:00:00 2001 From: neuecc Date: Thu, 19 Dec 2024 19:04:39 +0900 Subject: [PATCH] analyzer done --- sandbox/ConsoleApp/Program.cs | 4 +- sandbox/GeneratorSandbox/Program.cs | 9 +- .../DiagnosticDescriptors.cs | 84 +++++++++++++ .../GeneratorCore/CodeGenerator.cs | 118 ++++++++---------- .../MasterMemoryGenerator.cs | 30 +++-- .../DiagnosticsTest.cs | 38 ++++++ 6 files changed, 197 insertions(+), 86 deletions(-) create mode 100644 src/MasterMemory.SourceGenerator/DiagnosticDescriptors.cs create mode 100644 tests/MasterMemory.SourceGenerator.Tests/DiagnosticsTest.cs diff --git a/sandbox/ConsoleApp/Program.cs b/sandbox/ConsoleApp/Program.cs index 9934ed4..c1aef1e 100644 --- a/sandbox/ConsoleApp/Program.cs +++ b/sandbox/ConsoleApp/Program.cs @@ -10,8 +10,8 @@ using System.Text; -[assembly: MasterMemoryGeneratorOptions( - Namespace = "ConsoleApp")] +//[assembly: MasterMemoryGeneratorOptions( +// Namespace = "ConsoleApp")] [MemoryTable("quest_master"), MessagePackObject(true)] public class Quest : IValidatable diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index e3cf137..438f5fa 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -12,9 +12,12 @@ - - - +[MemoryTable("mytakoyaki")] +public class MyTakoyaki +{ + [PrimaryKey] + public int Id { get; set; } +} public enum Gender diff --git a/src/MasterMemory.SourceGenerator/DiagnosticDescriptors.cs b/src/MasterMemory.SourceGenerator/DiagnosticDescriptors.cs new file mode 100644 index 0000000..8c7e326 --- /dev/null +++ b/src/MasterMemory.SourceGenerator/DiagnosticDescriptors.cs @@ -0,0 +1,84 @@ +using Microsoft.CodeAnalysis; +using System; +using System.Collections.Generic; +using System.Text; + +namespace MasterMemory; + +internal sealed class DiagnosticReporter : IEquatable +{ + List? diagnostics; + + public bool HasDiagnostics => diagnostics != null && diagnostics.Count != 0; + + public void ReportDiagnostic(DiagnosticDescriptor diagnosticDescriptor, Location location, params object?[]? messageArgs) + { + var diagnostic = Diagnostic.Create(diagnosticDescriptor, location, messageArgs); + if (diagnostics == null) + { + diagnostics = new(); + } + diagnostics.Add(diagnostic); + } + + public void ReportToContext(SourceProductionContext context) + { + if (diagnostics != null) + { + foreach (var item in diagnostics) + { + context.ReportDiagnostic(item); + } + } + } + + public bool Equals(DiagnosticReporter other) + { + // if error, always false and otherwise ignore + if (diagnostics == null && other.diagnostics == null) + { + return true; + } + + return false; + } +} + +internal static class DiagnosticDescriptors +{ + const string Category = "GenerateMasterMemory"; + + public static void ReportDiagnostic(this SourceProductionContext context, DiagnosticDescriptor diagnosticDescriptor, Location location, params object?[]? messageArgs) + { + var diagnostic = Diagnostic.Create(diagnosticDescriptor, location, messageArgs); + context.ReportDiagnostic(diagnostic); + } + + public static DiagnosticDescriptor Create(int id, string message) + { + return Create(id, message, message); + } + + public static DiagnosticDescriptor Create(int id, string title, string messageFormat) + { + return new DiagnosticDescriptor( + id: "MAM" + id.ToString("000"), + title: title, + messageFormat: messageFormat, + category: Category, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + } + + public static DiagnosticDescriptor RequirePrimaryKey { get; } = Create( + 1, + "MemoryTable does not found PrimaryKey property, Type:{0}."); + + public static DiagnosticDescriptor DuplicatePrimaryKey { get; } = Create( + 2, + "Duplicate PrimaryKey:{0}.{1}"); + + public static DiagnosticDescriptor DuplicateSecondaryKey { get; } = Create( + 3, + "Duplicate SecondaryKey, doesn't allow to add multiple attribute in same attribute list:{0}.{1}"); +} diff --git a/src/MasterMemory.SourceGenerator/GeneratorCore/CodeGenerator.cs b/src/MasterMemory.SourceGenerator/GeneratorCore/CodeGenerator.cs index b548085..bff3e50 100644 --- a/src/MasterMemory.SourceGenerator/GeneratorCore/CodeGenerator.cs +++ b/src/MasterMemory.SourceGenerator/GeneratorCore/CodeGenerator.cs @@ -6,10 +6,45 @@ namespace MasterMemory.GeneratorCore { - public static class CodeGenerator + internal static class CodeGenerator { - public static GenerationContext CreateGenerationContext(TypeDeclarationSyntax classDecl) + // return GenerationContext? + public static GenerationContext CreateGenerationContext(TypeDeclarationSyntax classDecl, AttributeData memoryTableAttribute, DiagnosticReporter reporter) { + var context = new GenerationContext(); + + context.ClassName = classDecl.Identifier.ToFullString().Trim(); + context.MemoryTableName = memoryTableAttribute.ConstructorArguments[0].Value as string ?? context.ClassName; + + var hasError = false; + var members = classDecl.Members.OfType() + .Select(x => + { + var prop = ExtractPropertyAttribute(x, reporter); + if (prop == null) + { + hasError = true; + return default!; + } + return prop.Value; + }) + .ToArray(); + if (hasError) return null; + + var primaryKey = AggregatePrimaryKey(members.Where(x => x.Item1 != null).Select(x => x.Item1)); + if (primaryKey.Properties.Length == 0) + { + reporter.ReportDiagnostic(DiagnosticDescriptors.RequirePrimaryKey, classDecl.Identifier.GetLocation(), context.ClassName); + return null; + } + + var secondaryKeys = members.SelectMany(x => x.Item2).GroupBy(x => x.IndexNo).Select(x => AggregateSecondaryKey(x)).ToArray(); + var properties = members.Where(x => x.Item3 != null).Select(x => new Property + { + Type = x.Item3.Type.ToFullStringTrim(), + Name = x.Item3.Identifier.Text, + }).ToArray(); + var root = classDecl.SyntaxTree.GetRoot(); var ns = root.DescendantNodes().OfType() @@ -26,52 +61,16 @@ public static GenerationContext CreateGenerationContext(TypeDeclarationSyntax cl .OrderBy(x => x, StringComparer.Ordinal) .ToArray(); - var context = new GenerationContext(); - - foreach (var attr in classDecl.AttributeLists.SelectMany(x => x.Attributes)) - { - var attrName = attr.Name.ToFullString().Trim(); - if (attrName == "MemoryTable" || attrName == "MasterMemory.Annotations.MemoryTable") - { - context.ClassName = classDecl.Identifier.ToFullString().Trim(); - context.MemoryTableName = AttributeExpressionToString(attr.ArgumentList.Arguments[0].Expression) ?? context.ClassName; - - var members = classDecl.Members.OfType() - .Select(x => ExtractPropertyAttribute(x)) - .ToArray(); - - var primaryKey = AggregatePrimaryKey(members.Where(x => x.Item1 != null).Select(x => x.Item1)); - if (primaryKey.Properties.Length == 0) - { - throw new InvalidOperationException("MemoryTable does not found PrimaryKey property, Type:" + context.ClassName); - } - - var secondaryKeys = members.SelectMany(x => x.Item2).GroupBy(x => x.IndexNo).Select(x => AggregateSecondaryKey(x)).ToArray(); - var properties = members.Where(x => x.Item3 != null).Select(x => new Property - { - Type = x.Item3.Type.ToFullStringTrim(), - Name = x.Item3.Identifier.Text, - }).ToArray(); - - context.PrimaryKey = primaryKey; - context.SecondaryKeys = secondaryKeys; - context.Properties = properties; - } - } - - if (context.PrimaryKey != null) - { - context.UsingStrings = usingStrings; - context.OriginalClassDeclaration = classDecl; - return context; - } - - // If primary key not found, validate from another place. - throw new InvalidOperationException("PrimaryKey not found."); + context.PrimaryKey = primaryKey; + context.SecondaryKeys = secondaryKeys; + context.Properties = properties; + context.UsingStrings = usingStrings; + context.OriginalClassDeclaration = classDecl; + return context; } - static (PrimaryKey, List, PropertyDeclarationSyntax) ExtractPropertyAttribute(PropertyDeclarationSyntax property) + static (PrimaryKey, List, PropertyDeclarationSyntax)? ExtractPropertyAttribute(PropertyDeclarationSyntax property, DiagnosticReporter reporter) { // Attribute Parterns: // Primarykey(keyOrder = 0) @@ -96,7 +95,9 @@ public static GenerationContext CreateGenerationContext(TypeDeclarationSyntax cl { if (resultPrimaryKey != null) { - throw new InvalidOperationException("Duplicate PrimaryKey:" + property.Type.ToFullString() + "." + property.Identifier.ToFullString()); + // PrimaryKey is AllowMultiple:false so this code is dead + reporter.ReportDiagnostic(DiagnosticDescriptors.DuplicatePrimaryKey, property.Identifier.GetLocation(), property.Type.ToFullString(), property.Identifier.ToFullString()); + return null; } primaryKey = new PrimaryKey(); @@ -117,7 +118,8 @@ public static GenerationContext CreateGenerationContext(TypeDeclarationSyntax cl { if (secondaryKey != null) { - throw new InvalidOperationException("Duplicate SecondaryKey, doesn't allow to add multiple attribute in same attribute list:" + property.Type.ToFullString() + "." + property.Identifier.ToFullString()); + reporter.ReportDiagnostic(DiagnosticDescriptors.DuplicateSecondaryKey, property.Identifier.GetLocation(), property.Type.ToFullString(), property.Identifier.ToFullString()); + return null; } secondaryKey = new SecondaryKey(); @@ -221,28 +223,6 @@ static SecondaryKey AggregateSecondaryKey(IGrouping secondary secondaryKey.Properties = list.OrderBy(x => x.KeyOrder).ToArray(); return secondaryKey; } - - static string AttributeExpressionToString(ExpressionSyntax expression) - { - if (expression is InvocationExpressionSyntax ie) - { - var expr = ie.ArgumentList.Arguments.Last().Expression; - if (expr is MemberAccessExpressionSyntax mae) - { - return mae.Name?.ToString(); - } - else if (expr is IdentifierNameSyntax inx) - { - return inx.Identifier.ValueText; - } - return null; - } - else if (expression is LiteralExpressionSyntax le) - { - return le.Token.ValueText; - } - return null; - } } internal static class Extensions diff --git a/src/MasterMemory.SourceGenerator/MasterMemoryGenerator.cs b/src/MasterMemory.SourceGenerator/MasterMemoryGenerator.cs index 321e886..23a7305 100644 --- a/src/MasterMemory.SourceGenerator/MasterMemoryGenerator.cs +++ b/src/MasterMemory.SourceGenerator/MasterMemoryGenerator.cs @@ -35,20 +35,25 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var memoryTables = context.SyntaxProvider.ForAttributeWithMetadataName("MasterMemory.MemoryTableAttribute", (node, token) => true, - (ctx, token) => - { - // class or record - var classDecl = ctx.TargetNode as TypeDeclarationSyntax; - var context = CodeGenerator.CreateGenerationContext(classDecl!); - return context; - }) + (ctx, token) => ctx) .WithTrackingName("MasterMemory.SyntaxProvider.0_ForAttributeWithMetadataName") .Collect() .Select((xs, _) => { - var array = xs.ToArray(); - Array.Sort(array, (a, b) => string.Compare(a.ClassName, b.ClassName, StringComparison.Ordinal)); - return new EquatableArray(array); + var list = new List(); + var reporter = new DiagnosticReporter(); + foreach (var ctx in xs) + { + var memoryTableAttr = ctx.Attributes[0]; // AllowMultiple=false + var classDecl = ctx.TargetNode as TypeDeclarationSyntax; // class or record + var context = CodeGenerator.CreateGenerationContext(classDecl!, memoryTableAttr, reporter); + if (context != null) + { + list.Add(context); + } + } + list.Sort((a, b) => string.Compare(a.ClassName, b.ClassName, StringComparison.Ordinal)); + return (reporter, new EquatableArray(list.ToArray())); }) .WithTrackingName("MasterMemory.SyntaxProvider.1_CollectAndSelect"); @@ -60,9 +65,10 @@ public void Initialize(IncrementalGeneratorInitializationContext context) context.RegisterSourceOutput(allCombined, EmitMemoryTable); } - void EmitMemoryTable(SourceProductionContext context, ((EquatableArray, string?), MasterMemoryGeneratorOptions) value) + void EmitMemoryTable(SourceProductionContext context, (((DiagnosticReporter, EquatableArray), string?), MasterMemoryGeneratorOptions) value) { - var ((memoryTables, defaultNamespace), generatorOptions) = value; + var (((diagnostic, memoryTables), defaultNamespace), generatorOptions) = value; + diagnostic.ReportToContext(context); var usingNamespace = generatorOptions.Namespace ?? defaultNamespace ?? "MasterMemory"; var prefixClassName = generatorOptions.PrefixClassName ?? ""; diff --git a/tests/MasterMemory.SourceGenerator.Tests/DiagnosticsTest.cs b/tests/MasterMemory.SourceGenerator.Tests/DiagnosticsTest.cs new file mode 100644 index 0000000..a0926b7 --- /dev/null +++ b/tests/MasterMemory.SourceGenerator.Tests/DiagnosticsTest.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MasterMemory.SourceGenerator.Tests; + +public class DiagnosticsTest(ITestOutputHelper outputHelper) : TestBase(outputHelper) +{ + [Fact] + public void RequirePrimaryKey() + { + Helper.Verify(1, """ +[MemoryTable("item")] +public class Item +{ + // [PrimaryKey] // No PrimaryKey + public int ItemId { get; set; } +} +""", "Item"); + } + + [Fact] + public void DuplicateSecondaryKey() + { + Helper.Verify(3, """ +[MemoryTable("item")] +public class Item +{ + [PrimaryKey] + public int ItemId1 { get; set; } + [SecondaryKey(0), SecondaryKey(1)] + public int ItemId2 { get; set; } +} +""", "ItemId2"); + } +}