-
Notifications
You must be signed in to change notification settings - Fork 401
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Source Generator for Bindable Property (#1321)
* 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
Showing
13 changed files
with
774 additions
and
95 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
200 changes: 200 additions & 0 deletions
200
...mmunityToolkit.Maui.SourceGenerators.Internal/BindablePropertyAttributeSourceGenerator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; | ||
} |
22 changes: 22 additions & 0 deletions
22
...kit.Maui.SourceGenerators.Internal/CommunityToolkit.Maui.SourceGenerators.Internal.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
27 changes: 27 additions & 0 deletions
27
src/CommunityToolkit.Maui.SourceGenerators.Internal/Helpers/AttributeExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
|
||
} |
Oops, something went wrong.