diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 16fbcd89..118be6c9 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -12,6 +12,8 @@ variables: PathToCommunityToolkitUnitTestCsproj: 'src/CommunityToolkit.Maui.Markup.UnitTests/CommunityToolkit.Maui.Markup.UnitTests.csproj' PathToCommunityToolkitBenchmarkCsproj: 'src/CommunityToolkit.Maui.Markup.Benchmarks/CommunityToolkit.Maui.Markup.Benchmarks.csproj' PathToCommunityToolkitSourceGeneratorsCsproj: 'src/CommunityToolkit.Maui.Markup.SourceGenerators/CommunityToolkit.Maui.Markup.SourceGenerators.csproj' + PathToCommunityToolkitSourceGeneratorsUnitTestCsproj: 'src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests.csproj' + PathToCommunityToolkitSourceGeneratorsBenchmarkCsproj: 'src/CommunityToolkit.Maui.Markup.SourceGenerators.Benchmarks/CommunityToolkit.Maui.Markup.SourceGenerators.Benchmarks.csproj' CommunityToolkitSampleApp_Xcode_Version: '16' CommunityToolkitLibrary_Xcode_Version: '16' ShouldCheckDependencies: true @@ -190,6 +192,11 @@ jobs: script: 'dotnet build -c Release $(PathToCommunityToolkitUnitTestCsproj)' # test + - task: CmdLine@2 + displayName: 'Run Source Generators Unit Tests' + inputs: + script: 'dotnet test -c Release $(PathToCommunityToolkitSourceGeneratorsUnitTestCsproj) --settings ".runsettings" --collect "XPlat code coverage" --logger trx --results-directory $(Agent.TempDirectory)' + - task: CmdLine@2 displayName: 'Run Unit Tests' inputs: @@ -306,10 +313,15 @@ jobs: script: dotnet --info - task: CmdLine@2 - displayName: 'Run Benchmarks' + displayName: 'Run Library Benchmarks' inputs: script : 'dotnet run --project $(PathToCommunityToolkitBenchmarkCsproj) -c Release -- -a $(Build.ArtifactStagingDirectory)' + - task: CmdLine@2 + displayName: 'Run Source Generator Benchmarks' + inputs: + script : 'dotnet run --project $(PathToCommunityToolkitSourceGeneratorsBenchmarkCsproj) -c Release -- -a $(Build.ArtifactStagingDirectory)' + # publish the Benchmark Results - task: PublishBuildArtifacts@1 condition: eq(variables['Agent.OS'], 'Windows_NT') # Only run this step on Windows diff --git a/samples/CommunityToolkit.Maui.Markup.Sample.sln b/samples/CommunityToolkit.Maui.Markup.Sample.sln index 14dd228a..d55ee66f 100644 --- a/samples/CommunityToolkit.Maui.Markup.Sample.sln +++ b/samples/CommunityToolkit.Maui.Markup.Sample.sln @@ -20,7 +20,15 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{A919 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Maui.Markup.SourceGenerators", "..\src\CommunityToolkit.Maui.Markup.SourceGenerators\CommunityToolkit.Maui.Markup.SourceGenerators.csproj", "{533792FE-99CD-4B5B-A8B2-51A8BE3852A5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Maui.Markup.Benchmarks", "..\src\CommunityToolkit.Maui.Markup.Benchmarks\CommunityToolkit.Maui.Markup.Benchmarks.csproj", "{8C1B7D06-75D7-40AC-9FDB-344BF5FCCD5E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Maui.Markup.Benchmarks", "..\src\CommunityToolkit.Maui.Markup.Benchmarks\CommunityToolkit.Maui.Markup.Benchmarks.csproj", "{8C1B7D06-75D7-40AC-9FDB-344BF5FCCD5E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests", "..\src\CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests\CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests.csproj", "{26D5485B-68EA-454A-BBB7-11C6FF9B1B98}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Benchmarks", "Benchmarks", "{6F9076FC-4329-417E-80ED-0B57CBBC4BDC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{16554827-15BF-471E-B979-11A9D16A058B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Maui.Markup.SourceGenerators.Benchmarks", "..\src\CommunityToolkit.Maui.Markup.SourceGenerators.Benchmarks\CommunityToolkit.Maui.Markup.SourceGenerators.Benchmarks.csproj", "{1CB34564-4764-4102-AD84-1C11960C97E9}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -50,12 +58,24 @@ Global {8C1B7D06-75D7-40AC-9FDB-344BF5FCCD5E}.Debug|Any CPU.Build.0 = Debug|Any CPU {8C1B7D06-75D7-40AC-9FDB-344BF5FCCD5E}.Release|Any CPU.ActiveCfg = Release|Any CPU {8C1B7D06-75D7-40AC-9FDB-344BF5FCCD5E}.Release|Any CPU.Build.0 = Release|Any CPU + {26D5485B-68EA-454A-BBB7-11C6FF9B1B98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {26D5485B-68EA-454A-BBB7-11C6FF9B1B98}.Debug|Any CPU.Build.0 = Debug|Any CPU + {26D5485B-68EA-454A-BBB7-11C6FF9B1B98}.Release|Any CPU.ActiveCfg = Release|Any CPU + {26D5485B-68EA-454A-BBB7-11C6FF9B1B98}.Release|Any CPU.Build.0 = Release|Any CPU + {1CB34564-4764-4102-AD84-1C11960C97E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1CB34564-4764-4102-AD84-1C11960C97E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1CB34564-4764-4102-AD84-1C11960C97E9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1CB34564-4764-4102-AD84-1C11960C97E9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {A15CD688-94E8-483B-8B87-A173B2DD0E40} = {A919D3AA-043D-441B-9DF5-18ED84B7FC08} + {F45A2C29-DDD2-49A8-B4D9-57150F80AD36} = {16554827-15BF-471E-B979-11A9D16A058B} + {8C1B7D06-75D7-40AC-9FDB-344BF5FCCD5E} = {6F9076FC-4329-417E-80ED-0B57CBBC4BDC} + {26D5485B-68EA-454A-BBB7-11C6FF9B1B98} = {16554827-15BF-471E-B979-11A9D16A058B} + {1CB34564-4764-4102-AD84-1C11960C97E9} = {6F9076FC-4329-417E-80ED-0B57CBBC4BDC} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {61F7FB11-1E47-470C-91E2-47F8143E1572} diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators.Benchmarks/CommunityToolkit.Maui.Markup.SourceGenerators.Benchmarks.csproj b/src/CommunityToolkit.Maui.Markup.SourceGenerators.Benchmarks/CommunityToolkit.Maui.Markup.SourceGenerators.Benchmarks.csproj new file mode 100644 index 00000000..9c5fb88b --- /dev/null +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators.Benchmarks/CommunityToolkit.Maui.Markup.SourceGenerators.Benchmarks.csproj @@ -0,0 +1,22 @@ + + + $(NetVersion) + Exe + + + AnyCPU + pdbonly + true + true + true + Release + false + + + + + + + + + \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators.Benchmarks/Program.cs b/src/CommunityToolkit.Maui.Markup.SourceGenerators.Benchmarks/Program.cs new file mode 100644 index 00000000..1fb96b75 --- /dev/null +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators.Benchmarks/Program.cs @@ -0,0 +1,12 @@ +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Running; + +namespace CommunityToolkit.Maui.Markup.SourceGenerators.Benchmarks; +public class Program +{ + public static void Main(string[] args) + { + var config = DefaultConfig.Instance; + var summary = BenchmarkRunner.Run(config, args); + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators.Benchmarks/TextAlignmentExtensionsGeneratorBenchmarks.cs b/src/CommunityToolkit.Maui.Markup.SourceGenerators.Benchmarks/TextAlignmentExtensionsGeneratorBenchmarks.cs new file mode 100644 index 00000000..e9dd9702 --- /dev/null +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators.Benchmarks/TextAlignmentExtensionsGeneratorBenchmarks.cs @@ -0,0 +1,22 @@ +using BenchmarkDotNet.Attributes; +using CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests; + +namespace CommunityToolkit.Maui.Markup.SourceGenerators.Benchmarks; + +[MemoryDiagnoser] +public class TextAlignmentExtensionsGeneratorBenchmarks +{ + readonly TextAlignmentExtensionsGeneratorTests textAlignmentExtensionsGeneratorTests = new(); + + [Benchmark] + public Task VerifyGeneratedSource_WhenClassIsGeneric() + { + return textAlignmentExtensionsGeneratorTests.VerifyGeneratedSource_WhenClassIsGeneric(); + } + + [Benchmark] + public Task VerifyGeneratedSource_WhenClassImplementsITextAlignmentInterface() + { + return textAlignmentExtensionsGeneratorTests.VerifyGeneratedSource_WhenClassImplementsITextAlignmentInterface(); + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests.csproj b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests.csproj new file mode 100644 index 00000000..c458156f --- /dev/null +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests.csproj @@ -0,0 +1,31 @@ + + + + $(NetVersion) + false + true + $(BaseIntermediateOutputPath)\GF + true + true + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/TextAlignmentExtensionsGeneratorTests.cs b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/TextAlignmentExtensionsGeneratorTests.cs new file mode 100644 index 00000000..a3a2d378 --- /dev/null +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/TextAlignmentExtensionsGeneratorTests.cs @@ -0,0 +1,308 @@ +using System.Runtime.InteropServices; +using NUnit.Framework; +using static CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests.CSharpSourceGeneratorVerifier; + +namespace CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests; + +public class TextAlignmentExtensionsGeneratorTests +{ + static readonly string assemblyVersion = typeof(TextAlignmentExtensionsGenerator).Assembly.GetName().Version?.ToString() ?? throw new InvalidOleVariantTypeException("Assembly name cannot be null"); + static readonly string textAlignmentExtensionsGeneratorFullName = typeof(TextAlignmentExtensionsGenerator).Assembly.FullName ?? throw new InvalidOleVariantTypeException("Assembly fullname cannot be null"); + + [Test] + public async Task VerifyGeneratedSource_WhenClassImplementsITextAlignmentInterface() + { + // Arrange + const string source = /* language=C#-test */ """ +using Microsoft.Maui; +namespace MyNamespace; + +public class MyClass : ITextAlignment +{ + public TextAlignment HorizontalTextAlignment { get; set; } = TextAlignment.Center; + public TextAlignment VerticalTextAlignment { get; set; } = TextAlignment.Center; +} +"""; + + // Act // Assert + await VerifySourceGeneratorAsync( + source, + GenerateSourceCode(textAlignmentExtensionsGeneratorFullName, + new("MyClass", "public", "MyNamespace", string.Empty, string.Empty, string.Empty)), + []); + } + + [Test] + public async Task VerifyGeneratedSource_WhenClassIsGeneric() + { + // Arrange + const string source = /* language=C#-test */ """ +using System; +using Microsoft.Maui; +namespace MyNamespace; + +public class GenericClass : Microsoft.Maui.ITextAlignment + where T : IDisposable, new() + where U : class +{ + public TextAlignment HorizontalTextAlignment { get; set; } = TextAlignment.Center; + public TextAlignment VerticalTextAlignment { get; set; } = TextAlignment.Center; +} +"""; + + // Act // Assert + await VerifySourceGeneratorAsync( + source, + GenerateSourceCode(textAlignmentExtensionsGeneratorFullName, + new("GenericClass", "public", "MyNamespace", "", "", "where T : IDisposable, new() where U : class")), + []); + } + + static string GenerateSourceCode(string fullClassName, TextAlignmentClassMetadata textAlignmentClassMetadata) => + /* language=C#-test */ $$""" +// +// See: CommunityToolkit.Maui.Markup.SourceGenerators.TextAlignmentGenerator + +#nullable enable +#pragma warning disable + +using System; +using Microsoft.Maui; +using Microsoft.Maui.Controls; + +namespace CommunityToolkit.Maui.Markup +{ + /// + /// Extension Methods for + /// + [global::System.CodeDom.Compiler.GeneratedCode("{{fullClassName}}", "{{assemblyVersion}}")] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + {{textAlignmentClassMetadata.ClassAccessModifier}} static partial class TextAlignmentExtensions_{{textAlignmentClassMetadata.ClassName}} + { + /// + /// = + /// + /// + /// with added + public static TAssignable TextStart{{textAlignmentClassMetadata.GenericTypeParameters}}(this TAssignable textAlignmentControl) + where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{textAlignmentClassMetadata.GenericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} + { + ArgumentNullException.ThrowIfNull(textAlignmentControl); + + if (textAlignmentControl is not ITextAlignment) + { + throw new ArgumentException($"Element must implement {nameof(ITextAlignment)}", nameof(textAlignmentControl)); + } + + textAlignmentControl.HorizontalTextAlignment = TextAlignment.Start; + return textAlignmentControl; + } + + /// + /// = + /// + /// + /// with added + public static TAssignable TextCenterHorizontal{{textAlignmentClassMetadata.GenericTypeParameters}}(this TAssignable textAlignmentControl) + where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{textAlignmentClassMetadata.GenericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} + { + ArgumentNullException.ThrowIfNull(textAlignmentControl); + + if (textAlignmentControl is not ITextAlignment) + { + throw new ArgumentException($"Element must implement {nameof(ITextAlignment)}", nameof(textAlignmentControl)); + } + + textAlignmentControl.HorizontalTextAlignment = TextAlignment.Center; + return textAlignmentControl; + } + + /// + /// = + /// + /// + /// with added + public static TAssignable TextEnd{{textAlignmentClassMetadata.GenericTypeParameters}}(this TAssignable textAlignmentControl) + where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{textAlignmentClassMetadata.GenericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} + { + ArgumentNullException.ThrowIfNull(textAlignmentControl); + + if (textAlignmentControl is not ITextAlignment) + { + throw new ArgumentException($"Element must implement {nameof(ITextAlignment)}", nameof(textAlignmentControl)); + } + + textAlignmentControl.HorizontalTextAlignment = TextAlignment.End; + return textAlignmentControl; + } + + /// + /// = + /// + /// + /// with added + public static TAssignable TextTop{{textAlignmentClassMetadata.GenericTypeParameters}}(this TAssignable textAlignmentControl) + where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{textAlignmentClassMetadata.GenericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} + { + ArgumentNullException.ThrowIfNull(textAlignmentControl); + + if (textAlignmentControl is not ITextAlignment) + { + throw new ArgumentException($"Element must implement {nameof(ITextAlignment)}", nameof(textAlignmentControl)); + } + + textAlignmentControl.VerticalTextAlignment = TextAlignment.Start; + return textAlignmentControl; + } + + /// + /// = + /// + /// + /// with added + public static TAssignable TextCenterVertical{{textAlignmentClassMetadata.GenericTypeParameters}}(this TAssignable textAlignmentControl) + where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{textAlignmentClassMetadata.GenericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} + { + ArgumentNullException.ThrowIfNull(textAlignmentControl); + + if (textAlignmentControl is not ITextAlignment) + { + throw new ArgumentException($"Element must implement {nameof(ITextAlignment)}", nameof(textAlignmentControl)); + } + + textAlignmentControl.VerticalTextAlignment = TextAlignment.Center; + return textAlignmentControl; + } + + /// + /// = + /// + /// + /// with added + public static TAssignable TextBottom{{textAlignmentClassMetadata.GenericTypeParameters}}(this TAssignable textAlignmentControl) + where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{textAlignmentClassMetadata.GenericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} + { + ArgumentNullException.ThrowIfNull(textAlignmentControl); + + if (textAlignmentControl is not ITextAlignment) + { + throw new ArgumentException($"Element must implement {nameof(ITextAlignment)}", nameof(textAlignmentControl)); + } + + textAlignmentControl.VerticalTextAlignment = TextAlignment.End; + return textAlignmentControl; + } + + /// + /// = = + /// + /// + /// with added + public static TAssignable TextCenter{{textAlignmentClassMetadata.GenericTypeParameters}}(this TAssignable textAlignmentControl) + where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{textAlignmentClassMetadata.GenericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} + => textAlignmentControl.TextCenterHorizontal{{textAlignmentClassMetadata.GenericTypeParameters}}().TextCenterVertical{{textAlignmentClassMetadata.GenericTypeParameters}}(); + } + + + // The extensions in these sub-namespaces are designed to be used together with the extensions in the parent namespace. + // Keep them in a single file for better maintainability + + namespace LeftToRight + { + /// + /// Extension Methods for + /// + [global::System.CodeDom.Compiler.GeneratedCode("{{fullClassName}}", "{{assemblyVersion}}")] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + {{textAlignmentClassMetadata.ClassAccessModifier}} static partial class TextAlignmentExtensions_{{textAlignmentClassMetadata.ClassName}} + { + /// + /// = + /// + /// + /// with + public static TAssignable TextLeft{{textAlignmentClassMetadata.GenericTypeParameters}}(this TAssignable textAlignmentControl) + where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{textAlignmentClassMetadata.GenericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} + { + ArgumentNullException.ThrowIfNull(textAlignmentControl); + + if (textAlignmentControl is not ITextAlignment) + { + throw new ArgumentException($"Element must implement {nameof(ITextAlignment)}", nameof(textAlignmentControl)); + } + + textAlignmentControl.HorizontalTextAlignment = TextAlignment.Start; + return textAlignmentControl; + } + + /// + /// = + /// + /// + /// with + public static TAssignable TextRight{{textAlignmentClassMetadata.GenericTypeParameters}}(this TAssignable textAlignmentControl) where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{textAlignmentClassMetadata.GenericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} + { + ArgumentNullException.ThrowIfNull(textAlignmentControl); + + if (textAlignmentControl is not ITextAlignment) + { + throw new ArgumentException($"Element must implement {nameof(ITextAlignment)}", nameof(textAlignmentControl)); + } + + textAlignmentControl.HorizontalTextAlignment = TextAlignment.End; + return textAlignmentControl; + } + } + } + + // The extensions in these sub-namespaces are designed to be used together with the extensions in the parent namespace. + // Keep them in a single file for better maintainability + namespace RightToLeft + { + /// + /// Extension methods for + /// + {{textAlignmentClassMetadata.ClassAccessModifier}} static partial class TextAlignmentExtensions_{{textAlignmentClassMetadata.ClassName}} + { + /// + /// = + /// + /// + /// with + public static TAssignable TextLeft{{textAlignmentClassMetadata.GenericTypeParameters}}(this TAssignable textAlignmentControl) + where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{textAlignmentClassMetadata.GenericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} + { + ArgumentNullException.ThrowIfNull(textAlignmentControl); + + if (textAlignmentControl is not ITextAlignment) + { + throw new ArgumentException($"Element must implement {nameof(ITextAlignment)}", nameof(textAlignmentControl)); + } + + textAlignmentControl.HorizontalTextAlignment = TextAlignment.End; + return textAlignmentControl; + } + + /// + /// = + /// + /// + /// with + public static TAssignable TextRight{{textAlignmentClassMetadata.GenericTypeParameters}}(this TAssignable textAlignmentControl) + where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{textAlignmentClassMetadata.GenericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} + { + ArgumentNullException.ThrowIfNull(textAlignmentControl); + + if (textAlignmentControl is not ITextAlignment) + { + throw new ArgumentException($"Element must implement {nameof(ITextAlignment)}", nameof(textAlignmentControl)); + } + + textAlignmentControl.HorizontalTextAlignment = TextAlignment.Start; + return textAlignmentControl; + } + } + } +} +"""; +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpAnalyzerVerifier+Test.cs b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpAnalyzerVerifier+Test.cs new file mode 100644 index 00000000..21e265d4 --- /dev/null +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpAnalyzerVerifier+Test.cs @@ -0,0 +1,49 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests; +public static partial class CSharpAnalyzerVerifier + where TAnalyzer : DiagnosticAnalyzer, new() +{ + class Test : CSharpAnalyzerTest + { + public Test(params Type[] assembliesUnderTest) + { +#if NET8_0 + ReferenceAssemblies = Microsoft.CodeAnalysis.Testing.ReferenceAssemblies.Net.Net80; +#else +#error ReferenceAssemblies must be updated to current version of .NET +#endif + List typesForAssembliesUnderTest = + [ + typeof(Microsoft.Maui.Controls.Xaml.Extensions), // Microsoft.Maui.Controls.Xaml + typeof(MauiApp),// Microsoft.Maui.Hosting + typeof(Application), // Microsoft.Maui.Controls + ]; + typesForAssembliesUnderTest.AddRange(assembliesUnderTest); + + foreach (Type type in typesForAssembliesUnderTest) + { + TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(type.Assembly.Location)); + } + + SolutionTransforms.Add((solution, projectId) => + { + ArgumentNullException.ThrowIfNull(solution); + + if (solution.GetProject(projectId) is not Project project) + { + throw new ArgumentException("Invalid ProjectId"); + } + + var compilationOptions = project.CompilationOptions ?? throw new InvalidOperationException($"{nameof(project.CompilationOptions)} cannot be null"); + compilationOptions = compilationOptions.WithSpecificDiagnosticOptions( + compilationOptions.SpecificDiagnosticOptions.SetItems(CSharpVerifierHelper.NullableWarnings)); + solution = solution.WithProjectCompilationOptions(projectId, compilationOptions); + + return solution; + }); + } + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpAnalyzerVerifier.cs b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpAnalyzerVerifier.cs new file mode 100644 index 00000000..6a1770f1 --- /dev/null +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpAnalyzerVerifier.cs @@ -0,0 +1,34 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; + +namespace CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests; + +public static partial class CSharpAnalyzerVerifier + where TAnalyzer : DiagnosticAnalyzer, new() +{ + /// + public static DiagnosticResult Diagnostic() + => CSharpAnalyzerVerifier.Diagnostic(); + + /// + public static DiagnosticResult Diagnostic(string diagnosticId) + => CSharpAnalyzerVerifier.Diagnostic(diagnosticId); + + /// + public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor) + => CSharpAnalyzerVerifier.Diagnostic(descriptor); + + /// + public static async Task VerifyAnalyzerAsync(string source, Type[] assembliesUnderTest, params DiagnosticResult[] expected) + { + var test = new Test(assembliesUnderTest) + { + TestCode = source, + }; + + test.ExpectedDiagnostics.AddRange(expected); + await test.RunAsync(CancellationToken.None); + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpCodeFixVerifier+Test.cs b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpCodeFixVerifier+Test.cs new file mode 100644 index 00000000..094f6c9c --- /dev/null +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpCodeFixVerifier+Test.cs @@ -0,0 +1,54 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; + +namespace CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests; + +public static partial class CSharpCodeFixVerifier + where TAnalyzer : DiagnosticAnalyzer, new() + where TCodeFix : CodeFixProvider, new() +{ + class Test : CSharpCodeFixTest + { + public Test(params Type[] assembliesUnderTest) + { +#if NET8_0 + ReferenceAssemblies = ReferenceAssemblies.Net.Net80; +#else +#error ReferenceAssemblies must be updated to current version of .NET +#endif + List typesForAssembliesUnderTest = + [ + typeof(Microsoft.Maui.Controls.Xaml.Extensions), // Microsoft.Maui.Controls.Xaml + typeof(MauiApp),// Microsoft.Maui.Hosting + typeof(Application), // Microsoft.Maui.Controls + ]; + typesForAssembliesUnderTest.AddRange(assembliesUnderTest); + + foreach (Type type in typesForAssembliesUnderTest) + { + TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(type.Assembly.Location)); + } + + SolutionTransforms.Add((solution, projectId) => + { + ArgumentNullException.ThrowIfNull(solution); + + if (solution.GetProject(projectId) is not Project project) + { + throw new ArgumentException("Invalid ProjectId"); + } + + var compilationOptions = project.CompilationOptions ?? throw new InvalidOperationException($"{nameof(project.CompilationOptions)} cannot be null"); + compilationOptions = compilationOptions.WithSpecificDiagnosticOptions( + compilationOptions.SpecificDiagnosticOptions.SetItems(CSharpVerifierHelper.NullableWarnings)); + + solution = solution.WithProjectCompilationOptions(projectId, compilationOptions); + + return solution; + }); + } + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpCodeFixVerifier.cs b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpCodeFixVerifier.cs new file mode 100644 index 00000000..de8a8b38 --- /dev/null +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpCodeFixVerifier.cs @@ -0,0 +1,56 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; + +namespace CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests; +public static partial class CSharpCodeFixVerifier + where TAnalyzer : DiagnosticAnalyzer, new() + where TCodeFix : CodeFixProvider, new() +{ + /// + public static DiagnosticResult Diagnostic() + => CSharpCodeFixVerifier.Diagnostic(); + + /// + public static DiagnosticResult Diagnostic(string diagnosticId) + => CSharpCodeFixVerifier.Diagnostic(diagnosticId); + + /// + public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor) + => CSharpCodeFixVerifier.Diagnostic(descriptor); + + /// + public static async Task VerifyAnalyzerAsync(string source, Type[] assembliesUnderTest, params DiagnosticResult[] expected) + { + var test = new Test(assembliesUnderTest) + { + TestCode = source, + }; + + test.ExpectedDiagnostics.AddRange(expected); + await test.RunAsync(CancellationToken.None); + } + + /// + public static async Task VerifyCodeFixAsync(string source, string fixedSource) + => await VerifyCodeFixAsync(source, DiagnosticResult.EmptyDiagnosticResults, fixedSource); + + /// + public static async Task VerifyCodeFixAsync(string source, DiagnosticResult expected, string fixedSource) + => await VerifyCodeFixAsync(source, [expected], fixedSource); + + /// + public static async Task VerifyCodeFixAsync(string source, DiagnosticResult[] expected, string fixedSource, params Type[] assembliesUnderTest) + { + var test = new Test(assembliesUnderTest) + { + TestCode = source, + FixedCode = fixedSource, + }; + + test.ExpectedDiagnostics.AddRange(expected); + await test.RunAsync(CancellationToken.None); + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpSourceGeneratorVerifier+Test.cs b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpSourceGeneratorVerifier+Test.cs new file mode 100644 index 00000000..6ad79573 --- /dev/null +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpSourceGeneratorVerifier+Test.cs @@ -0,0 +1,74 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Testing; + +namespace CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests; + +public static partial class CSharpSourceGeneratorVerifier + where TSourceGenerator : IIncrementalGenerator, new() +{ + class Test : CSharpSourceGeneratorTest + { + public Test(params Type[] assembliesUnderTest) + { +#if NET8_0 + ReferenceAssemblies = Microsoft.CodeAnalysis.Testing.ReferenceAssemblies.Net.Net80; +#else +#error ReferenceAssemblies must be updated to current version of .NET +#endif + List typesForAssembliesUnderTest = + [ + typeof(Microsoft.Maui.Controls.Xaml.Extensions), // Microsoft.Maui.Controls.Xaml + typeof(MauiApp),// Microsoft.Maui.Hosting + typeof(Application), // Microsoft.Maui.Controls + ]; + typesForAssembliesUnderTest.AddRange(assembliesUnderTest); + + foreach (var type in typesForAssembliesUnderTest) + { + TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(type.Assembly.Location)); + } + + SolutionTransforms.Add((solution, projectId) => + { + ArgumentNullException.ThrowIfNull(solution); + + if (solution.GetProject(projectId) is not Project project) + { + throw new ArgumentException("Invalid ProjectId"); + } + + var compilationOptions = project.CompilationOptions ?? throw new InvalidOperationException($"{nameof(project.CompilationOptions)} cannot be null"); + compilationOptions = compilationOptions.WithSpecificDiagnosticOptions( + compilationOptions.SpecificDiagnosticOptions.SetItems(CSharpVerifierHelper.NullableWarnings)); + solution = solution.WithProjectCompilationOptions(projectId, compilationOptions); + + return solution; + }); + } + + protected override CompilationOptions CreateCompilationOptions() + { + var compilationOptions = base.CreateCompilationOptions(); + return compilationOptions.WithSpecificDiagnosticOptions( + compilationOptions.SpecificDiagnosticOptions.SetItems(GetNullableWarningsFromCompiler())); + } + + public LanguageVersion LanguageVersion { get; } = LanguageVersion.Default; + + static ImmutableDictionary GetNullableWarningsFromCompiler() + { + string[] args = { "/warnaserror:nullable" }; + var commandLineArguments = CSharpCommandLineParser.Default.Parse(args, baseDirectory: Environment.CurrentDirectory, sdkDirectory: Environment.CurrentDirectory); + var nullableWarnings = commandLineArguments.CompilationOptions.SpecificDiagnosticOptions; + + return nullableWarnings; + } + + protected override ParseOptions CreateParseOptions() + { + return ((CSharpParseOptions)base.CreateParseOptions()).WithLanguageVersion(LanguageVersion); + } + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpSourceGeneratorVerifier.cs b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpSourceGeneratorVerifier.cs new file mode 100644 index 00000000..4f2d3d1b --- /dev/null +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpSourceGeneratorVerifier.cs @@ -0,0 +1,33 @@ +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Text; + +namespace CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests; + +public static partial class CSharpSourceGeneratorVerifier + where TSourceGenerator : IIncrementalGenerator, new() +{ + /// + public static async Task VerifySourceGeneratorAsync(string source, string expectedGeneratedCode, Type[] assembliesUnderTest, params DiagnosticResult[] expectedDiagnosticResults) + { + var test = new Test(assembliesUnderTest) + { + TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck, + TestState = + { + Sources = { source }, + GeneratedSources = + { + (typeof(TSourceGenerator), string.Empty, SourceText.From(expectedGeneratedCode, Encoding.UTF8, SourceHashAlgorithm.Sha256)), + } + } + }; + + test.ExpectedDiagnostics.AddRange(expectedDiagnosticResults); + + await test.RunAsync(CancellationToken.None).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpVerifierHelper.cs b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpVerifierHelper.cs new file mode 100644 index 00000000..9aef35cb --- /dev/null +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpVerifierHelper.cs @@ -0,0 +1,31 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests; + +static class CSharpVerifierHelper +{ + /// + /// By default, the compiler reports diagnostics for nullable reference types at + /// , and the analyzer test framework defaults to only validating + /// diagnostics at . This map contains all compiler diagnostic IDs + /// related to nullability mapped to , which is then used to enable all + /// of these warnings for default validation during analyzer and code fix tests. + /// + internal static ImmutableDictionary NullableWarnings { get; } = GetNullableWarningsFromCompiler(); + + static ImmutableDictionary GetNullableWarningsFromCompiler() + { + string[] args = ["/warnaserror:nullable"]; + var commandLineArguments = CSharpCommandLineParser.Default.Parse(args, baseDirectory: Environment.CurrentDirectory, sdkDirectory: Environment.CurrentDirectory); + var nullableWarnings = commandLineArguments.CompilationOptions.SpecificDiagnosticOptions; + + // Workaround for https://github.com/dotnet/roslyn/issues/41610 + nullableWarnings = nullableWarnings + .SetItem("CS8632", ReportDiagnostic.Error) + .SetItem("CS8669", ReportDiagnostic.Error); + + return nullableWarnings; + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators/CommunityToolkit.Maui.Markup.SourceGenerators.csproj b/src/CommunityToolkit.Maui.Markup.SourceGenerators/CommunityToolkit.Maui.Markup.SourceGenerators.csproj index f7d22719..d7b53cc9 100644 --- a/src/CommunityToolkit.Maui.Markup.SourceGenerators/CommunityToolkit.Maui.Markup.SourceGenerators.csproj +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators/CommunityToolkit.Maui.Markup.SourceGenerators.csproj @@ -7,6 +7,10 @@ true + + + + diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs b/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs index 4b119947..49eda672 100644 --- a/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs @@ -1,365 +1,95 @@ using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; + namespace CommunityToolkit.Maui.Markup.SourceGenerators; [Generator(LanguageNames.CSharp)] -class TextAlignmentExtensionsGenerator : IIncrementalGenerator +public class TextAlignmentExtensionsGenerator : IIncrementalGenerator { - const string iTextAlignmentInterface = "Microsoft.Maui.ITextAlignment"; + const string textAlignmentInterface = "Microsoft.Maui.ITextAlignment"; const string mauiControlsAssembly = "Microsoft.Maui.Controls"; public void Initialize(IncrementalGeneratorInitializationContext context) { - // Get All Classes in User Library - var userGeneratedClassesProvider = context.SyntaxProvider.CreateSyntaxProvider( - static (syntaxNode, cancellationToken) => syntaxNode is ClassDeclarationSyntax { BaseList: not null }, - static (context, cancellationToken) => - { - var compilation = context.SemanticModel.Compilation; - - var iTextAlignmentInterfaceSymbol = compilation.GetTypeByMetadataName(iTextAlignmentInterface) ?? throw new Exception("There's no .NET MAUI referenced in the project."); - var classSymbol = context.SemanticModel.GetDeclaredSymbol((ClassDeclarationSyntax)context.Node, cancellationToken); - - if (classSymbol is null || classSymbol.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken) != context.Node) + IncrementalValuesProvider<(INamedTypeSymbol ClassSymbol, INamedTypeSymbol ITextAlignmentInterfaceSymbol)> userGeneratedClassesProvider = context.SyntaxProvider + .CreateSyntaxProvider( + static (syntaxNode, _) => syntaxNode is ClassDeclarationSyntax { BaseList: not null }, + static (syntaxContext, ct) => { - // In case of multiple partial declarations, we want to run only once. - // So we run only for the first syntax reference. - return null; - } - - return ShouldGenerateTextAlignmentExtension(classSymbol, iTextAlignmentInterfaceSymbol) - ? GenerateMetadata(classSymbol) - : null; - - }).Where(static m => m is not null); - - // Get Microsoft.Maui.Controls Symbols that implements the desired interfaces - var mauiControlsAssemblySymbolProvider = context.CompilationProvider.Select( - static (compilation, token) => - { - var iTextAlignmentInterfaceSymbol = compilation.GetTypeByMetadataName(iTextAlignmentInterface) ?? throw new Exception("There's no .NET MAUI referenced in the project."); - var mauiAssembly = compilation.SourceModule.ReferencedAssemblySymbols.Single(q => q.Name == mauiControlsAssembly); - - return GetMauiInterfaceImplementors(mauiAssembly, iTextAlignmentInterfaceSymbol).ToImmutableArray().AsEquatableArray(); - }); - - - // Here we Collect all the Classes candidates from the first pipeline - // Then we merge them with the Maui.Controls that implements the desired interfaces - // Then we make sure they are unique and the user control doesn't inherit from any Maui control that implements the desired interface already - // Then we transform the ISymbol to be a type that we can compare and preserve the Incremental behavior of this Source Generator - context.RegisterSourceOutput(userGeneratedClassesProvider, Execute); - context.RegisterSourceOutput(mauiControlsAssemblySymbolProvider, ExecuteArray); + var compilation = syntaxContext.SemanticModel.Compilation; + var iTextAlignmentInterfaceSymbol = compilation.GetTypeByMetadataName(textAlignmentInterface); + if (iTextAlignmentInterfaceSymbol is null) + { + return default; + } + + var classSymbol = syntaxContext.SemanticModel.GetDeclaredSymbol((ClassDeclarationSyntax)syntaxContext.Node, ct); + if (classSymbol is null || classSymbol.DeclaringSyntaxReferences[0].GetSyntax(ct) != syntaxContext.Node) + { + return default; + } + + return (classSymbol, iTextAlignmentInterfaceSymbol); + }) + .Where(static tuple => tuple != default && ShouldGenerateTextAlignmentExtension(tuple.classSymbol, tuple.iTextAlignmentInterfaceSymbol)); + + var compilationProvider = context.CompilationProvider; + + var combined = userGeneratedClassesProvider + .Collect() + .Combine(compilationProvider); + + context.RegisterSourceOutput(combined, static (spc, source) => Execute(spc, source.Right, source.Left.ToImmutableArray())); } static bool ShouldGenerateTextAlignmentExtension(INamedTypeSymbol classSymbol, INamedTypeSymbol iTextAlignmentInterfaceSymbol) { - return ImplementsInterfaceIgnoringBaseType(classSymbol, iTextAlignmentInterfaceSymbol) - && DoesNotImplementInterface(classSymbol.BaseType, iTextAlignmentInterfaceSymbol); + return DoesImplementInterfaceIgnoringBaseType(classSymbol, iTextAlignmentInterfaceSymbol) + && !DoesImplementInterface(classSymbol.BaseType, iTextAlignmentInterfaceSymbol); - static bool ImplementsInterfaceIgnoringBaseType(INamedTypeSymbol classSymbol, INamedTypeSymbol iTextAlignmentInterfaceSymbol) - => classSymbol.Interfaces.Any(i => i.Equals(iTextAlignmentInterfaceSymbol, SymbolEqualityComparer.Default) || i.AllInterfaces.Contains(iTextAlignmentInterfaceSymbol, SymbolEqualityComparer.Default)); + static bool DoesImplementInterfaceIgnoringBaseType(INamedTypeSymbol classSymbol, INamedTypeSymbol interfaceSymbol) + => classSymbol.AllInterfaces.Contains(interfaceSymbol); - static bool DoesNotImplementInterface(INamedTypeSymbol? classSymbol, INamedTypeSymbol iTextAlignmentInterfaceSymbol) - => classSymbol is null || !classSymbol.AllInterfaces.Any(i => i.Equals(iTextAlignmentInterfaceSymbol, SymbolEqualityComparer.Default)); + static bool DoesImplementInterface(INamedTypeSymbol? classSymbol, INamedTypeSymbol interfaceSymbol) + => classSymbol?.AllInterfaces.Contains(interfaceSymbol) ?? false; } - static void ExecuteArray(SourceProductionContext context, EquatableArray metadataArray) + static void Execute(SourceProductionContext context, Compilation compilation, ImmutableArray<(INamedTypeSymbol ClassSymbol, INamedTypeSymbol ITextAlignmentInterfaceSymbol)> userClasses) { - foreach (var metadata in metadataArray.AsImmutableArray()) + var mauiAssembly = compilation.SourceModule.ReferencedAssemblySymbols.FirstOrDefault(static a => a.Name == mauiControlsAssembly); + if (mauiAssembly is null) { - Execute(context, metadata); + return; } - } - static void Execute(SourceProductionContext context, [NotNull] TextAlignmentClassMetadata? textAlignmentClassMetadata) - { - if (textAlignmentClassMetadata is null) + var iTextAlignmentInterfaceSymbol = compilation.GetTypeByMetadataName(textAlignmentInterface); + if (iTextAlignmentInterfaceSymbol is null) { - throw new ArgumentNullException(nameof(textAlignmentClassMetadata)); + return; } - var className = typeof(TextAlignmentExtensionsGenerator).FullName; - var assemblyVersion = typeof(TextAlignmentExtensionsGenerator).Assembly.GetName().Version.ToString(); - - var genericTypeParameters = GetGenericTypeParametersDeclarationString(textAlignmentClassMetadata.GenericArguments); - var genericArguments = GetGenericArgumentsString(textAlignmentClassMetadata.GenericArguments); - var source = /* language=C#-test */$$""" -// -// See: CommunityToolkit.Maui.Markup.SourceGenerators.TextAlignmentGenerator - -#nullable enable -#pragma warning disable + var mauiClasses = GetMauiInterfaceImplementors(mauiAssembly, iTextAlignmentInterfaceSymbol); -using System; -using Microsoft.Maui; -using Microsoft.Maui.Controls; + var processedClasses = new HashSet(SymbolEqualityComparer.Default); -namespace CommunityToolkit.Maui.Markup -{ - /// - /// Extension Methods for - /// - [global::System.CodeDom.Compiler.GeneratedCode("{{className}}", "{{assemblyVersion}}")] - [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - {{textAlignmentClassMetadata.ClassAcessModifier}} static partial class TextAlignmentExtensions_{{textAlignmentClassMetadata.ClassName}} - { - /// - /// = - /// - /// - /// with added - public static TAssignable TextJustify{{genericTypeParameters}}(this TAssignable textAlignmentControl) - where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{genericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} - { - ArgumentNullException.ThrowIfNull(textAlignmentControl); - - if (textAlignmentControl is not ITextAlignment) - { - throw new ArgumentException($"Element must implement {nameof(ITextAlignment)}", nameof(textAlignmentControl)); - } - - textAlignmentControl.HorizontalTextAlignment = TextAlignment.Justify; - return textAlignmentControl; - } - - /// - /// = - /// - /// - /// with added - public static TAssignable TextStart{{genericTypeParameters}}(this TAssignable textAlignmentControl) - where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{genericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} - { - ArgumentNullException.ThrowIfNull(textAlignmentControl); - - if (textAlignmentControl is not ITextAlignment) - { - throw new ArgumentException($"Element must implement {nameof(ITextAlignment)}", nameof(textAlignmentControl)); - } - - textAlignmentControl.HorizontalTextAlignment = TextAlignment.Start; - return textAlignmentControl; - } - - /// - /// = - /// - /// - /// with added - public static TAssignable TextCenterHorizontal{{genericTypeParameters}}(this TAssignable textAlignmentControl) - where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{genericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} - { - ArgumentNullException.ThrowIfNull(textAlignmentControl); - - if (textAlignmentControl is not ITextAlignment) - { - throw new ArgumentException($"Element must implement {nameof(ITextAlignment)}", nameof(textAlignmentControl)); - } - - textAlignmentControl.HorizontalTextAlignment = TextAlignment.Center; - return textAlignmentControl; - } - - /// - /// = - /// - /// - /// with added - public static TAssignable TextEnd{{genericTypeParameters}}(this TAssignable textAlignmentControl) - where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{genericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} - { - ArgumentNullException.ThrowIfNull(textAlignmentControl); - - if (textAlignmentControl is not ITextAlignment) - { - throw new ArgumentException($"Element must implement {nameof(ITextAlignment)}", nameof(textAlignmentControl)); - } - - textAlignmentControl.HorizontalTextAlignment = TextAlignment.End; - return textAlignmentControl; - } - - /// - /// = - /// - /// - /// with added - public static TAssignable TextTop{{genericTypeParameters}}(this TAssignable textAlignmentControl) - where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{genericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} - { - ArgumentNullException.ThrowIfNull(textAlignmentControl); - - if (textAlignmentControl is not ITextAlignment) - { - throw new ArgumentException($"Element must implement {nameof(ITextAlignment)}", nameof(textAlignmentControl)); - } - - textAlignmentControl.VerticalTextAlignment = TextAlignment.Start; - return textAlignmentControl; - } - - /// - /// = - /// - /// - /// with added - public static TAssignable TextCenterVertical{{genericTypeParameters}}(this TAssignable textAlignmentControl) - where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{genericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} - { - ArgumentNullException.ThrowIfNull(textAlignmentControl); - - if (textAlignmentControl is not ITextAlignment) - { - throw new ArgumentException($"Element must implement {nameof(ITextAlignment)}", nameof(textAlignmentControl)); - } - - textAlignmentControl.VerticalTextAlignment = TextAlignment.Center; - return textAlignmentControl; - } - - /// - /// = - /// - /// - /// with added - public static TAssignable TextBottom{{genericTypeParameters}}(this TAssignable textAlignmentControl) - where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{genericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} - { - ArgumentNullException.ThrowIfNull(textAlignmentControl); - - if (textAlignmentControl is not ITextAlignment) - { - throw new ArgumentException($"Element must implement {nameof(ITextAlignment)}", nameof(textAlignmentControl)); - } - - textAlignmentControl.VerticalTextAlignment = TextAlignment.End; - return textAlignmentControl; - } - - /// - /// = = - /// - /// - /// with added - public static TAssignable TextCenter{{genericTypeParameters}}(this TAssignable textAlignmentControl) - where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{genericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} - => textAlignmentControl.TextCenterHorizontal{{genericTypeParameters}}().TextCenterVertical{{genericTypeParameters}}(); - } - - - // The extensions in these sub-namespaces are designed to be used together with the extensions in the parent namespace. - // Keep them in a single file for better maintainability - - namespace LeftToRight - { - /// - /// Extension Methods for - /// - [global::System.CodeDom.Compiler.GeneratedCode("{{className}}", "{{assemblyVersion}}")] - [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - {{textAlignmentClassMetadata.ClassAcessModifier}} static partial class TextAlignmentExtensions_{{textAlignmentClassMetadata.ClassName}} - { - /// - /// = - /// - /// - /// with - public static TAssignable TextLeft{{genericTypeParameters}}(this TAssignable textAlignmentControl) - where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{genericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} - { - ArgumentNullException.ThrowIfNull(textAlignmentControl); - - if (textAlignmentControl is not ITextAlignment) - { - throw new ArgumentException($"Element must implement {nameof(ITextAlignment)}", nameof(textAlignmentControl)); - } - - textAlignmentControl.HorizontalTextAlignment = TextAlignment.Start; - return textAlignmentControl; - } - - /// - /// = - /// - /// - /// with - public static TAssignable TextRight{{genericTypeParameters}}(this TAssignable textAlignmentControl) where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{genericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} - { - ArgumentNullException.ThrowIfNull(textAlignmentControl); - - if (textAlignmentControl is not ITextAlignment) - { - throw new ArgumentException($"Element must implement {nameof(ITextAlignment)}", nameof(textAlignmentControl)); - } - - textAlignmentControl.HorizontalTextAlignment = TextAlignment.End; - return textAlignmentControl; - } - } - } - - // The extensions in these sub-namespaces are designed to be used together with the extensions in the parent namespace. - // Keep them in a single file for better maintainability - namespace RightToLeft - { - /// - /// Extension methods for - /// - {{textAlignmentClassMetadata.ClassAcessModifier}} static partial class TextAlignmentExtensions_{{textAlignmentClassMetadata.ClassName}} - { - /// - /// = - /// - /// - /// with - public static TAssignable TextLeft{{genericTypeParameters}}(this TAssignable textAlignmentControl) - where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{genericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} - { - ArgumentNullException.ThrowIfNull(textAlignmentControl); - - if (textAlignmentControl is not ITextAlignment) - { - throw new ArgumentException($"Element must implement {nameof(ITextAlignment)}", nameof(textAlignmentControl)); - } - - textAlignmentControl.HorizontalTextAlignment = TextAlignment.End; - return textAlignmentControl; - } - - /// - /// = - /// - /// - /// with - public static TAssignable TextRight{{genericTypeParameters}}(this TAssignable textAlignmentControl) - where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{genericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} - { - ArgumentNullException.ThrowIfNull(textAlignmentControl); - - if (textAlignmentControl is not ITextAlignment) - { - throw new ArgumentException($"Element must implement {nameof(ITextAlignment)}", nameof(textAlignmentControl)); - } - - textAlignmentControl.HorizontalTextAlignment = TextAlignment.Start; - return textAlignmentControl; - } - } - } -} -"""; - context.AddSource($"{textAlignmentClassMetadata.ClassName}TextAlignmentExtensions.g.cs", SourceText.From(source, Encoding.UTF8)); + foreach (var (classSymbol, _) in userClasses.Concat(mauiClasses.Select(c => (c, iTextAlignmentInterfaceSymbol)))) + { + if (processedClasses.Add(classSymbol)) + { + var metadata = GenerateMetadata(classSymbol); + GenerateExtensionClass(context, metadata); + } + } } - static IEnumerable GetMauiInterfaceImplementors(IAssemblySymbol mauiControlsAssemblySymbolProvider, INamedTypeSymbol itextAlignmentSymbol) + static IEnumerable GetMauiInterfaceImplementors(IAssemblySymbol mauiAssembly, INamedTypeSymbol iTextAlignmentSymbol) { - return mauiControlsAssemblySymbolProvider.GlobalNamespace.GetNamedTypeSymbols().Where(x => ShouldGenerateTextAlignmentExtension(x, itextAlignmentSymbol)).Select(GenerateMetadata); + return mauiAssembly.GlobalNamespace.GetNamedTypeSymbols() + .Where(x => ShouldGenerateTextAlignmentExtension(x, iTextAlignmentSymbol)); } static string GetClassAccessModifier(INamedTypeSymbol namedTypeSymbol) => namedTypeSymbol.DeclaredAccessibility switch @@ -369,34 +99,366 @@ static IEnumerable GetMauiInterfaceImplementors(IAss _ => string.Empty }; - static string GetGenericTypeParametersDeclarationString(in string genericArguments) + static void GenerateExtensionClass(SourceProductionContext context, in TextAlignmentClassMetadata metadata) { - if (string.IsNullOrWhiteSpace(genericArguments)) - { - return ""; - } - - return $""; + var source = GenerateExtensionClassSource(in metadata); + context.AddSource($"{metadata.ClassName}TextAlignmentExtensions.g.cs", SourceText.From(source, Encoding.UTF8)); } - static string GetGenericArgumentsString(in string genericArguments) + static string GenerateExtensionClassSource(in TextAlignmentClassMetadata metadata) { - if (string.IsNullOrWhiteSpace(genericArguments)) - { - return string.Empty; - } + var assemblyVersion = typeof(TextAlignmentExtensionsGenerator).Assembly.GetName().Version.ToString(); + var className = typeof(TextAlignmentExtensionsGenerator).FullName; - return $"<{genericArguments}>"; + var sb = new StringBuilder(8192); // Pre-allocate with an estimated capacity + sb.AppendLine( /* language=C#-test */ + $$""" + // + // See: CommunityToolkit.Maui.Markup.SourceGenerators.TextAlignmentGenerator + + #nullable enable + #pragma warning disable + + using System; + using Microsoft.Maui; + using Microsoft.Maui.Controls; + + namespace CommunityToolkit.Maui.Markup + { + /// + /// Extension Methods for + /// + [global::System.CodeDom.Compiler.GeneratedCode("{{className}}", "{{assemblyVersion}}")] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + {{metadata.ClassAccessModifier}} static partial class TextAlignmentExtensions_{{metadata.ClassName}} + { + /// + /// = + /// + /// + /// with added + public static TAssignable TextStart{{metadata.GenericTypeParameters}}(this TAssignable textAlignmentControl) + where TAssignable : {{metadata.Namespace}}.{{metadata.ClassName}}{{metadata.GenericArguments}}{{metadata.GenericConstraints}} + { + ArgumentNullException.ThrowIfNull(textAlignmentControl); + + if (textAlignmentControl is not ITextAlignment) + { + throw new ArgumentException($"Element must implement {nameof(ITextAlignment)}", nameof(textAlignmentControl)); + } + + textAlignmentControl.HorizontalTextAlignment = TextAlignment.Start; + return textAlignmentControl; + } + + /// + /// = + /// + /// + /// with added + public static TAssignable TextCenterHorizontal{{metadata.GenericTypeParameters}}(this TAssignable textAlignmentControl) + where TAssignable : {{metadata.Namespace}}.{{metadata.ClassName}}{{metadata.GenericArguments}}{{metadata.GenericConstraints}} + { + ArgumentNullException.ThrowIfNull(textAlignmentControl); + + if (textAlignmentControl is not ITextAlignment) + { + throw new ArgumentException($"Element must implement {nameof(ITextAlignment)}", nameof(textAlignmentControl)); + } + + textAlignmentControl.HorizontalTextAlignment = TextAlignment.Center; + return textAlignmentControl; + } + + /// + /// = + /// + /// + /// with added + public static TAssignable TextEnd{{metadata.GenericTypeParameters}}(this TAssignable textAlignmentControl) + where TAssignable : {{metadata.Namespace}}.{{metadata.ClassName}}{{metadata.GenericArguments}}{{metadata.GenericConstraints}} + { + ArgumentNullException.ThrowIfNull(textAlignmentControl); + + if (textAlignmentControl is not ITextAlignment) + { + throw new ArgumentException($"Element must implement {nameof(ITextAlignment)}", nameof(textAlignmentControl)); + } + + textAlignmentControl.HorizontalTextAlignment = TextAlignment.End; + return textAlignmentControl; + } + + /// + /// = + /// + /// + /// with added + public static TAssignable TextTop{{metadata.GenericTypeParameters}}(this TAssignable textAlignmentControl) + where TAssignable : {{metadata.Namespace}}.{{metadata.ClassName}}{{metadata.GenericArguments}}{{metadata.GenericConstraints}} + { + ArgumentNullException.ThrowIfNull(textAlignmentControl); + + if (textAlignmentControl is not ITextAlignment) + { + throw new ArgumentException($"Element must implement {nameof(ITextAlignment)}", nameof(textAlignmentControl)); + } + + textAlignmentControl.VerticalTextAlignment = TextAlignment.Start; + return textAlignmentControl; + } + + /// + /// = + /// + /// + /// with added + public static TAssignable TextCenterVertical{{metadata.GenericTypeParameters}}(this TAssignable textAlignmentControl) + where TAssignable : {{metadata.Namespace}}.{{metadata.ClassName}}{{metadata.GenericArguments}}{{metadata.GenericConstraints}} + { + ArgumentNullException.ThrowIfNull(textAlignmentControl); + + if (textAlignmentControl is not ITextAlignment) + { + throw new ArgumentException($"Element must implement {nameof(ITextAlignment)}", nameof(textAlignmentControl)); + } + + textAlignmentControl.VerticalTextAlignment = TextAlignment.Center; + return textAlignmentControl; + } + + /// + /// = + /// + /// + /// with added + public static TAssignable TextBottom{{metadata.GenericTypeParameters}}(this TAssignable textAlignmentControl) + where TAssignable : {{metadata.Namespace}}.{{metadata.ClassName}}{{metadata.GenericArguments}}{{metadata.GenericConstraints}} + { + ArgumentNullException.ThrowIfNull(textAlignmentControl); + + if (textAlignmentControl is not ITextAlignment) + { + throw new ArgumentException($"Element must implement {nameof(ITextAlignment)}", nameof(textAlignmentControl)); + } + + textAlignmentControl.VerticalTextAlignment = TextAlignment.End; + return textAlignmentControl; + } + + /// + /// = = + /// + /// + /// with added + public static TAssignable TextCenter{{metadata.GenericTypeParameters}}(this TAssignable textAlignmentControl) + where TAssignable : {{metadata.Namespace}}.{{metadata.ClassName}}{{metadata.GenericArguments}}{{metadata.GenericConstraints}} + => textAlignmentControl.TextCenterHorizontal{{metadata.GenericTypeParameters}}().TextCenterVertical{{metadata.GenericTypeParameters}}(); + } + + + // The extensions in these sub-namespaces are designed to be used together with the extensions in the parent namespace. + // Keep them in a single file for better maintainability + + namespace LeftToRight + { + /// + /// Extension Methods for + /// + [global::System.CodeDom.Compiler.GeneratedCode("{{className}}", "{{assemblyVersion}}")] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + {{metadata.ClassAccessModifier}} static partial class TextAlignmentExtensions_{{metadata.ClassName}} + { + /// + /// = + /// + /// + /// with + public static TAssignable TextLeft{{metadata.GenericTypeParameters}}(this TAssignable textAlignmentControl) + where TAssignable : {{metadata.Namespace}}.{{metadata.ClassName}}{{metadata.GenericArguments}}{{metadata.GenericConstraints}} + { + ArgumentNullException.ThrowIfNull(textAlignmentControl); + + if (textAlignmentControl is not ITextAlignment) + { + throw new ArgumentException($"Element must implement {nameof(ITextAlignment)}", nameof(textAlignmentControl)); + } + + textAlignmentControl.HorizontalTextAlignment = TextAlignment.Start; + return textAlignmentControl; + } + + /// + /// = + /// + /// + /// with + public static TAssignable TextRight{{metadata.GenericTypeParameters}}(this TAssignable textAlignmentControl) where TAssignable : {{metadata.Namespace}}.{{metadata.ClassName}}{{metadata.GenericArguments}}{{metadata.GenericConstraints}} + { + ArgumentNullException.ThrowIfNull(textAlignmentControl); + + if (textAlignmentControl is not ITextAlignment) + { + throw new ArgumentException($"Element must implement {nameof(ITextAlignment)}", nameof(textAlignmentControl)); + } + + textAlignmentControl.HorizontalTextAlignment = TextAlignment.End; + return textAlignmentControl; + } + } + } + + // The extensions in these sub-namespaces are designed to be used together with the extensions in the parent namespace. + // Keep them in a single file for better maintainability + namespace RightToLeft + { + /// + /// Extension methods for + /// + {{metadata.ClassAccessModifier}} static partial class TextAlignmentExtensions_{{metadata.ClassName}} + { + /// + /// = + /// + /// + /// with + public static TAssignable TextLeft{{metadata.GenericTypeParameters}}(this TAssignable textAlignmentControl) + where TAssignable : {{metadata.Namespace}}.{{metadata.ClassName}}{{metadata.GenericArguments}}{{metadata.GenericConstraints}} + { + ArgumentNullException.ThrowIfNull(textAlignmentControl); + + if (textAlignmentControl is not ITextAlignment) + { + throw new ArgumentException($"Element must implement {nameof(ITextAlignment)}", nameof(textAlignmentControl)); + } + + textAlignmentControl.HorizontalTextAlignment = TextAlignment.End; + return textAlignmentControl; + } + + /// + /// = + /// + /// + /// with + public static TAssignable TextRight{{metadata.GenericTypeParameters}}(this TAssignable textAlignmentControl) + where TAssignable : {{metadata.Namespace}}.{{metadata.ClassName}}{{metadata.GenericArguments}}{{metadata.GenericConstraints}} + { + ArgumentNullException.ThrowIfNull(textAlignmentControl); + + if (textAlignmentControl is not ITextAlignment) + { + throw new ArgumentException($"Element must implement {nameof(ITextAlignment)}", nameof(textAlignmentControl)); + } + + textAlignmentControl.HorizontalTextAlignment = TextAlignment.Start; + return textAlignmentControl; + } + } + } + } + """); + + return sb.ToString(); } static TextAlignmentClassMetadata GenerateMetadata(INamedTypeSymbol namedTypeSymbol) { - var accessModifier = mauiControlsAssembly == namedTypeSymbol.ContainingNamespace.ToDisplayString() + var accessModifier = namedTypeSymbol.ContainingNamespace.ToDisplayString() == mauiControlsAssembly ? "internal" : GetClassAccessModifier(namedTypeSymbol); - return new(namedTypeSymbol.Name, accessModifier, namedTypeSymbol.ContainingNamespace.ToDisplayString(), namedTypeSymbol.TypeArguments.GetGenericTypeArgumentsString(), namedTypeSymbol.GetGenericTypeConstraintsAsString()); + var genericTypeParameters = GetGenericTypeParametersDeclarationString(namedTypeSymbol); + var genericArguments = GetGenericArgumentsString(namedTypeSymbol); + var genericConstraints = GetGenericConstraintsString(namedTypeSymbol); + + return new TextAlignmentClassMetadata( + namedTypeSymbol.Name, + accessModifier, + namedTypeSymbol.ContainingNamespace.ToDisplayString(), + genericTypeParameters, + genericArguments, + genericConstraints + ); + } + + static string GetGenericTypeParametersDeclarationString(INamedTypeSymbol namedTypeSymbol) + { + if (namedTypeSymbol.TypeParameters.Length is 0) + { + return ""; + } + + var typeParams = string.Join(", ", namedTypeSymbol.TypeParameters.Select(t => t.Name)); + return $""; } - record TextAlignmentClassMetadata(string ClassName, string ClassAcessModifier, string Namespace, string GenericArguments, string GenericConstraints); + static string GetGenericArgumentsString(INamedTypeSymbol namedTypeSymbol) + { + return namedTypeSymbol.TypeParameters.Length > 0 + ? $"<{string.Join(", ", namedTypeSymbol.TypeParameters.Select(t => t.Name))}>" + : string.Empty; + } + + static string GetGenericConstraintsString(INamedTypeSymbol namedTypeSymbol) + { + var constraints = namedTypeSymbol.TypeParameters + .Select(GetGenericParameterConstraints) + .Where(static c => !string.IsNullOrEmpty(c)); + + return string.Join(" ", constraints); + } + + static string GetGenericParameterConstraints(ITypeParameterSymbol typeParameter) + { + var constraints = new List(); + + // Primary constraint (class, struct, unmanaged) + if (typeParameter.HasReferenceTypeConstraint) + { + constraints.Add(typeParameter.ReferenceTypeConstraintNullableAnnotation is NullableAnnotation.Annotated + ? "class?" + : "class"); + } + else if (typeParameter.HasValueTypeConstraint) + { + constraints.Add(typeParameter.HasUnmanagedTypeConstraint ? "unmanaged" : "struct"); + } + else if (typeParameter.HasUnmanagedTypeConstraint) + { + constraints.Add("unmanaged"); + } + else if (typeParameter.HasNotNullConstraint) + { + constraints.Add("notnull"); + } + + // Secondary constraints (specific types) + foreach (var constraintType in typeParameter.ConstraintTypes) + { + var symbolDisplayFormat = new SymbolDisplayFormat( + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, + miscellaneousOptions: SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier); + + var constraintTypeString = constraintType.ToDisplayString(symbolDisplayFormat); + + constraints.Add(constraintTypeString); + } + + // Check for record constraint + if (typeParameter.IsRecord) + { + constraints.Add("record"); + } + + // Constructor constraint (must be last) + if (typeParameter.HasConstructorConstraint) + { + constraints.Add("new()"); + } + + return constraints.Count > 0 + ? $"where {typeParameter.Name} : {string.Join(", ", constraints)}" + : string.Empty; + } } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators/TextAlignmentClassMetadata.cs b/src/CommunityToolkit.Maui.Markup.SourceGenerators/TextAlignmentClassMetadata.cs new file mode 100644 index 00000000..427482ed --- /dev/null +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators/TextAlignmentClassMetadata.cs @@ -0,0 +1,9 @@ +namespace CommunityToolkit.Maui.Markup.SourceGenerators; + +public record TextAlignmentClassMetadata( + string ClassName, + string ClassAccessModifier, + string Namespace, + string GenericTypeParameters, + string GenericArguments, + string GenericConstraints); \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Markup.UnitTests/TextAlignmentExtensionsTests.cs b/src/CommunityToolkit.Maui.Markup.UnitTests/TextAlignmentExtensionsTests.cs index 481069a5..1801dfde 100644 --- a/src/CommunityToolkit.Maui.Markup.UnitTests/TextAlignmentExtensionsTests.cs +++ b/src/CommunityToolkit.Maui.Markup.UnitTests/TextAlignmentExtensionsTests.cs @@ -81,9 +81,9 @@ public void Extensions_For_Generic_Class() ClassConstraintWithInterface?, ClassConstraint[], ClassConstraintWithInterface, - RecordClassContstraint, - RecordClassContstraint[], - RecordStructContstraint>() + RecordClassConstraint, + RecordClassConstraint[], + RecordStructConstraint>() .TextCenter, + RecordClassConstraint, + RecordClassConstraint[], + RecordStructConstraint>, ClassConstraintWithInterface, ClassConstraint, StructConstraint, @@ -107,9 +107,9 @@ public void Extensions_For_Generic_Class() ClassConstraintWithInterface?, ClassConstraint[], ClassConstraintWithInterface, - RecordClassContstraint, - RecordClassContstraint[], - RecordStructContstraint>(); + RecordClassConstraint, + RecordClassConstraint[], + RecordStructConstraint>(); Assert.That(textAlignmentView.HorizontalTextAlignment, Is.EqualTo(TextAlignment.Center)); @@ -123,9 +123,9 @@ public void Extensions_For_Generic_Class() ClassConstraintWithInterface?, ClassConstraint[], ClassConstraintWithInterface, - RecordClassContstraint, - RecordClassContstraint[], - RecordStructContstraint>, + RecordClassConstraint, + RecordClassConstraint[], + RecordStructConstraint>, ClassConstraintWithInterface, ClassConstraint, StructConstraint, @@ -136,9 +136,9 @@ public void Extensions_For_Generic_Class() ClassConstraintWithInterface?, ClassConstraint[], ClassConstraintWithInterface, - RecordClassContstraint, - RecordClassContstraint[], - RecordStructContstraint>(); + RecordClassConstraint, + RecordClassConstraint[], + RecordStructConstraint>(); Assert.That(textAlignmentView.HorizontalTextAlignment, Is.EqualTo(TextAlignment.End)); } @@ -460,13 +460,13 @@ class MyGenericPicker : Picker } - public record RecordClassContstraint + public record RecordClassConstraint { } - public readonly record struct RecordStructContstraint + public readonly record struct RecordStructConstraint { } diff --git a/src/CommunityToolkit.Maui.Markup.sln b/src/CommunityToolkit.Maui.Markup.sln index 044c1144..13cea65f 100644 --- a/src/CommunityToolkit.Maui.Markup.sln +++ b/src/CommunityToolkit.Maui.Markup.sln @@ -14,9 +14,17 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ..\global.json = ..\global.json EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Maui.Markup.SourceGenerators", "CommunityToolkit.Maui.Markup.SourceGenerators\CommunityToolkit.Maui.Markup.SourceGenerators.csproj", "{C66CEA39-565E-479C-974D-72795D3502CB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Maui.Markup.SourceGenerators", "CommunityToolkit.Maui.Markup.SourceGenerators\CommunityToolkit.Maui.Markup.SourceGenerators.csproj", "{C66CEA39-565E-479C-974D-72795D3502CB}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Maui.Markup.Benchmarks", "CommunityToolkit.Maui.Markup.Benchmarks\CommunityToolkit.Maui.Markup.Benchmarks.csproj", "{9A46B6CE-CC4B-4F26-80F8-779C94A88C18}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Maui.Markup.Benchmarks", "CommunityToolkit.Maui.Markup.Benchmarks\CommunityToolkit.Maui.Markup.Benchmarks.csproj", "{9A46B6CE-CC4B-4F26-80F8-779C94A88C18}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests", "CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests\CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests.csproj", "{BB9611DF-34B9-4BBF-A564-EB2AADD943C8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{0716CCB2-1629-4E73-ACE8-643A6F9F5AD3}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Benchmarks", "Benchmarks", "{F690A1B1-9B74-4DB7-8A98-D0CBF794EDAD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Maui.Markup.SourceGenerators.Benchmarks", "CommunityToolkit.Maui.Markup.SourceGenerators.Benchmarks\CommunityToolkit.Maui.Markup.SourceGenerators.Benchmarks.csproj", "{2B682080-908C-467C-B429-3700AFFFD612}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -40,10 +48,24 @@ Global {9A46B6CE-CC4B-4F26-80F8-779C94A88C18}.Debug|Any CPU.Build.0 = Debug|Any CPU {9A46B6CE-CC4B-4F26-80F8-779C94A88C18}.Release|Any CPU.ActiveCfg = Release|Any CPU {9A46B6CE-CC4B-4F26-80F8-779C94A88C18}.Release|Any CPU.Build.0 = Release|Any CPU + {BB9611DF-34B9-4BBF-A564-EB2AADD943C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BB9611DF-34B9-4BBF-A564-EB2AADD943C8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BB9611DF-34B9-4BBF-A564-EB2AADD943C8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BB9611DF-34B9-4BBF-A564-EB2AADD943C8}.Release|Any CPU.Build.0 = Release|Any CPU + {2B682080-908C-467C-B429-3700AFFFD612}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2B682080-908C-467C-B429-3700AFFFD612}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2B682080-908C-467C-B429-3700AFFFD612}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2B682080-908C-467C-B429-3700AFFFD612}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {480F4FB8-F50A-4349-B5B6-C4D6D9151343} = {0716CCB2-1629-4E73-ACE8-643A6F9F5AD3} + {9A46B6CE-CC4B-4F26-80F8-779C94A88C18} = {F690A1B1-9B74-4DB7-8A98-D0CBF794EDAD} + {BB9611DF-34B9-4BBF-A564-EB2AADD943C8} = {0716CCB2-1629-4E73-ACE8-643A6F9F5AD3} + {2B682080-908C-467C-B429-3700AFFFD612} = {F690A1B1-9B74-4DB7-8A98-D0CBF794EDAD} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {42B065EB-DA54-40BE-B676-3D47D59A81F2} EndGlobalSection diff --git a/src/CommunityToolkit.Maui.Markup/CommunityToolkit.Maui.Markup.csproj b/src/CommunityToolkit.Maui.Markup/CommunityToolkit.Maui.Markup.csproj index 0290633e..edf0a8c4 100644 --- a/src/CommunityToolkit.Maui.Markup/CommunityToolkit.Maui.Markup.csproj +++ b/src/CommunityToolkit.Maui.Markup/CommunityToolkit.Maui.Markup.csproj @@ -14,7 +14,7 @@ Microsoft Microsoft en - CommunityToolkit.Maui.Markup (net6.0) + CommunityToolkit.Maui.Markup © Microsoft Corporation. All rights reserved. MIT https://github.com/communitytoolkit/Maui.Markup