Skip to content

Commit

Permalink
Backport '[CanvasEffectProperty]' generator
Browse files Browse the repository at this point in the history
  • Loading branch information
Sergio0694 committed Dec 18, 2024
1 parent 30b2223 commit 418a2e8
Show file tree
Hide file tree
Showing 19 changed files with 1,154 additions and 0 deletions.
27 changes: 27 additions & 0 deletions ComputeSharp.sln
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ComputeSharp.D2D1.CodeFixer
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ComputeSharp.D2D1.Tests.SourceGenerators", "tests\ComputeSharp.D2D1.Tests.SourceGenerators\ComputeSharp.D2D1.Tests.SourceGenerators.csproj", "{59A17380-24A0-4BD7-9012-B105A9E7D46F}"
EndProject
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "ComputeSharp.D2D1.UI.SourceGenerators", "src\ComputeSharp.D2D1.UI.SourceGenerators\ComputeSharp.D2D1.UI.SourceGenerators.shproj", "{D20E610F-EB37-46E7-B028-04784D7400D5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ComputeSharp.D2D1.Uwp.SourceGenerators", "src\ComputeSharp.D2D1.Uwp.SourceGenerators\ComputeSharp.D2D1.Uwp.SourceGenerators.csproj", "{3FEE9A61-27F6-415B-B601-2B445B81FC97}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ComputeSharp.D2D1.WinUI.SourceGenerators", "src\ComputeSharp.D2D1.WinUI.SourceGenerators\ComputeSharp.D2D1.WinUI.SourceGenerators.csproj", "{BCA23B96-7C93-4021-B30F-DC0B44A8F228}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Expand Down Expand Up @@ -474,6 +480,22 @@ Global
{59A17380-24A0-4BD7-9012-B105A9E7D46F}.Release|ARM64.Build.0 = Release|Any CPU
{59A17380-24A0-4BD7-9012-B105A9E7D46F}.Release|x64.ActiveCfg = Release|Any CPU
{59A17380-24A0-4BD7-9012-B105A9E7D46F}.Release|x64.Build.0 = Release|Any CPU
{3FEE9A61-27F6-415B-B601-2B445B81FC97}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{3FEE9A61-27F6-415B-B601-2B445B81FC97}.Debug|ARM64.Build.0 = Debug|Any CPU
{3FEE9A61-27F6-415B-B601-2B445B81FC97}.Debug|x64.ActiveCfg = Debug|Any CPU
{3FEE9A61-27F6-415B-B601-2B445B81FC97}.Debug|x64.Build.0 = Debug|Any CPU
{3FEE9A61-27F6-415B-B601-2B445B81FC97}.Release|ARM64.ActiveCfg = Release|Any CPU
{3FEE9A61-27F6-415B-B601-2B445B81FC97}.Release|ARM64.Build.0 = Release|Any CPU
{3FEE9A61-27F6-415B-B601-2B445B81FC97}.Release|x64.ActiveCfg = Release|Any CPU
{3FEE9A61-27F6-415B-B601-2B445B81FC97}.Release|x64.Build.0 = Release|Any CPU
{BCA23B96-7C93-4021-B30F-DC0B44A8F228}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{BCA23B96-7C93-4021-B30F-DC0B44A8F228}.Debug|ARM64.Build.0 = Debug|Any CPU
{BCA23B96-7C93-4021-B30F-DC0B44A8F228}.Debug|x64.ActiveCfg = Debug|Any CPU
{BCA23B96-7C93-4021-B30F-DC0B44A8F228}.Debug|x64.Build.0 = Debug|Any CPU
{BCA23B96-7C93-4021-B30F-DC0B44A8F228}.Release|ARM64.ActiveCfg = Release|Any CPU
{BCA23B96-7C93-4021-B30F-DC0B44A8F228}.Release|ARM64.Build.0 = Release|Any CPU
{BCA23B96-7C93-4021-B30F-DC0B44A8F228}.Release|x64.ActiveCfg = Release|Any CPU
{BCA23B96-7C93-4021-B30F-DC0B44A8F228}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -529,6 +551,8 @@ Global
samples\ComputeSharp.SwapChain.Shaders.D2D1.Shared\ComputeSharp.SwapChain.Shaders.D2D1.Shared.projitems*{209c95a3-fa53-431b-b688-3299ca6c29d2}*SharedItemsImports = 5
src\TerraFX.Interop.Windows\TerraFX.Interop.Windows.projitems*{314eb353-e7e3-4146-8111-685cc83ec1de}*SharedItemsImports = 5
tests\ComputeSharp.D2D1.UI.Tests\ComputeSharp.D2D1.UI.Tests.projitems*{346aeb10-a4ea-427e-8578-f60915db3053}*SharedItemsImports = 13
src\ComputeSharp.D2D1.UI.SourceGenerators\ComputeSharp.D2D1.UI.SourceGenerators.projitems*{3fee9a61-27f6-415b-b601-2b445b81fc97}*SharedItemsImports = 5
src\ComputeSharp.SourceGeneration\ComputeSharp.SourceGeneration.projitems*{3fee9a61-27f6-415b-b601-2b445b81fc97}*SharedItemsImports = 5
samples\ComputeSharp.SwapChain.Shaders.D2D1.Shared\ComputeSharp.SwapChain.Shaders.D2D1.Shared.projitems*{4d4bb2f6-5653-4db5-a8dd-90d58d8fe4d3}*SharedItemsImports = 4
src\ComputeSharp.NetStandard\ComputeSharp.NetStandard.projitems*{4d4bb2f6-5653-4db5-a8dd-90d58d8fe4d3}*SharedItemsImports = 4
src\TerraFX.Interop.Windows.D2D1\TerraFX.Interop.Windows.D2D1.projitems*{4d4bb2f6-5653-4db5-a8dd-90d58d8fe4d3}*SharedItemsImports = 4
Expand All @@ -547,10 +571,13 @@ Global
samples\ComputeSharp.SwapChain.Shaders.D2D1.Shared\ComputeSharp.SwapChain.Shaders.D2D1.Shared.projitems*{9ea5ae9d-c39a-4f43-b03e-0a848ea2558a}*SharedItemsImports = 5
src\ComputeSharp.NetStandard\ComputeSharp.NetStandard.projitems*{a5997814-d8d2-4541-87ac-c3e53caa67c0}*SharedItemsImports = 13
src\ComputeSharp.UI\ComputeSharp.UI.projitems*{afaafdc9-be42-45d4-8c79-0ee8c8edbf52}*SharedItemsImports = 5
src\ComputeSharp.D2D1.UI.SourceGenerators\ComputeSharp.D2D1.UI.SourceGenerators.projitems*{bca23b96-7c93-4021-b30f-dc0b44a8f228}*SharedItemsImports = 5
src\ComputeSharp.SourceGeneration\ComputeSharp.SourceGeneration.projitems*{bca23b96-7c93-4021-b30f-dc0b44a8f228}*SharedItemsImports = 5
src\ComputeSharp.D2D1.UI\ComputeSharp.D2D1.UI.projitems*{bd9e6556-357e-4c20-bfcd-fb131f9372fa}*SharedItemsImports = 5
src\ComputeSharp.NetStandard\ComputeSharp.NetStandard.projitems*{bfb003d1-788b-48de-8065-39479c162cb0}*SharedItemsImports = 5
src\ComputeSharp.SourceGeneration\ComputeSharp.SourceGeneration.projitems*{bfb003d1-788b-48de-8065-39479c162cb0}*SharedItemsImports = 5
samples\ComputeSharp.SwapChain.Shaders.Shared\ComputeSharp.SwapChain.Shaders.Shared.projitems*{c12d7ace-98ed-4813-8118-6667c34f484f}*SharedItemsImports = 5
src\ComputeSharp.D2D1.UI.SourceGenerators\ComputeSharp.D2D1.UI.SourceGenerators.projitems*{d20e610f-eb37-46e7-b028-04784d7400d5}*SharedItemsImports = 13
src\ComputeSharp.NetStandard\ComputeSharp.NetStandard.projitems*{e44053bd-a761-47fb-aa78-087a599672ea}*SharedItemsImports = 5
src\ComputeSharp.SourceGeneration.Hlsl\ComputeSharp.SourceGeneration.Hlsl.projitems*{e44053bd-a761-47fb-aa78-087a599672ea}*SharedItemsImports = 5
src\ComputeSharp.SourceGeneration\ComputeSharp.SourceGeneration.projitems*{e44053bd-a761-47fb-aa78-087a599672ea}*SharedItemsImports = 5
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
using System.Threading;
#if WINDOWS_UWP
using ComputeSharp.D2D1.Uwp.SourceGenerators.Constants;
using ComputeSharp.D2D1.Uwp.SourceGenerators.Models;
#else
using ComputeSharp.D2D1.WinUI.SourceGenerators.Constants;
using ComputeSharp.D2D1.WinUI.SourceGenerators.Models;
#endif
using ComputeSharp.SourceGeneration.Extensions;
using ComputeSharp.SourceGeneration.Helpers;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

