Skip to content

Commit

Permalink
Source Generator for Bindable Property (#1321)
Browse files Browse the repository at this point in the history
* Created BindablePropertySG

* Added as reference for MCT.Controls project

* Added inlinedocs

* used BPSG on Expander control

* added BindableSG on the sample sln

* Rename to `CommunityToolkit,.Maui.SourceGenerators.Internal`

* changed string to char

* Move `CommunityToolkit.Maui.SourceGenerators.Internal` to `Analyzers` Solution Folder

* `NullReferenceException` -> `InvalidOperationException`

* Fix field naming

* Remove Unnecessary pragma

* `foreach` -> `for` loop, `NullReferenceException` -> `ArgumentException`, Use Collection Expressions, Replace Strings with `nameof`

* Update BindablePropertyAttributeSourceGenerator.cs

* Remove Unused Solution

* Add `Camera` Analyzers to `Analyzers` Solution Folder

* Add `Camera` Analyzers to `Analyzers` Solution Folder

---------

Co-authored-by: Brandon Minnick <13558917+brminnick@users.noreply.github.com>
  • Loading branch information
pictos and brminnick authored Aug 2, 2024
1 parent 636cf46 commit 2baab31
Show file tree
Hide file tree
Showing 13 changed files with 774 additions and 95 deletions.
23 changes: 15 additions & 8 deletions samples/CommunityToolkit.Maui.Sample.sln
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
# 17
VisualStudioVersion = 17.0.31521.260
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Maui.UnitTests", "..\src\CommunityToolkit.Maui.UnitTests\CommunityToolkit.Maui.UnitTests.csproj", "{7D49CC16-93CF-471B-B1FA-0BA44DECC15D}"
Expand Down Expand Up @@ -43,9 +43,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Maui.Media
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Maui.Camera", "..\src\CommunityToolkit.Maui.Camera\CommunityToolkit.Maui.Camera.csproj", "{7F1458F2-BA8C-4BE0-ACBF-C0A2835E588A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Maui.Camera.Analyzers", "..\src\CommunityToolkit.Maui.Camera.Analyzers\CommunityToolkit.Maui.Camera.Analyzers.csproj", "{56D98FC8-5FC2-4231-9161-9FD2672E8B6E}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Maui.SourceGenerators.Internal", "..\src\CommunityToolkit.Maui.SourceGenerators.Internal\CommunityToolkit.Maui.SourceGenerators.Internal.csproj", "{8CDCF66E-D969-4BFD-A6E3-816BBE5F80B6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Maui.Camera.Analyzers.CodeFixes", "..\src\CommunityToolkit.Maui.Camera.Analyzers.CodeFixes\CommunityToolkit.Maui.Camera.Analyzers.CodeFixes.csproj", "{372D6A40-A4E0-434A-A463-C001441C68EB}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Maui.Camera.Analyzers.CodeFixes", "..\src\CommunityToolkit.Maui.Camera.Analyzers.CodeFixes\CommunityToolkit.Maui.Camera.Analyzers.CodeFixes.csproj", "{372D6A40-A4E0-434A-A463-C001441C68EB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Maui.Camera.Analyzers", "..\src\CommunityToolkit.Maui.Camera.Analyzers\CommunityToolkit.Maui.Camera.Analyzers.csproj", "{02C5B93A-B8D6-421D-B0EA-D0CC41A00F0B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -109,14 +111,18 @@ Global
{7F1458F2-BA8C-4BE0-ACBF-C0A2835E588A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7F1458F2-BA8C-4BE0-ACBF-C0A2835E588A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7F1458F2-BA8C-4BE0-ACBF-C0A2835E588A}.Release|Any CPU.Build.0 = Release|Any CPU
{56D98FC8-5FC2-4231-9161-9FD2672E8B6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{56D98FC8-5FC2-4231-9161-9FD2672E8B6E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{56D98FC8-5FC2-4231-9161-9FD2672E8B6E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{56D98FC8-5FC2-4231-9161-9FD2672E8B6E}.Release|Any CPU.Build.0 = Release|Any CPU
{8CDCF66E-D969-4BFD-A6E3-816BBE5F80B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8CDCF66E-D969-4BFD-A6E3-816BBE5F80B6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8CDCF66E-D969-4BFD-A6E3-816BBE5F80B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8CDCF66E-D969-4BFD-A6E3-816BBE5F80B6}.Release|Any CPU.Build.0 = Release|Any CPU
{372D6A40-A4E0-434A-A463-C001441C68EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{372D6A40-A4E0-434A-A463-C001441C68EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{372D6A40-A4E0-434A-A463-C001441C68EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{372D6A40-A4E0-434A-A463-C001441C68EB}.Release|Any CPU.Build.0 = Release|Any CPU
{02C5B93A-B8D6-421D-B0EA-D0CC41A00F0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{02C5B93A-B8D6-421D-B0EA-D0CC41A00F0B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{02C5B93A-B8D6-421D-B0EA-D0CC41A00F0B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{02C5B93A-B8D6-421D-B0EA-D0CC41A00F0B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -130,7 +136,8 @@ Global
{60B976B2-F3FA-494E-A28B-7BED2EAE990E} = {9F7D54C0-EA17-409A-804F-B2E8D7F4A7F3}
{85B875BD-62F6-4EC3-BCFF-4DC25E94BCAE} = {9BFC4026-BC8F-43E2-BAA9-5BC2D764D37D}
{215083C8-D9CA-48FA-8B0A-1D21A989D055} = {9BFC4026-BC8F-43E2-BAA9-5BC2D764D37D}
{56D98FC8-5FC2-4231-9161-9FD2672E8B6E} = {9BFC4026-BC8F-43E2-BAA9-5BC2D764D37D}
{8CDCF66E-D969-4BFD-A6E3-816BBE5F80B6} = {9BFC4026-BC8F-43E2-BAA9-5BC2D764D37D}
{02C5B93A-B8D6-421D-B0EA-D0CC41A00F0B} = {9BFC4026-BC8F-43E2-BAA9-5BC2D764D37D}
{372D6A40-A4E0-434A-A463-C001441C68EB} = {9BFC4026-BC8F-43E2-BAA9-5BC2D764D37D}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
<IsPackable>false</IsPackable>
<IsRoslynComponent>true</IsRoslynComponent>

<!-- Fixes https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/Microsoft.CodeAnalysis.Analyzers.md#rs1036-specify-analyzer-banned-api-enforcement-setting -->
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>

<!-- Avoid ID conflicts with the package project. -->
<PackageId>*$(MSBuildProjectFile)*</PackageId>
</PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.UI.Xaml.Controls;
using Microsoft.Web.WebView2.Core;
using Windows.Devices.Geolocation;
using Microsoft.Maui.Handlers;
using IMap = Microsoft.Maui.Maps.IMap;

namespace CommunityToolkit.Maui.Maps.Handlers;
Expand Down Expand Up @@ -48,7 +49,7 @@ protected override FrameworkElement CreatePlatformView()
}

var mapPage = GetMapHtmlPage(MapsKey);
var webView = new MauiWebView();
var webView = new MauiWebView(new WebViewHandler());
webView.NavigationCompleted += WebViewNavigationCompleted;
webView.WebMessageReceived += WebViewWebMessageReceived;
webView.LoadHtml(mapPage, null);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
using System.Collections.Immutable;
using System.Runtime.CompilerServices;
using System.Text;
using CommunityToolkit.Maui.SourceGenerators.Internal.Helpers;
using CommunityToolkit.Maui.SourceGenerators.Internal.Models;
using CommunityToolkit.Maui.SourceGenerators.Helpers;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;

namespace CommunityToolkit.Maui.SourceGenerators.Internal;

[Generator]
public class BindablePropertyAttributeSourceGenerator : IIncrementalGenerator
{
static readonly SemanticValues emptySemanticValues = new(default, []);

const string bpFullName = "global::Microsoft.Maui.Controls.BindableProperty";
const string bindingModeFullName = "global::Microsoft.Maui.Controls.";

const string bpAttribute = """
#nullable enable
namespace CommunityToolkit.Maui;

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
sealed class BindablePropertyAttribute<TReturnType> : Attribute
{
public string PropertyName { get; } = string.Empty;
public Type? DeclaringType { get; set; }
public object? DefaultValue { get; set; }
public string DefaultBindingMode { get; set; } = string.Empty;
public string ValidateValueMethodName { get; set; } = string.Empty;
public string PropertyChangedMethodName { get; set; } = string.Empty;
public string PropertyChangingMethodName { get; set; } = string.Empty;
public string CoerceValueMethodName { get; set; } = string.Empty;
public string DefaultValueCreatorMethodName { get; set; } = string.Empty;

public BindablePropertyAttribute(string propertyName)
{
PropertyName = propertyName;
}
}
""";

public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.RegisterPostInitializationOutput(ctx => ctx.AddSource("BindablePropertyAttribute.g.cs", SourceText.From(bpAttribute, Encoding.UTF8)));

var provider = context.SyntaxProvider.ForAttributeWithMetadataName("CommunityToolkit.Maui.BindablePropertyAttribute`1",
SyntaxPredicate, SemanticTransform)
.Where(static x => x.ClassInformation != default || !x.BindableProperties.IsEmpty)
.Collect()
.SelectMany(static (types, _) => types);


context.RegisterSourceOutput(provider, Execute);
}

void Execute(SourceProductionContext context, SemanticValues semanticValues)
{
var source = GenerateSource(semanticValues);
SourceStringService.FormatText(ref source);
context.AddSource($"{semanticValues.ClassInformation.ClassName}.g.cs", SourceText.From(source, Encoding.UTF8));
}

static string GenerateSource(SemanticValues value)
{
var sb = new StringBuilder($@"
// <auto-generated>
// Test2 : {DateTime.Now}
namespace {value.ClassInformation.ContainingNamespace};
{value.ClassInformation.DeclaredAccessibility} partial class {value.ClassInformation.ClassName}
{{
");

foreach (var info in value.BindableProperties)
{
GenerateBindableProperty(sb, info);
GenerateProperty(sb, info);
}

sb.AppendLine().Append('}');
return sb.ToString();

static void GenerateBindableProperty(StringBuilder sb, BindablePropertyModel info)
{
/*
/// <summary>
/// Backing BindableProperty for the <see cref="PropertyName"/> property.
/// </summary>
*/
sb.AppendLine("/// <summary>")
.AppendLine($"/// Backing BindableProperty for the <see cref=\"{info.PropertyName}\"/> property.")
.AppendLine("/// </summary>");

// public static readonly BindableProperty TextProperty = BindableProperty.Create(...);
sb.AppendLine($"public static readonly {bpFullName} {info.PropertyName}Property = ")
.Append($"{bpFullName}.Create(")
.Append($"\"{info.PropertyName}\", ")
.Append($"typeof({info.ReturnType}), ")
.Append($"typeof({info.DeclaringType}), ")
.Append($"{info.DefaultValue}, ")
.Append($"{bindingModeFullName}{info.DefaultBindingMode}, ")
.Append($"{info.ValidateValueMethodName}, ")
.Append($"{info.PropertyChangedMethodName}, ")
.Append($"{info.PropertyChangingMethodName}, ")
.Append($"{info.CoerceValueMethodName}, ")
.Append($"{info.DefaultValueCreatorMethodName}")
.Append(");");

sb.AppendLine().AppendLine();
}

static void GenerateProperty(StringBuilder sb, BindablePropertyModel info)
{
/*
/// <inheritdoc />
*/
sb.AppendLine("/// <inheritdoc />");

//public string Text
//{
// get => (string)GetValue(TextProperty);
// set => SetValue(TextProperty, value);
//}
sb.AppendLine($"public {info.ReturnType} {info.PropertyName}")
.AppendLine("{")
.Append("get => (")
.Append(info.ReturnType)
.Append(")GetValue(")
.AppendLine($"{info.PropertyName}Property);")
.Append("set => SetValue(")
.AppendLine($"{info.PropertyName}Property, value);")
.AppendLine("}");
}
}

static SemanticValues SemanticTransform(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken)
{
var classDeclarationSyntax = Unsafe.As<ClassDeclarationSyntax>(context.TargetNode);
var semanticModel = context.SemanticModel;
var classSymbol = (ITypeSymbol?)semanticModel.GetDeclaredSymbol(classDeclarationSyntax, cancellationToken);

if (classSymbol is null)
{
return emptySemanticValues;
}

var classInfo = new ClassInformation(classSymbol.Name, classSymbol.DeclaredAccessibility.ToString().ToLower(), classSymbol.ContainingNamespace.ToDisplayString());


var bindablePropertyModels = new List<BindablePropertyModel>(context.Attributes.Length);

for (var index = 0; index < context.Attributes.Length; index++)
{
var attributeData = context.Attributes[index];
bindablePropertyModels.Add(GetAttributeValues(attributeData, classSymbol?.ToString() ?? string.Empty));
}

return new(classInfo, bindablePropertyModels.ToImmutableArray());
}

static BindablePropertyModel GetAttributeValues(in AttributeData attributeData, in string declaringTypeString)
{
if (attributeData.AttributeClass is null)
{
throw new ArgumentException($"{nameof(attributeData.AttributeClass)} Cannot Be Null", nameof(attributeData.AttributeClass));
}

var bpType = attributeData.AttributeClass.TypeArguments[0];
var defaultValue = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.DefaultValue));
var coerceValueMethodName = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.CoerceValueMethodName));
var defaultBindingMode = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.DefaultBindingMode), "BindingMode.OneWay");
var defaultValueCreatorMethodName = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.DefaultValueCreatorMethodName));
var declaringType = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.DeclaringType), declaringTypeString);
var propertyChangedMethodName = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.PropertyChangedMethodName));
var propertyChangingMethodName = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.PropertyChangingMethodName));
var propertyName = attributeData.GetConstructorArgumentsAttributeValueByNameAsString();
var validateValueMethodName = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.ValidateValueMethodName));

return new BindablePropertyModel
{
CoerceValueMethodName = coerceValueMethodName,
DefaultBindingMode = defaultBindingMode,
DefaultValue = defaultValue,
DefaultValueCreatorMethodName = defaultValueCreatorMethodName,
DeclaringType = declaringType,
PropertyChangedMethodName = propertyChangedMethodName,
PropertyChangingMethodName = propertyChangingMethodName,
PropertyName = propertyName,
ReturnType = bpType,
ValidateValueMethodName = validateValueMethodName
};
}

static bool SyntaxPredicate(SyntaxNode node, CancellationToken cancellationToken) =>
node is ClassDeclarationSyntax { AttributeLists.Count: > 0 };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsPackable>false</IsPackable>
<IsRoslynComponent>true</IsRoslynComponent>

<!-- Fixes https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/Microsoft.CodeAnalysis.Analyzers.md#rs1036-specify-analyzer-banned-api-enforcement-setting -->
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

<ItemGroup>
<Compile Include="..\CommunityToolkit.Maui.SourceGenerators\Services\SourceStringService.cs" Link="Services\SourceStringService.cs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Microsoft.CodeAnalysis;

namespace CommunityToolkit.Maui.SourceGenerators.Internal.Helpers;

static class AttributeExtensions
{
public static TypedConstant GetAttributeValueByName(this AttributeData attribute, string name)
{
var x = attribute.NamedArguments.SingleOrDefault(kvp => kvp.Key == name).Value;
return x;
}

public static string GetNamedArgumentsAttributeValueByNameAsString(this AttributeData attribute, string name, string placeholder = "null")
{
var data = attribute.NamedArguments.SingleOrDefault(kvp => kvp.Key == name).Value;

return data.Value is null ? placeholder : data.Value.ToString();
}

public static string GetConstructorArgumentsAttributeValueByNameAsString(this AttributeData attribute)
{
var data = attribute.ConstructorArguments[0];

return data.Value is null ? throw new InvalidOperationException() : data.Value.ToString();
}

}
Loading

0 comments on commit 2baab31

Please sign in to comment.