Skip to content

Commit

Permalink
analyzer done
Browse files Browse the repository at this point in the history
  • Loading branch information
neuecc committed Dec 19, 2024
1 parent 555634a commit 5ccf37d
Show file tree
Hide file tree
Showing 6 changed files with 197 additions and 86 deletions.
4 changes: 2 additions & 2 deletions sandbox/ConsoleApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
using System.Text;


[assembly: MasterMemoryGeneratorOptions(
Namespace = "ConsoleApp")]
//[assembly: MasterMemoryGeneratorOptions(
// Namespace = "ConsoleApp")]

[MemoryTable("quest_master"), MessagePackObject(true)]
public class Quest : IValidatable<Quest>
Expand Down
9 changes: 6 additions & 3 deletions sandbox/GeneratorSandbox/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@






[MemoryTable("mytakoyaki")]
public class MyTakoyaki
{
[PrimaryKey]
public int Id { get; set; }
}


public enum Gender
Expand Down
84 changes: 84 additions & 0 deletions src/MasterMemory.SourceGenerator/DiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using Microsoft.CodeAnalysis;
using System;
using System.Collections.Generic;
using System.Text;

namespace MasterMemory;

internal sealed class DiagnosticReporter : IEquatable<DiagnosticReporter>
{
List<Diagnostic>? 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}");
}
118 changes: 49 additions & 69 deletions src/MasterMemory.SourceGenerator/GeneratorCore/CodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PropertyDeclarationSyntax>()
.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<NamespaceDeclarationSyntax>()
Expand All @@ -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<PropertyDeclarationSyntax>()
.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<SecondaryKey>, PropertyDeclarationSyntax) ExtractPropertyAttribute(PropertyDeclarationSyntax property)
static (PrimaryKey, List<SecondaryKey>, PropertyDeclarationSyntax)? ExtractPropertyAttribute(PropertyDeclarationSyntax property, DiagnosticReporter reporter)
{
// Attribute Parterns:
// Primarykey(keyOrder = 0)
Expand All @@ -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();
Expand All @@ -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();
Expand Down Expand Up @@ -221,28 +223,6 @@ static SecondaryKey AggregateSecondaryKey(IGrouping<int, SecondaryKey> 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
Expand Down
30 changes: 18 additions & 12 deletions src/MasterMemory.SourceGenerator/MasterMemoryGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<GenerationContext>(array);
var list = new List<GenerationContext>();
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<GenerationContext>(list.ToArray()));
})
.WithTrackingName("MasterMemory.SyntaxProvider.1_CollectAndSelect");

Expand All @@ -60,9 +65,10 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
context.RegisterSourceOutput(allCombined, EmitMemoryTable);
}

void EmitMemoryTable(SourceProductionContext context, ((EquatableArray<GenerationContext>, string?), MasterMemoryGeneratorOptions) value)
void EmitMemoryTable(SourceProductionContext context, (((DiagnosticReporter, EquatableArray<GenerationContext>), 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 ?? "";
Expand Down
38 changes: 38 additions & 0 deletions tests/MasterMemory.SourceGenerator.Tests/DiagnosticsTest.cs
Original file line number Diff line number Diff line change
@@ -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");
}
}

0 comments on commit 5ccf37d

Please sign in to comment.