#if WINDOWS_UWP
namespace ComputeSharp.D2D1.Uwp.SourceGenerators;
#else
namespace ComputeSharp.D2D1.WinUI.SourceGenerators;
#endif

/// <inheritdoc/>
partial class CanvasEffectPropertyGenerator
{
/// <summary>
/// A container for all the logic for <see cref="CanvasEffectPropertyGenerator"/>.
/// </summary>
private static partial class Execute
{
/// <summary>
/// Checks whether an input syntax node is a candidate property declaration for the generator.
/// </summary>
/// <param name="node">The input syntax node to check.</param>
/// <param name="token">The <see cref="CancellationToken"/> used to cancel the operation, if needed.</param>
/// <returns>Whether <paramref name="node"/> is a candidate property declaration.</returns>
public static bool IsCandidatePropertyDeclaration(SyntaxNode node, CancellationToken token)
{
// The node must be a property declaration with two accessors
if (node is not PropertyDeclarationSyntax { AccessorList.Accessors: { Count: 2 } accessors } property)
{
return false;
}

// The property must be partial (we'll check that it's a declaration from its symbol)
if (!property.Modifiers.Any(SyntaxKind.PartialKeyword))
{
return false;
}

// Static properties are not supported
if (property.Modifiers.Any(SyntaxKind.StaticKeyword))
{
return false;
}

// The accessors must be a get and a set (with any accessibility)
if (accessors[0].Kind() is not (SyntaxKind.GetAccessorDeclaration or SyntaxKind.SetAccessorDeclaration) ||
accessors[1].Kind() is not (SyntaxKind.GetAccessorDeclaration or SyntaxKind.SetAccessorDeclaration))
{
return false;
}

// The property must be in a type with a base type (as it must derive from CanvasEffect)
return node.Parent?.IsTypeDeclarationWithOrPotentiallyWithBaseTypes<ClassDeclarationSyntax>() == true;
}

/// <summary>
/// Tries to get the accessibility of the property and accessors, if possible.
/// </summary>
/// <param name="node">The input <see cref="PropertyDeclarationSyntax"/> node.</param>
/// <param name="symbol">The input <see cref="IPropertySymbol"/> instance.</param>
/// <param name="declaredAccessibility">The accessibility of the property, if available.</param>
/// <param name="getterAccessibility">The accessibility of the <see langword="get"/> accessor, if available.</param>
/// <param name="setterAccessibility">The accessibility of the <see langword="set"/> accessor, if available.</param>
/// <returns>Whether the property was valid and the accessibilities could be retrieved.</returns>
public static bool TryGetAccessibilityModifiers(
PropertyDeclarationSyntax node,
IPropertySymbol symbol,
out Accessibility declaredAccessibility,
out Accessibility getterAccessibility,
out Accessibility setterAccessibility)
{
declaredAccessibility = Accessibility.NotApplicable;
getterAccessibility = Accessibility.NotApplicable;
setterAccessibility = Accessibility.NotApplicable;

// Ensure that we have a getter and a setter, and that the setter is not init-only
if (symbol is not { GetMethod: { } getMethod, SetMethod: { IsInitOnly: false } setMethod })
{
return false;
}

// Track the property accessibility if explicitly set
if (node.Modifiers.Count > 0)
{
declaredAccessibility = symbol.DeclaredAccessibility;
}

// Track the accessors accessibility, if explicitly set
foreach (AccessorDeclarationSyntax accessor in node.AccessorList?.Accessors ?? [])
{
if (accessor.Modifiers.Count == 0)
{
continue;
}

switch (accessor.Kind())
{
case SyntaxKind.GetAccessorDeclaration:
getterAccessibility = getMethod.DeclaredAccessibility;
break;
case SyntaxKind.SetAccessorDeclaration:
setterAccessibility = setMethod.DeclaredAccessibility;
break;
}
}

return true;
}

/// <summary>
/// Gets the invalidation type to use for the generated effect property.
/// </summary>
/// <param name="attributeData">The <see cref="AttributeData"/> instance for the processed attribute.</param>
/// <returns>The resulting <see cref="CanvasEffectInvalidationType"/> to use for the generated property.</returns>
public static CanvasEffectInvalidationType GetCanvasEffectInvalidationType(AttributeData attributeData)
{
if (attributeData.ConstructorArguments is [{ Kind: TypedConstantKind.Enum, Value: byte enumValue }])
{
return (CanvasEffectInvalidationType)enumValue;
}

// No constructor parameter, or an invalid one. In this case we either just use the default
// invalidation mode, or let the analyzer emit a diagnostic to let the user know.
return CanvasEffectInvalidationType.Update;
}

/// <summary>
/// Writes all implementations of partial effect property declarations.
/// </summary>
/// <param name="properties">The input set of declared effect properties.</param>
/// <param name="writer">The <see cref="IndentedTextWriter"/> instance to write into.</param>
public static void WritePropertyDeclarations(EquatableArray<CanvasEffectPropertyInfo> properties, IndentedTextWriter writer)
{
// Helper to get the nullable type name for the initial property value
static string GetOldValueTypeNameAsNullable(CanvasEffectPropertyInfo propertyInfo)
{
// Prepare the nullable type for the previous property value. This is needed because if the type is a reference
// type, the previous value might be null even if the property type is not nullable, as the first invocation would
// happen when the property is first set to some value that is not null (but the backing field would still be so).
// As a cheap way to check whether we need to add nullable, we can simply check whether the type name with nullability
// annotations ends with a '?'. If it doesn't and the type is a reference type, we add it. Otherwise, we keep it.
return propertyInfo.IsReferenceTypeOrUnconstraindTypeParameter switch
{
true when !propertyInfo.TypeNameWithNullabilityAnnotations.EndsWith("?")
=> $"{propertyInfo.TypeNameWithNullabilityAnnotations}?",
_ => propertyInfo.TypeNameWithNullabilityAnnotations
};
}

// Helper to get the accessibility with a trailing space
static string GetExpressionWithTrailingSpace(Accessibility accessibility)
{
return accessibility.GetExpression() switch
{
{ Length: > 0 } expression => expression + " ",
_ => ""
};
}

// First, generate all partial property implementations at the top of the partial type declaration
foreach (CanvasEffectPropertyInfo propertyInfo in properties)
{
string oldValueTypeNameAsNullable = GetOldValueTypeNameAsNullable(propertyInfo);

writer.WriteLine("/// <inheritdoc/>");
writer.WriteGeneratedAttributes(GeneratorName);
writer.Write(GetExpressionWithTrailingSpace(propertyInfo.DeclaredAccessibility));
writer.WriteIf(propertyInfo.IsRequired, "required ");
writer.WriteLine($"partial {propertyInfo.TypeNameWithNullabilityAnnotations} {propertyInfo.PropertyName}");
writer.WriteLine($$"""
{
{{GetExpressionWithTrailingSpace(propertyInfo.GetterAccessibility)}}get => field;
{{GetExpressionWithTrailingSpace(propertyInfo.SetterAccessibility)}}set
{
if (global::System.Collections.Generic.EqualityComparer<{{oldValueTypeNameAsNullable}}>.Default.Equals(field, value))
{
return;
}
{{oldValueTypeNameAsNullable}} oldValue = field;
On{{propertyInfo.PropertyName}}Changing(value);
On{{propertyInfo.PropertyName}}Changing(oldValue, value);
field = value;
On{{propertyInfo.PropertyName}}Changed(value);
On{{propertyInfo.PropertyName}}Changed(oldValue, value);
InvalidateEffectGraph(global::{{WellKnownTypeNames.CanvasEffectInvalidationType}}.{{propertyInfo.InvalidationType}});
}
}
""", isMultiline: true);
writer.WriteLine();
}

// Next, emit all partial method declarations at the bottom of the file
foreach (CanvasEffectPropertyInfo propertyInfo in properties)
{
// On<PROPERTY_NAME>Changing, only with new value
writer.WriteLine(skipIfPresent: true);
writer.WriteLine($"""
/// <summary>Executes the logic for when <see cref="{propertyInfo.PropertyName}"/> is changing.</summary>
/// <param name="value">The new property value being set.</param>
/// <remarks>This method is invoked right before the value of <see cref="{propertyInfo.PropertyName}"/> is changed.</remarks>
""", isMultiline: true);
writer.WriteGeneratedAttributes(GeneratorName, includeNonUserCodeAttributes: false);
writer.WriteLine($"partial void On{propertyInfo.PropertyName}Changing({propertyInfo.TypeNameWithNullabilityAnnotations} newValue);");

string oldValueTypeNameAsNullable = GetOldValueTypeNameAsNullable(propertyInfo);

// On<PROPERTY_NAME>Changing, with both values
writer.WriteLine();
writer.WriteLine($"""
/// <summary>Executes the logic for when <see cref="{propertyInfo.PropertyName}"/> is changing.</summary>
/// <param name="oldValue">The previous property value that is being replaced.</param>
/// <param name="newValue">The new property value being set.</param>
/// <remarks>This method is invoked right before the value of <see cref="{propertyInfo.PropertyName}"/> is changed.</remarks>
""", isMultiline: true);
writer.WriteGeneratedAttributes(GeneratorName, includeNonUserCodeAttributes: false);
writer.WriteLine($"partial void On{propertyInfo.PropertyName}Changing({oldValueTypeNameAsNullable} oldValue, {propertyInfo.TypeNameWithNullabilityAnnotations} newValue);");

// On<PROPERTY_NAME>Changed, only with new value
writer.WriteLine();
writer.WriteLine($"""
/// <summary>Executes the logic for when <see cref="{propertyInfo.PropertyName}"/> has just changed.</summary>
/// <param name="value">The new property value that has been set.</param>
/// <remarks>This method is invoked right after the value of <see cref="{propertyInfo.PropertyName}"/> is changed.</remarks>
""", isMultiline: true);
writer.WriteGeneratedAttributes(GeneratorName, includeNonUserCodeAttributes: false);
writer.WriteLine($"partial void On{propertyInfo.PropertyName}Changed({propertyInfo.TypeNameWithNullabilityAnnotations} newValue);");

// On<PROPERTY_NAME>Changed, with both values
writer.WriteLine();
writer.WriteLine($"""
/// <summary>Executes the logic for when <see cref="{propertyInfo.PropertyName}"/> has just changed.</summary>
/// <param name="oldValue">The previous property value that has been replaced.</param>
/// <param name="newValue">The new property value that has been set.</param>
/// <remarks>This method is invoked right after the value of <see cref="{propertyInfo.PropertyName}"/> is changed.</remarks>
""", isMultiline: true);
writer.WriteGeneratedAttributes(GeneratorName, includeNonUserCodeAttributes: false);
writer.WriteLine($"partial void On{propertyInfo.PropertyName}Changed({oldValueTypeNameAsNullable} oldValue, {propertyInfo.TypeNameWithNullabilityAnnotations} newValue);");
}
}
}
}
Loading

0 comments on commit 418a2e8

Please sign in to comment.