From f64408afc5cdd35b9bda87efc8c55407a4a6bb87 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Fri, 4 Oct 2024 12:40:54 -0700 Subject: [PATCH 01/20] Add Unit Tests --- .../CommunityToolkit.Maui.Markup.Sample.sln | 6 ++ ...i.Markup.SourceGenerators.UnitTests.csproj | 32 ++++++++ .../TextAlignmentExtensionsGeneratorTests.cs | 63 ++++++++++++++++ .../Verifiers/CSharpAnalyzerVerifier+Test.cs | 49 ++++++++++++ .../Verifiers/CSharpAnalyzerVerifier.cs | 34 +++++++++ .../Verifiers/CSharpCodeFixVerifier+Test.cs | 54 ++++++++++++++ .../Verifiers/CSharpCodeFixVerifier.cs | 56 ++++++++++++++ .../CSharpSourceGeneratorVerifier+Test.cs | 74 +++++++++++++++++++ .../CSharpSourceGeneratorVerifier.cs | 22 ++++++ .../Verifiers/CSharpVerifierHelper.cs | 31 ++++++++ ...oolkit.Maui.Markup.SourceGenerators.csproj | 4 + .../TextAlignmentExtensionsGenerator.cs | 22 +++--- src/CommunityToolkit.Maui.Markup.sln | 6 ++ .../CommunityToolkit.Maui.Markup.csproj | 2 +- 14 files changed, 443 insertions(+), 12 deletions(-) create mode 100644 src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests.csproj create mode 100644 src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/TextAlignmentExtensionsGeneratorTests.cs create mode 100644 src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpAnalyzerVerifier+Test.cs create mode 100644 src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpAnalyzerVerifier.cs create mode 100644 src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpCodeFixVerifier+Test.cs create mode 100644 src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpCodeFixVerifier.cs create mode 100644 src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpSourceGeneratorVerifier+Test.cs create mode 100644 src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpSourceGeneratorVerifier.cs create mode 100644 src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpVerifierHelper.cs diff --git a/samples/CommunityToolkit.Maui.Markup.Sample.sln b/samples/CommunityToolkit.Maui.Markup.Sample.sln index 14dd228a..37316951 100644 --- a/samples/CommunityToolkit.Maui.Markup.Sample.sln +++ b/samples/CommunityToolkit.Maui.Markup.Sample.sln @@ -22,6 +22,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Maui.Marku 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}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests", "..\src\CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests\CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests.csproj", "{26D5485B-68EA-454A-BBB7-11C6FF9B1B98}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -50,6 +52,10 @@ 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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE 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..097cc1ad --- /dev/null +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests.csproj @@ -0,0 +1,32 @@ + + + + $(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..dad5f8e1 --- /dev/null +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/TextAlignmentExtensionsGeneratorTests.cs @@ -0,0 +1,63 @@ +using NUnit.Framework; +using static CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests.CSharpSourceGeneratorVerifier; + +namespace CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests; + +public class TextAlignmentExtensionsGeneratorTests +{ + [Test] + public async Task VerifyGeneratedSource_WhenClassImplementsITextAlignmentInterface() + { + // Arrange + const string source = """ + + namespace MyNamespace + { + public class MyClass : Microsoft.Maui.ITextAlignment + { + } + } + """; + + // Act // Assert + await VerifySourceGeneratorAsync(source, [typeof(TextAlignmentExtensionsGenerator)]); + } + + [Test] + public async Task VerifyGeneratedSource_WhenClassDoesNotImplementITextAlignmentInterface() + { + // Arrange + const string source = """ + + namespace MyNamespace + { + public class MyClass + { + } + } + """; + + // Act // Assert + await VerifySourceGeneratorAsync(source, [typeof(TextAlignmentExtensionsGenerator)]); + } + + [Test] + public async Task VerifyGeneratedSource_WhenClassIsGeneric() + { + // Arrange + const string source = """ + + namespace MyNamespace + { + public class MyClass : Microsoft.Maui.ITextAlignment + where T : IDisposable, new() + where U : class + { + } + } + """; + + // Act // Assert + await VerifySourceGeneratorAsync(source, [typeof(TextAlignmentExtensionsGenerator)]); + } +} \ 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..e883ee8b --- /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() +{ + public 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..9a5eafed --- /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() +{ + protected 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..e9ab495c --- /dev/null +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpSourceGeneratorVerifier.cs @@ -0,0 +1,22 @@ +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 CSharpSourceGeneratorVerifier + where TSourceGenerator : IIncrementalGenerator, new() +{ + /// + public static async Task VerifySourceGeneratorAsync(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/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 da3bfa2c..c7ae9a34 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 606d6b65..5735d8df 100644 --- a/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs @@ -8,7 +8,7 @@ namespace CommunityToolkit.Maui.Markup.SourceGenerators; [Generator(LanguageNames.CSharp)] -class TextAlignmentExtensionsGenerator : IIncrementalGenerator +public class TextAlignmentExtensionsGenerator : IIncrementalGenerator { const string iTextAlignmentInterface = "Microsoft.Maui.ITextAlignment"; const string mauiControlsAssembly = "Microsoft.Maui.Controls"; @@ -56,6 +56,16 @@ public void Initialize(IncrementalGeneratorInitializationContext context) context.RegisterSourceOutput(userGeneratedClassesProvider, Execute); context.RegisterSourceOutput(mauiControlsAssemblySymbolProvider, ExecuteArray); } + + internal static string GetGenericArgumentsString(in string genericArguments) + { + if (string.IsNullOrWhiteSpace(genericArguments)) + { + return string.Empty; + } + + return $"<{genericArguments}>"; + } static bool ShouldGenerateTextAlignmentExtension(INamedTypeSymbol classSymbol, INamedTypeSymbol iTextAlignmentInterfaceSymbol) { @@ -360,16 +370,6 @@ static string GetGenericTypeParametersDeclarationString(in string genericArgumen return $""; } - static string GetGenericArgumentsString(in string genericArguments) - { - if (string.IsNullOrWhiteSpace(genericArguments)) - { - return string.Empty; - } - - return $"<{genericArguments}>"; - } - static TextAlignmentClassMetadata GenerateMetadata(INamedTypeSymbol namedTypeSymbol) { var accessModifier = mauiControlsAssembly == namedTypeSymbol.ContainingNamespace.ToDisplayString() diff --git a/src/CommunityToolkit.Maui.Markup.sln b/src/CommunityToolkit.Maui.Markup.sln index 044c1144..eb7c3c43 100644 --- a/src/CommunityToolkit.Maui.Markup.sln +++ b/src/CommunityToolkit.Maui.Markup.sln @@ -18,6 +18,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Maui.Marku 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}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests", "CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests\CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests.csproj", "{BB9611DF-34B9-4BBF-A564-EB2AADD943C8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -40,6 +42,10 @@ 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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/CommunityToolkit.Maui.Markup/CommunityToolkit.Maui.Markup.csproj b/src/CommunityToolkit.Maui.Markup/CommunityToolkit.Maui.Markup.csproj index abb4ea2f..c8cd985b 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 From 3d80496d393a2466f2e961019426fc2af152e98e Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Fri, 4 Oct 2024 12:49:47 -0700 Subject: [PATCH 02/20] Update TextAlignmentExtensionsGenerator.cs --- .../TextAlignmentExtensionsGenerator.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs b/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs index 5735d8df..c2dd458a 100644 --- a/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs @@ -56,16 +56,6 @@ public void Initialize(IncrementalGeneratorInitializationContext context) context.RegisterSourceOutput(userGeneratedClassesProvider, Execute); context.RegisterSourceOutput(mauiControlsAssemblySymbolProvider, ExecuteArray); } - - internal static string GetGenericArgumentsString(in string genericArguments) - { - if (string.IsNullOrWhiteSpace(genericArguments)) - { - return string.Empty; - } - - return $"<{genericArguments}>"; - } static bool ShouldGenerateTextAlignmentExtension(INamedTypeSymbol classSymbol, INamedTypeSymbol iTextAlignmentInterfaceSymbol) { @@ -369,6 +359,16 @@ static string GetGenericTypeParametersDeclarationString(in string genericArgumen return $""; } + + static string GetGenericArgumentsString(in string genericArguments) + { + if (string.IsNullOrWhiteSpace(genericArguments)) + { + return string.Empty; + } + + return $"<{genericArguments}>"; + } static TextAlignmentClassMetadata GenerateMetadata(INamedTypeSymbol namedTypeSymbol) { From 2fac95ba6f160f1f685769d7bf79ac773606de75 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Fri, 4 Oct 2024 13:33:16 -0700 Subject: [PATCH 03/20] Update Tests --- .../Temp.cs | 7 + .../TextAlignmentExtensionsGeneratorTests.cs | 269 +++++++++++++++++- .../Verifiers/CSharpAnalyzerVerifier+Test.cs | 2 +- .../Verifiers/CSharpCodeFixVerifier+Test.cs | 2 +- .../CSharpSourceGeneratorVerifier+Test.cs | 7 +- .../CSharpSourceGeneratorVerifier.cs | 17 +- .../TextAlignmentExtensionsGenerator.cs | 8 +- .../TextAlignmentClassMetadata.cs | 3 + 8 files changed, 293 insertions(+), 22 deletions(-) create mode 100644 src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Temp.cs create mode 100644 src/CommunityToolkit.Maui.Markup.SourceGenerators/TextAlignmentClassMetadata.cs diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Temp.cs b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Temp.cs new file mode 100644 index 00000000..30808950 --- /dev/null +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Temp.cs @@ -0,0 +1,7 @@ +namespace MyNamespace; + +public class MyClass : Microsoft.Maui.ITextAlignment +{ + public TextAlignment HorizontalTextAlignment { get; } + public TextAlignment VerticalTextAlignment { get; } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/TextAlignmentExtensionsGeneratorTests.cs b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/TextAlignmentExtensionsGeneratorTests.cs index dad5f8e1..732fcfe7 100644 --- a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/TextAlignmentExtensionsGeneratorTests.cs +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/TextAlignmentExtensionsGeneratorTests.cs @@ -1,16 +1,19 @@ -using NUnit.Framework; +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 = """ - namespace MyNamespace { public class MyClass : Microsoft.Maui.ITextAlignment @@ -20,7 +23,7 @@ public class MyClass : Microsoft.Maui.ITextAlignment """; // Act // Assert - await VerifySourceGeneratorAsync(source, [typeof(TextAlignmentExtensionsGenerator)]); + await VerifySourceGeneratorAsync(source, "MyNamespaceTextAlignmentExtensions.g.cs", GenerateSourceCode(textAlignmentExtensionsGeneratorFullName, assemblyVersion, new("MyClass", "public", "MyNamespace", string.Empty, string.Empty), string.Empty, string.Empty), []); } [Test] @@ -37,8 +40,12 @@ public class MyClass } """; + const string generated = """ + + """; + // Act // Assert - await VerifySourceGeneratorAsync(source, [typeof(TextAlignmentExtensionsGenerator)]); + await VerifySourceGeneratorAsync(source, "MyClassTextAlignmentExtensions.g.cs", generated, []); } [Test] @@ -57,7 +64,259 @@ public class MyClass : Microsoft.Maui.ITextAlignment } """; + const string generated = """ + + """; + // Act // Assert - await VerifySourceGeneratorAsync(source, [typeof(TextAlignmentExtensionsGenerator)]); + await VerifySourceGeneratorAsync(source, "MyClassTextAlignmentExtensions.g.cs", generated, []); } + + static string GenerateSourceCode(string fullClassName, string assemblyVersion, TextAlignmentClassMetadata textAlignmentClassMetadata, string genericArguments, string genericTypeParameters) => + $$""" + // + // 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{{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("{{fullClassName}}", "{{assemblyVersion}}")] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + {{textAlignmentClassMetadata.ClassAccessModifier}} 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.ClassAccessModifier}} 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; + } + } + } + } + """; } \ 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 index e883ee8b..21e265d4 100644 --- a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpAnalyzerVerifier+Test.cs +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpAnalyzerVerifier+Test.cs @@ -6,7 +6,7 @@ namespace CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests; public static partial class CSharpAnalyzerVerifier where TAnalyzer : DiagnosticAnalyzer, new() { - public class Test : CSharpAnalyzerTest + class Test : CSharpAnalyzerTest { public Test(params Type[] assembliesUnderTest) { diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpCodeFixVerifier+Test.cs b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpCodeFixVerifier+Test.cs index 9a5eafed..094f6c9c 100644 --- a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpCodeFixVerifier+Test.cs +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpCodeFixVerifier+Test.cs @@ -10,7 +10,7 @@ public static partial class CSharpCodeFixVerifier where TAnalyzer : DiagnosticAnalyzer, new() where TCodeFix : CodeFixProvider, new() { - protected class Test : CSharpCodeFixTest + class Test : CSharpCodeFixTest { public Test(params Type[] assembliesUnderTest) { diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpSourceGeneratorVerifier+Test.cs b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpSourceGeneratorVerifier+Test.cs index 6ad79573..cd9e063e 100644 --- a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpSourceGeneratorVerifier+Test.cs +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpSourceGeneratorVerifier+Test.cs @@ -17,12 +17,7 @@ public Test(params Type[] assembliesUnderTest) #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 - ]; + List typesForAssembliesUnderTest = []; typesForAssembliesUnderTest.AddRange(assembliesUnderTest); foreach (var type in typesForAssembliesUnderTest) diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpSourceGeneratorVerifier.cs b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpSourceGeneratorVerifier.cs index e9ab495c..84945ce6 100644 --- a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpSourceGeneratorVerifier.cs +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpSourceGeneratorVerifier.cs @@ -1,7 +1,9 @@ +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; @@ -9,14 +11,21 @@ public static partial class CSharpSourceGeneratorVerifier where TSourceGenerator : IIncrementalGenerator, new() { /// - public static async Task VerifySourceGeneratorAsync(string source, Type[] assembliesUnderTest, params DiagnosticResult[] expected) + public static async Task VerifySourceGeneratorAsync(string source, string generatedFileName, string expectedGeneratedCode, Type[] assembliesUnderTest, params DiagnosticResult[] expectedDiagnosticResults) { var test = new Test(assembliesUnderTest) { - TestCode = source, + TestState = + { + Sources = { source }, + GeneratedSources = + { + (typeof(TSourceGenerator), generatedFileName, SourceText.From(expectedGeneratedCode, Encoding.UTF8, SourceHashAlgorithm.Sha256)), + } + } }; - test.ExpectedDiagnostics.AddRange(expected); - await test.RunAsync(CancellationToken.None); + 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/SourceGenerators/TextAlignmentExtensionsGenerator.cs b/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs index c2dd458a..a104c0c1 100644 --- a/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs @@ -107,7 +107,7 @@ namespace CommunityToolkit.Maui.Markup /// [global::System.CodeDom.Compiler.GeneratedCode("{{className}}", "{{assemblyVersion}}")] [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - {{textAlignmentClassMetadata.ClassAcessModifier}} static partial class TextAlignmentExtensions_{{textAlignmentClassMetadata.ClassName}} + {{textAlignmentClassMetadata.ClassAccessModifier}} static partial class TextAlignmentExtensions_{{textAlignmentClassMetadata.ClassName}} { /// /// = @@ -244,7 +244,7 @@ namespace LeftToRight /// [global::System.CodeDom.Compiler.GeneratedCode("{{className}}", "{{assemblyVersion}}")] [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - {{textAlignmentClassMetadata.ClassAcessModifier}} static partial class TextAlignmentExtensions_{{textAlignmentClassMetadata.ClassName}} + {{textAlignmentClassMetadata.ClassAccessModifier}} static partial class TextAlignmentExtensions_{{textAlignmentClassMetadata.ClassName}} { /// /// = @@ -292,7 +292,7 @@ namespace RightToLeft /// /// Extension methods for /// - {{textAlignmentClassMetadata.ClassAcessModifier}} static partial class TextAlignmentExtensions_{{textAlignmentClassMetadata.ClassName}} + {{textAlignmentClassMetadata.ClassAccessModifier}} static partial class TextAlignmentExtensions_{{textAlignmentClassMetadata.ClassName}} { /// /// = @@ -378,6 +378,4 @@ static TextAlignmentClassMetadata GenerateMetadata(INamedTypeSymbol namedTypeSym return new(namedTypeSymbol.Name, accessModifier, namedTypeSymbol.ContainingNamespace.ToDisplayString(), namedTypeSymbol.TypeArguments.GetGenericTypeArgumentsString(), namedTypeSymbol.GetGenericTypeConstraintsAsString()); } - - record TextAlignmentClassMetadata(string ClassName, string ClassAcessModifier, string Namespace, string GenericArguments, string GenericConstraints); } \ 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..05c9e92c --- /dev/null +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators/TextAlignmentClassMetadata.cs @@ -0,0 +1,3 @@ +namespace CommunityToolkit.Maui.Markup.SourceGenerators; + +public record TextAlignmentClassMetadata(string ClassName, string ClassAccessModifier, string Namespace, string GenericArguments, string GenericConstraints); \ No newline at end of file From 93e985b7e5cc779cce62a8b087d747c6771a0dce Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Fri, 4 Oct 2024 14:33:47 -0700 Subject: [PATCH 04/20] Fix Unit Tests --- .../Temp.cs | 7 - .../TextAlignmentExtensionsGeneratorTests.cs | 572 +++++++++--------- .../CSharpSourceGeneratorVerifier+Test.cs | 7 +- .../CSharpSourceGeneratorVerifier.cs | 2 + .../TextAlignmentExtensionsTests.cs | 4 +- 5 files changed, 290 insertions(+), 302 deletions(-) delete mode 100644 src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Temp.cs diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Temp.cs b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Temp.cs deleted file mode 100644 index 30808950..00000000 --- a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Temp.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace MyNamespace; - -public class MyClass : Microsoft.Maui.ITextAlignment -{ - public TextAlignment HorizontalTextAlignment { get; } - public TextAlignment VerticalTextAlignment { get; } -} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/TextAlignmentExtensionsGeneratorTests.cs b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/TextAlignmentExtensionsGeneratorTests.cs index 732fcfe7..789378a2 100644 --- a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/TextAlignmentExtensionsGeneratorTests.cs +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/TextAlignmentExtensionsGeneratorTests.cs @@ -6,317 +6,305 @@ 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"); - + 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 = """ - namespace MyNamespace - { - public class MyClass : Microsoft.Maui.ITextAlignment - { - } - } - """; + 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, "MyNamespaceTextAlignmentExtensions.g.cs", GenerateSourceCode(textAlignmentExtensionsGeneratorFullName, assemblyVersion, new("MyClass", "public", "MyNamespace", string.Empty, string.Empty), string.Empty, string.Empty), []); + await VerifySourceGeneratorAsync( + source, + "MyClassTextAlignmentExtensions.g.cs", + GenerateSourceCode(textAlignmentExtensionsGeneratorFullName, + new("MyClass", "public", "MyNamespace", string.Empty, string.Empty), string.Empty, string.Empty), + []); } [Test] - public async Task VerifyGeneratedSource_WhenClassDoesNotImplementITextAlignmentInterface() + public async Task VerifyGeneratedSource_WhenClassIsGeneric() { // Arrange - const string source = """ - - namespace MyNamespace - { - public class MyClass - { - } - } - """; - - const string generated = """ + const string source = /* language=C#-test */ """ +using System; +using Microsoft.Maui; +namespace MyNamespace; - """; +public class MyClass : 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, "MyClassTextAlignmentExtensions.g.cs", generated, []); + await VerifySourceGeneratorAsync( + source, + "MyClassTextAlignmentExtensions.g.cs", + GenerateSourceCode(textAlignmentExtensionsGeneratorFullName, + new("MyClass", "public", "MyNamespace", "", "where T : IDisposable, new() where U : class"), string.Empty, string.Empty), + []); } - [Test] - public async Task VerifyGeneratedSource_WhenClassIsGeneric() - { - // Arrange - const string source = """ + static string GenerateSourceCode(string fullClassName, TextAlignmentClassMetadata textAlignmentClassMetadata, string genericArguments, string genericTypeParameters) => + /* language=C#-test */ $$""" +// +// See: CommunityToolkit.Maui.Markup.SourceGenerators.TextAlignmentGenerator - namespace MyNamespace - { - public class MyClass : Microsoft.Maui.ITextAlignment - where T : IDisposable, new() - where U : class - { - } - } - """; +#nullable enable +#pragma warning disable - const string generated = """ +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{{genericTypeParameters}}(this TAssignable textAlignmentControl) + where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{genericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} + { + ArgumentNullException.ThrowIfNull(textAlignmentControl); - // Act // Assert - await VerifySourceGeneratorAsync(source, "MyClassTextAlignmentExtensions.g.cs", generated, []); - } + 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("{{fullClassName}}", "{{assemblyVersion}}")] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + {{textAlignmentClassMetadata.ClassAccessModifier}} 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.ClassAccessModifier}} 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)); + } - static string GenerateSourceCode(string fullClassName, string assemblyVersion, TextAlignmentClassMetadata textAlignmentClassMetadata, string genericArguments, string genericTypeParameters) => - $$""" - // - // 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{{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("{{fullClassName}}", "{{assemblyVersion}}")] - [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - {{textAlignmentClassMetadata.ClassAccessModifier}} 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.ClassAccessModifier}} 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; - } - } - } - } - """; + textAlignmentControl.HorizontalTextAlignment = TextAlignment.Start; + return textAlignmentControl; + } + } + } +} +"""; } \ 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 index cd9e063e..6ad79573 100644 --- a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpSourceGeneratorVerifier+Test.cs +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpSourceGeneratorVerifier+Test.cs @@ -17,7 +17,12 @@ public Test(params Type[] assembliesUnderTest) #else #error ReferenceAssemblies must be updated to current version of .NET #endif - List typesForAssembliesUnderTest = []; + 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) diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpSourceGeneratorVerifier.cs b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpSourceGeneratorVerifier.cs index 84945ce6..d3d61b5d 100644 --- a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpSourceGeneratorVerifier.cs +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpSourceGeneratorVerifier.cs @@ -15,6 +15,7 @@ public static async Task VerifySourceGeneratorAsync(string source, string genera { var test = new Test(assembliesUnderTest) { + TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck, TestState = { Sources = { source }, @@ -26,6 +27,7 @@ public static async Task VerifySourceGeneratorAsync(string source, string genera }; test.ExpectedDiagnostics.AddRange(expectedDiagnosticResults); + await test.RunAsync(CancellationToken.None).ConfigureAwait(false); } } \ 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 213453cd..fb4ed88d 100644 --- a/src/CommunityToolkit.Maui.Markup.UnitTests/TextAlignmentExtensionsTests.cs +++ b/src/CommunityToolkit.Maui.Markup.UnitTests/TextAlignmentExtensionsTests.cs @@ -251,14 +251,14 @@ where type.Name.StartsWith("TextAlignmentExtensions_") class DerivedFromSearchBar : SearchBar { } } - class CustomTextAlignmentControl : ITextAlignment + sealed class CustomTextAlignmentControl : ITextAlignment { public TextAlignment HorizontalTextAlignment { get; set; } = TextAlignment.Center; public TextAlignment VerticalTextAlignment { get; set; } = TextAlignment.Center; } - class MyEntry : Entry, IEntry + sealed class MyEntry : Entry, IEntry { public string Type { get; set; } = string.Empty; } From 9f51055cdc30d6f6f91039c48bd76de1a1fb2302 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Fri, 4 Oct 2024 14:36:03 -0700 Subject: [PATCH 05/20] Update TextAlignmentExtensionsTests.cs --- .../TextAlignmentExtensionsTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Maui.Markup.UnitTests/TextAlignmentExtensionsTests.cs b/src/CommunityToolkit.Maui.Markup.UnitTests/TextAlignmentExtensionsTests.cs index fb4ed88d..213453cd 100644 --- a/src/CommunityToolkit.Maui.Markup.UnitTests/TextAlignmentExtensionsTests.cs +++ b/src/CommunityToolkit.Maui.Markup.UnitTests/TextAlignmentExtensionsTests.cs @@ -251,14 +251,14 @@ where type.Name.StartsWith("TextAlignmentExtensions_") class DerivedFromSearchBar : SearchBar { } } - sealed class CustomTextAlignmentControl : ITextAlignment + class CustomTextAlignmentControl : ITextAlignment { public TextAlignment HorizontalTextAlignment { get; set; } = TextAlignment.Center; public TextAlignment VerticalTextAlignment { get; set; } = TextAlignment.Center; } - sealed class MyEntry : Entry, IEntry + class MyEntry : Entry, IEntry { public string Type { get; set; } = string.Empty; } From 7dd2c3f2104a93371f82ec0d225072cfa734ecee Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Fri, 4 Oct 2024 14:37:24 -0700 Subject: [PATCH 06/20] Update azure-pipelines.yml --- azure-pipelines.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 1b0b92a7..7afd6edd 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -12,6 +12,7 @@ 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' CommunityToolkitSampleApp_Xcode_Version: '16' CommunityToolkitLibrary_Xcode_Version: '16' ShouldCheckDependencies: true @@ -170,6 +171,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: From f751a621b6a5862dae0e5ba93e4f32db0b967284 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Fri, 4 Oct 2024 14:40:48 -0700 Subject: [PATCH 07/20] Remove Generated File Name --- .../TextAlignmentExtensionsGeneratorTests.cs | 2 -- .../Verifiers/CSharpSourceGeneratorVerifier.cs | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/TextAlignmentExtensionsGeneratorTests.cs b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/TextAlignmentExtensionsGeneratorTests.cs index 789378a2..02abe956 100644 --- a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/TextAlignmentExtensionsGeneratorTests.cs +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/TextAlignmentExtensionsGeneratorTests.cs @@ -27,7 +27,6 @@ public class MyClass : ITextAlignment // Act // Assert await VerifySourceGeneratorAsync( source, - "MyClassTextAlignmentExtensions.g.cs", GenerateSourceCode(textAlignmentExtensionsGeneratorFullName, new("MyClass", "public", "MyNamespace", string.Empty, string.Empty), string.Empty, string.Empty), []); @@ -54,7 +53,6 @@ public class MyClass : Microsoft.Maui.ITextAlignment // Act // Assert await VerifySourceGeneratorAsync( source, - "MyClassTextAlignmentExtensions.g.cs", GenerateSourceCode(textAlignmentExtensionsGeneratorFullName, new("MyClass", "public", "MyNamespace", "", "where T : IDisposable, new() where U : class"), string.Empty, string.Empty), []); diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpSourceGeneratorVerifier.cs b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpSourceGeneratorVerifier.cs index d3d61b5d..4f2d3d1b 100644 --- a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpSourceGeneratorVerifier.cs +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/Verifiers/CSharpSourceGeneratorVerifier.cs @@ -11,7 +11,7 @@ public static partial class CSharpSourceGeneratorVerifier where TSourceGenerator : IIncrementalGenerator, new() { /// - public static async Task VerifySourceGeneratorAsync(string source, string generatedFileName, string expectedGeneratedCode, Type[] assembliesUnderTest, params DiagnosticResult[] expectedDiagnosticResults) + public static async Task VerifySourceGeneratorAsync(string source, string expectedGeneratedCode, Type[] assembliesUnderTest, params DiagnosticResult[] expectedDiagnosticResults) { var test = new Test(assembliesUnderTest) { @@ -21,7 +21,7 @@ public static async Task VerifySourceGeneratorAsync(string source, string genera Sources = { source }, GeneratedSources = { - (typeof(TSourceGenerator), generatedFileName, SourceText.From(expectedGeneratedCode, Encoding.UTF8, SourceHashAlgorithm.Sha256)), + (typeof(TSourceGenerator), string.Empty, SourceText.From(expectedGeneratedCode, Encoding.UTF8, SourceHashAlgorithm.Sha256)), } } }; From f661c5fa20bbccd3b2dda11a7c50550cb1205164 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Sun, 6 Oct 2024 12:03:42 -0700 Subject: [PATCH 08/20] Refactor TextAlignmentExtensionsGenerator --- .../TextAlignmentExtensionsGeneratorTests.cs | 50 +- .../TextAlignmentExtensionsGenerator.cs | 727 ++++++++++-------- .../TextAlignmentClassMetadata.cs | 8 +- 3 files changed, 438 insertions(+), 347 deletions(-) diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/TextAlignmentExtensionsGeneratorTests.cs b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/TextAlignmentExtensionsGeneratorTests.cs index 02abe956..0e4c8917 100644 --- a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/TextAlignmentExtensionsGeneratorTests.cs +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/TextAlignmentExtensionsGeneratorTests.cs @@ -28,7 +28,7 @@ public class MyClass : ITextAlignment await VerifySourceGeneratorAsync( source, GenerateSourceCode(textAlignmentExtensionsGeneratorFullName, - new("MyClass", "public", "MyNamespace", string.Empty, string.Empty), string.Empty, string.Empty), + new("MyClass", "public", "MyNamespace", string.Empty, string.Empty, string.Empty)), []); } @@ -54,11 +54,11 @@ public class MyClass : Microsoft.Maui.ITextAlignment await VerifySourceGeneratorAsync( source, GenerateSourceCode(textAlignmentExtensionsGeneratorFullName, - new("MyClass", "public", "MyNamespace", "", "where T : IDisposable, new() where U : class"), string.Empty, string.Empty), + new("MyClass", "public", "MyNamespace", "", string.Empty, "where T : IDisposable, new() where U : class")), []); } - static string GenerateSourceCode(string fullClassName, TextAlignmentClassMetadata textAlignmentClassMetadata, string genericArguments, string genericTypeParameters) => + static string GenerateSourceCode(string fullClassName, TextAlignmentClassMetadata textAlignmentClassMetadata) => /* language=C#-test */ $$""" // // See: CommunityToolkit.Maui.Markup.SourceGenerators.TextAlignmentGenerator @@ -84,8 +84,8 @@ namespace CommunityToolkit.Maui.Markup /// /// /// with added - public static TAssignable TextStart{{genericTypeParameters}}(this TAssignable textAlignmentControl) - where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{genericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} + public static TAssignable TextStart{{textAlignmentClassMetadata.GenericTypeParameters}}(this TAssignable textAlignmentControl) + where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{textAlignmentClassMetadata.GenericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} { ArgumentNullException.ThrowIfNull(textAlignmentControl); @@ -103,8 +103,8 @@ namespace CommunityToolkit.Maui.Markup /// /// /// with added - public static TAssignable TextCenterHorizontal{{genericTypeParameters}}(this TAssignable textAlignmentControl) - where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{genericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} + public static TAssignable TextCenterHorizontal{{textAlignmentClassMetadata.GenericTypeParameters}}(this TAssignable textAlignmentControl) + where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{textAlignmentClassMetadata.GenericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} { ArgumentNullException.ThrowIfNull(textAlignmentControl); @@ -122,8 +122,8 @@ namespace CommunityToolkit.Maui.Markup /// /// /// with added - public static TAssignable TextEnd{{genericTypeParameters}}(this TAssignable textAlignmentControl) - where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{genericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} + public static TAssignable TextEnd{{textAlignmentClassMetadata.GenericTypeParameters}}(this TAssignable textAlignmentControl) + where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{textAlignmentClassMetadata.GenericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} { ArgumentNullException.ThrowIfNull(textAlignmentControl); @@ -141,8 +141,8 @@ namespace CommunityToolkit.Maui.Markup /// /// /// with added - public static TAssignable TextTop{{genericTypeParameters}}(this TAssignable textAlignmentControl) - where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{genericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} + public static TAssignable TextTop{{textAlignmentClassMetadata.GenericTypeParameters}}(this TAssignable textAlignmentControl) + where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{textAlignmentClassMetadata.GenericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} { ArgumentNullException.ThrowIfNull(textAlignmentControl); @@ -160,8 +160,8 @@ namespace CommunityToolkit.Maui.Markup /// /// /// with added - public static TAssignable TextCenterVertical{{genericTypeParameters}}(this TAssignable textAlignmentControl) - where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{genericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} + public static TAssignable TextCenterVertical{{textAlignmentClassMetadata.GenericTypeParameters}}(this TAssignable textAlignmentControl) + where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{textAlignmentClassMetadata.GenericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} { ArgumentNullException.ThrowIfNull(textAlignmentControl); @@ -179,8 +179,8 @@ namespace CommunityToolkit.Maui.Markup /// /// /// with added - public static TAssignable TextBottom{{genericTypeParameters}}(this TAssignable textAlignmentControl) - where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{genericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} + public static TAssignable TextBottom{{textAlignmentClassMetadata.GenericTypeParameters}}(this TAssignable textAlignmentControl) + where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{textAlignmentClassMetadata.GenericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} { ArgumentNullException.ThrowIfNull(textAlignmentControl); @@ -198,9 +198,9 @@ namespace CommunityToolkit.Maui.Markup /// /// /// with added - public static TAssignable TextCenter{{genericTypeParameters}}(this TAssignable textAlignmentControl) - where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{genericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} - => textAlignmentControl.TextCenterHorizontal{{genericTypeParameters}}().TextCenterVertical{{genericTypeParameters}}(); + 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}}(); } @@ -221,8 +221,8 @@ namespace LeftToRight /// /// /// with - public static TAssignable TextLeft{{genericTypeParameters}}(this TAssignable textAlignmentControl) - where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{genericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} + public static TAssignable TextLeft{{textAlignmentClassMetadata.GenericTypeParameters}}(this TAssignable textAlignmentControl) + where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{textAlignmentClassMetadata.GenericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} { ArgumentNullException.ThrowIfNull(textAlignmentControl); @@ -240,7 +240,7 @@ namespace LeftToRight /// /// /// with - public static TAssignable TextRight{{genericTypeParameters}}(this TAssignable textAlignmentControl) where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{genericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} + public static TAssignable TextRight{{textAlignmentClassMetadata.GenericTypeParameters}}(this TAssignable textAlignmentControl) where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{textAlignmentClassMetadata.GenericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} { ArgumentNullException.ThrowIfNull(textAlignmentControl); @@ -269,8 +269,8 @@ namespace RightToLeft /// /// /// with - public static TAssignable TextLeft{{genericTypeParameters}}(this TAssignable textAlignmentControl) - where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{genericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} + public static TAssignable TextLeft{{textAlignmentClassMetadata.GenericTypeParameters}}(this TAssignable textAlignmentControl) + where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{textAlignmentClassMetadata.GenericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} { ArgumentNullException.ThrowIfNull(textAlignmentControl); @@ -288,8 +288,8 @@ namespace RightToLeft /// /// /// with - public static TAssignable TextRight{{genericTypeParameters}}(this TAssignable textAlignmentControl) - where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{genericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} + public static TAssignable TextRight{{textAlignmentClassMetadata.GenericTypeParameters}}(this TAssignable textAlignmentControl) + where TAssignable : {{textAlignmentClassMetadata.Namespace}}.{{textAlignmentClassMetadata.ClassName}}{{textAlignmentClassMetadata.GenericArguments}}{{textAlignmentClassMetadata.GenericConstraints}} { ArgumentNullException.ThrowIfNull(textAlignmentControl); diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs b/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs index a104c0c1..49e8c633 100644 --- a/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs @@ -1,346 +1,99 @@ 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)] 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) + // Optimize: Use ValueTuple for lightweight data passing + 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; // Early return to avoid unnecessary processing + } + + 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; + + // Optimize: Combine providers to reduce the number of operations + var combined = userGeneratedClassesProvider + .Collect() + .Combine(compilationProvider); + + // Register the source output + context.RegisterSourceOutput(combined, static (spc, source) => Execute(spc, source.Right, [..source.Left.Select(static x => x.ClassSymbol)])); } static bool ShouldGenerateTextAlignmentExtension(INamedTypeSymbol classSymbol, INamedTypeSymbol iTextAlignmentInterfaceSymbol) { return ImplementsInterfaceIgnoringBaseType(classSymbol, iTextAlignmentInterfaceSymbol) - && DoesNotImplementInterface(classSymbol.BaseType, iTextAlignmentInterfaceSymbol); + && !ImplementsInterface(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 ImplementsInterfaceIgnoringBaseType(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 ImplementsInterface(INamedTypeSymbol? classSymbol, INamedTypeSymbol interfaceSymbol) + => classSymbol?.AllInterfaces.Contains(interfaceSymbol) ?? false; } - static void ExecuteArray(SourceProductionContext context, EquatableArray metadataArray) + static void Execute(SourceProductionContext context, Compilation compilation, ImmutableArray 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 = $$""" -// -// See: CommunityToolkit.Maui.Markup.SourceGenerators.TextAlignmentGenerator + var mauiClasses = GetMauiInterfaceImplementors(mauiAssembly, iTextAlignmentInterfaceSymbol); -#nullable enable -#pragma warning disable + // Use HashSet for faster lookup + var processedClasses = new HashSet(SymbolEqualityComparer.Default); -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] - {{textAlignmentClassMetadata.ClassAccessModifier}} static partial class TextAlignmentExtensions_{{textAlignmentClassMetadata.ClassName}} - { - /// - /// = - /// - /// - /// 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.ClassAccessModifier}} 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.ClassAccessModifier}} 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)) + { + 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 @@ -350,32 +103,364 @@ static IEnumerable GetMauiInterfaceImplementors(IAss _ => string.Empty }; - static string GetGenericTypeParametersDeclarationString(in string genericArguments) + static void GenerateExtensionClass(SourceProductionContext context, TextAlignmentClassMetadata metadata) + { + var source = GenerateExtensionClassSource(metadata); + context.AddSource($"{metadata.ClassName}TextAlignmentExtensions.g.cs", SourceText.From(source, Encoding.UTF8)); + } + + static string GenerateExtensionClassSource(TextAlignmentClassMetadata metadata) + { + var assemblyVersion = typeof(TextAlignmentExtensionsGenerator).Assembly.GetName().Version.ToString(); + var className = typeof(TextAlignmentExtensionsGenerator).FullName; + + var sb = new StringBuilder(); + 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 = namedTypeSymbol.ContainingNamespace.ToDisplayString() == mauiControlsAssembly + ? "internal" + : GetClassAccessModifier(namedTypeSymbol); + + 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 (string.IsNullOrWhiteSpace(genericArguments)) + if (namedTypeSymbol.TypeParameters.Length is 0) { return ""; } - return $""; + var typeParams = string.Join(", ", namedTypeSymbol.TypeParameters.Select(t => t.Name)); + return $""; } - - static string GetGenericArgumentsString(in string genericArguments) + + static string GetGenericArgumentsString(INamedTypeSymbol namedTypeSymbol) { - if (string.IsNullOrWhiteSpace(genericArguments)) - { - return string.Empty; - } + 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(c => !string.IsNullOrEmpty(c)); - return $"<{genericArguments}>"; + return string.Join(" ", constraints); } - static TextAlignmentClassMetadata GenerateMetadata(INamedTypeSymbol namedTypeSymbol) + static string GetGenericParameterConstraints(ITypeParameterSymbol typeParameter) { - var accessModifier = mauiControlsAssembly == namedTypeSymbol.ContainingNamespace.ToDisplayString() - ? "internal" - : GetClassAccessModifier(namedTypeSymbol); + var constraints = new StringBuilder(); + + // Primary constraint (class, struct, unmanaged) + if (typeParameter.HasReferenceTypeConstraint) + { + constraints.Append("class"); + } + else if (typeParameter.HasValueTypeConstraint) + { + constraints.Append(typeParameter.HasUnmanagedTypeConstraint ? "unmanaged" : "struct"); + } + else if (typeParameter.HasUnmanagedTypeConstraint) + { + constraints.Append("unmanaged"); + } + else if (typeParameter.HasNotNullConstraint) + { + constraints.Append("notnull"); + } + + // Secondary constraints (specific types) + foreach (var constraintType in typeParameter.ConstraintTypes) + { + var constraintTypeString = constraintType.NullableAnnotation switch + { + NullableAnnotation.Annotated => constraintType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + '?', + _ => constraintType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + }; + + constraints.Append(constraintTypeString); + } + + // Check for record constraint + if (typeParameter.IsRecord) + { + constraints.Append("record"); + } - return new(namedTypeSymbol.Name, accessModifier, namedTypeSymbol.ContainingNamespace.ToDisplayString(), namedTypeSymbol.TypeArguments.GetGenericTypeArgumentsString(), namedTypeSymbol.GetGenericTypeConstraintsAsString()); + // Constructor constraint (must be last) + if (typeParameter.HasConstructorConstraint) + { + constraints.Append("new()"); + } + + return constraints.Length > 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 index 05c9e92c..427482ed 100644 --- a/src/CommunityToolkit.Maui.Markup.SourceGenerators/TextAlignmentClassMetadata.cs +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators/TextAlignmentClassMetadata.cs @@ -1,3 +1,9 @@ namespace CommunityToolkit.Maui.Markup.SourceGenerators; -public record TextAlignmentClassMetadata(string ClassName, string ClassAccessModifier, string Namespace, string GenericArguments, string GenericConstraints); \ No newline at end of file +public record TextAlignmentClassMetadata( + string ClassName, + string ClassAccessModifier, + string Namespace, + string GenericTypeParameters, + string GenericArguments, + string GenericConstraints); \ No newline at end of file From 83eb152a280ffb8024092ee01a579a0e14c9e7e8 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Sun, 6 Oct 2024 12:49:39 -0700 Subject: [PATCH 09/20] Update Source Generator --- ...i.Markup.SourceGenerators.UnitTests.csproj | 1 + .../TextAlignmentExtensionsGeneratorTests.cs | 7 ++-- .../TextAlignmentExtensionsGenerator.cs | 20 +++++------ .../TextAlignmentExtensionsTests.cs | 34 +++++++++---------- 4 files changed, 33 insertions(+), 29 deletions(-) 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 index 097cc1ad..752e70f8 100644 --- 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 @@ -11,6 +11,7 @@ + diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/TextAlignmentExtensionsGeneratorTests.cs b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/TextAlignmentExtensionsGeneratorTests.cs index 0e4c8917..ce0cfb1c 100644 --- a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/TextAlignmentExtensionsGeneratorTests.cs +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/TextAlignmentExtensionsGeneratorTests.cs @@ -41,7 +41,7 @@ public async Task VerifyGeneratedSource_WhenClassIsGeneric() using Microsoft.Maui; namespace MyNamespace; -public class MyClass : Microsoft.Maui.ITextAlignment +public class GenericClass : Microsoft.Maui.ITextAlignment where T : IDisposable, new() where U : class { @@ -50,11 +50,14 @@ public class MyClass : Microsoft.Maui.ITextAlignment } """; + var temp = GenerateSourceCode(textAlignmentExtensionsGeneratorFullName, + new("GenericClass", "public", "MyNamespace", "", "", "where T : IDisposable, new() where U : class")); + // Act // Assert await VerifySourceGeneratorAsync( source, GenerateSourceCode(textAlignmentExtensionsGeneratorFullName, - new("MyClass", "public", "MyNamespace", "", string.Empty, "where T : IDisposable, new() where U : class")), + new("GenericClass", "public", "MyNamespace", "", "", "where T : IDisposable, new() where U : class")), []); } diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs b/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs index 49e8c633..bbb02f89 100644 --- a/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs @@ -415,24 +415,24 @@ static string GetGenericConstraintsString(INamedTypeSymbol namedTypeSymbol) static string GetGenericParameterConstraints(ITypeParameterSymbol typeParameter) { - var constraints = new StringBuilder(); + var constraints = new List(); // Primary constraint (class, struct, unmanaged) if (typeParameter.HasReferenceTypeConstraint) { - constraints.Append("class"); + constraints.Add("class"); } else if (typeParameter.HasValueTypeConstraint) { - constraints.Append(typeParameter.HasUnmanagedTypeConstraint ? "unmanaged" : "struct"); + constraints.Add(typeParameter.HasUnmanagedTypeConstraint ? "unmanaged" : "struct"); } else if (typeParameter.HasUnmanagedTypeConstraint) { - constraints.Append("unmanaged"); + constraints.Add("unmanaged"); } else if (typeParameter.HasNotNullConstraint) { - constraints.Append("notnull"); + constraints.Add("notnull"); } // Secondary constraints (specific types) @@ -440,26 +440,26 @@ static string GetGenericParameterConstraints(ITypeParameterSymbol typeParameter) { var constraintTypeString = constraintType.NullableAnnotation switch { - NullableAnnotation.Annotated => constraintType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + '?', + NullableAnnotation.Annotated => string.Concat(constraintType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), '?'), _ => constraintType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) }; - constraints.Append(constraintTypeString); + constraints.Add(constraintTypeString); } // Check for record constraint if (typeParameter.IsRecord) { - constraints.Append("record"); + constraints.Add("record"); } // Constructor constraint (must be last) if (typeParameter.HasConstructorConstraint) { - constraints.Append("new()"); + constraints.Add("new()"); } - return constraints.Length > 0 + return constraints.Count > 0 ? $"where {typeParameter.Name} : {string.Join(", ", constraints)}" : string.Empty; } diff --git a/src/CommunityToolkit.Maui.Markup.UnitTests/TextAlignmentExtensionsTests.cs b/src/CommunityToolkit.Maui.Markup.UnitTests/TextAlignmentExtensionsTests.cs index 213453cd..1258c7ce 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 { } From 064be4b376bfb4237c9032e52151c791515d78fa Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Sun, 6 Oct 2024 12:52:15 -0700 Subject: [PATCH 10/20] Update TextAlignmentExtensionsGenerator.cs --- .../SourceGenerators/TextAlignmentExtensionsGenerator.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs b/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs index bbb02f89..9b566f29 100644 --- a/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs @@ -15,7 +15,6 @@ public class TextAlignmentExtensionsGenerator : IIncrementalGenerator public void Initialize(IncrementalGeneratorInitializationContext context) { - // Optimize: Use ValueTuple for lightweight data passing IncrementalValuesProvider<(INamedTypeSymbol ClassSymbol, INamedTypeSymbol ITextAlignmentInterfaceSymbol)> userGeneratedClassesProvider = context.SyntaxProvider .CreateSyntaxProvider( static (syntaxNode, _) => syntaxNode is ClassDeclarationSyntax { BaseList: not null }, @@ -40,7 +39,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var compilationProvider = context.CompilationProvider; - // Optimize: Combine providers to reduce the number of operations + // Combine providers to reduce the number of operations var combined = userGeneratedClassesProvider .Collect() .Combine(compilationProvider); @@ -408,7 +407,7 @@ static string GetGenericConstraintsString(INamedTypeSymbol namedTypeSymbol) { var constraints = namedTypeSymbol.TypeParameters .Select(GetGenericParameterConstraints) - .Where(c => !string.IsNullOrEmpty(c)); + .Where(static c => !string.IsNullOrEmpty(c)); return string.Join(" ", constraints); } From 1744eee0f50fdba646a28d04e73f2cdc9822f349 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Sun, 6 Oct 2024 12:53:53 -0700 Subject: [PATCH 11/20] Remove temporary variable --- .../TextAlignmentExtensionsGeneratorTests.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/TextAlignmentExtensionsGeneratorTests.cs b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/TextAlignmentExtensionsGeneratorTests.cs index ce0cfb1c..a3a2d378 100644 --- a/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/TextAlignmentExtensionsGeneratorTests.cs +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests/TextAlignmentExtensionsGeneratorTests.cs @@ -50,9 +50,6 @@ public class GenericClass : Microsoft.Maui.ITextAlignment } """; - var temp = GenerateSourceCode(textAlignmentExtensionsGeneratorFullName, - new("GenericClass", "public", "MyNamespace", "", "", "where T : IDisposable, new() where U : class")); - // Act // Assert await VerifySourceGeneratorAsync( source, From 1e00aea06f1c0f86bb9dcf73e12f29ee92230aa7 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Sun, 6 Oct 2024 13:11:49 -0700 Subject: [PATCH 12/20] Refactor Source Generator --- ...i.Markup.SourceGenerators.UnitTests.csproj | 1 - .../TextAlignmentExtensionsGenerator.cs | 24 ++++++++++--------- 2 files changed, 13 insertions(+), 12 deletions(-) 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 index 752e70f8..097cc1ad 100644 --- 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 @@ -11,7 +11,6 @@ - diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs b/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs index 9b566f29..4b23ffa1 100644 --- a/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs @@ -50,13 +50,13 @@ public void Initialize(IncrementalGeneratorInitializationContext context) static bool ShouldGenerateTextAlignmentExtension(INamedTypeSymbol classSymbol, INamedTypeSymbol iTextAlignmentInterfaceSymbol) { - return ImplementsInterfaceIgnoringBaseType(classSymbol, iTextAlignmentInterfaceSymbol) - && !ImplementsInterface(classSymbol.BaseType, iTextAlignmentInterfaceSymbol); + return DoesImplementInterfaceIgnoringBaseType(classSymbol, iTextAlignmentInterfaceSymbol) + && !DoesImplementInterface(classSymbol.BaseType, iTextAlignmentInterfaceSymbol); - static bool ImplementsInterfaceIgnoringBaseType(INamedTypeSymbol classSymbol, INamedTypeSymbol interfaceSymbol) + static bool DoesImplementInterfaceIgnoringBaseType(INamedTypeSymbol classSymbol, INamedTypeSymbol interfaceSymbol) => classSymbol.AllInterfaces.Contains(interfaceSymbol); - static bool ImplementsInterface(INamedTypeSymbol? classSymbol, INamedTypeSymbol interfaceSymbol) + static bool DoesImplementInterface(INamedTypeSymbol? classSymbol, INamedTypeSymbol interfaceSymbol) => classSymbol?.AllInterfaces.Contains(interfaceSymbol) ?? false; } @@ -419,7 +419,9 @@ static string GetGenericParameterConstraints(ITypeParameterSymbol typeParameter) // Primary constraint (class, struct, unmanaged) if (typeParameter.HasReferenceTypeConstraint) { - constraints.Add("class"); + constraints.Add(typeParameter.ReferenceTypeConstraintNullableAnnotation is NullableAnnotation.Annotated + ? "class?" + : "class"); } else if (typeParameter.HasValueTypeConstraint) { @@ -437,12 +439,12 @@ static string GetGenericParameterConstraints(ITypeParameterSymbol typeParameter) // Secondary constraints (specific types) foreach (var constraintType in typeParameter.ConstraintTypes) { - var constraintTypeString = constraintType.NullableAnnotation switch - { - NullableAnnotation.Annotated => string.Concat(constraintType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), '?'), - _ => constraintType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) - }; + var symbolDisplayFormat = new SymbolDisplayFormat( + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, + miscellaneousOptions: SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier); + var constraintTypeString = constraintType.ToDisplayString(symbolDisplayFormat); + constraints.Add(constraintTypeString); } @@ -457,7 +459,7 @@ static string GetGenericParameterConstraints(ITypeParameterSymbol typeParameter) { constraints.Add("new()"); } - + return constraints.Count > 0 ? $"where {typeParameter.Name} : {string.Join(", ", constraints)}" : string.Empty; From 8b00b91df850421fade638bcfd4b77cef2be85df Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Sun, 6 Oct 2024 15:27:59 -0700 Subject: [PATCH 13/20] Update CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests.csproj --- ...ommunityToolkit.Maui.Markup.SourceGenerators.UnitTests.csproj | 1 + 1 file changed, 1 insertion(+) 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 index 097cc1ad..752e70f8 100644 --- 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 @@ -11,6 +11,7 @@ + From 696fbf3df15181f0f4e61fe9437bea7c784c7112 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Sun, 6 Oct 2024 15:41:29 -0700 Subject: [PATCH 14/20] Add Benchmarks --- azure-pipelines.yml | 8 ++++++- .../CommunityToolkit.Maui.Markup.Sample.sln | 18 +++++++++++++-- ....Markup.SourceGenerators.Benchmarks.csproj | 22 +++++++++++++++++++ .../Program.cs | 12 ++++++++++ ...tAlignmentExtensionsGeneratorBenchmarks.cs | 22 +++++++++++++++++++ src/CommunityToolkit.Maui.Markup.sln | 22 ++++++++++++++++--- 6 files changed, 98 insertions(+), 6 deletions(-) create mode 100644 src/CommunityToolkit.Maui.Markup.SourceGenerators.Benchmarks/CommunityToolkit.Maui.Markup.SourceGenerators.Benchmarks.csproj create mode 100644 src/CommunityToolkit.Maui.Markup.SourceGenerators.Benchmarks/Program.cs create mode 100644 src/CommunityToolkit.Maui.Markup.SourceGenerators.Benchmarks/TextAlignmentExtensionsGeneratorBenchmarks.cs diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 7afd6edd..f461feb5 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -13,6 +13,7 @@ variables: 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 @@ -291,10 +292,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 37316951..d55ee66f 100644 --- a/samples/CommunityToolkit.Maui.Markup.Sample.sln +++ b/samples/CommunityToolkit.Maui.Markup.Sample.sln @@ -20,9 +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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests", "..\src\CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests\CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests.csproj", "{26D5485B-68EA-454A-BBB7-11C6FF9B1B98}" +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 @@ -56,12 +62,20 @@ Global {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..14ab12ac --- /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 VerifyNoErrorsWhenUseMauiCommunityToolkitHasAdditonalWhitespace() + { + return textAlignmentExtensionsGeneratorTests.VerifyGeneratedSource_WhenClassImplementsITextAlignmentInterface(); + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Markup.sln b/src/CommunityToolkit.Maui.Markup.sln index eb7c3c43..13cea65f 100644 --- a/src/CommunityToolkit.Maui.Markup.sln +++ b/src/CommunityToolkit.Maui.Markup.sln @@ -14,11 +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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests", "CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests\CommunityToolkit.Maui.Markup.SourceGenerators.UnitTests.csproj", "{BB9611DF-34B9-4BBF-A564-EB2AADD943C8}" +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 @@ -46,10 +52,20 @@ Global {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 From 30d55d8754f37d2c01b72567685dab7ef56f6e19 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Sun, 6 Oct 2024 15:43:39 -0700 Subject: [PATCH 15/20] Update TextAlignmentExtensionsGeneratorBenchmarks.cs --- .../TextAlignmentExtensionsGeneratorBenchmarks.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators.Benchmarks/TextAlignmentExtensionsGeneratorBenchmarks.cs b/src/CommunityToolkit.Maui.Markup.SourceGenerators.Benchmarks/TextAlignmentExtensionsGeneratorBenchmarks.cs index 14ab12ac..e9dd9702 100644 --- a/src/CommunityToolkit.Maui.Markup.SourceGenerators.Benchmarks/TextAlignmentExtensionsGeneratorBenchmarks.cs +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators.Benchmarks/TextAlignmentExtensionsGeneratorBenchmarks.cs @@ -15,7 +15,7 @@ public Task VerifyGeneratedSource_WhenClassIsGeneric() } [Benchmark] - public Task VerifyNoErrorsWhenUseMauiCommunityToolkitHasAdditonalWhitespace() + public Task VerifyGeneratedSource_WhenClassImplementsITextAlignmentInterface() { return textAlignmentExtensionsGeneratorTests.VerifyGeneratedSource_WhenClassImplementsITextAlignmentInterface(); } From f8697d0f6de78301a5f9233229fba30f635a3272 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Sun, 6 Oct 2024 15:52:10 -0700 Subject: [PATCH 16/20] Remove `PrivateAssets` --- ...unityToolkit.Maui.Markup.SourceGenerators.UnitTests.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 752e70f8..e604ddf1 100644 --- 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 @@ -10,8 +10,8 @@ - - + + From 2ed054ebc576b3002f2b0f8207259ba61c4e4448 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Sun, 6 Oct 2024 15:53:44 -0700 Subject: [PATCH 17/20] Remove unnecessary NuGet Packages --- ...mmunityToolkit.Maui.Markup.SourceGenerators.UnitTests.csproj | 2 -- 1 file changed, 2 deletions(-) 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 index e604ddf1..c458156f 100644 --- 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 @@ -20,8 +20,6 @@ - - From efa18ffb93e63ff64c05f35b9e79e4af46fc454c Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Sun, 6 Oct 2024 16:10:35 -0700 Subject: [PATCH 18/20] Update TextAlignmentExtensionsGenerator.cs --- .../TextAlignmentExtensionsGenerator.cs | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs b/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs index 4b23ffa1..49eda672 100644 --- a/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs @@ -24,7 +24,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var iTextAlignmentInterfaceSymbol = compilation.GetTypeByMetadataName(textAlignmentInterface); if (iTextAlignmentInterfaceSymbol is null) { - return default; // Early return to avoid unnecessary processing + return default; } var classSymbol = syntaxContext.SemanticModel.GetDeclaredSymbol((ClassDeclarationSyntax)syntaxContext.Node, ct); @@ -39,13 +39,11 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var compilationProvider = context.CompilationProvider; - // Combine providers to reduce the number of operations var combined = userGeneratedClassesProvider .Collect() .Combine(compilationProvider); - // Register the source output - context.RegisterSourceOutput(combined, static (spc, source) => Execute(spc, source.Right, [..source.Left.Select(static x => x.ClassSymbol)])); + context.RegisterSourceOutput(combined, static (spc, source) => Execute(spc, source.Right, source.Left.ToImmutableArray())); } static bool ShouldGenerateTextAlignmentExtension(INamedTypeSymbol classSymbol, INamedTypeSymbol iTextAlignmentInterfaceSymbol) @@ -60,7 +58,7 @@ static bool DoesImplementInterface(INamedTypeSymbol? classSymbol, INamedTypeSymb => classSymbol?.AllInterfaces.Contains(interfaceSymbol) ?? false; } - static void Execute(SourceProductionContext context, Compilation compilation, ImmutableArray userClasses) + static void Execute(SourceProductionContext context, Compilation compilation, ImmutableArray<(INamedTypeSymbol ClassSymbol, INamedTypeSymbol ITextAlignmentInterfaceSymbol)> userClasses) { var mauiAssembly = compilation.SourceModule.ReferencedAssemblySymbols.FirstOrDefault(static a => a.Name == mauiControlsAssembly); if (mauiAssembly is null) @@ -76,10 +74,9 @@ static void Execute(SourceProductionContext context, Compilation compilation, Im var mauiClasses = GetMauiInterfaceImplementors(mauiAssembly, iTextAlignmentInterfaceSymbol); - // Use HashSet for faster lookup var processedClasses = new HashSet(SymbolEqualityComparer.Default); - foreach (var classSymbol in userClasses.Concat(mauiClasses)) + foreach (var (classSymbol, _) in userClasses.Concat(mauiClasses.Select(c => (c, iTextAlignmentInterfaceSymbol)))) { if (processedClasses.Add(classSymbol)) { @@ -102,18 +99,18 @@ static IEnumerable GetMauiInterfaceImplementors(IAssemblySymbo _ => string.Empty }; - static void GenerateExtensionClass(SourceProductionContext context, TextAlignmentClassMetadata metadata) + static void GenerateExtensionClass(SourceProductionContext context, in TextAlignmentClassMetadata metadata) { - var source = GenerateExtensionClassSource(metadata); + var source = GenerateExtensionClassSource(in metadata); context.AddSource($"{metadata.ClassName}TextAlignmentExtensions.g.cs", SourceText.From(source, Encoding.UTF8)); } - static string GenerateExtensionClassSource(TextAlignmentClassMetadata metadata) + static string GenerateExtensionClassSource(in TextAlignmentClassMetadata metadata) { var assemblyVersion = typeof(TextAlignmentExtensionsGenerator).Assembly.GetName().Version.ToString(); var className = typeof(TextAlignmentExtensionsGenerator).FullName; - var sb = new StringBuilder(); + var sb = new StringBuilder(8192); // Pre-allocate with an estimated capacity sb.AppendLine( /* language=C#-test */ $$""" // @@ -419,8 +416,8 @@ static string GetGenericParameterConstraints(ITypeParameterSymbol typeParameter) // Primary constraint (class, struct, unmanaged) if (typeParameter.HasReferenceTypeConstraint) { - constraints.Add(typeParameter.ReferenceTypeConstraintNullableAnnotation is NullableAnnotation.Annotated - ? "class?" + constraints.Add(typeParameter.ReferenceTypeConstraintNullableAnnotation is NullableAnnotation.Annotated + ? "class?" : "class"); } else if (typeParameter.HasValueTypeConstraint) @@ -442,7 +439,7 @@ static string GetGenericParameterConstraints(ITypeParameterSymbol typeParameter) var symbolDisplayFormat = new SymbolDisplayFormat( typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, miscellaneousOptions: SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier); - + var constraintTypeString = constraintType.ToDisplayString(symbolDisplayFormat); constraints.Add(constraintTypeString); From f93e9d91e83b6885d8afc573a8b27063e3578abf Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Sun, 6 Oct 2024 16:43:36 -0700 Subject: [PATCH 19/20] Revert Source Generator for Benchmarking --- .../TextAlignmentExtensionsGenerator.cs | 729 ++++++++---------- 1 file changed, 324 insertions(+), 405 deletions(-) diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs b/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs index 49eda672..606d6b65 100644 --- a/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs @@ -1,95 +1,346 @@ 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)] -public class TextAlignmentExtensionsGenerator : IIncrementalGenerator +class TextAlignmentExtensionsGenerator : IIncrementalGenerator { - const string textAlignmentInterface = "Microsoft.Maui.ITextAlignment"; + const string iTextAlignmentInterface = "Microsoft.Maui.ITextAlignment"; const string mauiControlsAssembly = "Microsoft.Maui.Controls"; public void Initialize(IncrementalGeneratorInitializationContext context) { - IncrementalValuesProvider<(INamedTypeSymbol ClassSymbol, INamedTypeSymbol ITextAlignmentInterfaceSymbol)> userGeneratedClassesProvider = context.SyntaxProvider - .CreateSyntaxProvider( - static (syntaxNode, _) => syntaxNode is ClassDeclarationSyntax { BaseList: not null }, - static (syntaxContext, ct) => + // 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) { - 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())); + // 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); } static bool ShouldGenerateTextAlignmentExtension(INamedTypeSymbol classSymbol, INamedTypeSymbol iTextAlignmentInterfaceSymbol) { - return DoesImplementInterfaceIgnoringBaseType(classSymbol, iTextAlignmentInterfaceSymbol) - && !DoesImplementInterface(classSymbol.BaseType, iTextAlignmentInterfaceSymbol); + return ImplementsInterfaceIgnoringBaseType(classSymbol, iTextAlignmentInterfaceSymbol) + && DoesNotImplementInterface(classSymbol.BaseType, iTextAlignmentInterfaceSymbol); - static bool DoesImplementInterfaceIgnoringBaseType(INamedTypeSymbol classSymbol, INamedTypeSymbol interfaceSymbol) - => classSymbol.AllInterfaces.Contains(interfaceSymbol); + static bool ImplementsInterfaceIgnoringBaseType(INamedTypeSymbol classSymbol, INamedTypeSymbol iTextAlignmentInterfaceSymbol) + => classSymbol.Interfaces.Any(i => i.Equals(iTextAlignmentInterfaceSymbol, SymbolEqualityComparer.Default) || i.AllInterfaces.Contains(iTextAlignmentInterfaceSymbol, SymbolEqualityComparer.Default)); - static bool DoesImplementInterface(INamedTypeSymbol? classSymbol, INamedTypeSymbol interfaceSymbol) - => classSymbol?.AllInterfaces.Contains(interfaceSymbol) ?? false; + static bool DoesNotImplementInterface(INamedTypeSymbol? classSymbol, INamedTypeSymbol iTextAlignmentInterfaceSymbol) + => classSymbol is null || !classSymbol.AllInterfaces.Any(i => i.Equals(iTextAlignmentInterfaceSymbol, SymbolEqualityComparer.Default)); } - static void Execute(SourceProductionContext context, Compilation compilation, ImmutableArray<(INamedTypeSymbol ClassSymbol, INamedTypeSymbol ITextAlignmentInterfaceSymbol)> userClasses) + static void ExecuteArray(SourceProductionContext context, EquatableArray metadataArray) { - var mauiAssembly = compilation.SourceModule.ReferencedAssemblySymbols.FirstOrDefault(static a => a.Name == mauiControlsAssembly); - if (mauiAssembly is null) + foreach (var metadata in metadataArray.AsImmutableArray()) { - return; + Execute(context, metadata); } + } - var iTextAlignmentInterfaceSymbol = compilation.GetTypeByMetadataName(textAlignmentInterface); - if (iTextAlignmentInterfaceSymbol is null) + static void Execute(SourceProductionContext context, [NotNull] TextAlignmentClassMetadata? textAlignmentClassMetadata) + { + if (textAlignmentClassMetadata is null) { - return; + throw new ArgumentNullException(nameof(textAlignmentClassMetadata)); } - var mauiClasses = GetMauiInterfaceImplementors(mauiAssembly, iTextAlignmentInterfaceSymbol); + var className = typeof(TextAlignmentExtensionsGenerator).FullName; + var assemblyVersion = typeof(TextAlignmentExtensionsGenerator).Assembly.GetName().Version.ToString(); - var processedClasses = new HashSet(SymbolEqualityComparer.Default); + var genericTypeParameters = GetGenericTypeParametersDeclarationString(textAlignmentClassMetadata.GenericArguments); + var genericArguments = GetGenericArgumentsString(textAlignmentClassMetadata.GenericArguments); + var source = $$""" +// +// See: CommunityToolkit.Maui.Markup.SourceGenerators.TextAlignmentGenerator - foreach (var (classSymbol, _) in userClasses.Concat(mauiClasses.Select(c => (c, iTextAlignmentInterfaceSymbol)))) - { - if (processedClasses.Add(classSymbol)) - { - var metadata = GenerateMetadata(classSymbol); - GenerateExtensionClass(context, metadata); - } - } +#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] + {{textAlignmentClassMetadata.ClassAcessModifier}} static partial class TextAlignmentExtensions_{{textAlignmentClassMetadata.ClassName}} + { + /// + /// = + /// + /// + /// 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)); } - static IEnumerable GetMauiInterfaceImplementors(IAssemblySymbol mauiAssembly, INamedTypeSymbol iTextAlignmentSymbol) + static IEnumerable GetMauiInterfaceImplementors(IAssemblySymbol mauiControlsAssemblySymbolProvider, INamedTypeSymbol itextAlignmentSymbol) { - return mauiAssembly.GlobalNamespace.GetNamedTypeSymbols() - .Where(x => ShouldGenerateTextAlignmentExtension(x, iTextAlignmentSymbol)); + return mauiControlsAssemblySymbolProvider.GlobalNamespace.GetNamedTypeSymbols().Where(x => ShouldGenerateTextAlignmentExtension(x, itextAlignmentSymbol)).Select(GenerateMetadata); } static string GetClassAccessModifier(INamedTypeSymbol namedTypeSymbol) => namedTypeSymbol.DeclaredAccessibility switch @@ -99,366 +350,34 @@ static IEnumerable GetMauiInterfaceImplementors(IAssemblySymbo _ => string.Empty }; - static void GenerateExtensionClass(SourceProductionContext context, in TextAlignmentClassMetadata metadata) - { - var source = GenerateExtensionClassSource(in metadata); - context.AddSource($"{metadata.ClassName}TextAlignmentExtensions.g.cs", SourceText.From(source, Encoding.UTF8)); - } - - static string GenerateExtensionClassSource(in TextAlignmentClassMetadata metadata) - { - var assemblyVersion = typeof(TextAlignmentExtensionsGenerator).Assembly.GetName().Version.ToString(); - var className = typeof(TextAlignmentExtensionsGenerator).FullName; - - 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 = namedTypeSymbol.ContainingNamespace.ToDisplayString() == mauiControlsAssembly - ? "internal" - : GetClassAccessModifier(namedTypeSymbol); - - 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) + static string GetGenericTypeParametersDeclarationString(in string genericArguments) { - if (namedTypeSymbol.TypeParameters.Length is 0) + if (string.IsNullOrWhiteSpace(genericArguments)) { return ""; } - var typeParams = string.Join(", ", namedTypeSymbol.TypeParameters.Select(t => t.Name)); - return $""; + return $""; } - static string GetGenericArgumentsString(INamedTypeSymbol namedTypeSymbol) + static string GetGenericArgumentsString(in string genericArguments) { - 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) + if (string.IsNullOrWhiteSpace(genericArguments)) { - constraints.Add("unmanaged"); + return string.Empty; } - 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"); - } + return $"<{genericArguments}>"; + } - // Constructor constraint (must be last) - if (typeParameter.HasConstructorConstraint) - { - constraints.Add("new()"); - } + static TextAlignmentClassMetadata GenerateMetadata(INamedTypeSymbol namedTypeSymbol) + { + var accessModifier = mauiControlsAssembly == namedTypeSymbol.ContainingNamespace.ToDisplayString() + ? "internal" + : GetClassAccessModifier(namedTypeSymbol); - return constraints.Count > 0 - ? $"where {typeParameter.Name} : {string.Join(", ", constraints)}" - : string.Empty; + return new(namedTypeSymbol.Name, accessModifier, namedTypeSymbol.ContainingNamespace.ToDisplayString(), namedTypeSymbol.TypeArguments.GetGenericTypeArgumentsString(), namedTypeSymbol.GetGenericTypeConstraintsAsString()); } + + record TextAlignmentClassMetadata(string ClassName, string ClassAcessModifier, string Namespace, string GenericArguments, string GenericConstraints); } \ No newline at end of file From 59c136f204bfa35ec6e7e330c6683e0cf2957a16 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Sun, 6 Oct 2024 16:56:12 -0700 Subject: [PATCH 20/20] Revert "Revert Source Generator for Benchmarking" This reverts commit f93e9d91e83b6885d8afc573a8b27063e3578abf. --- .../TextAlignmentExtensionsGenerator.cs | 729 ++++++++++-------- 1 file changed, 405 insertions(+), 324 deletions(-) diff --git a/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs b/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs index 606d6b65..49eda672 100644 --- a/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs +++ b/src/CommunityToolkit.Maui.Markup.SourceGenerators/SourceGenerators/TextAlignmentExtensionsGenerator.cs @@ -1,346 +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 = $$""" -// -// 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 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 @@ -350,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