baseline: OpenMU upstream b5a0961 (fresh source)
This commit is contained in:
57
src/SourceGenerators/CaptionHelper.cs
Normal file
57
src/SourceGenerators/CaptionHelper.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
// <copyright file="CaptionHelper.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.SourceGenerators;
|
||||
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class to generate captions for types.
|
||||
/// </summary>
|
||||
public static class CaptionHelper
|
||||
{
|
||||
private static readonly Regex WordSeparatorRegex = new("([a-z])([A-Z])", RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Separates the words by a space. Words are detected by upper case letters.
|
||||
/// </summary>
|
||||
/// <param name="input">The input.</param>
|
||||
/// <returns>The separated words.</returns>
|
||||
public static string SeparateWords(string input)
|
||||
{
|
||||
return WordSeparatorRegex.Replace(input, "$1 $2")
|
||||
.Replace(" Definitions", "s")
|
||||
.Replace(" Definition", "s");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a nice caption for types.
|
||||
/// </summary>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <returns>A nice caption for types.</returns>
|
||||
public static string GetTypeCaption(ClassDeclarationSyntax type)
|
||||
{
|
||||
return SeparateWords(type.Identifier.Text);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a pluralized caption for a type.
|
||||
/// </summary>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <returns>A nice caption for types.</returns>
|
||||
public static string GetPluralizedTypeCaption(ClassDeclarationSyntax type)
|
||||
{
|
||||
var result = GetTypeCaption(type);
|
||||
result = result
|
||||
.Replace(" Definitions", "s")
|
||||
.Replace(" Definition", "s");
|
||||
if (!result.EndsWith("s"))
|
||||
{
|
||||
result += "s";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
186
src/SourceGenerators/CloneableGenerator.cs
Normal file
186
src/SourceGenerators/CloneableGenerator.cs
Normal file
@@ -0,0 +1,186 @@
|
||||
// <copyright file="CloneableGenerator.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.SourceGenerators;
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="IIncrementalGenerator"/> which implements <see cref="ICloneable{}"/>
|
||||
/// and for convenience also <see cref="IAssignable"/> and <see cref="IAssignable{}"/>.
|
||||
/// </summary>
|
||||
[Generator]
|
||||
public class CloneableGenerator : IIncrementalGenerator
|
||||
{
|
||||
private const string CloneableAttributeFullName = "MUnique.OpenMU.Annotations.CloneableAttribute";
|
||||
|
||||
private const string CloneableAttributeName = "CloneableAttribute";
|
||||
|
||||
private const string IgnoreWhenCloningAttributeName = "IgnoreWhenCloningAttribute";
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
var classDeclarations = context.SyntaxProvider
|
||||
.CreateSyntaxProvider(
|
||||
predicate: static (node, _) => node is ClassDeclarationSyntax { AttributeLists.Count: > 0 },
|
||||
transform: static (ctx, _) => GetClassWithCloneableAttribute(ctx))
|
||||
.Where(static m => m is not null);
|
||||
|
||||
var compilationAndClasses = context.CompilationProvider.Combine(classDeclarations.Collect());
|
||||
|
||||
context.RegisterSourceOutput(compilationAndClasses, (spc, source) => Execute(source.Left, source.Right!, spc));
|
||||
}
|
||||
|
||||
private static ClassDeclarationSyntax? GetClassWithCloneableAttribute(GeneratorSyntaxContext context)
|
||||
{
|
||||
var classDeclaration = (ClassDeclarationSyntax)context.Node;
|
||||
|
||||
foreach (var attributeList in classDeclaration.AttributeLists)
|
||||
{
|
||||
foreach (var attribute in attributeList.Attributes)
|
||||
{
|
||||
var symbolInfo = context.SemanticModel.GetSymbolInfo(attribute);
|
||||
if (symbolInfo.Symbol is IMethodSymbol attributeSymbol)
|
||||
{
|
||||
var attributeContainingType = attributeSymbol.ContainingType;
|
||||
var fullName = attributeContainingType.ToDisplayString();
|
||||
|
||||
if (fullName == CloneableAttributeFullName)
|
||||
{
|
||||
return classDeclaration;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void Execute(Compilation compilation, ImmutableArray<ClassDeclarationSyntax?> classes, SourceProductionContext context)
|
||||
{
|
||||
if (classes.IsDefaultOrEmpty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var classDeclaration in classes.Distinct())
|
||||
{
|
||||
if (classDeclaration is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var semanticModel = compilation.GetSemanticModel(classDeclaration.SyntaxTree);
|
||||
var declaredClassSymbol = semanticModel.GetDeclaredSymbol(classDeclaration);
|
||||
if (declaredClassSymbol is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var generatedClass = GeneratePartialClass(classDeclaration, declaredClassSymbol);
|
||||
context.AddSource($"{classDeclaration.Identifier}_Cloneable", SourceText.From(generatedClass.ToString(), Encoding.UTF8));
|
||||
}
|
||||
}
|
||||
|
||||
private static StringBuilder GeneratePartialClass(ClassDeclarationSyntax annotatedClass, INamedTypeSymbol declaredClassSymbol)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var className = annotatedClass.Identifier.Text;
|
||||
var ns = declaredClassSymbol.ContainingNamespace?.ToString() ?? string.Empty;
|
||||
var isInheritedClonable = declaredClassSymbol.BaseType?.GetAttributes().Any(a => a.AttributeClass?.Name == CloneableAttributeName) ?? false;
|
||||
|
||||
sb.AppendLine($"""
|
||||
// <copyright file="{className}_Cloneable.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace {ns};
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MUnique.OpenMU.DataModel;
|
||||
using MUnique.OpenMU.DataModel.Configuration;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class {className} : IAssignable, IAssignable<{className}>, ICloneable<{className}>
|
||||
""");
|
||||
sb.AppendLine("{");
|
||||
sb.AppendLine($"""
|
||||
/// <inheritdoc />
|
||||
public {(isInheritedClonable ? "override" : "virtual")} {className} Clone(GameConfiguration gameConfiguration)
|
||||
""");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine($"""
|
||||
var clone = new {className}();
|
||||
clone.AssignValuesOf(this, gameConfiguration);
|
||||
return clone;
|
||||
""");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($$"""
|
||||
/// <inheritdoc />
|
||||
public {{(isInheritedClonable ? "override" : "virtual")}} void AssignValuesOf(object other, GameConfiguration gameConfiguration)
|
||||
{
|
||||
if (other is {{className}} typedOther)
|
||||
{
|
||||
this.AssignValuesOf(typedOther, gameConfiguration);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void AssignValuesOf({{className}} other, GameConfiguration gameConfiguration)
|
||||
""");
|
||||
|
||||
sb.AppendLine(" {");
|
||||
if (isInheritedClonable)
|
||||
{
|
||||
sb.AppendLine(" base.AssignValuesOf(other, gameConfiguration);");
|
||||
}
|
||||
|
||||
GenerateAssignments(sb, declaredClassSymbol);
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine("}");
|
||||
return sb;
|
||||
}
|
||||
|
||||
private static void GenerateAssignments(StringBuilder sb, INamedTypeSymbol declaredClassSymbol)
|
||||
{
|
||||
var properties = declaredClassSymbol
|
||||
.GetMembers()
|
||||
.OfType<IPropertySymbol>()
|
||||
.Where(p => p.SetMethod is not null)
|
||||
.Where(p => !p.GetAttributes().Any(a => object.Equals(a.AttributeClass?.Name, IgnoreWhenCloningAttributeName)));
|
||||
foreach (var property in properties)
|
||||
{
|
||||
if (property.SetMethod?.DeclaredAccessibility == Accessibility.Protected && property.IsVirtual)
|
||||
{
|
||||
// collection
|
||||
var genericElementType = (property.Type as INamedTypeSymbol)?.TypeArguments.FirstOrDefault();
|
||||
if (genericElementType?.IsValueType ?? false)
|
||||
{
|
||||
sb.AppendLine($" this.{property.Name}.AssignCollection(other.{property.Name});");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($" this.{property.Name}.AssignCollection(other.{property.Name}, gameConfiguration);");
|
||||
}
|
||||
}
|
||||
else if (property.IsVirtual)
|
||||
{
|
||||
sb.AppendLine($" this.{property.Name} = gameConfiguration.GetObjectOfConfig(other.{property.Name});");
|
||||
}
|
||||
else
|
||||
{
|
||||
// for normal value properties, just assign the value
|
||||
sb.AppendLine($" this.{property.Name} = other.{property.Name};");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/SourceGenerators/MUnique.OpenMU.SourceGenerators.csproj
Normal file
24
src/SourceGenerators/MUnique.OpenMU.SourceGenerators.csproj
Normal file
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<Nullable>enable</Nullable>
|
||||
<WarningsAsErrors>nullable;CS4014;VSTHRD110;VSTHRD100</WarningsAsErrors>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
182
src/SourceGenerators/ResourceGenerator.cs
Normal file
182
src/SourceGenerators/ResourceGenerator.cs
Normal file
@@ -0,0 +1,182 @@
|
||||
// <copyright file="ResourceGenerator.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.SourceGenerators;
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="IIncrementalGenerator"/> which creates resource strings for all classes and properties of
|
||||
/// the data model.
|
||||
/// </summary>
|
||||
// [Generator]
|
||||
public class ResourceGenerator : IIncrementalGenerator
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
var typeDeclarations = context.SyntaxProvider
|
||||
.CreateSyntaxProvider(
|
||||
predicate: static (node, _) => node is ClassDeclarationSyntax or EnumDeclarationSyntax,
|
||||
transform: static (ctx, _) => (BaseTypeDeclarationSyntax)ctx.Node)
|
||||
.Collect();
|
||||
|
||||
var projectDirProvider = context.AnalyzerConfigOptionsProvider
|
||||
.Select((options, _) =>
|
||||
{
|
||||
options.GlobalOptions.TryGetValue("build_property.projectdir", out var projectDir);
|
||||
return projectDir;
|
||||
});
|
||||
|
||||
var compilationAndTypes = context.CompilationProvider
|
||||
.Combine(typeDeclarations)
|
||||
.Combine(projectDirProvider);
|
||||
|
||||
context.RegisterSourceOutput(compilationAndTypes, (_, source) =>
|
||||
{
|
||||
var ((compilation, types), projectDir) = source;
|
||||
Execute(compilation, types, projectDir);
|
||||
});
|
||||
}
|
||||
|
||||
private static void Execute(Compilation compilation, ImmutableArray<BaseTypeDeclarationSyntax> types, string? projectDir)
|
||||
{
|
||||
if (types.IsDefaultOrEmpty || string.IsNullOrEmpty(projectDir))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var declaredTypes = new List<(BaseTypeDeclarationSyntax, INamedTypeSymbol)>();
|
||||
|
||||
foreach (var typeDeclaration in types)
|
||||
{
|
||||
var semanticModel = compilation.GetSemanticModel(typeDeclaration.SyntaxTree);
|
||||
var declaredSymbol = semanticModel.GetDeclaredSymbol(typeDeclaration);
|
||||
if (declaredSymbol is INamedTypeSymbol namedTypeSymbol)
|
||||
{
|
||||
declaredTypes.Add((typeDeclaration, namedTypeSymbol));
|
||||
}
|
||||
}
|
||||
|
||||
var sb = StartResourceFile();
|
||||
|
||||
foreach (var (declarationSyntax, namedTypeSymbol) in declaredTypes.OrderBy(tuple => tuple.Item1.Identifier.Text))
|
||||
{
|
||||
AppendResourceStrings(sb, declarationSyntax, namedTypeSymbol);
|
||||
}
|
||||
|
||||
sb.AppendLine("</root>");
|
||||
|
||||
var targetPath = Path.Combine(projectDir!, "Properties", "ModelResources.resx");
|
||||
#pragma warning disable RS1035
|
||||
File.WriteAllText(targetPath, sb.ToString(), Encoding.UTF8);
|
||||
#pragma warning restore RS1035
|
||||
}
|
||||
|
||||
private static StringBuilder StartResourceFile()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"""
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
|
||||
""");
|
||||
return sb;
|
||||
}
|
||||
|
||||
private static void AppendResourceStrings(StringBuilder sb, BaseTypeDeclarationSyntax annotatedClass, INamedTypeSymbol declaredClassSymbol)
|
||||
{
|
||||
switch (annotatedClass)
|
||||
{
|
||||
case ClassDeclarationSyntax classDecl:
|
||||
AppendResourceStrings(sb, classDecl, declaredClassSymbol);
|
||||
break;
|
||||
case EnumDeclarationSyntax enumDecl:
|
||||
AppendResourceStrings(sb, enumDecl);
|
||||
break;
|
||||
default:
|
||||
// do nothing
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void AppendResourceStrings(StringBuilder sb, ClassDeclarationSyntax annotatedClass, INamedTypeSymbol declaredClassSymbol)
|
||||
{
|
||||
var className = annotatedClass.Identifier.Text;
|
||||
|
||||
sb.AppendLine($"""
|
||||
<data name="{className}_TypeCaption" xml:space="preserve">
|
||||
<value>{CaptionHelper.GetTypeCaption(annotatedClass)}</value>
|
||||
</data>
|
||||
<data name="{className}_TypeCaptionPlural" xml:space="preserve">
|
||||
<value>{CaptionHelper.GetPluralizedTypeCaption(annotatedClass)}</value>
|
||||
</data>
|
||||
<data name="{className}_TypeDescription" xml:space="preserve">
|
||||
<value></value>
|
||||
</data>
|
||||
""");
|
||||
|
||||
GenerateProperties(sb, declaredClassSymbol, className);
|
||||
}
|
||||
|
||||
private static void AppendResourceStrings(StringBuilder sb, EnumDeclarationSyntax annotatedEnum)
|
||||
{
|
||||
var enumName = annotatedEnum.Identifier.Text;
|
||||
sb.AppendLine($"""
|
||||
<data name="{enumName}_TypeCaption" xml:space="preserve">
|
||||
<value>{CaptionHelper.SeparateWords(enumName)}</value>
|
||||
</data>
|
||||
<data name="{enumName}_TypeDescription" xml:space="preserve">
|
||||
<value></value>
|
||||
</data>
|
||||
""");
|
||||
|
||||
foreach (var member in annotatedEnum.Members)
|
||||
{
|
||||
sb.AppendLine($"""
|
||||
<data name="{enumName}_{member.Identifier.Text}_Caption" xml:space="preserve">
|
||||
<value>{CaptionHelper.SeparateWords(member.Identifier.Text)}</value>
|
||||
</data>
|
||||
<data name="{enumName}_{member.Identifier.Text}_Description" xml:space="preserve">
|
||||
<value></value>
|
||||
</data>
|
||||
""");
|
||||
}
|
||||
}
|
||||
|
||||
private static void GenerateProperties(StringBuilder sb, INamedTypeSymbol declaredClassSymbol, string className)
|
||||
{
|
||||
var properties = declaredClassSymbol
|
||||
.GetMembers()
|
||||
.OfType<IPropertySymbol>()
|
||||
.Where(ps => ps.DeclaredAccessibility == Accessibility.Public);
|
||||
foreach (var property in properties)
|
||||
{
|
||||
sb.AppendLine($"""
|
||||
<data name="{className}_{property.Name}_Caption" xml:space="preserve">
|
||||
<value>{CaptionHelper.SeparateWords(property.Name)}</value>
|
||||
</data>
|
||||
<data name="{className}_{property.Name}_Description" xml:space="preserve">
|
||||
<value></value>
|
||||
</data>
|
||||
""");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user