From 4fa29bc4366b70a40fb91f8fe9dc90474e49edeb Mon Sep 17 00:00:00 2001 From: Marco Goertz Date: Fri, 23 Feb 2024 08:02:48 -0800 Subject: [PATCH] Restructured CodeBehindGenerator pipeline (#20524) * - Restructured CodeBehindGenerator pipeline to maximize SourceGen cachability - Split out CSS SourceGen, which does not depend on Compilation at all - Added TrackingNames to support new SourceGen unit test project Fixes Issue #12978 CodeBehindGenerator has improper pipeline Fixes AB#1947659: `CodeBehindGenerator` has improper pipeline * - Use file-scoped namespaces throughout PR - Use raw string literals for SourceGen tests --- Microsoft.Maui-dev.sln | 14 + Microsoft.Maui-windows.slnf | 8 +- .../src/SourceGen/CodeBehindGenerator.cs | 1019 ++++++++++------- src/Controls/src/SourceGen/TrackingNames.cs | 15 + .../SourceGen.UnitTests.csproj | 39 + .../SourceGen.UnitTests/SourceGenCssTests.cs | 84 ++ .../SourceGen.UnitTests/SourceGenTestsBase.cs | 18 + .../SourceGen.UnitTests/SourceGenXamlTests.cs | 210 ++++ .../SourceGeneratorDriver.cs | 174 +++ 9 files changed, 1168 insertions(+), 413 deletions(-) create mode 100644 src/Controls/src/SourceGen/TrackingNames.cs create mode 100644 src/Controls/tests/SourceGen.UnitTests/SourceGen.UnitTests.csproj create mode 100644 src/Controls/tests/SourceGen.UnitTests/SourceGenCssTests.cs create mode 100644 src/Controls/tests/SourceGen.UnitTests/SourceGenTestsBase.cs create mode 100644 src/Controls/tests/SourceGen.UnitTests/SourceGenXamlTests.cs create mode 100644 src/Controls/tests/SourceGen.UnitTests/SourceGeneratorDriver.cs diff --git a/Microsoft.Maui-dev.sln b/Microsoft.Maui-dev.sln index 7d10d8c8e63a..5269c698cefb 100644 --- a/Microsoft.Maui-dev.sln +++ b/Microsoft.Maui-dev.sln @@ -241,6 +241,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UITest.Appium", "src\TestUt EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UITest.NUnit", "src\TestUtils\src\UITest.NUnit\UITest.NUnit.csproj", "{A307B624-48D4-494E-A70D-5B3CDF6620CF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Controls.SourceGen.UnitTests", "src\Controls\tests\SourceGen.UnitTests\Conrtrols.SourceGen.UnitTests\Controls.SourceGen.UnitTests.csproj", "{06747B55-91DB-47F5-B7A2-56526C28A0D3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SourceGen.UnitTests", "src\Controls\tests\SourceGen.UnitTests\SourceGen.UnitTests.csproj", "{BC7F7C82-694F-4B97-86FC-273FB3FACA25}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -610,6 +614,14 @@ Global {A307B624-48D4-494E-A70D-5B3CDF6620CF}.Debug|Any CPU.Build.0 = Debug|Any CPU {A307B624-48D4-494E-A70D-5B3CDF6620CF}.Release|Any CPU.ActiveCfg = Release|Any CPU {A307B624-48D4-494E-A70D-5B3CDF6620CF}.Release|Any CPU.Build.0 = Release|Any CPU + {06747B55-91DB-47F5-B7A2-56526C28A0D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {06747B55-91DB-47F5-B7A2-56526C28A0D3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {06747B55-91DB-47F5-B7A2-56526C28A0D3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {06747B55-91DB-47F5-B7A2-56526C28A0D3}.Release|Any CPU.Build.0 = Release|Any CPU + {BC7F7C82-694F-4B97-86FC-273FB3FACA25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BC7F7C82-694F-4B97-86FC-273FB3FACA25}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC7F7C82-694F-4B97-86FC-273FB3FACA25}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BC7F7C82-694F-4B97-86FC-273FB3FACA25}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -721,6 +733,8 @@ Global {8F7B825D-24A8-4E09-AC5B-9117926B7BF3} = {7AC28763-9C68-4BF9-A1BA-25CBFFD2D15C} {26379D0E-4D4D-48CA-94B1-A2C1972AB335} = {7AC28763-9C68-4BF9-A1BA-25CBFFD2D15C} {A307B624-48D4-494E-A70D-5B3CDF6620CF} = {7AC28763-9C68-4BF9-A1BA-25CBFFD2D15C} + {06747B55-91DB-47F5-B7A2-56526C28A0D3} = {25D0D27A-C5FE-443D-8B65-D6C987F4A80E} + {BC7F7C82-694F-4B97-86FC-273FB3FACA25} = {25D0D27A-C5FE-443D-8B65-D6C987F4A80E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0B8ABEAD-D2B5-4370-A187-62B5ABE4EE50} diff --git a/Microsoft.Maui-windows.slnf b/Microsoft.Maui-windows.slnf index 7e739762efa2..87fda3963db9 100644 --- a/Microsoft.Maui-windows.slnf +++ b/Microsoft.Maui-windows.slnf @@ -27,6 +27,7 @@ "src\\Controls\\tests\\Core.UnitTests\\Controls.Core.UnitTests.csproj", "src\\Controls\\tests\\CustomAttributes\\Controls.CustomAttributes.csproj", "src\\Controls\\tests\\DeviceTests\\Controls.DeviceTests.csproj", + "src\\Controls\\tests\\SourceGen.UnitTests\\SourceGen.UnitTests.csproj", "src\\Controls\\tests\\UITests\\Controls.AppiumTests.csproj", "src\\Controls\\tests\\Xaml.UnitTests.ExternalAssembly\\Controls.Xaml.UnitTests.ExternalAssembly.csproj", "src\\Controls\\tests\\Xaml.UnitTests.InternalsHiddenAssembly\\Controls.Xaml.UnitTests.InternalsHiddenAssembly.csproj", @@ -47,7 +48,6 @@ "src\\Graphics\\samples\\GraphicsTester.Android\\GraphicsTester.Android.csproj", "src\\Graphics\\samples\\GraphicsTester.Portable\\GraphicsTester.Portable.csproj", "src\\Graphics\\samples\\GraphicsTester.Skia.Console\\GraphicsTester.Skia.Console.csproj", - "src\\Graphics\\samples\\GraphicsTester.Skia.Tizen\\GraphicsTester.Skia.Tizen.csproj", "src\\Graphics\\samples\\GraphicsTester.Skia.Windows\\GraphicsTester.Skia.Windows.csproj", "src\\Graphics\\samples\\GraphicsTester.WinUI.Desktop\\GraphicsTester.WinUI.Desktop.csproj", "src\\Graphics\\samples\\GraphicsTester.iOS\\GraphicsTester.iOS.csproj", @@ -63,14 +63,14 @@ "src\\SingleProject\\Resizetizer\\test\\UnitTests\\Resizetizer.UnitTests.csproj", "src\\Templates\\src\\Microsoft.Maui.Templates.csproj", "src\\TestUtils\\samples\\DeviceTests.Sample\\TestUtils.DeviceTests.Sample.csproj", - "src\\TestUtils\\src\\UITest.Core\\UITest.Core.csproj", - "src\\TestUtils\\src\\UITest.Appium\\UITest.Appium.csproj", - "src\\TestUtils\\src\\UITest.NUnit\\UITest.NUnit.csproj", "src\\TestUtils\\src\\DeviceTests.Runners.SourceGen\\TestUtils.DeviceTests.Runners.SourceGen.csproj", "src\\TestUtils\\src\\DeviceTests.Runners\\TestUtils.DeviceTests.Runners.csproj", "src\\TestUtils\\src\\DeviceTests\\TestUtils.DeviceTests.csproj", "src\\TestUtils\\src\\Microsoft.Maui.IntegrationTests\\Microsoft.Maui.IntegrationTests.csproj", "src\\TestUtils\\src\\TestUtils\\TestUtils.csproj", + "src\\TestUtils\\src\\UITest.Appium\\UITest.Appium.csproj", + "src\\TestUtils\\src\\UITest.Core\\UITest.Core.csproj", + "src\\TestUtils\\src\\UITest.NUnit\\UITest.NUnit.csproj", "src\\Workload\\Microsoft.Maui.Sdk\\Microsoft.Maui.Sdk.csproj", "src\\Workload\\Microsoft.NET.Sdk.Maui.Manifest\\Microsoft.NET.Sdk.Maui.Manifest.csproj" ] diff --git a/src/Controls/src/SourceGen/CodeBehindGenerator.cs b/src/Controls/src/SourceGen/CodeBehindGenerator.cs index 556ebc499b4e..d94a2415abd4 100644 --- a/src/Controls/src/SourceGen/CodeBehindGenerator.cs +++ b/src/Controls/src/SourceGen/CodeBehindGenerator.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; @@ -16,12 +15,12 @@ using Microsoft.Maui.Controls.Xaml; -namespace Microsoft.Maui.Controls.SourceGen +namespace Microsoft.Maui.Controls.SourceGen; + +[Generator(LanguageNames.CSharp)] +public class CodeBehindGenerator : IIncrementalGenerator { - [Generator(LanguageNames.CSharp)] - public class CodeBehindGenerator : IIncrementalGenerator - { - const string AutoGeneratedHeaderText = @" + const string AutoGeneratedHeaderText = @" //------------------------------------------------------------------------------ // // This code was generated by a .NET MAUI source generator. @@ -32,558 +31,760 @@ public class CodeBehindGenerator : IIncrementalGenerator //------------------------------------------------------------------------------ "; - public void Initialize(IncrementalGeneratorInitializationContext initContext) - { + public void Initialize(IncrementalGeneratorInitializationContext initContext) + { #if DEBUG - //if (!System.Diagnostics.Debugger.IsAttached) - // System.Diagnostics.Debugger.Launch(); + //if (!System.Diagnostics.Debugger.IsAttached) + //{ + // System.Diagnostics.Debugger.Launch(); + //} #endif - var projectItemProvider = initContext.AdditionalTextsProvider - .Combine(initContext.AnalyzerConfigOptionsProvider) - .Select(ComputeProjectItem); + var projectItemProvider = initContext.AdditionalTextsProvider + .Combine(initContext.AnalyzerConfigOptionsProvider) + .Select(ComputeProjectItem) + .WithTrackingName(TrackingNames.ProjectItemProvider); + + var xamlProjectItemProvider = projectItemProvider + .Where(static p => p?.Kind == "Xaml") + .Select(ComputeXamlProjectItem) + .WithTrackingName(TrackingNames.XamlProjectItemProvider); + + var cssProjectItemProvider = projectItemProvider + .Where(static p => p?.Kind == "Css") + .WithTrackingName(TrackingNames.CssProjectItemProvider); + + // Only provide a new Compilation when the references change + var referenceCompilationProvider = initContext.CompilationProvider + .WithComparer(new CompilationReferencesComparer()) + .WithTrackingName(TrackingNames.ReferenceCompilationProvider); + + var xmlnsDefinitionsProvider = referenceCompilationProvider + .Select(GetAssemblyAttributes) + .WithTrackingName(TrackingNames.XmlnsDefinitionsProvider); + + var referenceTypeCacheProvider = referenceCompilationProvider + .Select(GetTypeCache) + .WithTrackingName(TrackingNames.ReferenceTypeCacheProvider); + + var xamlSourceProvider = xamlProjectItemProvider + .Combine(xmlnsDefinitionsProvider) + .Combine(referenceTypeCacheProvider) + .Combine(referenceCompilationProvider) + .Select(static (t, _) => (t.Left.Left, t.Left.Right, t.Right)) + .WithTrackingName(TrackingNames.XamlSourceProvider); + + // Register the XAML pipeline + initContext.RegisterSourceOutput(xamlSourceProvider, static (sourceProductionContext, provider) => + { + var ((xamlItem, xmlnsCache), typeCache, compilation) = provider; - var xmlnsDefinitionsProvider = initContext.CompilationProvider - .Select(GetAssemblyAttributes); + GenerateXamlCodeBehind(xamlItem, compilation, sourceProductionContext, xmlnsCache, typeCache); + }); - var typeCacheProvider = initContext.CompilationProvider - .Select(GetTypeCache); + // Register the CSS pipeline + initContext.RegisterSourceOutput(cssProjectItemProvider, static (sourceProductionContext, cssItem) => + { + if (cssItem == null) + { + return; + } - var sourceProvider = projectItemProvider - .Combine(xmlnsDefinitionsProvider) - .Combine(typeCacheProvider) - .Combine(initContext.CompilationProvider) - .Select(static (t, _) => (t.Left.Left, t.Left.Right, t.Right)); + GenerateCssCodeBehind(cssItem, sourceProductionContext); + }); + } - initContext.RegisterSourceOutput(sourceProvider, static (sourceProductionContext, provider) => - { - var ((projectItem, xmlnsCache), typeCache, compilation) = provider; - if (projectItem == null) - return; + static string EscapeIdentifier(string identifier) + { + var kind = SyntaxFacts.GetKeywordKind(identifier); + return kind == SyntaxKind.None + ? identifier + : $"@{identifier}"; + } - switch (projectItem.Kind) - { - case "Xaml": - GenerateXamlCodeBehind(projectItem, compilation, sourceProductionContext, xmlnsCache, typeCache); - break; - case "Css": - GenerateCssCodeBehind(projectItem, sourceProductionContext); - break; - } - }); + static ProjectItem? ComputeProjectItem((AdditionalText, AnalyzerConfigOptionsProvider) tuple, CancellationToken cancellationToken) + { + var (additionalText, optionsProvider) = tuple; + var fileOptions = optionsProvider.GetOptions(additionalText); + if (!fileOptions.TryGetValue("build_metadata.additionalfiles.GenKind", out string? kind) || kind is null) + { + return null; } - static string EscapeIdentifier(string identifier) + fileOptions.TryGetValue("build_metadata.additionalfiles.TargetPath", out var targetPath); + fileOptions.TryGetValue("build_metadata.additionalfiles.ManifestResourceName", out var manifestResourceName); + fileOptions.TryGetValue("build_metadata.additionalfiles.RelativePath", out var relativePath); + fileOptions.TryGetValue("build_property.targetframework", out var targetFramework); + return new ProjectItem(additionalText, targetPath: targetPath, relativePath: relativePath, manifestResourceName: manifestResourceName, kind: kind, targetFramework: targetFramework); + } + + static XamlProjectItem? ComputeXamlProjectItem(ProjectItem? projectItem, CancellationToken cancellationToken) + { + if (projectItem == null) { - var kind = SyntaxFacts.GetKeywordKind(identifier); - return kind == SyntaxKind.None - ? identifier - : $"@{identifier}"; + return null; } - static ProjectItem? ComputeProjectItem((AdditionalText, AnalyzerConfigOptionsProvider) tuple, CancellationToken cancellationToken) + var text = projectItem.AdditionalText.GetText(cancellationToken); + if (text == null) { - var (additionalText, optionsProvider) = tuple; - var fileOptions = optionsProvider.GetOptions(additionalText); - var globalOptions = optionsProvider.GlobalOptions; - if (!fileOptions.TryGetValue("build_metadata.additionalfiles.GenKind", out string? kind) || kind is null) - return null; - fileOptions.TryGetValue("build_metadata.additionalfiles.TargetPath", out var targetPath); - fileOptions.TryGetValue("build_metadata.additionalfiles.ManifestResourceName", out var manifestResourceName); - fileOptions.TryGetValue("build_metadata.additionalfiles.RelativePath", out var relativePath); - fileOptions.TryGetValue("build_property.targetframework", out var targetFramework); - return new ProjectItem(additionalText, targetPath: targetPath, relativePath: relativePath, manifestResourceName: manifestResourceName, kind: kind, targetFramework: targetFramework); + return null; } - static AssemblyCaches GetAssemblyAttributes(Compilation compilation, CancellationToken cancellationToken) + var xmlDoc = new XmlDocument(); + try { - // [assembly: XmlnsDefinition] - INamedTypeSymbol? xmlnsDefinitonAttribute = compilation.GetTypesByMetadataName(typeof(XmlnsDefinitionAttribute).FullName) - .SingleOrDefault(t => t.ContainingAssembly.Identity.Name == "Microsoft.Maui.Controls"); + xmlDoc.LoadXml(text.ToString()); + } + catch (XmlException xe) + { + return new XamlProjectItem(projectItem, xe); + } + +#pragma warning disable CS0618 // Type or member is obsolete + if (xmlDoc.DocumentElement.NamespaceURI == XamlParser.FormsUri) + { + return new XamlProjectItem(projectItem, new Exception($"{XamlParser.FormsUri} is not a valid namespace. Use {XamlParser.MauiUri} instead")); + } +#pragma warning restore CS0618 // Type or member is obsolete + + cancellationToken.ThrowIfCancellationRequested(); - // [assembly: InternalsVisibleTo] - INamedTypeSymbol? internalsVisibleToAttribute = compilation.GetTypeByMetadataName(typeof(InternalsVisibleToAttribute).FullName); + var nsmgr = new XmlNamespaceManager(xmlDoc.NameTable); + nsmgr.AddNamespace("__f__", XamlParser.MauiUri); - if (xmlnsDefinitonAttribute is null || internalsVisibleToAttribute is null) - return AssemblyCaches.Empty; + var root = xmlDoc.SelectSingleNode("/*", nsmgr); + if (root == null) + { + return null; + } - var xmlnsDefinitions = new List(); - var internalsVisible = new List(); + ApplyTransforms(root, projectItem.TargetFramework, nsmgr); - internalsVisible.Add(compilation.Assembly); + foreach (XmlAttribute attr in root.Attributes) + { + cancellationToken.ThrowIfCancellationRequested(); - // load from references - foreach (var reference in compilation.References) + if (attr.Name == "xmlns") { - cancellationToken.ThrowIfCancellationRequested(); + nsmgr.AddNamespace("", attr.Value); //Add default xmlns + } - if (compilation.GetAssemblyOrModuleSymbol(reference) is not IAssemblySymbol symbol) - continue; - foreach (var attr in symbol.GetAttributes()) - { - if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, xmlnsDefinitonAttribute)) - { - // [assembly: XmlnsDefinition] - var xmlnsDef = new XmlnsDefinitionAttribute(attr.ConstructorArguments[0].Value as string, attr.ConstructorArguments[1].Value as string); - if (attr.NamedArguments.Length == 1 && attr.NamedArguments[0].Key == nameof(XmlnsDefinitionAttribute.AssemblyName)) - xmlnsDef.AssemblyName = attr.NamedArguments[0].Value.Value as string; - else - xmlnsDef.AssemblyName = symbol.Name; - xmlnsDefinitions.Add(xmlnsDef); - } - else if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, internalsVisibleToAttribute)) - { - // [assembly: InternalsVisibleTo] - if (attr.ConstructorArguments[0].Value is string assemblyName && new AssemblyName(assemblyName).Name == compilation.Assembly.Identity.Name) - { - internalsVisible.Add(symbol); - } - } - } + if (attr.Prefix != "xmlns") + { + continue; } - return new AssemblyCaches(xmlnsDefinitions, internalsVisible); + + nsmgr.AddNamespace(attr.LocalName, attr.Value); } - static IDictionary GetTypeCache(Compilation compilation, CancellationToken cancellationToken) + return new XamlProjectItem(projectItem, root, nsmgr); + } + + static AssemblyCaches GetAssemblyAttributes(Compilation compilation, CancellationToken cancellationToken) + { + // [assembly: XmlnsDefinition] + INamedTypeSymbol? xmlnsDefinitonAttribute = compilation.GetTypesByMetadataName(typeof(XmlnsDefinitionAttribute).FullName) + .SingleOrDefault(t => t.ContainingAssembly.Identity.Name == "Microsoft.Maui.Controls"); + + // [assembly: InternalsVisibleTo] + INamedTypeSymbol? internalsVisibleToAttribute = compilation.GetTypeByMetadataName(typeof(InternalsVisibleToAttribute).FullName); + + if (xmlnsDefinitonAttribute is null || internalsVisibleToAttribute is null) { - return new Dictionary(); + return AssemblyCaches.Empty; } - static void GenerateXamlCodeBehind(ProjectItem projItem, Compilation compilation, SourceProductionContext context, AssemblyCaches caches, IDictionary typeCache) + var xmlnsDefinitions = new List(); + var internalsVisible = new List(); + + internalsVisible.Add(compilation.Assembly); + + // load from references + foreach (var reference in compilation.References) { - var text = projItem.AdditionalText.GetText(context.CancellationToken); - if (text == null) - return; + cancellationToken.ThrowIfCancellationRequested(); - // Get a unique string for this xaml project item - var itemName = projItem.ManifestResourceName ?? projItem.RelativePath; - if (itemName == null) - return; - var uid = Crc64.ComputeHashString($"{compilation.AssemblyName}.{itemName}"); + if (compilation.GetAssemblyOrModuleSymbol(reference) is not IAssemblySymbol symbol) + { + continue; + } - if (!TryParseXaml(text, uid, compilation, caches, typeCache, context.CancellationToken, projItem.TargetFramework, out var accessModifier, out var rootType, out var rootClrNamespace, out var generateDefaultCtor, out var addXamlCompilationAttribute, out var hideFromIntellisense, out var XamlResourceIdOnly, out var baseType, out var namedFields, out var parseException)) + foreach (var attr in symbol.GetAttributes()) { - if (parseException != null) + if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, xmlnsDefinitonAttribute)) + { + // [assembly: XmlnsDefinition] + var xmlnsDef = new XmlnsDefinitionAttribute(attr.ConstructorArguments[0].Value as string, attr.ConstructorArguments[1].Value as string); + if (attr.NamedArguments.Length == 1 && attr.NamedArguments[0].Key == nameof(XmlnsDefinitionAttribute.AssemblyName)) + { + xmlnsDef.AssemblyName = attr.NamedArguments[0].Value.Value as string; + } + else + { + xmlnsDef.AssemblyName = symbol.Name; + } + + xmlnsDefinitions.Add(xmlnsDef); + } + else if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, internalsVisibleToAttribute)) { - var location = projItem.RelativePath is not null ? Location.Create(projItem.RelativePath, new TextSpan(), new LinePositionSpan()) : null; - context.ReportDiagnostic(Diagnostic.Create(Descriptors.XamlParserError, location, parseException.Message)); + // [assembly: InternalsVisibleTo] + if (attr.ConstructorArguments[0].Value is string assemblyName && new AssemblyName(assemblyName).Name == compilation.Assembly.Identity.Name) + { + internalsVisible.Add(symbol); + } } - return; } - var sb = new StringBuilder(); - sb.AppendLine(AutoGeneratedHeaderText); + } + return new AssemblyCaches(xmlnsDefinitions, internalsVisible); + } + + static IDictionary GetTypeCache(Compilation compilation, CancellationToken cancellationToken) + { + return new Dictionary(); + } + + static void GenerateXamlCodeBehind(XamlProjectItem? xamlItem, Compilation compilation, SourceProductionContext context, AssemblyCaches xmlnsCache, IDictionary typeCache) + { + if (xamlItem == null) + { + return; + } - var hintName = $"{(string.IsNullOrEmpty(Path.GetDirectoryName(projItem.TargetPath)) ? "" : Path.GetDirectoryName(projItem.TargetPath) + Path.DirectorySeparatorChar)}{Path.GetFileNameWithoutExtension(projItem.TargetPath)}.{projItem.Kind.ToLowerInvariant()}.sg.cs".Replace(Path.DirectorySeparatorChar, '_'); + var projItem = xamlItem.ProjectItem; + if (projItem == null) + { + return; + } - if (projItem.ManifestResourceName != null && projItem.TargetPath != null) - sb.AppendLine($"[assembly: global::Microsoft.Maui.Controls.Xaml.XamlResourceId(\"{projItem.ManifestResourceName}\", \"{projItem.TargetPath.Replace('\\', '/')}\", {(rootType == null ? "null" : "typeof(global::" + rootClrNamespace + "." + rootType + ")")})]"); + // Get a unique string for this xaml project item + var itemName = projItem.ManifestResourceName ?? projItem.RelativePath; + if (itemName == null) + { + return; + } - if (XamlResourceIdOnly) + if (xamlItem.Root == null) + { + if (xamlItem.Exception != null) { - context.AddSource(hintName, SourceText.From(sb.ToString(), Encoding.UTF8)); - return; + var location = projItem.RelativePath is not null ? Location.Create(projItem.RelativePath, new TextSpan(), new LinePositionSpan()) : null; + context.ReportDiagnostic(Diagnostic.Create(Descriptors.XamlParserError, location, xamlItem.Exception.Message)); } + return; + } - if (rootType == null) - throw new Exception("Something went wrong"); - - sb.AppendLine($"namespace {rootClrNamespace}"); - sb.AppendLine("{"); - sb.AppendLine($"\t[global::Microsoft.Maui.Controls.Xaml.XamlFilePath(\"{projItem.RelativePath?.Replace("\\", "\\\\")}\")]"); - if (addXamlCompilationAttribute) - sb.AppendLine($"\t[global::Microsoft.Maui.Controls.Xaml.XamlCompilation(global::Microsoft.Maui.Controls.Xaml.XamlCompilationOptions.Compile)]"); - if (hideFromIntellisense) - sb.AppendLine($"\t[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]"); + var uid = Crc64.ComputeHashString($"{compilation.AssemblyName}.{itemName}"); + if (!TryParseXaml(xamlItem, uid, compilation, xmlnsCache, typeCache, context.CancellationToken, out var accessModifier, out var rootType, out var rootClrNamespace, out var generateDefaultCtor, out var addXamlCompilationAttribute, out var hideFromIntellisense, out var XamlResourceIdOnly, out var baseType, out var namedFields)) + { + return; + } - sb.AppendLine($"\t{accessModifier} partial class {rootType} : {baseType}"); - sb.AppendLine("\t{"); + var sb = new StringBuilder(); + sb.AppendLine(AutoGeneratedHeaderText); - //optional default ctor - if (generateDefaultCtor) - { - sb.AppendLine($"\t\t[global::System.CodeDom.Compiler.GeneratedCode(\"Microsoft.Maui.Controls.SourceGen\", \"1.0.0.0\")]"); - sb.AppendLine($"\t\tpublic {rootType}()"); - sb.AppendLine("\t\t{"); - sb.AppendLine("\t\t\tInitializeComponent();"); - sb.AppendLine("\t\t}"); - sb.AppendLine(); - } + var hintName = $"{(string.IsNullOrEmpty(Path.GetDirectoryName(projItem.TargetPath)) ? "" : Path.GetDirectoryName(projItem.TargetPath) + Path.DirectorySeparatorChar)}{Path.GetFileNameWithoutExtension(projItem.TargetPath)}.{projItem.Kind.ToLowerInvariant()}.sg.cs".Replace(Path.DirectorySeparatorChar, '_'); - //create fields - if (namedFields != null) - foreach ((var fname, var ftype, var faccess) in namedFields) - { - sb.AppendLine($"\t\t[global::System.CodeDom.Compiler.GeneratedCode(\"Microsoft.Maui.Controls.SourceGen\", \"1.0.0.0\")]"); + if (projItem.ManifestResourceName != null && projItem.TargetPath != null) + { + sb.AppendLine($"[assembly: global::Microsoft.Maui.Controls.Xaml.XamlResourceId(\"{projItem.ManifestResourceName}\", \"{projItem.TargetPath.Replace('\\', '/')}\", {(rootType == null ? "null" : "typeof(global::" + rootClrNamespace + "." + rootType + ")")})]"); + } - sb.AppendLine($"\t\t{faccess} {ftype} {EscapeIdentifier(fname)};"); - sb.AppendLine(); - } + if (XamlResourceIdOnly) + { + context.AddSource(hintName, SourceText.From(sb.ToString(), Encoding.UTF8)); + return; + } - //initializeComponent - sb.AppendLine($"\t\t[global::System.CodeDom.Compiler.GeneratedCode(\"Microsoft.Maui.Controls.SourceGen\", \"1.0.0.0\")]"); + if (rootType == null) + { + throw new Exception("Something went wrong"); + } - // add MemberNotNull attributes - if (namedFields != null) - { - sb.AppendLine($"#if NET5_0_OR_GREATER"); - foreach ((var fname, _, _) in namedFields) - { + sb.AppendLine($"namespace {rootClrNamespace}"); + sb.AppendLine("{"); + sb.AppendLine($"\t[global::Microsoft.Maui.Controls.Xaml.XamlFilePath(\"{projItem.RelativePath?.Replace("\\", "\\\\")}\")]"); + if (addXamlCompilationAttribute) + { + sb.AppendLine($"\t[global::Microsoft.Maui.Controls.Xaml.XamlCompilation(global::Microsoft.Maui.Controls.Xaml.XamlCompilationOptions.Compile)]"); + } - sb.AppendLine($"\t\t[global::System.Diagnostics.CodeAnalysis.MemberNotNullAttribute(nameof({EscapeIdentifier(fname)}))]"); - } + if (hideFromIntellisense) + { + sb.AppendLine($"\t[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]"); + } - sb.AppendLine($"#endif"); - } + sb.AppendLine($"\t{accessModifier} partial class {rootType} : {baseType}"); + sb.AppendLine("\t{"); - sb.AppendLine("\t\tprivate void InitializeComponent()"); + //optional default ctor + if (generateDefaultCtor) + { + sb.AppendLine($"\t\t[global::System.CodeDom.Compiler.GeneratedCode(\"Microsoft.Maui.Controls.SourceGen\", \"1.0.0.0\")]"); + sb.AppendLine($"\t\tpublic {rootType}()"); sb.AppendLine("\t\t{"); - sb.AppendLine($"\t\t\tglobal::Microsoft.Maui.Controls.Xaml.Extensions.LoadFromXaml(this, typeof({rootType}));"); - if (namedFields != null) - foreach ((var fname, var ftype, var faccess) in namedFields) - sb.AppendLine($"\t\t\t{EscapeIdentifier(fname)} = global::Microsoft.Maui.Controls.NameScopeExtensions.FindByName<{ftype}>(this, \"{fname}\");"); - + sb.AppendLine("\t\t\tInitializeComponent();"); sb.AppendLine("\t\t}"); - sb.AppendLine("\t}"); - sb.AppendLine("}"); - - context.AddSource(hintName, SourceText.From(sb.ToString(), Encoding.UTF8)); + sb.AppendLine(); } - static bool TryParseXaml(SourceText text, string uid, Compilation compilation, AssemblyCaches caches, IDictionary typeCache, CancellationToken cancellationToken, string? targetFramework, out string? accessModifier, out string? rootType, out string? rootClrNamespace, out bool generateDefaultCtor, out bool addXamlCompilationAttribute, out bool hideFromIntellisense, out bool xamlResourceIdOnly, out string? baseType, out IEnumerable<(string, string, string)>? namedFields, out Exception? exception) + //create fields + if (namedFields != null) { - cancellationToken.ThrowIfCancellationRequested(); - - accessModifier = null; - rootType = null; - rootClrNamespace = null; - generateDefaultCtor = false; - addXamlCompilationAttribute = false; - hideFromIntellisense = false; - xamlResourceIdOnly = false; - namedFields = null; - baseType = null; - exception = null; - - var xmlDoc = new XmlDocument(); - try - { - xmlDoc.LoadXml(text.ToString()); - } - catch (XmlException xe) + foreach ((var fname, var ftype, var faccess) in namedFields) { - exception = xe; - return false; - } + sb.AppendLine($"\t\t[global::System.CodeDom.Compiler.GeneratedCode(\"Microsoft.Maui.Controls.SourceGen\", \"1.0.0.0\")]"); -#pragma warning disable CS0618 // Type or member is obsolete - if (xmlDoc.DocumentElement.NamespaceURI == XamlParser.FormsUri) - { - exception = new Exception($"{XamlParser.FormsUri} is not a valid namespace. Use {XamlParser.MauiUri} instead"); - return false; + sb.AppendLine($"\t\t{faccess} {ftype} {EscapeIdentifier(fname)};"); + sb.AppendLine(); } -#pragma warning restore CS0618 // Type or member is obsolete - - cancellationToken.ThrowIfCancellationRequested(); + } - // if the following xml processing instruction is present - // - // - // - // we will generate a xaml.g.cs file with the default ctor calling InitializeComponent, and a XamlCompilation attribute - var hasXamlCompilationProcessingInstruction = GetXamlCompilationProcessingInstruction(xmlDoc); + //initializeComponent + sb.AppendLine($"\t\t[global::System.CodeDom.Compiler.GeneratedCode(\"Microsoft.Maui.Controls.SourceGen\", \"1.0.0.0\")]"); - var nsmgr = new XmlNamespaceManager(xmlDoc.NameTable); - nsmgr.AddNamespace("__f__", XamlParser.MauiUri); + // add MemberNotNull attributes + if (namedFields != null) + { + sb.AppendLine($"#if NET5_0_OR_GREATER"); + foreach ((var fname, _, _) in namedFields) + { - var root = xmlDoc.SelectSingleNode("/*", nsmgr); - if (root == null) - return false; + sb.AppendLine($"\t\t[global::System.Diagnostics.CodeAnalysis.MemberNotNullAttribute(nameof({EscapeIdentifier(fname)}))]"); + } - ApplyTransforms(root, targetFramework, nsmgr); - cancellationToken.ThrowIfCancellationRequested(); + sb.AppendLine($"#endif"); + } - foreach (XmlAttribute attr in root.Attributes) + sb.AppendLine("\t\tprivate void InitializeComponent()"); + sb.AppendLine("\t\t{"); + sb.AppendLine($"\t\t\tglobal::Microsoft.Maui.Controls.Xaml.Extensions.LoadFromXaml(this, typeof({rootType}));"); + if (namedFields != null) + { + foreach ((var fname, var ftype, var faccess) in namedFields) { - if (attr.Name == "xmlns") - nsmgr.AddNamespace("", attr.Value); //Add default xmlns - if (attr.Prefix != "xmlns") - continue; - nsmgr.AddNamespace(attr.LocalName, attr.Value); + sb.AppendLine($"\t\t\t{EscapeIdentifier(fname)} = global::Microsoft.Maui.Controls.NameScopeExtensions.FindByName<{ftype}>(this, \"{fname}\");"); } + } - cancellationToken.ThrowIfCancellationRequested(); + sb.AppendLine("\t\t}"); + sb.AppendLine("\t}"); + sb.AppendLine("}"); - var rootClass = root.Attributes["Class", XamlParser.X2006Uri] - ?? root.Attributes["Class", XamlParser.X2009Uri]; + context.AddSource(hintName, SourceText.From(sb.ToString(), Encoding.UTF8)); + } - if (rootClass != null) - XmlnsHelper.ParseXmlns(rootClass.Value, out rootType, out rootClrNamespace, out var rootAsm, out var targetPlatform); - else if (hasXamlCompilationProcessingInstruction && root.NamespaceURI == XamlParser.MauiUri) - { - rootClrNamespace = "__XamlGeneratedCode__"; - rootType = $"__Type{uid}"; - generateDefaultCtor = true; - addXamlCompilationAttribute = true; - hideFromIntellisense = true; - } - else - { // rootClass == null && !hasXamlCompilationProcessingInstruction) { - xamlResourceIdOnly = true; //only generate the XamlResourceId assembly attribute - return true; - } + static bool TryParseXaml(XamlProjectItem parseResult, string uid, Compilation compilation, AssemblyCaches xmlnsCache, IDictionary typeCache, CancellationToken cancellationToken, out string? accessModifier, out string? rootType, out string? rootClrNamespace, out bool generateDefaultCtor, out bool addXamlCompilationAttribute, out bool hideFromIntellisense, out bool xamlResourceIdOnly, out string? baseType, out IEnumerable<(string, string, string)>? namedFields) + { + accessModifier = null; + rootType = null; + rootClrNamespace = null; + generateDefaultCtor = false; + addXamlCompilationAttribute = false; + hideFromIntellisense = false; + xamlResourceIdOnly = false; + namedFields = null; + baseType = null; + + cancellationToken.ThrowIfCancellationRequested(); + + var root = parseResult.Root; + var nsmgr = parseResult.Nsmgr; + + if (root == null || nsmgr == null) + { + return false; + } - namedFields = GetNamedFields(root, nsmgr, compilation, caches, typeCache, cancellationToken); - var typeArguments = GetAttributeValue(root, "TypeArguments", XamlParser.X2006Uri, XamlParser.X2009Uri); - baseType = GetTypeName(new XmlType(root.NamespaceURI, root.LocalName, typeArguments != null ? TypeArgumentsParser.ParseExpression(typeArguments, nsmgr, null) : null), compilation, caches, typeCache); + // if the following xml processing instruction is present + // + // + // + // we will generate a xaml.g.cs file with the default ctor calling InitializeComponent, and a XamlCompilation attribute + var hasXamlCompilationProcessingInstruction = GetXamlCompilationProcessingInstruction(root.OwnerDocument); - // x:ClassModifier attribute - var classModifier = GetAttributeValue(root, "ClassModifier", XamlParser.X2006Uri, XamlParser.X2009Uri); - accessModifier = classModifier?.ToLowerInvariant().Replace("notpublic", "internal") ?? "public"; // notpublic is WPF for internal + var rootClass = root.Attributes["Class", XamlParser.X2006Uri] + ?? root.Attributes["Class", XamlParser.X2009Uri]; + if (rootClass != null) + { + XmlnsHelper.ParseXmlns(rootClass.Value, out rootType, out rootClrNamespace, out _, out _); + } + else if (hasXamlCompilationProcessingInstruction && root.NamespaceURI == XamlParser.MauiUri) + { + rootClrNamespace = "__XamlGeneratedCode__"; + rootType = $"__Type{uid}"; + generateDefaultCtor = true; + addXamlCompilationAttribute = true; + hideFromIntellisense = true; + } + else + { // rootClass == null && !hasXamlCompilationProcessingInstruction) { + xamlResourceIdOnly = true; //only generate the XamlResourceId assembly attribute return true; } + namedFields = GetNamedFields(root, nsmgr, compilation, xmlnsCache, typeCache, cancellationToken); + var typeArguments = GetAttributeValue(root, "TypeArguments", XamlParser.X2006Uri, XamlParser.X2009Uri); + baseType = GetTypeName(new XmlType(root.NamespaceURI, root.LocalName, typeArguments != null ? TypeArgumentsParser.ParseExpression(typeArguments, nsmgr, null) : null), compilation, xmlnsCache, typeCache); - static bool GetXamlCompilationProcessingInstruction(XmlDocument xmlDoc) - { - var instruction = xmlDoc.SelectSingleNode("processing-instruction('xaml-comp')") as XmlProcessingInstruction; - if (instruction == null) - return true; + // x:ClassModifier attribute + var classModifier = GetAttributeValue(root, "ClassModifier", XamlParser.X2006Uri, XamlParser.X2009Uri); + accessModifier = classModifier?.ToLowerInvariant().Replace("notpublic", "internal") ?? "public"; // notpublic is WPF for internal + + return true; + } - var parts = instruction.Data.Split(' ', '='); - var indexOfCompile = Array.IndexOf(parts, "compile"); - if (indexOfCompile != -1) - return !parts[indexOfCompile + 1].Trim('"', '\'').Equals("false", StringComparison.OrdinalIgnoreCase); + + static bool GetXamlCompilationProcessingInstruction(XmlDocument xmlDoc) + { + var instruction = xmlDoc.SelectSingleNode("processing-instruction('xaml-comp')") as XmlProcessingInstruction; + if (instruction == null) + { return true; } - static IEnumerable<(string name, string type, string accessModifier)> GetNamedFields(XmlNode root, XmlNamespaceManager nsmgr, Compilation compilation, AssemblyCaches caches, IDictionary typeCache, CancellationToken cancellationToken) + var parts = instruction.Data.Split(' ', '='); + var indexOfCompile = Array.IndexOf(parts, "compile"); + if (indexOfCompile != -1) { - var xPrefix = nsmgr.LookupPrefix(XamlParser.X2006Uri) ?? nsmgr.LookupPrefix(XamlParser.X2009Uri); - if (xPrefix == null) - yield break; - - XmlNodeList names = - root.SelectNodes( - "//*[@" + xPrefix + ":Name" + - "][not(ancestor:: __f__:DataTemplate) and not(ancestor:: __f__:ControlTemplate) and not(ancestor:: __f__:Style) and not(ancestor:: __f__:VisualStateManager.VisualStateGroups)]", nsmgr); - foreach (XmlNode node in names) - { - cancellationToken.ThrowIfCancellationRequested(); - - var name = GetAttributeValue(node, "Name", XamlParser.X2006Uri, XamlParser.X2009Uri) ?? throw new Exception(); - var typeArguments = GetAttributeValue(node, "TypeArguments", XamlParser.X2006Uri, XamlParser.X2009Uri); - var fieldModifier = GetAttributeValue(node, "FieldModifier", XamlParser.X2006Uri, XamlParser.X2009Uri); + return !parts[indexOfCompile + 1].Trim('"', '\'').Equals("false", StringComparison.OrdinalIgnoreCase); + } - var xmlType = new XmlType(node.NamespaceURI, node.LocalName, - typeArguments != null - ? TypeArgumentsParser.ParseExpression(typeArguments, nsmgr, null) - : null); + return true; + } - var accessModifier = fieldModifier?.ToLowerInvariant().Replace("notpublic", "internal") ?? "private"; //notpublic is WPF for internal - if (!new[] { "private", "public", "internal", "protected" }.Contains(accessModifier)) //quick validation - accessModifier = "private"; - yield return (name ?? "", GetTypeName(xmlType, compilation, caches, typeCache), accessModifier); - } + static IEnumerable<(string name, string type, string accessModifier)> GetNamedFields(XmlNode root, XmlNamespaceManager nsmgr, Compilation compilation, AssemblyCaches xmlnsCache, IDictionary typeCache, CancellationToken cancellationToken) + { + var xPrefix = nsmgr.LookupPrefix(XamlParser.X2006Uri) ?? nsmgr.LookupPrefix(XamlParser.X2009Uri); + if (xPrefix == null) + { + yield break; } - static string GetTypeName(XmlType xmlType, Compilation compilation, AssemblyCaches caches, IDictionary typeCache) + XmlNodeList names = + root.SelectNodes( + "//*[@" + xPrefix + ":Name" + + "][not(ancestor:: __f__:DataTemplate) and not(ancestor:: __f__:ControlTemplate) and not(ancestor:: __f__:Style) and not(ancestor:: __f__:VisualStateManager.VisualStateGroups)]", nsmgr); + foreach (XmlNode node in names) { - if (typeCache.TryGetValue(xmlType, out string returnType)) - { - return returnType; - } + cancellationToken.ThrowIfCancellationRequested(); + + var name = GetAttributeValue(node, "Name", XamlParser.X2006Uri, XamlParser.X2009Uri) ?? throw new Exception(); + var typeArguments = GetAttributeValue(node, "TypeArguments", XamlParser.X2006Uri, XamlParser.X2009Uri); + var fieldModifier = GetAttributeValue(node, "FieldModifier", XamlParser.X2006Uri, XamlParser.X2009Uri); + + var xmlType = new XmlType(node.NamespaceURI, node.LocalName, + typeArguments != null + ? TypeArgumentsParser.ParseExpression(typeArguments, nsmgr, null) + : null); - var ns = GetClrNamespace(xmlType.NamespaceUri); - if (ns != null) - returnType = $"{ns}.{xmlType.Name}"; - else + var accessModifier = fieldModifier?.ToLowerInvariant().Replace("notpublic", "internal") ?? "private"; //notpublic is WPF for internal + if (!new[] { "private", "public", "internal", "protected" }.Contains(accessModifier)) //quick validation { - // It's an external, non-built-in namespace URL. - returnType = GetTypeNameFromCustomNamespace(xmlType, compilation, caches); + accessModifier = "private"; } - if (xmlType.TypeArguments != null) - returnType = $"{returnType}<{string.Join(", ", xmlType.TypeArguments.Select(typeArg => GetTypeName(typeArg, compilation, caches, typeCache)))}>"; + yield return (name ?? "", GetTypeName(xmlType, compilation, xmlnsCache, typeCache), accessModifier); + } + } - returnType = $"global::{returnType}"; - typeCache[xmlType] = returnType; + static string GetTypeName(XmlType xmlType, Compilation compilation, AssemblyCaches xmlnsCache, IDictionary typeCache) + { + if (typeCache.TryGetValue(xmlType, out string returnType)) + { return returnType; } - static string? GetClrNamespace(string namespaceuri) + var ns = GetClrNamespace(xmlType.NamespaceUri); + if (ns != null) { - if (namespaceuri == XamlParser.X2009Uri) - return "System"; - if (namespaceuri != XamlParser.X2006Uri && - !namespaceuri.StartsWith("clr-namespace", StringComparison.InvariantCulture) && - !namespaceuri.StartsWith("using:", StringComparison.InvariantCulture)) - return null; - return XmlnsHelper.ParseNamespaceFromXmlns(namespaceuri); + returnType = $"{ns}.{xmlType.Name}"; + } + else + { + // It's an external, non-built-in namespace URL. + returnType = GetTypeNameFromCustomNamespace(xmlType, compilation, xmlnsCache); + } + + if (xmlType.TypeArguments != null) + { + returnType = $"{returnType}<{string.Join(", ", xmlType.TypeArguments.Select(typeArg => GetTypeName(typeArg, compilation, xmlnsCache, typeCache)))}>"; + } + + returnType = $"global::{returnType}"; + typeCache[xmlType] = returnType; + return returnType; + } + + static string? GetClrNamespace(string namespaceuri) + { + if (namespaceuri == XamlParser.X2009Uri) + { + return "System"; } - static string GetTypeNameFromCustomNamespace(XmlType xmlType, Compilation compilation, AssemblyCaches caches) + if (namespaceuri != XamlParser.X2006Uri && + !namespaceuri.StartsWith("clr-namespace", StringComparison.InvariantCulture) && + !namespaceuri.StartsWith("using:", StringComparison.InvariantCulture)) { + return null; + } + + return XmlnsHelper.ParseNamespaceFromXmlns(namespaceuri); + } + + static string GetTypeNameFromCustomNamespace(XmlType xmlType, Compilation compilation, AssemblyCaches xmlnsCache) + { #nullable disable - string typeName = xmlType.GetTypeReference(caches.XmlnsDefinitions, null, - (typeInfo) => + string typeName = xmlType.GetTypeReference(xmlnsCache.XmlnsDefinitions, null, + (typeInfo) => + { + string typeName = typeInfo.typeName.Replace('+', '/'); //Nested types + string fullName = $"{typeInfo.clrNamespace}.{typeInfo.typeName}"; + IList types = compilation.GetTypesByMetadataName(fullName); + + if (types.Count == 0) { - string typeName = typeInfo.typeName.Replace('+', '/'); //Nested types - string fullName = $"{typeInfo.clrNamespace}.{typeInfo.typeName}"; - IList types = compilation.GetTypesByMetadataName(fullName); + return null; + } - if (types.Count == 0) - return null; + foreach (INamedTypeSymbol type in types) + { + // skip over types that are not in the correct assemblies + if (type.ContainingAssembly.Identity.Name != typeInfo.assemblyName) + { + continue; + } - foreach (INamedTypeSymbol type in types) + if (!IsPublicOrVisibleInternal(type, xmlnsCache.InternalsVisible)) { - // skip over types that are not in the correct assemblies - if (type.ContainingAssembly.Identity.Name != typeInfo.assemblyName) - continue; - - if (!IsPublicOrVisibleInternal(type, caches.InternalsVisible)) - continue; - - int i = fullName.IndexOf('`'); - if (i > 0) - { - fullName = fullName.Substring(0, i); - } - return fullName; + continue; } - return null; - }); + int i = fullName.IndexOf('`'); + if (i > 0) + { + fullName = fullName.Substring(0, i); + } + return fullName; + } - return typeName; + return null; + }); + + return typeName; #nullable enable + } + + static bool IsPublicOrVisibleInternal(INamedTypeSymbol type, IEnumerable internalsVisible) + { + // return types that are public + if (type.DeclaredAccessibility == Accessibility.Public) + { + return true; } - static bool IsPublicOrVisibleInternal(INamedTypeSymbol type, IEnumerable internalsVisible) + // only return internal types if they are visible to us + if (type.DeclaredAccessibility == Accessibility.Internal && internalsVisible.Contains(type.ContainingAssembly, SymbolEqualityComparer.Default)) { - // return types that are public - if (type.DeclaredAccessibility == Accessibility.Public) - return true; + return true; + } - // only return internal types if they are visible to us - if (type.DeclaredAccessibility == Accessibility.Internal && internalsVisible.Contains(type.ContainingAssembly, SymbolEqualityComparer.Default)) - return true; + return false; + } - return false; + static string? GetAttributeValue(XmlNode node, string localName, params string[] namespaceURIs) + { + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + if (localName == null) + { + throw new ArgumentNullException(nameof(localName)); } - static string? GetAttributeValue(XmlNode node, string localName, params string[] namespaceURIs) + if (namespaceURIs == null) { - if (node == null) - throw new ArgumentNullException(nameof(node)); - if (localName == null) - throw new ArgumentNullException(nameof(localName)); - if (namespaceURIs == null) - throw new ArgumentNullException(nameof(namespaceURIs)); - foreach (var namespaceURI in namespaceURIs) + throw new ArgumentNullException(nameof(namespaceURIs)); + } + + foreach (var namespaceURI in namespaceURIs) + { + var attr = node.Attributes[localName, namespaceURI]; + if (attr == null) { - var attr = node.Attributes[localName, namespaceURI]; - if (attr == null) - continue; - return attr.Value; + continue; } - return null; + + return attr.Value; + } + return null; + } + + static void GenerateCssCodeBehind(ProjectItem projItem, SourceProductionContext sourceProductionContext) + { + var sb = new StringBuilder(); + var hintName = $"{(string.IsNullOrEmpty(Path.GetDirectoryName(projItem.TargetPath)) ? "" : Path.GetDirectoryName(projItem.TargetPath) + Path.DirectorySeparatorChar)}{Path.GetFileNameWithoutExtension(projItem.TargetPath)}.{projItem.Kind.ToLowerInvariant()}.sg.cs".Replace(Path.DirectorySeparatorChar, '_'); + + if (projItem.ManifestResourceName != null && projItem.TargetPath != null) + { + sb.AppendLine($"[assembly: global::Microsoft.Maui.Controls.Xaml.XamlResourceId(\"{projItem.ManifestResourceName}\", \"{projItem.TargetPath.Replace('\\', '/')}\", null)]"); } - static void GenerateCssCodeBehind(ProjectItem projItem, SourceProductionContext sourceProductionContext) + sourceProductionContext.AddSource(hintName, SourceText.From(sb.ToString(), Encoding.UTF8)); + } + + static void ApplyTransforms(XmlNode node, string? targetFramework, XmlNamespaceManager nsmgr) + { + SimplifyOnPlatform(node, targetFramework, nsmgr); + } + + static void SimplifyOnPlatform(XmlNode node, string? targetFramework, XmlNamespaceManager nsmgr) + { + //remove OnPlatform nodes if the platform doesn't match, so we don't generate field for x:Name of elements being removed + if (targetFramework == null) { - var sb = new StringBuilder(); - var hintName = $"{(string.IsNullOrEmpty(Path.GetDirectoryName(projItem.TargetPath)) ? "" : Path.GetDirectoryName(projItem.TargetPath) + Path.DirectorySeparatorChar)}{Path.GetFileNameWithoutExtension(projItem.TargetPath)}.{projItem.Kind.ToLowerInvariant()}.sg.cs".Replace(Path.DirectorySeparatorChar, '_'); + return; + } - if (projItem.ManifestResourceName != null && projItem.TargetPath != null) - sb.AppendLine($"[assembly: global::Microsoft.Maui.Controls.Xaml.XamlResourceId(\"{projItem.ManifestResourceName}\", \"{projItem.TargetPath.Replace('\\', '/')}\", null)]"); + string? target = null; + targetFramework = targetFramework.Trim(); + if (targetFramework.IndexOf("-android", StringComparison.OrdinalIgnoreCase) != -1) + { + target = "Android"; + } - sourceProductionContext.AddSource(hintName, SourceText.From(sb.ToString(), Encoding.UTF8)); + if (targetFramework.IndexOf("-ios", StringComparison.OrdinalIgnoreCase) != -1) + { + target = "iOS"; } - static void ApplyTransforms(XmlNode node, string? targetFramework, XmlNamespaceManager nsmgr) + if (targetFramework.IndexOf("-macos", StringComparison.OrdinalIgnoreCase) != -1) { - SimplifyOnPlatform(node, targetFramework, nsmgr); + target = "macOS"; } - static void SimplifyOnPlatform(XmlNode node, string? targetFramework, XmlNamespaceManager nsmgr) + if (targetFramework.IndexOf("-maccatalyst", StringComparison.OrdinalIgnoreCase) != -1) { - //remove OnPlatform nodes if the platform doesn't match, so we don't generate field for x:Name of elements being removed - if (targetFramework == null) - return; + target = "MacCatalyst"; + } - string? target = null; - targetFramework = targetFramework.Trim(); - if (targetFramework.IndexOf("-android", StringComparison.OrdinalIgnoreCase) != -1) - target = "Android"; - if (targetFramework.IndexOf("-ios", StringComparison.OrdinalIgnoreCase) != -1) - target = "iOS"; - if (targetFramework.IndexOf("-macos", StringComparison.OrdinalIgnoreCase) != -1) - target = "macOS"; - if (targetFramework.IndexOf("-maccatalyst", StringComparison.OrdinalIgnoreCase) != -1) - target = "MacCatalyst"; - if (target == null) - return; + if (target == null) + { + return; + } - //no need to handle {OnPlatform} markup extension, as you can't x:Name there - var onPlatformNodes = node.SelectNodes("//__f__:OnPlatform", nsmgr); - foreach (XmlNode onPlatformNode in onPlatformNodes) + //no need to handle {OnPlatform} markup extension, as you can't x:Name there + var onPlatformNodes = node.SelectNodes("//__f__:OnPlatform", nsmgr); + foreach (XmlNode onPlatformNode in onPlatformNodes) + { + var onNodes = onPlatformNode.SelectNodes("__f__:On", nsmgr); + foreach (XmlNode onNode in onNodes) { - var onNodes = onPlatformNode.SelectNodes("__f__:On", nsmgr); - foreach (XmlNode onNode in onNodes) + var platforms = onNode.SelectSingleNode("@Platform"); + var plats = platforms.Value.Split(','); + var match = false; + + foreach (var plat in plats) { - var platforms = onNode.SelectSingleNode("@Platform"); - var plats = platforms.Value.Split(','); - var match = false; + if (string.IsNullOrWhiteSpace(plat)) + { + continue; + } - foreach (var plat in plats) + if (plat.Trim() == target) { - if (string.IsNullOrWhiteSpace(plat)) - continue; - if (plat.Trim() == target) - { - match = true; - break; - } + match = true; + break; } - if (!match) - onNode.ParentNode.RemoveChild(onNode); + } + if (!match) + { + onNode.ParentNode.RemoveChild(onNode); } } } + } - class ProjectItem + class ProjectItem + { + public ProjectItem(AdditionalText additionalText, string? targetPath, string? relativePath, string? manifestResourceName, string kind, string? targetFramework) { - public ProjectItem(AdditionalText additionalText, string? targetPath, string? relativePath, string? manifestResourceName, string kind, string? targetFramework) - { - AdditionalText = additionalText; - TargetPath = targetPath ?? additionalText.Path; - RelativePath = relativePath; - ManifestResourceName = manifestResourceName; - Kind = kind; - TargetFramework = targetFramework; - } + AdditionalText = additionalText; + TargetPath = targetPath ?? additionalText.Path; + RelativePath = relativePath; + ManifestResourceName = manifestResourceName; + Kind = kind; + TargetFramework = targetFramework; + } - public AdditionalText AdditionalText { get; } - public string? TargetPath { get; } - public string? RelativePath { get; } - public string? ManifestResourceName { get; } - public string Kind { get; } - public string? TargetFramework { get; } + public AdditionalText AdditionalText { get; } + public string? TargetPath { get; } + public string? RelativePath { get; } + public string? ManifestResourceName { get; } + public string Kind { get; } + public string? TargetFramework { get; } + } + + class XamlProjectItem + { + public XamlProjectItem(ProjectItem projectItem, XmlNode root, XmlNamespaceManager nsmgr) + { + ProjectItem = projectItem; + Root = root; + Nsmgr = nsmgr; } - class AssemblyCaches + public XamlProjectItem(ProjectItem projectItem, Exception exception) { - public static readonly AssemblyCaches Empty = new(Array.Empty(), Array.Empty()); + ProjectItem = projectItem; + Exception = exception; + } + + public ProjectItem? ProjectItem { get; } + public XmlNode? Root { get; } + public XmlNamespaceManager? Nsmgr { get; } + public Exception? Exception { get; } + } - public AssemblyCaches(IReadOnlyList xmlnsDefinitions, IReadOnlyList internalsVisible) + class AssemblyCaches + { + public static readonly AssemblyCaches Empty = new(Array.Empty(), Array.Empty()); + + public AssemblyCaches(IReadOnlyList xmlnsDefinitions, IReadOnlyList internalsVisible) + { + XmlnsDefinitions = xmlnsDefinitions; + InternalsVisible = internalsVisible; + } + + public IReadOnlyList XmlnsDefinitions { get; } + + public IReadOnlyList InternalsVisible { get; } + } + + class CompilationReferencesComparer : IEqualityComparer + { + public bool Equals(Compilation x, Compilation y) + { + if (x.AssemblyName != y.AssemblyName) + { + return false; + } + + if (x.ExternalReferences.Length != y.ExternalReferences.Length) { - XmlnsDefinitions = xmlnsDefinitions; - InternalsVisible = internalsVisible; + return false; } - public IReadOnlyList XmlnsDefinitions { get; } + return x.ExternalReferences.OfType().SequenceEqual(y.ExternalReferences.OfType()); + } - public IReadOnlyList InternalsVisible { get; } + public int GetHashCode(Compilation obj) + { + return obj.References.GetHashCode(); } } } diff --git a/src/Controls/src/SourceGen/TrackingNames.cs b/src/Controls/src/SourceGen/TrackingNames.cs new file mode 100644 index 000000000000..fda8e5263e67 --- /dev/null +++ b/src/Controls/src/SourceGen/TrackingNames.cs @@ -0,0 +1,15 @@ +namespace Microsoft.Maui.Controls.SourceGen; + +/// +/// Names for tracking source generator stages +/// +public class TrackingNames +{ + public const string CssProjectItemProvider = nameof(CssProjectItemProvider); + public const string ProjectItemProvider = nameof(ProjectItemProvider); + public const string ReferenceCompilationProvider = nameof(ReferenceCompilationProvider); + public const string ReferenceTypeCacheProvider = nameof(ReferenceTypeCacheProvider); + public const string XmlnsDefinitionsProvider = nameof(XmlnsDefinitionsProvider); + public const string XamlProjectItemProvider = nameof(XamlProjectItemProvider); + public const string XamlSourceProvider = nameof(XamlSourceProvider); +} diff --git a/src/Controls/tests/SourceGen.UnitTests/SourceGen.UnitTests.csproj b/src/Controls/tests/SourceGen.UnitTests/SourceGen.UnitTests.csproj new file mode 100644 index 000000000000..1ffe98708687 --- /dev/null +++ b/src/Controls/tests/SourceGen.UnitTests/SourceGen.UnitTests.csproj @@ -0,0 +1,39 @@ + + + + $(_MauiDotNetTfm) + Microsoft.Maui.Controls.SourceGen.UnitTests + Microsoft.Maui.Controls.SourceGen.UnitTests + 4 + $(NoWarn);0672;0219;0414;CS0436;CS0618 + $(WarningsNotAsErrors);XC0618;XC0022;XC0023 + false + enable + true + + + + DEBUG + prompt + full + true + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Controls/tests/SourceGen.UnitTests/SourceGenCssTests.cs b/src/Controls/tests/SourceGen.UnitTests/SourceGenCssTests.cs new file mode 100644 index 000000000000..ff9f8b9242fa --- /dev/null +++ b/src/Controls/tests/SourceGen.UnitTests/SourceGenCssTests.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.Maui.Controls.SourceGen; +using NUnit.Framework; + +using static Microsoft.Maui.Controls.Xaml.UnitTests.SourceGen.SourceGeneratorDriver; + +namespace Microsoft.Maui.Controls.Xaml.UnitTests.SourceGen; + +public class SourceGenCssTests : SourceGenTestsBase +{ + private record AdditionalCssFile(string Path, string Content, string? RelativePath = null, string? TargetPath = null, string? ManifestResourceName = null, string? TargetFramework = null) + : AdditionalFile(Text: SourceGeneratorDriver.ToAdditionalText(Path, Content), Kind: "Css", RelativePath: RelativePath ?? Path, TargetPath: TargetPath, ManifestResourceName: ManifestResourceName ?? Path, TargetFramework: TargetFramework); + + [Test] + public void TestCodeBehindGenerator_BasicCss() + { + var css = +""" +h1 {color: purple; + background-color: lightcyan; + font-weight: 800; +} +"""; + var compilation = SourceGeneratorDriver.CreateMauiCompilation(); + var cssFile = new AdditionalCssFile("Test.css", css); + var result = SourceGeneratorDriver.RunGenerator(compilation, cssFile); + + Assert.IsFalse(result.Diagnostics.Any()); + + var generated = result.Results.Single().GeneratedSources.Single().SourceText.ToString(); + + Assert.IsTrue(generated.Contains($"XamlResourceId(\"{cssFile.ManifestResourceName}\", \"{cssFile.Path}\"", StringComparison.Ordinal)); + } + + [Test] + public void TestCodeBehindGenerator_ModifiedCss() + { + var css = +""" +h1 {color: purple; + background-color: lightcyan; + font-weight: 800; +} +"""; + var newCss = +""" +h1 {color: red; + background-color: lightcyan; + font-weight: 800; +} +"""; + var cssFile = new AdditionalCssFile("Test.css", css); + var compilation = SourceGeneratorDriver.CreateMauiCompilation(); + var result = SourceGeneratorDriver.RunGeneratorWithChanges(compilation, ApplyChanges, cssFile); + + var result1 = result.result1.Results.Single(); + var result2 = result.result2.Results.Single(); + var output1 = result1.GeneratedSources.Single().SourceText.ToString(); + var output2 = result2.GeneratedSources.Single().SourceText.ToString(); + + Assert.IsTrue(result1.TrackedSteps.All(s => s.Value.Single().Outputs.Single().Reason == IncrementalStepRunReason.New)); + Assert.AreEqual(output1, output2); + + Assert.IsTrue(output1.Contains($"XamlResourceId(\"{cssFile.ManifestResourceName}\", \"{cssFile.Path}\"", StringComparison.Ordinal)); + + (GeneratorDriver, Compilation) ApplyChanges(GeneratorDriver driver, Compilation compilation) + { + var newCssFile = new AdditionalCssFile("Test.css", newCss); + driver = driver.ReplaceAdditionalText(cssFile.Text, newCssFile.Text); + return (driver, compilation); + } + + var expectedReasons = new Dictionary + { + { TrackingNames.ProjectItemProvider, IncrementalStepRunReason.Modified }, + { TrackingNames.CssProjectItemProvider, IncrementalStepRunReason.Modified } + }; + + VerifyStepRunReasons(result2, expectedReasons); + } +} diff --git a/src/Controls/tests/SourceGen.UnitTests/SourceGenTestsBase.cs b/src/Controls/tests/SourceGen.UnitTests/SourceGenTestsBase.cs new file mode 100644 index 000000000000..7d6f98b41679 --- /dev/null +++ b/src/Controls/tests/SourceGen.UnitTests/SourceGenTestsBase.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using NUnit.Framework; + +namespace Microsoft.Maui.Controls.Xaml.UnitTests.SourceGen; + +public class SourceGenTestsBase +{ + public static void VerifyStepRunReasons(GeneratorRunResult result2, Dictionary expectedReasons) + { + foreach (var expected in expectedReasons) + { + var actualReason = result2.TrackedSteps[expected.Key].Single().Outputs.Single().Reason; + Assert.AreEqual(expected.Value, actualReason, message: expected.Key); + } + } +} \ No newline at end of file diff --git a/src/Controls/tests/SourceGen.UnitTests/SourceGenXamlTests.cs b/src/Controls/tests/SourceGen.UnitTests/SourceGenXamlTests.cs new file mode 100644 index 000000000000..4c18f47858f9 --- /dev/null +++ b/src/Controls/tests/SourceGen.UnitTests/SourceGenXamlTests.cs @@ -0,0 +1,210 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.Maui.Controls.SourceGen; +using NUnit.Framework; + +using static Microsoft.Maui.Controls.Xaml.UnitTests.SourceGen.SourceGeneratorDriver; + +namespace Microsoft.Maui.Controls.Xaml.UnitTests.SourceGen; + +public class SourceGenXamlTests : SourceGenTestsBase +{ + private record AdditionalXamlFile(string Path, string Content, string? RelativePath = null, string? TargetPath = null, string? ManifestResourceName = null, string? TargetFramework = null) + : AdditionalFile(Text: SourceGeneratorDriver.ToAdditionalText(Path, Content), Kind: "Xaml", RelativePath: RelativePath ?? Path, TargetPath: TargetPath, ManifestResourceName: ManifestResourceName, TargetFramework: TargetFramework); + + [Test] + public void TestCodeBehindGenerator_BasicXaml() + { + var xaml = +""" + + +