Skip to content

Commit

Permalink
remove dependency
Browse files Browse the repository at this point in the history
  • Loading branch information
neuecc committed Dec 17, 2024
1 parent 4649014 commit 4e7432d
Show file tree
Hide file tree
Showing 5 changed files with 45 additions and 272 deletions.
2 changes: 1 addition & 1 deletion sandbox/GeneratorSandbox/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public override string ToString()
}

[MemoryTable("monster"), MessagePackObject(true)]
public class Monster
public partial class Monster
{
[PrimaryKey]
public int MonsterId { get; private set; }
Expand Down
265 changes: 2 additions & 263 deletions src/MasterMemory.SourceGenerator/GeneratorCore/CodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,200 +3,12 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

namespace MasterMemory.GeneratorCore
{
public class CodeGenerator
public static class CodeGenerator
{
static readonly Encoding NoBomUtf8 = new UTF8Encoding(false);

public void GenerateFile(string usingNamespace, string inputDirectory, string outputDirectory, string prefixClassName, bool addImmutableConstructor, bool throwIfKeyNotFound, bool forceOverwrite, Action<string> logger)
{
prefixClassName ??= "";
var list = new List<GenerationContext>();

// Collect
if (inputDirectory.EndsWith(".csproj"))
{
throw new InvalidOperationException("Path must be directory but it is csproj. inputDirectory:" + inputDirectory);
}

foreach (var item in Directory.GetFiles(inputDirectory, "*.cs", SearchOption.AllDirectories))
{
list.AddRange(CreateGenerationContext(item));
}

list.Sort((a, b) => string.Compare(a.ClassName, b.ClassName, StringComparison.Ordinal));

if (list.Count == 0)
{
throw new InvalidOperationException("Not found MemoryTable files, inputDir:" + inputDirectory);
}

// Output
{
if (!Directory.Exists(outputDirectory))
{
Directory.CreateDirectory(outputDirectory);
}

var usingStrings = string.Join(Environment.NewLine, list.SelectMany(x => x.UsingStrings).Distinct().OrderBy(x => x, StringComparer.Ordinal));

var builderTemplate = new DatabaseBuilderTemplate();
var databaseTemplate = new MemoryDatabaseTemplate();
var immutableBuilderTemplate = new ImmutableBuilderTemplate();
var resolverTemplate = new MessagePackResolverTemplate();
builderTemplate.Namespace = databaseTemplate.Namespace = immutableBuilderTemplate.Namespace = resolverTemplate.Namespace = usingNamespace;
builderTemplate.PrefixClassName = databaseTemplate.PrefixClassName = immutableBuilderTemplate.PrefixClassName = resolverTemplate.PrefixClassName = prefixClassName;
builderTemplate.Using = databaseTemplate.Using = immutableBuilderTemplate.Using = resolverTemplate.Using = (usingStrings + Environment.NewLine + ("using " + usingNamespace + ".Tables;"));
builderTemplate.GenerationContexts = databaseTemplate.GenerationContexts = immutableBuilderTemplate.GenerationContexts = resolverTemplate.GenerationContexts = list.ToArray();

logger(WriteToFile(outputDirectory, builderTemplate.ClassName, builderTemplate.TransformText(), forceOverwrite));
logger(WriteToFile(outputDirectory, immutableBuilderTemplate.ClassName, immutableBuilderTemplate.TransformText(), forceOverwrite));
logger(WriteToFile(outputDirectory, databaseTemplate.ClassName, databaseTemplate.TransformText(), forceOverwrite));
logger(WriteToFile(outputDirectory, resolverTemplate.ClassName, resolverTemplate.TransformText(), forceOverwrite));
}
{
var tableDir = Path.Combine(outputDirectory, "Tables");
if (!Directory.Exists(tableDir))
{
Directory.CreateDirectory(tableDir);
}

foreach (var context in list)
{
var template = new TableTemplate()
{
Namespace = usingNamespace,
GenerationContext = context,
Using = string.Join(Environment.NewLine, context.UsingStrings),
ThrowKeyIfNotFound = throwIfKeyNotFound
};

logger(WriteToFile(tableDir, context.ClassName + "Table", template.TransformText(), forceOverwrite));
}
}
// Modify
{
if (addImmutableConstructor)
{
var workspace = new AdhocWorkspace();
var byFilePath = list.GroupBy(x => x.InputFilePath);

foreach (var context in byFilePath)
{
var newFile = BuildRecordConstructorFile(workspace, context.Select(x => x.OriginalClassDeclaration));
if (newFile != null)
{
File.WriteAllText(context.Key, newFile, NoBomUtf8);
logger("Modified " + context.Key);
}
}
}
}
}

static string NormalizeNewLines(string content)
{
// The T4 generated code may be text with mixed line ending types. (CR + CRLF)
// We need to normalize the line ending type in each Operating Systems. (e.g. Windows=CRLF, Linux/macOS=LF)
return content.Replace("\r\n", "\n").Replace("\n", Environment.NewLine);
}

static string WriteToFile(string directory, string fileName, string content, bool forceOverwrite)
{
var path = Path.Combine(directory, fileName + ".cs");
var contentBytes = Encoding.UTF8.GetBytes(NormalizeNewLines(content));

// If the generated content is unchanged, skip the write.
if (!forceOverwrite && File.Exists(path))
{
if (new FileInfo(path).Length == contentBytes.Length && contentBytes.AsSpan().SequenceEqual(File.ReadAllBytes(path)))
{
return $"Generate {fileName} to: {path} (Skipped)";
}
}

File.WriteAllBytes(path, contentBytes);
return $"Generate {fileName} to: {path}";
}

IEnumerable<GenerationContext> CreateGenerationContext(string filePath)
{
var syntax = Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree.ParseText(File.ReadAllText(filePath));
var root = syntax.GetRoot();

var classDeclarations = root.DescendantNodes().OfType<ClassDeclarationSyntax>().ToArray();
if (classDeclarations.Length == 0) yield break;

var ns = root.DescendantNodes().OfType<NamespaceDeclarationSyntax>()
.Select(x => "using " + x.Name.ToFullStringTrim() + ";")
.ToArray();

var usingStrings = root.DescendantNodes()
.OfType<UsingDirectiveSyntax>()
.Select(x => x.ToFullString().Trim())
.Concat(new[] { "using MasterMemory", "using MasterMemory.Validation", "using System", "using System.Collections.Generic" })
.Concat(ns)
.Select(x => x.Trim(';') + ";")
.Distinct()
.OrderBy(x => x, StringComparer.Ordinal)
.ToArray();

foreach (var classDecl in classDeclarations)
{
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;
context.InputFilePath = filePath;
yield return context;
}
}
}

public static GenerationContext CreateGenerationContext2(ClassDeclarationSyntax classDecl)
public static GenerationContext CreateGenerationContext(ClassDeclarationSyntax classDecl)
{
var root = classDecl.SyntaxTree.GetRoot();

Expand Down Expand Up @@ -410,79 +222,6 @@ static SecondaryKey AggregateSecondaryKey(IGrouping<int, SecondaryKey> secondary
return secondaryKey;
}

string BuildRecordConstructorFile(AdhocWorkspace workspace, IEnumerable<ClassDeclarationSyntax> classDeclarations)
{
// using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

var modify = false;
var editor = new SyntaxEditor(classDeclarations.First().SyntaxTree.GetRoot(), workspace);
foreach (var classDeclaration in classDeclarations)
{
var members = classDeclaration.Members.OfType<PropertyDeclarationSyntax>()
.Where(x => x.Modifiers.Any(y => y.IsKind(SyntaxKind.PublicKeyword)))
.Where(x =>
{
foreach (var attr in x.AttributeLists.SelectMany(y => y.Attributes))
{
var attrName = attr.Name.ToFullString().Trim();
if (attrName == "IgnoreMember" || attrName == "MessagePack.IgnoreMember")
{
return false;
}
if (attrName == "IgnoreDataMember" || attrName == "System.Runtime.Serialization.IgnoreDataMember")
{
return false;
}
}
return true;
})
.ToArray();

var parameters = ParameterList(SeparatedList(members.Select(x => Parameter(attributeLists: default, modifiers: default, type: x.Type, x.Identifier, @default: null))));

var parameterStrings = parameters.Parameters.Select(x => x.Type.ToFullStringTrim()).ToArray();

// check existing constructor
var matchedConstructor = classDeclaration.Members.OfType<ConstructorDeclarationSyntax>()
.Where(x => x.ParameterList.Parameters.Select(y => y.Type.ToFullStringTrim()).SequenceEqual(parameterStrings))
.ToArray();

if (matchedConstructor.Length != 0)
{
continue;
}

var body = members.Select(x => AssignmentExpression(
SyntaxKind.SimpleAssignmentExpression, MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, ThisExpression(), IdentifierName(x.Identifier)),
Token(SyntaxKind.EqualsToken),
IdentifierName(x.Identifier)))
.Select(x => ExpressionStatement(x));

var recordCtor = ConstructorDeclaration(classDeclaration.Identifier)
.WithModifiers(TokenList(Token(Sy‌ntaxKind.PublicKeywo‌​rd)))
.WithParameterList(parameters)
.WithBody(Block(body))
.NormalizeWhitespace()
.WithLeadingTrivia(LineFeed)
.WithTrailingTrivia(LineFeed);

var newClassDecl = classDeclaration.AddMembers(recordCtor);

modify = true;
editor.ReplaceNode(classDeclaration, newClassDecl);
}

if (modify)
{
var newCodeString = Microsoft.CodeAnalysis.Formatting.Formatter.Format(editor.GetChangedRoot(), workspace).ToFullString();
return newCodeString;
}
else
{
return null;
}
}

static string AttributeExpressionToString(ExpressionSyntax expression)
{
if (expression is InvocationExpressionSyntax ie)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,6 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

<!-- TODO: remove it. -->
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.4.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="3.4.0" />
<PackageReference Include="System.CodeDom" Version="4.5.0" />
</ItemGroup>

<ItemGroup>
Expand Down
24 changes: 21 additions & 3 deletions src/MasterMemory.SourceGenerator/MasterMemoryGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
// TODO:...
// Input and Output is no needed.
// prefix should be configurable?(to assemblyattribute)
// immutableconstructor
// remove immutableconstructor feature
// returnnull

// new CodeGenerator().GenerateFile(UsingNamespace, InputDirectory, OutputDirectory, PrefixClassName, AddImmutableConstructor, !ReturnNullIfKeyNotFound, ForceOverwrite, x => Console.WriteLine(x));
Expand All @@ -63,7 +63,7 @@ void EmitMemoryTable(SourceProductionContext context, ImmutableArray<GeneratorAt
{
// TODO: RecordDeclaration
var classDecl = x.TargetNode as ClassDeclarationSyntax;
return CodeGenerator.CreateGenerationContext2(classDecl!);
return CodeGenerator.CreateGenerationContext(classDecl!);
})
.ToList();

Expand All @@ -85,7 +85,25 @@ void EmitMemoryTable(SourceProductionContext context, ImmutableArray<GeneratorAt
Log(WriteToFile(context, databaseTemplate.ClassName, databaseTemplate.TransformText()));
Log(WriteToFile(context, resolverTemplate.ClassName, resolverTemplate.TransformText()));

// TODO:Modify of SourceGenerator.GenerateFile
// TODO: output table
//var tableDir = Path.Combine(outputDirectory, "Tables");
//if (!Directory.Exists(tableDir))
//{
// Directory.CreateDirectory(tableDir);
//}

//foreach (var context in list)
//{
// var template = new TableTemplate()
// {
// Namespace = usingNamespace,
// GenerationContext = context,
// Using = string.Join(Environment.NewLine, context.UsingStrings),
// ThrowKeyIfNotFound = throwIfKeyNotFound
// };

// logger(WriteToFile(tableDir, context.ClassName + "Table", template.TransformText(), forceOverwrite));
//}
}

static void Log(string msg)
Expand Down
21 changes: 21 additions & 0 deletions src/MasterMemory.SourceGenerator/Polyfill/System.CodeDom.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#nullable disable

using System;
using System.Collections.Generic;
using System.Text;

namespace System.CodeDom.Compiler
{
public class CompilerError
{
public string ErrorText { get; set; }
public bool IsWarning { get; set; }
}

public class CompilerErrorCollection
{
public void Add(CompilerError error)
{
}
}
}

0 comments on commit 4e7432d

Please sign in to comment.