Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ Rule ID | Category | Severity | Notes
--------|----------|----------|-------
BESG001 | Beutl.Engine.SourceGenerators | Warning | EngineObjectResourceGenerator
BESG002 | Beutl.Engine.SourceGenerators | Warning | FallbackTypeGenerator
BESG003 | Beutl.Engine.SourceGenerators | Error | EngineObjectResourceGenerator
BESG004 | Beutl.Engine.SourceGenerators | Error | EngineObjectResourceGenerator
BESG005 | Beutl.Engine.SourceGenerators | Error | EngineObjectResourceGenerator
BESG006 | Beutl.Engine.SourceGenerators | Error | EngineObjectResourceGenerator
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,36 @@ public static class DiagnosticDescriptors
category: "Beutl.Engine.SourceGenerators",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor ResourcePropertyMissingInitializer = new(
id: "BESG003",
title: "Stable resource property declaration required",
messageFormat: "Property '{0}.{1}' must expose one stable declaration-time IProperty so detached Resource defaults can be generated; use a declaration initializer or a readonly computed backing field, do not replace it in a constructor, add a valid [ResourceDefaultValuesProvider] factory, or suppress Resource generation and implement Resource/ToResource manually",
category: "Beutl.Engine.SourceGenerators",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor ResourcePrimaryConstructorNotSupported = new(
id: "BESG004",
title: "Primary constructor is incompatible with generated Resource defaults",
messageFormat: "Type '{0}' cannot use a primary constructor with initializer-only detached Resource defaults; add a valid [ResourceDefaultValuesProvider] factory, move to an ordinary constructor, or suppress Resource generation and implement Resource/ToResource manually",
category: "Beutl.Engine.SourceGenerators",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor ResourceDefaultValuesProviderInvalid = new(
id: "BESG005",
title: "Resource defaults provider signature is invalid",
messageFormat: "Type '{0}' must declare exactly one [ResourceDefaultValuesProvider] method that is static, parameterless, non-generic, and returns '{0}'",
category: "Beutl.Engine.SourceGenerators",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor ResourceDefaultValuesProviderRequiredOnDerivedType = new(
id: "BESG006",
title: "Derived resource defaults provider required",
messageFormat: "Type '{0}' derives from a type with an explicit resource defaults provider and must declare its own [ResourceDefaultValuesProvider] factory so inherited detached defaults are not evaluated through the initializer-only path",
category: "Beutl.Engine.SourceGenerators",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
}
78 changes: 78 additions & 0 deletions src/Beutl.Engine.SourceGenerators/Emit/ResourceClassEmitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public static void Emit(StringBuilder sb, string indent, string currentTypeDispl

string innerIndent = indent + " ";

EmitConstructors(sb, innerIndent, currentTypeDisplay, info);
EmitFields(sb, innerIndent, info);
EmitProperties(sb, innerIndent, info);
EmitGetOriginal(sb, innerIndent, currentTypeDisplay);
Expand All @@ -48,6 +49,83 @@ public static void Emit(StringBuilder sb, string indent, string currentTypeDispl
sb.Append(indent).AppendLine("}");
}

private static void EmitConstructors(
StringBuilder sb,
string innerIndent,
string currentTypeDisplay,
ClassInfo info)
{
if (!info.Symbol.IsAbstract)
{
sb.Append(innerIndent)
.Append("internal static Resource __CreateAttached")
.Append(info.Symbol.Name)
.AppendLine("()");
sb.Append(innerIndent).AppendLine(" => new(skipDefaultInitialization: true);");
sb.AppendLine();

sb.Append(innerIndent).AppendLine("public Resource()");
sb.Append(innerIndent)
.Append(" : this(")
.Append(currentTypeDisplay)
.AppendLine(".__CreateResourceDefaultValues())");
sb.Append(innerIndent).AppendLine("{");
sb.Append(innerIndent).AppendLine("}");
sb.AppendLine();
}

sb.Append(innerIndent)
.Append("protected Resource(")
.Append(currentTypeDisplay)
.AppendLine(" defaultValues)");
sb.Append(innerIndent).AppendLine(" : base(defaultValues)");
sb.Append(innerIndent).AppendLine("{");
foreach (ValuePropertyInfo property in info.ValueProperties)
{
if (property.ExcludeFromResource) continue;

string fieldName = EmitHelpers.ToFieldName(property.Name);
sb.Append(innerIndent)
.Append(" ")
.Append(fieldName)
.Append(" = defaultValues.")
.Append(property.Name)
.AppendLine(".DefaultValue;");
}
foreach (ObjectPropertyInfo property in info.ObjectProperties)
{
if (property.ExcludeFromResource) continue;

string fieldName = EmitHelpers.ToFieldName(property.Name);
string resourceType = EmitHelpers.GetResourceTypeName(property.ValueType);
string localName = fieldName + "DefaultValue";
sb.Append(innerIndent)
.Append(" if (defaultValues.")
.Append(property.Name)
.Append(".DefaultValue is { } ")
.Append(localName)
.AppendLine(")");
sb.Append(innerIndent).AppendLine(" {");
sb.Append(innerIndent)
.Append(" ")
.Append(fieldName)
.Append(" = (")
.Append(resourceType)
.Append(")")
.Append(localName)
.AppendLine(".ToResource(global::Beutl.Composition.CompositionContext.Default);");
sb.Append(innerIndent).AppendLine(" }");
}
sb.Append(innerIndent).AppendLine("}");
sb.AppendLine();

sb.Append(innerIndent).AppendLine("protected Resource(bool skipDefaultInitialization)");
sb.Append(innerIndent).AppendLine(" : base(skipDefaultInitialization)");
sb.Append(innerIndent).AppendLine("{");
sb.Append(innerIndent).AppendLine("}");
sb.AppendLine();
}

private static void EmitFields(StringBuilder sb, string innerIndent, ClassInfo info)
{
foreach (ValuePropertyInfo property in info.ValueProperties)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System.Text;

using Beutl.Engine.SourceGenerators.Models;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

namespace Beutl.Engine.SourceGenerators.Emit;

public static class ResourceDefaultValuesEmitter
{
public static void Emit(
StringBuilder sb,
string indent,
string currentTypeDisplay,
ClassInfo info,
string? defaultsProviderMethod)
{
if (info.SuppressedResourceGeneration) return;

if (defaultsProviderMethod is not null)
{
EmitProviderFactory(sb, indent, currentTypeDisplay, defaultsProviderMethod);
return;
}

string constructorAccessibility = info.Symbol.IsSealed ? "private" : "protected";
string constructionType =
"global::Beutl.Engine.EngineObject.ResourceDefaultValuesConstruction";

IMethodSymbol? implicitParameterlessConstructor = info.Symbol.InstanceConstructors
.FirstOrDefault(static constructor =>
constructor.IsImplicitlyDeclared && constructor.Parameters.Length == 0);
if (implicitParameterlessConstructor is not null)
{
sb.Append(indent)
.Append(EmitHelpers.GetAccessibility(implicitParameterlessConstructor.DeclaredAccessibility))
.Append(' ')
.Append(info.Symbol.Name)
.AppendLine("()");
sb.Append(indent).AppendLine("{");
sb.Append(indent).AppendLine("}");
sb.AppendLine();
}

sb.Append(indent)
.AppendLine("[global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers]");
sb.Append(indent)
.Append(constructorAccessibility)
.Append(' ')
.Append(info.Symbol.Name)
.Append('(')
.Append(constructionType)
.AppendLine(" construction)");
sb.Append(indent).AppendLine(" : base(construction)");
sb.Append(indent).AppendLine("{");
sb.Append(indent).AppendLine("}");

if (!info.Symbol.IsAbstract)
{
sb.AppendLine();
sb.Append(indent)
.Append("private static ")
.Append(currentTypeDisplay)
.AppendLine(" __CreateResourceDefaultValues()");
sb.Append(indent)
.Append(" => new(default(")
.Append(constructionType)
.AppendLine("));");
}
}

private static void EmitProviderFactory(
StringBuilder sb,
string indent,
string currentTypeDisplay,
string defaultsProviderMethod)
{
string escapedMethodName = SyntaxFacts.GetKeywordKind(defaultsProviderMethod) == SyntaxKind.None
? defaultsProviderMethod
: "@" + defaultsProviderMethod;
sb.Append(indent)
.Append("private static ")
.Append(currentTypeDisplay)
.AppendLine(" __CreateResourceDefaultValues()");
sb.Append(indent)
.Append(" => ")
.Append(currentTypeDisplay)
.Append('.')
.Append(escapedMethodName)
.AppendLine("();");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ public static void Emit(StringBuilder sb, string indent, string currentTypeDispl
{
sb.Append(indent).AppendLine($"public override {currentTypeDisplay}.Resource ToResource({renderContextType} context)");
sb.Append(indent).AppendLine("{");
sb.Append(indent).AppendLine($" var resource = new {currentTypeDisplay}.Resource();");
sb.Append(indent)
.Append(" var resource = ")
.Append(currentTypeDisplay)
.Append(".Resource.__CreateAttached")
.Append(info.Symbol.Name)
.AppendLine("();");
sb.Append(indent).AppendLine(" bool updateOnly = true;");
sb.Append(indent).AppendLine(" resource.Update(this, context, ref updateOnly);");
sb.Append(indent).AppendLine($" return resource;");
Expand Down
Loading