From b9ce2119d11a814d84260d56cd1025ac21796c3e Mon Sep 17 00:00:00 2001 From: Marco Goertz Date: Mon, 12 Feb 2024 08:29:11 -0800 Subject: [PATCH 1/2] - 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 --- Microsoft.Maui-dev.sln | 14 + Microsoft.Maui-windows.slnf | 8 +- .../src/SourceGen/CodeBehindGenerator.cs | 366 ++++++++++++++---- src/Controls/src/SourceGen/TrackingNames.cs | 16 + .../SourceGen.UnitTests.csproj | 39 ++ .../SourceGen.UnitTests/SourceGenCssTests.cs | 84 ++++ .../SourceGen.UnitTests/SourceGenTestsBase.cs | 19 + .../SourceGen.UnitTests/SourceGenXamlTests.cs | 205 ++++++++++ .../SourceGeneratorDriver.cs | 174 +++++++++ 9 files changed, 839 insertions(+), 86 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..9cc9fce96c7b 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; @@ -36,39 +35,61 @@ public void Initialize(IncrementalGeneratorInitializationContext initContext) { #if DEBUG //if (!System.Diagnostics.Debugger.IsAttached) + //{ // System.Diagnostics.Debugger.Launch(); + //} #endif var projectItemProvider = initContext.AdditionalTextsProvider .Combine(initContext.AnalyzerConfigOptionsProvider) - .Select(ComputeProjectItem); + .Select(ComputeProjectItem) + .WithTrackingName(TrackingNames.ProjectItemProvider); - var xmlnsDefinitionsProvider = initContext.CompilationProvider - .Select(GetAssemblyAttributes); + var xamlProjectItemProvider = projectItemProvider + .Where(static p => p?.Kind == "Xaml") + .Select(ComputeXamlProjectItem) + .WithTrackingName(TrackingNames.XamlProjectItemProvider); - var typeCacheProvider = initContext.CompilationProvider - .Select(GetTypeCache); + var cssProjectItemProvider = projectItemProvider + .Where(static p => p?.Kind == "Css") + .WithTrackingName(TrackingNames.CssProjectItemProvider); - var sourceProvider = projectItemProvider + // 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(typeCacheProvider) - .Combine(initContext.CompilationProvider) - .Select(static (t, _) => (t.Left.Left, t.Left.Right, t.Right)); + .Combine(referenceTypeCacheProvider) + .Combine(referenceCompilationProvider) + .Select(static (t, _) => (t.Left.Left, t.Left.Right, t.Right)) + .WithTrackingName(TrackingNames.XamlSourceProvider); - initContext.RegisterSourceOutput(sourceProvider, static (sourceProductionContext, provider) => + // Register the XAML pipeline + initContext.RegisterSourceOutput(xamlSourceProvider, static (sourceProductionContext, provider) => { - var ((projectItem, xmlnsCache), typeCache, compilation) = provider; - if (projectItem == null) - return; + var ((xamlItem, xmlnsCache), typeCache, compilation) = provider; - switch (projectItem.Kind) + GenerateXamlCodeBehind(xamlItem, compilation, sourceProductionContext, xmlnsCache, typeCache); + }); + + // Register the CSS pipeline + initContext.RegisterSourceOutput(cssProjectItemProvider, static (sourceProductionContext, cssItem) => + { + if (cssItem == null) { - case "Xaml": - GenerateXamlCodeBehind(projectItem, compilation, sourceProductionContext, xmlnsCache, typeCache); - break; - case "Css": - GenerateCssCodeBehind(projectItem, sourceProductionContext); - break; + return; } + + GenerateCssCodeBehind(cssItem, sourceProductionContext); }); } @@ -84,9 +105,11 @@ static string EscapeIdentifier(string identifier) { 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); @@ -94,6 +117,69 @@ static string EscapeIdentifier(string identifier) return new ProjectItem(additionalText, targetPath: targetPath, relativePath: relativePath, manifestResourceName: manifestResourceName, kind: kind, targetFramework: targetFramework); } + static XamlProjectItem? ComputeXamlProjectItem(ProjectItem? projectItem, CancellationToken cancellationToken) + { + if (projectItem == null) + { + return null; + } + + var text = projectItem.AdditionalText.GetText(cancellationToken); + if (text == null) + { + return null; + } + + var xmlDoc = new XmlDocument(); + try + { + 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(); + + var nsmgr = new XmlNamespaceManager(xmlDoc.NameTable); + nsmgr.AddNamespace("__f__", XamlParser.MauiUri); + + var root = xmlDoc.SelectSingleNode("/*", nsmgr); + if (root == null) + { + return null; + } + + ApplyTransforms(root, projectItem.TargetFramework, nsmgr); + + foreach (XmlAttribute attr in root.Attributes) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (attr.Name == "xmlns") + { + nsmgr.AddNamespace("", attr.Value); //Add default xmlns + } + + if (attr.Prefix != "xmlns") + { + continue; + } + + nsmgr.AddNamespace(attr.LocalName, attr.Value); + } + + return new XamlProjectItem(projectItem, root, nsmgr); + } + static AssemblyCaches GetAssemblyAttributes(Compilation compilation, CancellationToken cancellationToken) { // [assembly: XmlnsDefinition] @@ -104,7 +190,9 @@ static AssemblyCaches GetAssemblyAttributes(Compilation compilation, Cancellatio INamedTypeSymbol? internalsVisibleToAttribute = compilation.GetTypeByMetadataName(typeof(InternalsVisibleToAttribute).FullName); if (xmlnsDefinitonAttribute is null || internalsVisibleToAttribute is null) + { return AssemblyCaches.Empty; + } var xmlnsDefinitions = new List(); var internalsVisible = new List(); @@ -117,7 +205,10 @@ static AssemblyCaches GetAssemblyAttributes(Compilation compilation, Cancellatio cancellationToken.ThrowIfCancellationRequested(); if (compilation.GetAssemblyOrModuleSymbol(reference) is not IAssemblySymbol symbol) + { continue; + } + foreach (var attr in symbol.GetAttributes()) { if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, xmlnsDefinitonAttribute)) @@ -125,9 +216,14 @@ static AssemblyCaches GetAssemblyAttributes(Compilation compilation, Cancellatio // [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)) @@ -148,34 +244,51 @@ static IDictionary GetTypeCache(Compilation compilation, Cancel return new Dictionary(); } - static void GenerateXamlCodeBehind(ProjectItem projItem, Compilation compilation, SourceProductionContext context, AssemblyCaches caches, IDictionary typeCache) + static void GenerateXamlCodeBehind(XamlProjectItem? xamlItem, Compilation compilation, SourceProductionContext context, AssemblyCaches xmlnsCache, IDictionary typeCache) { - var text = projItem.AdditionalText.GetText(context.CancellationToken); - if (text == null) + if (xamlItem == null) + { return; + } + + var projItem = xamlItem.ProjectItem; + if (projItem == null) + { + return; + } // 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 (!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)) + if (xamlItem.Root == null) { - if (parseException != null) + if (xamlItem.Exception != null) { 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)); + context.ReportDiagnostic(Diagnostic.Create(Descriptors.XamlParserError, location, xamlItem.Exception.Message)); } return; } + + 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; + } + var sb = new StringBuilder(); sb.AppendLine(AutoGeneratedHeaderText); 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('\\', '/')}\", {(rootType == null ? "null" : "typeof(global::" + rootClrNamespace + "." + rootType + ")")})]"); + } if (XamlResourceIdOnly) { @@ -184,15 +297,22 @@ static void GenerateXamlCodeBehind(ProjectItem projItem, Compilation compilation } 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)]"); + } sb.AppendLine($"\t{accessModifier} partial class {rootType} : {baseType}"); sb.AppendLine("\t{"); @@ -210,6 +330,7 @@ static void GenerateXamlCodeBehind(ProjectItem projItem, Compilation compilation //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\")]"); @@ -217,6 +338,7 @@ static void GenerateXamlCodeBehind(ProjectItem projItem, Compilation compilation sb.AppendLine($"\t\t{faccess} {ftype} {EscapeIdentifier(fname)};"); sb.AppendLine(); } + } //initializeComponent sb.AppendLine($"\t\t[global::System.CodeDom.Compiler.GeneratedCode(\"Microsoft.Maui.Controls.SourceGen\", \"1.0.0.0\")]"); @@ -238,8 +360,12 @@ static void GenerateXamlCodeBehind(ProjectItem projItem, Compilation compilation 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}"); sb.AppendLine("\t}"); @@ -248,10 +374,8 @@ static void GenerateXamlCodeBehind(ProjectItem projItem, Compilation compilation context.AddSource(hintName, SourceText.From(sb.ToString(), Encoding.UTF8)); } - 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) + 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) { - cancellationToken.ThrowIfCancellationRequested(); - accessModifier = null; rootType = null; rootClrNamespace = null; @@ -261,62 +385,31 @@ static bool TryParseXaml(SourceText text, string uid, Compilation compilation, A xamlResourceIdOnly = false; namedFields = null; baseType = null; - exception = null; - var xmlDoc = new XmlDocument(); - try - { - xmlDoc.LoadXml(text.ToString()); - } - catch (XmlException xe) - { - exception = xe; - return false; - } + cancellationToken.ThrowIfCancellationRequested(); -#pragma warning disable CS0618 // Type or member is obsolete - if (xmlDoc.DocumentElement.NamespaceURI == XamlParser.FormsUri) + var root = parseResult.Root; + var nsmgr = parseResult.Nsmgr; + + if (root == null || nsmgr == null) { - exception = new Exception($"{XamlParser.FormsUri} is not a valid namespace. Use {XamlParser.MauiUri} instead"); return false; } -#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); - - var nsmgr = new XmlNamespaceManager(xmlDoc.NameTable); - nsmgr.AddNamespace("__f__", XamlParser.MauiUri); - - var root = xmlDoc.SelectSingleNode("/*", nsmgr); - if (root == null) - return false; - - ApplyTransforms(root, targetFramework, nsmgr); - cancellationToken.ThrowIfCancellationRequested(); - - foreach (XmlAttribute attr in root.Attributes) - { - if (attr.Name == "xmlns") - nsmgr.AddNamespace("", attr.Value); //Add default xmlns - if (attr.Prefix != "xmlns") - continue; - nsmgr.AddNamespace(attr.LocalName, attr.Value); - } - - cancellationToken.ThrowIfCancellationRequested(); + var hasXamlCompilationProcessingInstruction = GetXamlCompilationProcessingInstruction(root.OwnerDocument); var rootClass = root.Attributes["Class", XamlParser.X2006Uri] ?? root.Attributes["Class", XamlParser.X2009Uri]; if (rootClass != null) - XmlnsHelper.ParseXmlns(rootClass.Value, out rootType, out rootClrNamespace, out var rootAsm, out var targetPlatform); + { + XmlnsHelper.ParseXmlns(rootClass.Value, out rootType, out rootClrNamespace, out _, out _); + } else if (hasXamlCompilationProcessingInstruction && root.NamespaceURI == XamlParser.MauiUri) { rootClrNamespace = "__XamlGeneratedCode__"; @@ -331,9 +424,9 @@ static bool TryParseXaml(SourceText text, string uid, Compilation compilation, A return true; } - namedFields = GetNamedFields(root, nsmgr, compilation, caches, typeCache, cancellationToken); + 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, caches, typeCache); + baseType = GetTypeName(new XmlType(root.NamespaceURI, root.LocalName, typeArguments != null ? TypeArgumentsParser.ParseExpression(typeArguments, nsmgr, null) : null), compilation, xmlnsCache, typeCache); // x:ClassModifier attribute var classModifier = GetAttributeValue(root, "ClassModifier", XamlParser.X2006Uri, XamlParser.X2009Uri); @@ -347,20 +440,27 @@ static bool GetXamlCompilationProcessingInstruction(XmlDocument xmlDoc) { var instruction = xmlDoc.SelectSingleNode("processing-instruction('xaml-comp')") as XmlProcessingInstruction; if (instruction == null) + { 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); + } + return true; } - static IEnumerable<(string name, string type, string accessModifier)> GetNamedFields(XmlNode root, XmlNamespaceManager nsmgr, Compilation compilation, AssemblyCaches caches, IDictionary typeCache, CancellationToken cancellationToken) + 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; + } XmlNodeList names = root.SelectNodes( @@ -381,12 +481,15 @@ static bool GetXamlCompilationProcessingInstruction(XmlDocument xmlDoc) 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); + } + + yield return (name ?? "", GetTypeName(xmlType, compilation, xmlnsCache, typeCache), accessModifier); } } - static string GetTypeName(XmlType xmlType, Compilation compilation, AssemblyCaches caches, IDictionary typeCache) + static string GetTypeName(XmlType xmlType, Compilation compilation, AssemblyCaches xmlnsCache, IDictionary typeCache) { if (typeCache.TryGetValue(xmlType, out string returnType)) { @@ -395,15 +498,19 @@ static string GetTypeName(XmlType xmlType, Compilation compilation, AssemblyCach var ns = GetClrNamespace(xmlType.NamespaceUri); if (ns != null) + { returnType = $"{ns}.{xmlType.Name}"; + } else { // It's an external, non-built-in namespace URL. - returnType = GetTypeNameFromCustomNamespace(xmlType, compilation, caches); + returnType = GetTypeNameFromCustomNamespace(xmlType, compilation, xmlnsCache); } if (xmlType.TypeArguments != null) - returnType = $"{returnType}<{string.Join(", ", xmlType.TypeArguments.Select(typeArg => GetTypeName(typeArg, compilation, caches, typeCache)))}>"; + { + returnType = $"{returnType}<{string.Join(", ", xmlType.TypeArguments.Select(typeArg => GetTypeName(typeArg, compilation, xmlnsCache, typeCache)))}>"; + } returnType = $"global::{returnType}"; typeCache[xmlType] = returnType; @@ -413,18 +520,24 @@ static string GetTypeName(XmlType xmlType, Compilation compilation, AssemblyCach static string? GetClrNamespace(string namespaceuri) { 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); } - static string GetTypeNameFromCustomNamespace(XmlType xmlType, Compilation compilation, AssemblyCaches caches) + static string GetTypeNameFromCustomNamespace(XmlType xmlType, Compilation compilation, AssemblyCaches xmlnsCache) { #nullable disable - string typeName = xmlType.GetTypeReference(caches.XmlnsDefinitions, null, + string typeName = xmlType.GetTypeReference(xmlnsCache.XmlnsDefinitions, null, (typeInfo) => { string typeName = typeInfo.typeName.Replace('+', '/'); //Nested types @@ -432,16 +545,22 @@ static string GetTypeNameFromCustomNamespace(XmlType xmlType, Compilation compil IList types = compilation.GetTypesByMetadataName(fullName); 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; + } - if (!IsPublicOrVisibleInternal(type, caches.InternalsVisible)) + if (!IsPublicOrVisibleInternal(type, xmlnsCache.InternalsVisible)) + { continue; + } int i = fullName.IndexOf('`'); if (i > 0) @@ -462,11 +581,15 @@ static bool IsPublicOrVisibleInternal(INamedTypeSymbol type, IEnumerable(), Array.Empty()); @@ -585,5 +764,28 @@ public AssemblyCaches(IReadOnlyList xmlnsDefinitions, 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) + { + return false; + } + + return x.ExternalReferences.OfType().SequenceEqual(y.ExternalReferences.OfType()); + } + + 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..a93215569067 --- /dev/null +++ b/src/Controls/src/SourceGen/TrackingNames.cs @@ -0,0 +1,16 @@ +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..04eb76d74bf0 --- /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..ccd5654424d8 --- /dev/null +++ b/src/Controls/tests/SourceGen.UnitTests/SourceGenTestsBase.cs @@ -0,0 +1,19 @@ +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..82bc03cb1fbd --- /dev/null +++ b/src/Controls/tests/SourceGen.UnitTests/SourceGenXamlTests.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +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 = +$@" + +