From 4460de529ebb617dbe5928bb89a8c44e424caad7 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 25 Nov 2022 00:56:43 -0500 Subject: [PATCH 01/22] Implement multiple documents loading and transforming in AvaloniaRuntimeXamlLoader --- .../DesignWindowLoader.cs | 3 +- .../AvaloniaRuntimeXamlLoader.cs | 46 +++--- .../AvaloniaXamlIlRuntimeCompiler.cs | 133 +++++++++++++----- .../AvaloniaXamlIlCompiler.cs | 48 ++++++- .../IXamlAstGroupTransformer.cs | 78 ++++++++++ .../IXamlDocumentResource.cs | 17 +++ .../AvaloniaXamlIlWellKnownTypes.cs | 9 ++ .../XamlDocumentParseException.cs | 21 +++ .../XamlDocumentResource.cs | 40 ++++++ .../Avalonia.Markup.Xaml.csproj | 1 + .../AvaloniaXamlLoader.cs | 7 +- .../RuntimeXamlLoaderConfiguration.cs | 13 +- .../RuntimeXamlLoaderDocument.cs | 70 +++++++++ .../DesignXamlLoader.cs | 6 +- .../CompiledBindingExtensionTests.cs | 6 +- .../XamlTestBase.cs | 4 +- 16 files changed, 413 insertions(+), 89 deletions(-) create mode 100644 src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/IXamlAstGroupTransformer.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/IXamlDocumentResource.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlDocumentParseException.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlDocumentResource.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml/RuntimeXamlLoaderDocument.cs diff --git a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs index 9a901f909a..b4cfffcdca 100644 --- a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs +++ b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs @@ -38,10 +38,9 @@ namespace Avalonia.DesignerSupport var useCompiledBindings = localAsm?.GetCustomAttributes() .FirstOrDefault(a => a.Key == "AvaloniaUseCompiledBindingsByDefault")?.Value; - var loaded = loader.Load(stream, new RuntimeXamlLoaderConfiguration + var loaded = loader.Load(new RuntimeXamlLoaderDocument(baseUri, stream), new RuntimeXamlLoaderConfiguration { LocalAssembly = localAsm, - BaseUri = baseUri, DesignMode = true, UseCompiledBindingsByDefault = bool.TryParse(useCompiledBindings, out var parsedValue ) && parsedValue }); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaRuntimeXamlLoader.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaRuntimeXamlLoader.cs index 9393bb0aa4..eb05844ffb 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaRuntimeXamlLoader.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaRuntimeXamlLoader.cs @@ -1,9 +1,10 @@ using System; +using System.Collections.Generic; using System.IO; using System.Reflection; using System.Text; using Avalonia.Markup.Xaml.XamlIl; - +#nullable enable namespace Avalonia.Markup.Xaml { public static class AvaloniaRuntimeXamlLoader @@ -17,31 +18,15 @@ namespace Avalonia.Markup.Xaml /// The URI of the XAML being loaded. /// Indicates whether the XAML is being loaded in design mode. /// The loaded object. - public static object Load(string xaml, Assembly localAssembly = null, object rootInstance = null, Uri uri = null, bool designMode = false) + public static object Load(string xaml, Assembly? localAssembly = null, object? rootInstance = null, Uri? uri = null, bool designMode = false) { - Contract.Requires(xaml != null); + xaml = xaml ?? throw new ArgumentNullException(nameof(xaml)); using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(xaml))) { return Load(stream, localAssembly, rootInstance, uri, designMode); } } - - /// - /// Loads XAML from a string. - /// - /// The string containing the XAML. - /// Xaml loader configuration. - /// The loaded object. - public static object Load(string xaml, RuntimeXamlLoaderConfiguration configuration) - { - Contract.Requires(xaml != null); - - using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(xaml))) - { - return Load(stream, configuration); - } - } /// /// Loads XAML from a stream. @@ -52,9 +37,10 @@ namespace Avalonia.Markup.Xaml /// The URI of the XAML being loaded. /// Indicates whether the XAML is being loaded in design mode. /// The loaded object. - public static object Load(Stream stream, Assembly localAssembly, object rootInstance = null, Uri uri = null, + public static object Load(Stream stream, Assembly? localAssembly = null, object? rootInstance = null, Uri? uri = null, bool designMode = false) - => AvaloniaXamlIlRuntimeCompiler.Load(stream, localAssembly, rootInstance, uri, designMode, false); + => AvaloniaXamlIlRuntimeCompiler.Load(new RuntimeXamlLoaderDocument(uri, rootInstance, stream), + new RuntimeXamlLoaderConfiguration { DesignMode = designMode, LocalAssembly = localAssembly }); /// /// Loads XAML from a stream. @@ -62,9 +48,17 @@ namespace Avalonia.Markup.Xaml /// The stream containing the XAML. /// Xaml loader configuration. /// The loaded object. - public static object Load(Stream stream, RuntimeXamlLoaderConfiguration configuration) - => AvaloniaXamlIlRuntimeCompiler.Load(stream, configuration.LocalAssembly, configuration.RootInstance, - configuration.BaseUri, configuration.DesignMode, configuration.UseCompiledBindingsByDefault); + public static object Load(RuntimeXamlLoaderDocument document, RuntimeXamlLoaderConfiguration? configuration = null) + => AvaloniaXamlIlRuntimeCompiler.Load(document, configuration ?? new RuntimeXamlLoaderConfiguration()); + + /// + /// Loads group of XAML files from a stream. + /// + /// Collection of documents. + /// Xaml loader configuration. + /// The loaded objects per each input document. + public static IReadOnlyList LoadGroup(IReadOnlyCollection documents, RuntimeXamlLoaderConfiguration? configuration = null) + => AvaloniaXamlIlRuntimeCompiler.LoadGroup(documents, configuration ?? new RuntimeXamlLoaderConfiguration()); /// /// Parse XAML from a string. @@ -72,7 +66,7 @@ namespace Avalonia.Markup.Xaml /// The string containing the XAML. /// Default assembly for clr-namespace:. /// The loaded object. - public static object Parse(string xaml, Assembly localAssembly = null) + public static object Parse(string xaml, Assembly? localAssembly = null) => Load(xaml, localAssembly); /// @@ -82,7 +76,7 @@ namespace Avalonia.Markup.Xaml /// >The string containing the XAML. /// >Default assembly for clr-namespace:. /// The loaded object. - public static T Parse(string xaml, Assembly localAssembly = null) + public static T Parse(string xaml, Assembly? localAssembly = null) => (T)Parse(xaml, localAssembly); } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs index b4b258e53e..1ee4402481 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs @@ -10,6 +10,7 @@ using System.Runtime.InteropServices; using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions; using Avalonia.Markup.Xaml.XamlIl.Runtime; using Avalonia.Platform; +using XamlX.Ast; using XamlX.Transform; using XamlX.TypeSystem; using XamlX.IL; @@ -150,12 +151,12 @@ namespace Avalonia.Markup.Xaml.XamlIl } - static object LoadSre(string xaml, Assembly localAssembly, object rootInstance, Uri uri, bool isDesignMode, bool useCompiledBindingsByDefault) + static object LoadSre(RuntimeXamlLoaderDocument document, RuntimeXamlLoaderConfiguration configuration) { var success = false; try { - var rv = LoadSreCore(xaml, localAssembly, rootInstance, uri, isDesignMode, useCompiledBindingsByDefault); + var rv = LoadSreCore(document, configuration); success = true; return rv; } @@ -166,45 +167,100 @@ namespace Avalonia.Markup.Xaml.XamlIl } } + static IReadOnlyList LoadGroupSre(IReadOnlyCollection documents, + RuntimeXamlLoaderConfiguration configuration) + { + var success = false; + try + { + var rv = LoadGroupSreCore(documents, configuration); + success = true; + return rv; + } + finally + { + if( _sreCanSave) + DumpRuntimeCompilationResults(); + } + } - static object LoadSreCore(string xaml, Assembly localAssembly, object rootInstance, Uri uri, bool isDesignMode, bool useCompiledBindingsByDefault) + static IReadOnlyList LoadGroupSreCore(IReadOnlyCollection documents, RuntimeXamlLoaderConfiguration configuration) { - InitializeSre(); + var localAssembly = configuration.LocalAssembly; if (localAssembly?.GetName() != null) EmitIgnoresAccessCheckToAttribute(localAssembly.GetName()); var asm = localAssembly == null ? null : _sreTypeSystem.GetAssembly(localAssembly); - var tb = _sreBuilder.DefineType("Builder_" + Guid.NewGuid().ToString("N") + "_" + uri); - var clrPropertyBuilder = tb.DefineNestedType("ClrProperties_" + Guid.NewGuid().ToString("N")); + var clrPropertyBuilder = _sreBuilder.DefineType("ClrProperties_" + Guid.NewGuid().ToString("N")); var indexerClosureType = _sreBuilder.DefineType("IndexerClosure_" + Guid.NewGuid().ToString("N")); var trampolineBuilder = _sreBuilder.DefineType("Trampolines_" + Guid.NewGuid().ToString("N")); - + var compiler = new AvaloniaXamlIlCompiler(new AvaloniaXamlIlCompilerConfiguration(_sreTypeSystem, asm, - _sreMappings, _sreXmlns, AvaloniaXamlIlLanguage.CustomValueConverter, - new XamlIlClrPropertyInfoEmitter(_sreTypeSystem.CreateTypeBuilder(clrPropertyBuilder)), - new XamlIlPropertyInfoAccessorFactoryEmitter(_sreTypeSystem.CreateTypeBuilder(indexerClosureType)), - new XamlIlTrampolineBuilder(_sreTypeSystem.CreateTypeBuilder(trampolineBuilder))), + _sreMappings, _sreXmlns, AvaloniaXamlIlLanguage.CustomValueConverter, + new XamlIlClrPropertyInfoEmitter(_sreTypeSystem.CreateTypeBuilder(clrPropertyBuilder)), + new XamlIlPropertyInfoAccessorFactoryEmitter(_sreTypeSystem.CreateTypeBuilder(indexerClosureType)), + new XamlIlTrampolineBuilder(_sreTypeSystem.CreateTypeBuilder(trampolineBuilder))), _sreEmitMappings, - _sreContextType) { EnableIlVerification = true, DefaultCompileBindings = useCompiledBindingsByDefault }; + _sreContextType) + { + EnableIlVerification = true, + DefaultCompileBindings = configuration.UseCompiledBindingsByDefault, + IsDesignMode = configuration.DesignMode + }; - IXamlType overrideType = null; - if (rootInstance != null) + var parsedDocuments = new List(); + var rootInstances = new List(); + + foreach (var document in documents) { - overrideType = _sreTypeSystem.GetType(rootInstance.GetType()); + string xaml; + using (var sr = new StreamReader(document.XamlStream)) + xaml = sr.ReadToEnd(); + + IXamlType overrideType = null; + if (document.RootInstance != null) + { + overrideType = _sreTypeSystem.GetType(document.RootInstance.GetType()); + } + + var parsed = compiler.Parse(xaml, overrideType); + compiler.Transform(parsed); + + var xamlName = GetSafeUriIdentifier(document.BaseUri) + ?? document.RootInstance?.GetType().Name + ?? ((IXamlAstValueNode)parsed.Root).Type.GetClrType().Name; + var tb = _sreBuilder.DefineType("Builder_" + Guid.NewGuid().ToString("N") + "_" + xamlName); + var builder = _sreTypeSystem.CreateTypeBuilder(tb); + parsedDocuments.Add(new XamlDocumentResource(parsed, document.BaseUri?.ToString(), null, null, + builder, + compiler.DefinePopulateMethod(builder, parsed, AvaloniaXamlIlCompiler.PopulateName, true), + compiler.DefineBuildMethod(builder, parsed, AvaloniaXamlIlCompiler.BuildName, true))); + rootInstances.Add(document.RootInstance); } - compiler.IsDesignMode = isDesignMode; - compiler.ParseAndCompile(xaml, uri?.ToString(), null, _sreTypeSystem.CreateTypeBuilder(tb), overrideType); - var created = tb.CreateTypeInfo(); + compiler.TransformGroup(parsedDocuments); + + var createdTypes = parsedDocuments.Select(document => + { + compiler.Compile(document.XamlDocument, document.TypeBuilder, document.PopulateMethod, + document.BuildMethod, document.Uri, document.FileSource); + return _sreTypeSystem.GetType(document.TypeBuilder.CreateType()); + }).ToArray(); + clrPropertyBuilder.CreateTypeInfo(); indexerClosureType.CreateTypeInfo(); trampolineBuilder.CreateTypeInfo(); - return LoadOrPopulate(created, rootInstance); + return createdTypes.Zip(rootInstances, (l, r) => (l, r)).Select(t => LoadOrPopulate(t.Item1, t.Item2)).ToArray(); + } + + static object LoadSreCore(RuntimeXamlLoaderDocument document, RuntimeXamlLoaderConfiguration configuration) + { + return LoadGroupSreCore(new[] { document }, configuration).Single(); } #endif - static object LoadOrPopulate(Type created, object rootInstance) + static object LoadOrPopulate(Type created, object rootInstance) { var isp = Expression.Parameter(typeof(IServiceProvider)); @@ -249,19 +305,37 @@ namespace Avalonia.Markup.Xaml.XamlIl } } - public static object Load(Stream stream, Assembly localAssembly, object rootInstance, Uri uri, - bool isDesignMode, bool useCompiledBindingsByDefault) + public static object Load(RuntimeXamlLoaderDocument document, RuntimeXamlLoaderConfiguration configuration) { +#if RUNTIME_XAML_CECIL string xaml; - using (var sr = new StreamReader(stream)) + using (var sr = new StreamReader(document.XamlStream)) xaml = sr.ReadToEnd(); + return LoadCecil(xaml, configuration.LocalAssembly, document.RootInstance,document.BaseUri, configuration.UseCompiledBindingsByDefault); +#else + return LoadSre(document, configuration); +#endif + } + + public static IReadOnlyList LoadGroup(IReadOnlyCollection documents, RuntimeXamlLoaderConfiguration configuration) + { #if RUNTIME_XAML_CECIL - return LoadCecil(xaml, localAssembly, rootInstance, uri, useCompiledBindingsByDefault); + throw new NotImplementedException("Load group was not implemented for the Cecil backend"); #else - return LoadSre(xaml, localAssembly, rootInstance, uri, isDesignMode, useCompiledBindingsByDefault); + return LoadGroupSre(documents, configuration); #endif } + private static string GetSafeUriIdentifier(Uri uri) + { + return uri?.ToString() + .Replace(":", "_") + .Replace("/", "_") + .Replace("?", "_") + .Replace("=", "_") + .Replace(".", "_"); + } + #if RUNTIME_XAML_CECIL private static Dictionary populate, Func build)> @@ -303,12 +377,7 @@ namespace Avalonia.Markup.Xaml.XamlIl overrideType = _cecilTypeSystem.GetType(rootInstance.GetType().FullName); } - var safeUri = uri.ToString() - .Replace(":", "_") - .Replace("/", "_") - .Replace("?", "_") - .Replace("=", "_") - .Replace(".", "_"); + var safeUri = GetSafeUriIdentifier(uri); if (_cecilGeneratedCache.TryGetValue(safeUri, out var cached)) return LoadOrPopulate(cached, rootInstance); @@ -335,7 +404,7 @@ namespace Avalonia.Markup.Xaml.XamlIl { DefaultCompileBindings = useCompiledBindingsByDefault }; - compiler.ParseAndCompile(xaml, uri.ToString(), tb, overrideType); + compiler.ParseAndCompile(xaml, uri.ToString(), null, tb, overrideType); var asmPath = Path.Combine(_cecilEmitDir, safeUri + ".dll"); using(var f = File.Create(asmPath)) asm.Write(f); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index e601701d5c..ccda94ff1a 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Linq; - +using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.GroupTransformers; using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; using XamlX; using XamlX.Ast; @@ -83,6 +83,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions Emitters.Add(new AvaloniaNameScopeRegistrationXamlIlNodeEmitter()); Emitters.Add(new AvaloniaXamlIlRootObjectScope.Emitter()); + + GroupTransformers = new() + { + new AvaloniaXamlIncludeTransformer() + }; } public AvaloniaXamlIlCompiler(TransformerConfiguration configuration, XamlLanguageEmitMappings emitMappings, @@ -115,7 +120,27 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions set => _bindingTransformer.CompileBindingsByDefault = value; } - public void ParseAndCompile(string xaml, string baseUri, IFileSource fileSource, IXamlTypeBuilder tb, IXamlType overrideRootType) + public List GroupTransformers { get; } + + public void TransformGroup(IReadOnlyCollection documents, bool strict = true) + { + var ctx = new AstGroupTransformationContext(documents, _configuration, strict); + foreach (var transformer in GroupTransformers) + { + foreach (var doc in documents) + { + var root = doc.XamlDocument.Root; + ctx.CurrentDocument = doc; + ctx.RootObject = (IXamlAstValueNode)root; + ctx.VisitChildren(ctx.RootObject, transformer); + root = ctx.Visit(root, transformer); + + doc.XamlDocument.Root = root; + } + } + } + + public XamlDocument Parse(string xaml, IXamlType overrideRootType) { var parsed = XDocumentXamlParser.Parse(xaml, new Dictionary { @@ -148,9 +173,26 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions OverrideRootType(parsed, rootType); + return parsed; + } + + public void Compile(XamlDocument document, IXamlTypeBuilder tb, IXamlMethodBuilder populateMethod, IXamlMethodBuilder buildMethod, string baseUri, IFileSource fileSource) + { + Compile(document, _contextType, populateMethod, buildMethod, + _configuration.TypeMappings.XmlNamespaceInfoProvider == null ? + null : + tb.DefineSubType(_configuration.WellKnownTypes.Object, + "__AvaloniaXamlIlNsInfo", false), (name, bt) => tb.DefineSubType(bt, name, false), + (s, returnType, parameters) => tb.DefineDelegateSubType(s, false, returnType, parameters), baseUri, + fileSource); + } + + public void ParseAndCompile(string xaml, string baseUri, IFileSource fileSource, IXamlTypeBuilder tb, IXamlType overrideRootType) + { + var parsed = Parse(xaml, overrideRootType); + Transform(parsed); Compile(parsed, tb, _contextType, PopulateName, BuildName, "__AvaloniaXamlIlNsInfo", baseUri, fileSource); - } public void OverrideRootType(XamlDocument doc, IXamlAstTypeReference newType) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/IXamlAstGroupTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/IXamlAstGroupTransformer.cs new file mode 100644 index 0000000000..32bf37431f --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/IXamlAstGroupTransformer.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Xml; +using XamlX; +using XamlX.Ast; +using XamlX.Transform; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.GroupTransformers; + +internal class AstGroupTransformationContext : AstTransformationContext +{ + public AstGroupTransformationContext(IReadOnlyCollection documents, TransformerConfiguration configuration, bool strictMode = true) + : base(configuration, null, strictMode) + { + Documents = documents; + } + + public IXamlDocumentResource CurrentDocument { get; set; } + + public IReadOnlyCollection Documents { get; } + + public new IXamlAstNode ParseError(string message, IXamlAstNode node) => + Error(node, new XamlDocumentParseException(CurrentDocument?.FileSource?.FilePath, message, node)); + + public new IXamlAstNode ParseError(string message, IXamlAstNode offender, IXamlAstNode ret) => + Error(ret, new XamlDocumentParseException(CurrentDocument?.FileSource?.FilePath, message, offender)); + + class Visitor : IXamlAstVisitor + { + private readonly AstGroupTransformationContext _context; + private readonly IXamlAstGroupTransformer _transformer; + + public Visitor(AstGroupTransformationContext context, IXamlAstGroupTransformer transformer) + { + _context = context; + _transformer = transformer; + } + + public IXamlAstNode Visit(IXamlAstNode node) + { +#if Xaml_DEBUG + return _transformer.Transform(_context, node); +#else + try + { + return _transformer.Transform(_context, node); + } + catch (Exception e) when (!(e is XmlException)) + { + throw new XamlDocumentParseException( + _context.CurrentDocument?.FileSource?.FilePath, + "Internal compiler error while transforming node " + node + ":\n" + e, + node); + } +#endif + } + + public void Push(IXamlAstNode node) => _context.PushParent(node); + + public void Pop() => _context.PopParent(); + } + + public IXamlAstNode Visit(IXamlAstNode root, IXamlAstGroupTransformer transformer) + { + root = root.Visit(new Visitor(this, transformer)); + return root; + } + + public void VisitChildren(IXamlAstNode root, IXamlAstGroupTransformer transformer) + { + root.VisitChildren(new Visitor(this, transformer)); + } +} + +internal interface IXamlAstGroupTransformer +{ + IXamlAstNode Transform(AstGroupTransformationContext context, IXamlAstNode node); +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/IXamlDocumentResource.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/IXamlDocumentResource.cs new file mode 100644 index 0000000000..b859b3ed59 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/IXamlDocumentResource.cs @@ -0,0 +1,17 @@ +using System; +using XamlX.Ast; +using XamlX.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions; + +#nullable enable + +internal interface IXamlDocumentResource +{ + IXamlMethod? BuildMethod { get; } + IXamlType? ClassType { get; } + string? Uri { get; } + IXamlMethod PopulateMethod { get; } + IFileSource? FileSource { get; } + XamlDocument XamlDocument { get; } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index dd37ae6c93..cb1da5ef6e 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.GroupTransformers; using XamlX.Emit; using XamlX.IL; using XamlX.Transform; @@ -266,5 +267,13 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers ctx.SetItem(rv = new AvaloniaXamlIlWellKnownTypes(ctx.Configuration)); return rv; } + + public static AvaloniaXamlIlWellKnownTypes GetAvaloniaTypes(this AstGroupTransformationContext ctx) + { + if (ctx.TryGetItem(out var rv)) + return rv; + ctx.SetItem(rv = new AvaloniaXamlIlWellKnownTypes(ctx.Configuration)); + return rv; + } } } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlDocumentParseException.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlDocumentParseException.cs new file mode 100644 index 0000000000..d031a6086b --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlDocumentParseException.cs @@ -0,0 +1,21 @@ +using XamlX; +using XamlX.Ast; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions; + +internal class XamlDocumentParseException : XamlParseException +{ + public string FilePath { get; } + + public XamlDocumentParseException(string path, XamlParseException parseException) + : base(parseException.Message, parseException.LineNumber, parseException.LinePosition) + { + FilePath = path; + } + + public XamlDocumentParseException(string path, string message, IXamlLineInfo lineInfo) + : base(message, lineInfo.Line, lineInfo.Position) + { + FilePath = path; + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlDocumentResource.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlDocumentResource.cs new file mode 100644 index 0000000000..d5d452a9f3 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlDocumentResource.cs @@ -0,0 +1,40 @@ +using System; +using XamlX.Ast; +using XamlX.IL; +using XamlX.TypeSystem; +#nullable enable + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions; + +internal class XamlDocumentResource : IXamlDocumentResource +{ + public XamlDocumentResource( + XamlDocument xamlDocument, + string? uri, + IFileSource? fileSource, + IXamlType? classType, + IXamlTypeBuilder typeBuilder, + IXamlMethodBuilder populateMethod, + IXamlMethodBuilder? buildMethod) + { + XamlDocument = xamlDocument; + Uri = uri; + FileSource = fileSource; + ClassType = classType; + TypeBuilder = typeBuilder; + PopulateMethod = populateMethod; + BuildMethod = buildMethod; + } + + public XamlDocument XamlDocument { get; } + public string? Uri { get; } + public IFileSource? FileSource { get; } + + public IXamlType? ClassType { get; } + public IXamlTypeBuilder TypeBuilder { get; } + public IXamlMethodBuilder PopulateMethod { get; } + public IXamlMethodBuilder? BuildMethod { get; } + + IXamlMethod? IXamlDocumentResource.BuildMethod => BuildMethod; + IXamlMethod IXamlDocumentResource.PopulateMethod => PopulateMethod; +} diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index b5ba49ce2c..0ab00007e7 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -43,6 +43,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs index 578af64abb..0032c01d2f 100644 --- a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs +++ b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs @@ -11,7 +11,7 @@ namespace Avalonia.Markup.Xaml { public interface IRuntimeXamlLoader { - object Load(Stream stream, RuntimeXamlLoaderConfiguration configuration); + object Load(RuntimeXamlLoaderDocument document, RuntimeXamlLoaderConfiguration configuration); } /// @@ -64,10 +64,9 @@ namespace Avalonia.Markup.Xaml using (var stream = asset.stream) { var absoluteUri = uri.IsAbsoluteUri ? uri : new Uri(baseUri, uri); - return runtimeLoader.Load(stream, new RuntimeXamlLoaderConfiguration + return runtimeLoader.Load(new RuntimeXamlLoaderDocument(absoluteUri, stream), new RuntimeXamlLoaderConfiguration { - LocalAssembly = asset.assembly, - BaseUri = absoluteUri + LocalAssembly = asset.assembly }); } } diff --git a/src/Markup/Avalonia.Markup.Xaml/RuntimeXamlLoaderConfiguration.cs b/src/Markup/Avalonia.Markup.Xaml/RuntimeXamlLoaderConfiguration.cs index 9caf94fba6..b06f9a927e 100644 --- a/src/Markup/Avalonia.Markup.Xaml/RuntimeXamlLoaderConfiguration.cs +++ b/src/Markup/Avalonia.Markup.Xaml/RuntimeXamlLoaderConfiguration.cs @@ -1,4 +1,3 @@ -using System; using System.Reflection; namespace Avalonia.Markup.Xaml; @@ -7,21 +6,11 @@ namespace Avalonia.Markup.Xaml; public class RuntimeXamlLoaderConfiguration { - /// - /// The URI of the XAML being loaded. - /// - public Uri? BaseUri { get; set; } - /// /// Default assembly for clr-namespace:. /// public Assembly? LocalAssembly { get; set; } - - /// - /// The optional instance into which the XAML should be loaded. - /// - public object? RootInstance { get; set; } - + /// /// Defines is CompiledBinding should be used by default. /// Default is 'false'. diff --git a/src/Markup/Avalonia.Markup.Xaml/RuntimeXamlLoaderDocument.cs b/src/Markup/Avalonia.Markup.Xaml/RuntimeXamlLoaderDocument.cs new file mode 100644 index 0000000000..be22888156 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/RuntimeXamlLoaderDocument.cs @@ -0,0 +1,70 @@ +#nullable enable +using System; +using System.IO; +using System.Text; + +namespace Avalonia.Markup.Xaml; + +public class RuntimeXamlLoaderDocument +{ + public RuntimeXamlLoaderDocument(string xaml) + { + XamlStream = new MemoryStream(Encoding.UTF8.GetBytes(xaml)); + } + + public RuntimeXamlLoaderDocument(Uri? baseUri, string xaml) + : this(xaml) + { + BaseUri = baseUri; + } + + public RuntimeXamlLoaderDocument(object? rootInstance, string xaml) + : this(xaml) + { + RootInstance = rootInstance; + } + + public RuntimeXamlLoaderDocument(Uri? baseUri, object? rootInstance, string xaml) + : this(baseUri, xaml) + { + RootInstance = rootInstance; + } + + public RuntimeXamlLoaderDocument(Stream stream) + { + XamlStream = stream; + } + + public RuntimeXamlLoaderDocument(Uri? baseUri, Stream stream) + : this(stream) + { + BaseUri = baseUri; + } + + public RuntimeXamlLoaderDocument(object? rootInstance, Stream stream) + : this(stream) + { + RootInstance = rootInstance; + } + + public RuntimeXamlLoaderDocument(Uri? baseUri, object? rootInstance, Stream stream) + : this(baseUri, stream) + { + RootInstance = rootInstance; + } + + /// + /// The URI of the XAML being loaded. + /// + public Uri? BaseUri { get; set; } + + /// + /// The optional instance into which the XAML should be loaded. + /// + public object? RootInstance { get; set; } + + /// + /// The stream containing the XAML. + /// + public Stream XamlStream { get; set; } +} diff --git a/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs b/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs index 7009151998..181883656c 100644 --- a/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs +++ b/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs @@ -8,11 +8,9 @@ namespace Avalonia.Designer.HostApp { class DesignXamlLoader : AvaloniaXamlLoader.IRuntimeXamlLoader { - public object Load(Stream stream, RuntimeXamlLoaderConfiguration configuration) + public object Load(RuntimeXamlLoaderDocument document, RuntimeXamlLoaderConfiguration configuration) { - return AvaloniaXamlIlRuntimeCompiler.Load(stream, - configuration.LocalAssembly, configuration.RootInstance, configuration.BaseUri, - configuration.DesignMode, configuration.UseCompiledBindingsByDefault); + return AvaloniaXamlIlRuntimeCompiler.Load(document, configuration); } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 88d2cc2912..cb34a9ac4d 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -1642,10 +1642,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests' X='{Binding StringProperty, DataType=local:TestDataContext}' />"; - var control = (AssignBindingControl)AvaloniaRuntimeXamlLoader.Load(xaml, new RuntimeXamlLoaderConfiguration - { - UseCompiledBindingsByDefault = true - }); + var control = (AssignBindingControl)AvaloniaRuntimeXamlLoader.Load(new RuntimeXamlLoaderDocument(xaml), + new RuntimeXamlLoaderConfiguration { UseCompiledBindingsByDefault = true }); var compiledPath = ((CompiledBindingExtension)control.X).Path; var node = Assert.IsType(Assert.Single(compiledPath.Elements)); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/XamlTestBase.cs b/tests/Avalonia.Markup.Xaml.UnitTests/XamlTestBase.cs index 2fc4867b35..ea03b003ca 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/XamlTestBase.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/XamlTestBase.cs @@ -20,8 +20,8 @@ namespace Avalonia.Markup.Xaml.UnitTests class TestXamlLoaderShim : AvaloniaXamlLoader.IRuntimeXamlLoader { - public object Load(Stream stream, RuntimeXamlLoaderConfiguration configuration) - => AvaloniaRuntimeXamlLoader.Load(stream, configuration); + public object Load(RuntimeXamlLoaderDocument document, RuntimeXamlLoaderConfiguration configuration) + => AvaloniaRuntimeXamlLoader.Load(document, configuration); } } } From a9e904b0d2112be95e53cb27f5c6b016dd6b85c2 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 25 Nov 2022 01:01:16 -0500 Subject: [PATCH 02/22] Support group transformations in the XAML Build Task --- .../Utilities/StringBuilderCache.cs | 1 + .../Avalonia.Build.Tasks.csproj | 3 + .../BuildEngineErrorCode.cs | 2 + src/Avalonia.Build.Tasks/Extensions.cs | 12 ++- .../XamlCompilerTaskExecutor.cs | 75 +++++++++++++++---- 5 files changed, 74 insertions(+), 19 deletions(-) diff --git a/src/Avalonia.Base/Utilities/StringBuilderCache.cs b/src/Avalonia.Base/Utilities/StringBuilderCache.cs index be8b24c848..cbbcf2b398 100644 --- a/src/Avalonia.Base/Utilities/StringBuilderCache.cs +++ b/src/Avalonia.Base/Utilities/StringBuilderCache.cs @@ -9,6 +9,7 @@ using System; using System.Text; namespace Avalonia.Utilities; +#nullable enable // Provide a cached reusable instance of stringbuilder per thread. internal static class StringBuilderCache diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index 74debed828..ce84f23dd4 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -55,6 +55,9 @@ Markup/%(RecursiveDir)%(FileName)%(Extension) + + Markup\AssetLoader.cs + Markup/%(RecursiveDir)%(FileName)%(Extension) diff --git a/src/Avalonia.Build.Tasks/BuildEngineErrorCode.cs b/src/Avalonia.Build.Tasks/BuildEngineErrorCode.cs index 43365346b3..a149a758f4 100644 --- a/src/Avalonia.Build.Tasks/BuildEngineErrorCode.cs +++ b/src/Avalonia.Build.Tasks/BuildEngineErrorCode.cs @@ -5,6 +5,8 @@ namespace Avalonia.Build.Tasks InvalidXAML = 1, DuplicateXClass = 2, LegacyResmScheme = 3, + TransformError = 4, + EmitError = 4, Unknown = 9999 } diff --git a/src/Avalonia.Build.Tasks/Extensions.cs b/src/Avalonia.Build.Tasks/Extensions.cs index 46c12eaf3d..7295f56b3a 100644 --- a/src/Avalonia.Build.Tasks/Extensions.cs +++ b/src/Avalonia.Build.Tasks/Extensions.cs @@ -7,15 +7,19 @@ namespace Avalonia.Build.Tasks { static string FormatErrorCode(BuildEngineErrorCode code) => $"AVLN:{(int)code:0000}"; - public static void LogError(this IBuildEngine engine, BuildEngineErrorCode code, string file, string message) + public static void LogError(this IBuildEngine engine, BuildEngineErrorCode code, string file, string message, + int lineNumber = 0, int linePosition = 0) { - engine.LogErrorEvent(new BuildErrorEventArgs("Avalonia", FormatErrorCode(code), file ?? "", 0, 0, 0, 0, message, + engine.LogErrorEvent(new BuildErrorEventArgs("Avalonia", FormatErrorCode(code), file ?? "", + lineNumber, linePosition, lineNumber, linePosition, message, "", "Avalonia")); } - public static void LogWarning(this IBuildEngine engine, BuildEngineErrorCode code, string file, string message) + public static void LogWarning(this IBuildEngine engine, BuildEngineErrorCode code, string file, string message, + int lineNumber = 0, int linePosition = 0) { - engine.LogWarningEvent(new BuildWarningEventArgs("Avalonia", FormatErrorCode(code), file ?? "", 0, 0, 0, 0, message, + engine.LogWarningEvent(new BuildWarningEventArgs("Avalonia", FormatErrorCode(code), file ?? "", + lineNumber, linePosition, lineNumber, linePosition, message, "", "Avalonia")); } diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index 974f8485c0..589472785d 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -233,13 +233,14 @@ namespace Avalonia.Build.Tasks var builder = typeSystem.CreateTypeBuilder(typeDef); var populateMethodsToTransform = new List<(MethodDefinition populateMethod, string resourceFilePath)>(); - - foreach (var res in group.Resources.Where(CheckXamlName).OrderBy(x=>x.FilePath.ToLowerInvariant())) + + IReadOnlyCollection parsedXamlDocuments = new List(); + foreach (var res in group.Resources.Where(CheckXamlName).OrderBy(x => x.FilePath.ToLowerInvariant())) { + engine.LogMessage($"XAMLIL: {res.Name} -> {res.Uri}", logImportance); + try { - engine.LogMessage($"XAMLIL: {res.Name} -> {res.Uri}", logImportance); - // StreamReader is needed here to handle BOM var xaml = new StreamReader(new MemoryStream(res.FileContents)).ReadToEnd(); var parsed = XDocumentXamlParser.Parse(xaml); @@ -276,9 +277,9 @@ namespace Avalonia.Build.Tasks compiler.Transform(parsed); + var populateName = classType == null ? "Populate:" + res.Name : "!XamlIlPopulate"; - var buildName = classType == null ? "Build:" + res.Name : null; - + var buildName = classType == null ? "Build:" + res.Name : null; var classTypeDefinition = classType == null ? null : typeSystem.GetTypeReference(classType).Resolve(); @@ -286,11 +287,57 @@ namespace Avalonia.Build.Tasks var populateBuilder = classTypeDefinition == null ? builder : typeSystem.CreateTypeBuilder(classTypeDefinition); - compiler.Compile(parsed, - contextClass, + + ((List)parsedXamlDocuments).Add(new XamlDocumentResource( + parsed, res.Uri, res, classType, + populateBuilder, compiler.DefinePopulateMethod(populateBuilder, parsed, populateName, classTypeDefinition == null), - buildName == null ? null : compiler.DefineBuildMethod(builder, parsed, buildName, true), + buildName == null ? null : compiler.DefineBuildMethod(builder, parsed, buildName, true))); + } + catch (Exception e) + { + int lineNumber = 0, linePosition = 0; + if (e is XamlParseException xe) + { + lineNumber = xe.LineNumber; + linePosition = xe.LinePosition; + } + + engine.LogError(BuildEngineErrorCode.TransformError, res.FilePath, e.Message, lineNumber, linePosition); + return false; + } + } + + try + { + compiler.TransformGroup(parsedXamlDocuments); + } + catch (XamlDocumentParseException e) + { + engine.LogError(BuildEngineErrorCode.TransformError, e.FilePath, e.Message, e.LineNumber, e.LinePosition); + } + catch (XamlParseException e) + { + engine.LogError(BuildEngineErrorCode.TransformError, "", e.Message, e.LineNumber, e.LinePosition); + } + + foreach (var document in parsedXamlDocuments) + { + var parsed = document.XamlDocument; + var res = (IResource)document.FileSource; + var classType = document.ClassType; + var populateBuilder = document.TypeBuilder; + + try + { + var classTypeDefinition = + classType == null ? null : typeSystem.GetTypeReference(classType).Resolve(); + + compiler.Compile(parsed, + contextClass, + document.PopulateMethod, + document.BuildMethod, builder.DefineSubType(compilerConfig.WellKnownTypes.Object, "NamespaceInfo:" + res.Name, true), (closureName, closureBaseType) => @@ -417,12 +464,12 @@ namespace Avalonia.Build.Tasks } - if (buildName != null || classTypeDefinition != null) + if (document.BuildMethod != null || classTypeDefinition != null) { - var compiledBuildMethod = buildName == null ? + var compiledBuildMethod = document.BuildMethod == null ? null : typeSystem.GetTypeReference(builder).Resolve() - .Methods.First(m => m.Name == buildName); + .Methods.First(m => m.Name == document.BuildMethod.Name); var parameterlessConstructor = compiledBuildMethod != null ? null : classTypeDefinition.GetConstructors().FirstOrDefault(c => @@ -459,9 +506,7 @@ namespace Avalonia.Build.Tasks lineNumber = xe.LineNumber; linePosition = xe.LinePosition; } - engine.LogErrorEvent(new BuildErrorEventArgs("Avalonia", "XAMLIL", res.FilePath, - lineNumber, linePosition, lineNumber, linePosition, - e.Message, "", "Avalonia")); + engine.LogError(BuildEngineErrorCode.EmitError, res.FilePath, e.Message, lineNumber, linePosition); return false; } res.Remove(); From 47b91121386735ae54e09a990583bf21fc53fbd9 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 25 Nov 2022 01:01:48 -0500 Subject: [PATCH 03/22] Make AvaloniaXamlIncludeTransformer a group transformer and remove old code with manual IL emit --- src/Avalonia.Base/Platform/AssetLoader.cs | 11 +- .../XamlCompilerTaskExecutor.cs | 139 +----------------- .../AvaloniaXamlIlCompiler.cs | 3 +- .../XamlIncludeGroupTransformer.cs | 133 +++++++++++++++++ .../AvaloniaXamlIlAssetIncludeTransformer.cs | 93 ------------ ...IlConstructorServiceProviderTransformer.cs | 24 ++- .../XamlIl/Runtime/XamlIlRuntimeHelpers.cs | 10 -- 7 files changed, 165 insertions(+), 248 deletions(-) create mode 100644 src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs delete mode 100644 src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlAssetIncludeTransformer.cs diff --git a/src/Avalonia.Base/Platform/AssetLoader.cs b/src/Avalonia.Base/Platform/AssetLoader.cs index a74da2a178..8d3576d180 100644 --- a/src/Avalonia.Base/Platform/AssetLoader.cs +++ b/src/Avalonia.Base/Platform/AssetLoader.cs @@ -3,16 +3,22 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; +#if !BUILDTASK using Avalonia.Platform.Internal; using Avalonia.Utilities; +#endif namespace Avalonia.Platform { /// /// Loads assets compiled into the application binary. /// - public class AssetLoader : IAssetLoader + public class AssetLoader +#if !BUILDTASK + : IAssetLoader +#endif { +#if !BUILDTASK private static IAssemblyDescriptorResolver s_assemblyDescriptorResolver = new AssemblyDescriptorResolver(); private AssemblyDescriptor? _defaultResmAssembly; @@ -206,7 +212,8 @@ namespace Avalonia.Platform return null; } - +#endif + public static void RegisterResUriParsers() { if (!UriParser.IsKnownScheme("avares")) diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index 589472785d..31649fd4b6 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -231,8 +231,6 @@ namespace Avalonia.Build.Tasks }); asm.MainModule.Types.Add(typeDef); var builder = typeSystem.CreateTypeBuilder(typeDef); - - var populateMethodsToTransform = new List<(MethodDefinition populateMethod, string resourceFilePath)>(); IReadOnlyCollection parsedXamlDocuments = new List(); foreach (var res in group.Resources.Where(CheckXamlName).OrderBy(x => x.FilePath.ToLowerInvariant())) @@ -347,23 +345,11 @@ namespace Avalonia.Build.Tasks res.Uri, res ); - var compiledPopulateMethod = typeSystem.GetTypeReference(populateBuilder).Resolve() - .Methods.First(m => m.Name == populateName); - - // Include populate method and all nested methods/closures used in the populate method, - // So we can replace style/resource includes in all of them. - populateMethodsToTransform.Add((compiledPopulateMethod, res.FilePath)); - populateMethodsToTransform.AddRange(compiledPopulateMethod.Body.Instructions - .Where(b => b.OpCode == OpCodes.Call || b.OpCode == OpCodes.Callvirt || b.OpCode == OpCodes.Ldftn) - .Select(b => b.Operand) - .OfType() - .Where(m => compiledPopulateMethod.DeclaringType.NestedTypes.Contains(m.DeclaringType)) - .Select(m => m.Resolve()) - .Where(m => m.HasBody) - .Select(m => (m, res.FilePath))); - if (classTypeDefinition != null) { + var compiledPopulateMethod = typeSystem.GetTypeReference(populateBuilder).Resolve() + .Methods.First(m => m.Name == document.PopulateMethod.Name); + var designLoaderFieldType = typeSystem .GetType("System.Action`1") .MakeGenericType(typeSystem.GetType("System.Object")); @@ -512,14 +498,6 @@ namespace Avalonia.Build.Tasks res.Remove(); } - foreach (var (populateMethod, resourceFilePath) in populateMethodsToTransform) - { - if (!TransformXamlIncludes(engine, typeSystem, populateMethod, resourceFilePath, createRootServiceProviderMethod)) - { - return false; - } - } - // Technically that's a hack, but it fixes corert incompatibility caused by deterministic builds int dupeCounter = 1; foreach (var grp in typeDef.NestedTypes.GroupBy(x => x.Name)) @@ -596,116 +574,5 @@ namespace Avalonia.Build.Tasks return true; } - - private static bool TransformXamlIncludes( - IBuildEngine engine, CecilTypeSystem typeSystem, - MethodDefinition populateMethod, string resourceFilePath, - MethodReference createRootServiceProviderMethod) - { - var asm = typeSystem.TargetAssemblyDefinition; - foreach (var instruction in populateMethod.Body.Instructions.ToArray()) - { - const string resolveStyleIncludeName = "ResolveStyleInclude"; - const string resolveResourceInclude = "ResolveResourceInclude"; - if (instruction.OpCode == OpCodes.Call - && instruction.Operand is MethodReference - { - Name: resolveStyleIncludeName or resolveResourceInclude, - DeclaringType: { Name: "XamlIlRuntimeHelpers" } - }) - { - int lineNumber = 0, linePosition = 0; - bool instructionsModified = false; - try - { - var assetSource = (string)instruction.Previous.Previous.Previous.Operand; - lineNumber = GetConstValue(instruction.Previous.Previous); - linePosition = GetConstValue(instruction.Previous); - - var index = populateMethod.Body.Instructions.IndexOf(instruction); - - assetSource = assetSource.Replace("avares://", ""); - - var assemblyNameSeparator = assetSource.IndexOf('/'); - var fileNameSeparator = assetSource.LastIndexOf('/'); - if (assemblyNameSeparator < 0 || fileNameSeparator < 0) - { - throw new InvalidProgramException( - $"Invalid asset source \"{assetSource}\". It must contain assembly name and relative path."); - } - - var assetAssemblyName = assetSource.Substring(0, assemblyNameSeparator); - var assetAssembly = typeSystem.FindAssembly(assetAssemblyName) - ?? throw new InvalidProgramException( - $"Unable to resolve assembly \"{assetAssemblyName}\""); - - var fileName = Path.GetFileNameWithoutExtension(assetSource.Replace('/', '.')); - if (assetAssembly.FindType(fileName) is { } type - && type.FindConstructor() is { } ctor) - { - var ctorMethod = - asm.MainModule.ImportReference(typeSystem.GetMethodReference(ctor)); - instructionsModified = true; - populateMethod.Body.Instructions[index - 3] = Instruction.Create(OpCodes.Nop); - populateMethod.Body.Instructions[index - 2] = Instruction.Create(OpCodes.Nop); - populateMethod.Body.Instructions[index - 1] = Instruction.Create(OpCodes.Nop); - populateMethod.Body.Instructions[index] = Instruction.Create(OpCodes.Newobj, ctorMethod); - } - else - { - var resources = assetAssembly.FindType("CompiledAvaloniaXaml.!AvaloniaResources") - ?? throw new InvalidOperationException( - $"Unable to resolve \"!AvaloniaResources\" type on \"{assetAssemblyName}\" assembly"); - - var relativeName = "Build:" + assetSource.Substring(assemblyNameSeparator); - var buildMethod = resources.FindMethod(m => m.Name == relativeName) - ?? throw new InvalidOperationException( - $"Unable to resolve build method \"{relativeName}\" resource on the \"{assetAssemblyName}\" assembly"); - - var methodReference = asm.MainModule.ImportReference(typeSystem.GetMethodReference(buildMethod)); - instructionsModified = true; - populateMethod.Body.Instructions[index - 3] = Instruction.Create(OpCodes.Nop); - populateMethod.Body.Instructions[index - 2] = Instruction.Create(OpCodes.Nop); - populateMethod.Body.Instructions[index - 1] = - Instruction.Create(OpCodes.Call, createRootServiceProviderMethod); - populateMethod.Body.Instructions[index] = Instruction.Create(OpCodes.Call, methodReference); - } - } - catch (Exception e) - { - if (instructionsModified) - { - engine.LogErrorEvent(new BuildErrorEventArgs("Avalonia", "XAMLIL", resourceFilePath, - lineNumber, linePosition, lineNumber, linePosition, - e.Message, "", "Avalonia")); - return false; - } - else - { - engine.LogWarningEvent(new BuildWarningEventArgs("Avalonia", "XAMLIL", - resourceFilePath, - lineNumber, linePosition, lineNumber, linePosition, - e.Message, "", "Avalonia")); - } - } - - static int GetConstValue(Instruction instruction) - { - if (instruction.OpCode is { Code : >= Code.Ldc_I4_0 and <= Code.Ldc_I4_8 }) - { - return instruction.OpCode.Code - Code.Ldc_I4_0; - } - if (instruction.Operand is not null) - { - return Convert.ToInt32(instruction.Operand); - } - - return 0; - } - } - } - - return true; - } } } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index ccda94ff1a..b8350c3f11 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -56,8 +56,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions new AvaloniaXamlIlSetterTransformer(), new AvaloniaXamlIlConstructorServiceProviderTransformer(), new AvaloniaXamlIlTransitionsTypeMetadataTransformer(), - new AvaloniaXamlIlResolveByNameMarkupExtensionReplacer(), - new AvaloniaXamlIlAssetIncludeTransformer() + new AvaloniaXamlIlResolveByNameMarkupExtensionReplacer() ); InsertBefore( new AvaloniaXamlIlOptionMarkupExtensionTransformer()); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs new file mode 100644 index 0000000000..2d30cd18b8 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; +using Avalonia.Platform; +using XamlX; +using XamlX.Ast; +using XamlX.Emit; +using XamlX.IL; +using XamlX.Transform; +using XamlX.Transform.Transformers; +using XamlX.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.GroupTransformers; + +internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer +{ + public IXamlAstNode Transform(AstGroupTransformationContext context, IXamlAstNode node) + { + if (node is not XamlValueWithManipulationNode valueNode + || valueNode.Value is not XamlAstNewClrObjectNode objectNode + || (objectNode.Type.GetClrType() != context.GetAvaloniaTypes().StyleInclude + && objectNode.Type.GetClrType() != context.GetAvaloniaTypes().ResourceInclude)) + { + return node; + } + + var nodeTypeName = objectNode.Type.GetClrType().Name; + if (valueNode.Manipulation is not XamlObjectInitializationNode + { + Manipulation: XamlPropertyAssignmentNode { Property: { Name: "Source" } } sourceProperty + }) + { + return context.ParseError($"Source property must be set on the \"{nodeTypeName}\" node.", node); + } + + if (sourceProperty.Values.OfType().FirstOrDefault() is not { } sourceUriNode + || sourceUriNode.Type.GetClrType() != context.GetAvaloniaTypes().Uri + || sourceUriNode.Arguments.FirstOrDefault() is not XamlConstantNode { Constant: string originalAssetPath }) + { + // TODO: make it a compiler warning + // Source value can be set with markup extension instead of the Uri object node, we don't support it here yet. + return node; + } + + if (originalAssetPath.StartsWith("avares://")) + { + } + else if (originalAssetPath.StartsWith("/")) + { + var baseUrl = context.CurrentDocument.Uri ?? throw new InvalidOperationException("CurrentDocument URI is null."); + originalAssetPath = baseUrl.Substring(0, baseUrl.LastIndexOf('/')) + originalAssetPath; + } + else + { + return context.ParseError( + $"Avalonia supports only \"avares://\" sources or relative sources starting with \"/\" on the \"{nodeTypeName}\" node.", + node); + } + + AssetLoader.RegisterResUriParsers(); + + originalAssetPath = Uri.UnescapeDataString(new Uri(originalAssetPath).AbsoluteUri); + var assetPath = originalAssetPath.Replace("avares://", ""); + var assemblyNameSeparator = assetPath.IndexOf('/'); + var assembly = assetPath.Substring(0, assemblyNameSeparator); + var fullTypeName = Path.GetFileNameWithoutExtension(assetPath.Replace('/', '.')); + + if (context.Documents.FirstOrDefault(d => string.Equals(d.Uri, originalAssetPath, StringComparison.InvariantCultureIgnoreCase)) is {} targetDocument) + { + if (targetDocument.ClassType is not null) + { + return FromType(context, targetDocument.ClassType, node); + } + + if (targetDocument.BuildMethod is null) + { + return context.ParseError($"\"{originalAssetPath}\" cannot be instantiated.", node); + } + + return FromMethod(context, targetDocument.BuildMethod, node); + } + + + if (context.Configuration.TypeSystem.FindAssembly(assembly) is not { } assetAssembly) + { + return context.ParseError($"Assembly \"{assembly}\" was not found from the \"{originalAssetPath}\" source.", node); + } + + if (assetAssembly.FindType(fullTypeName) is { } type + && type.FindMethod(m => m.Name == "!XamlIlPopulate") is not null) + { + return FromType(context, type, node); + } + else + { + var avaResType = assetAssembly.FindType("CompiledAvaloniaXaml.!AvaloniaResources"); + if (avaResType is null) + { + return context.ParseError( + $"Unable to resolve \"!AvaloniaResources\" type on \"{assembly}\" assembly.", node); + } + + var relativeName = "Build:" + assetPath.Substring(assemblyNameSeparator); + var buildMethod = avaResType.FindMethod(m => m.Name == relativeName); + if (buildMethod is null) + { + return context.ParseError( + $"Unable to resolve build method \"{relativeName}\" resource on the \"{assembly}\" assembly.", + node); + } + + return FromMethod(context, buildMethod, node); + } + } + + private static IXamlAstNode FromType(AstTransformationContext context, IXamlType type, IXamlLineInfo li) + { + IXamlAstNode newObjNode = new XamlAstObjectNode(li, new XamlAstClrTypeReference(li, type, false)); + newObjNode = new AvaloniaXamlIlConstructorServiceProviderTransformer().Transform(context, newObjNode); + newObjNode = new ConstructableObjectTransformer().Transform(context, newObjNode); + return new NewObjectTransformer().Transform(context, newObjNode); + } + + private static IXamlAstNode FromMethod(AstTransformationContext context, IXamlMethod method, IXamlLineInfo li) + { + var sp = context.Configuration.TypeMappings.ServiceProvider; + return new XamlStaticOrTargetedReturnMethodCallNode(li, method, + new[] { new AvaloniaXamlIlConstructorServiceProviderTransformer.InjectServiceProviderNode(sp, li, false) }); + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlAssetIncludeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlAssetIncludeTransformer.cs deleted file mode 100644 index 377a9e72d9..0000000000 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlAssetIncludeTransformer.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System.Linq; -using XamlX; -using XamlX.Ast; -using XamlX.Emit; -using XamlX.IL; -using XamlX.Transform; -using XamlX.TypeSystem; - -namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; - -internal class AvaloniaXamlIlAssetIncludeTransformer : IXamlAstTransformer -{ - public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) - { - if (node is not XamlAstObjectNode objectNode - || (objectNode.Type.GetClrType() != context.GetAvaloniaTypes().StyleInclude - && objectNode.Type.GetClrType() != context.GetAvaloniaTypes().ResourceInclude)) - { - return node; - } - - var nodeTypeName = objectNode.Type.GetClrType().Name; - - var sourceProperty = objectNode.Children.OfType().FirstOrDefault(n => n.Property.GetClrProperty().Name == "Source"); - var directives = objectNode.Children.OfType().ToList(); - if (sourceProperty is null - || objectNode.Children.Count != (directives.Count + 1)) - { - throw new XamlParseException($"Unexpected property on the {nodeTypeName} node", node); - } - - if (sourceProperty.Values.OfType().FirstOrDefault() is not { } sourceTextNode) - { - // TODO: make it a compiler warning - // Source value can be set with markup extension instead of a text node, we don't support it here yet. - return node; - } - - var originalAssetPath = sourceTextNode.Text; - if (!(originalAssetPath.StartsWith("avares://") || originalAssetPath.StartsWith("/"))) - { - return node; - } - - var runtimeHelpers = context.GetAvaloniaTypes().RuntimeHelpers; - var markerMethodName = "Resolve" + nodeTypeName; - var markerMethod = runtimeHelpers.FindMethod(m => m.Name == markerMethodName && m.Parameters.Count == 3); - if (markerMethod is null) - { - throw new XamlParseException($"Marker method \"{markerMethodName}\" was not found for the \"{nodeTypeName}\" node", node); - } - - return new XamlValueWithManipulationNode( - node, - new AssetIncludeMethodNode(node, markerMethod, originalAssetPath), - new XamlManipulationGroupNode(node, directives)); - } - - private class AssetIncludeMethodNode : XamlAstNode, IXamlAstValueNode, IXamlAstILEmitableNode - { - private readonly IXamlMethod _method; - private readonly string _originalAssetPath; - - public AssetIncludeMethodNode( - IXamlAstNode original, IXamlMethod method, string originalAssetPath) - : base(original) - { - _method = method; - _originalAssetPath = originalAssetPath; - } - - public IXamlAstTypeReference Type => new XamlAstClrTypeReference(this, _method.ReturnType, false); - - public XamlILNodeEmitResult Emit(XamlEmitContext context, IXamlILEmitter codeGen) - { - var absoluteSource = _originalAssetPath; - if (absoluteSource.StartsWith("/")) - { - // Avoid Uri class here to avoid potential problems with escaping. - // Keeping string as close to the original as possible. - var absoluteBaseUrl = context.RuntimeContext.BaseUrl; - absoluteSource = absoluteBaseUrl.Substring(0, absoluteBaseUrl.LastIndexOf('/')) + absoluteSource; - } - - codeGen.Ldstr(absoluteSource); - codeGen.Ldc_I4(Line); - codeGen.Ldc_I4(Position); - codeGen.EmitCall(_method); - - return XamlILNodeEmitResult.Type(0, _method.ReturnType); - } - } -} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlConstructorServiceProviderTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlConstructorServiceProviderTransformer.cs index 35e2624ff9..de654c4595 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlConstructorServiceProviderTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlConstructorServiceProviderTransformer.cs @@ -21,7 +21,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers c.IsPublic && !c.IsStatic && c.Parameters.Count == 1 && c.Parameters[0] .Equals(sp))) { - on.Arguments.Add(new InjectServiceProviderNode(sp, on)); + on.Arguments.Add(new InjectServiceProviderNode(sp, on, true)); } } } @@ -29,19 +29,33 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers return node; } - class InjectServiceProviderNode : XamlAstNode, IXamlAstValueNode,IXamlAstNodeNeedsParentStack, + internal class InjectServiceProviderNode : XamlAstNode, IXamlAstValueNode,IXamlAstNodeNeedsParentStack, IXamlAstEmitableNode { - public InjectServiceProviderNode(IXamlType type, IXamlLineInfo lineInfo) : base(lineInfo) + private readonly bool _inheritContext; + + public InjectServiceProviderNode(IXamlType type, IXamlLineInfo lineInfo, bool inheritContext) : base(lineInfo) { + _inheritContext = inheritContext; Type = new XamlAstClrTypeReference(lineInfo, type, false); } public IXamlAstTypeReference Type { get; } - public bool NeedsParentStack => true; + public bool NeedsParentStack => _inheritContext; public XamlILNodeEmitResult Emit(XamlEmitContext context, IXamlILEmitter codeGen) { - codeGen.Ldloc(context.ContextLocal); + if (_inheritContext) + { + codeGen.Ldloc(context.ContextLocal); + } + else + { + var method = context.GetAvaloniaTypes().RuntimeHelpers + .FindMethod(m => m.Name == "CreateRootServiceProviderV2"); + codeGen.EmitCall(method); + context.RuntimeContext.Factory(codeGen); + } + return XamlILNodeEmitResult.Type(0, Type.GetClrType()); } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs index f547888d6e..d07c354945 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs @@ -17,16 +17,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime { public static class XamlIlRuntimeHelpers { - public static IStyle ResolveStyleInclude(string absoluteSource, int line, int position) - { - return new StyleInclude((Uri)null) { Source = new Uri(absoluteSource) }.Loaded; - } - - public static IResourceDictionary ResolveResourceInclude(string absoluteSource, int line, int position) - { - return new ResourceInclude((Uri)null) { Source = new Uri(absoluteSource) }.Loaded; - } - public static Func DeferredTransformationFactoryV1(Func builder, IServiceProvider provider) { From c8120fbbe792ea3a42a7f026e59e185099a5e98c Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 25 Nov 2022 01:02:10 -0500 Subject: [PATCH 04/22] Update tests with "LoadGroup" method instead of assetloader hacks --- .../DynamicResourceExtensionTests.cs | 108 +++++----- .../MarkupExtensions/ResourceIncludeTests.cs | 44 +++-- .../StaticResourceExtensionTests.cs | 44 +++-- .../StyleIncludeTests.cs | 51 ----- .../Xaml/BasicTests.cs | 22 +-- .../Xaml/ResourceDictionaryTests.cs | 22 ++- .../Xaml/Style1.xaml | 3 +- .../Xaml/Style2.xaml | 3 +- .../Xaml/StyleIncludeTests.cs | 184 ++++++++++++++++++ .../Xaml/StyleTests.cs | 39 ---- 10 files changed, 306 insertions(+), 214 deletions(-) delete mode 100644 tests/Avalonia.Markup.Xaml.UnitTests/StyleIncludeTests.cs create mode 100644 tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs index 987725c314..f2e1a99006 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs @@ -250,30 +250,32 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions [Fact] public void DynamicResource_Can_Be_Assigned_To_Setter_In_Styles_File() { - var styleXaml = @" + var documents = new[] + { + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Style.xaml"), @" #ff506070 - -"; - - using (StyledWindow(assets: ("test:style.xaml", styleXaml))) - { - var xaml = @" +"), + new RuntimeXamlLoaderDocument(@" - + -"; - - var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); +") + }; + + using (StyledWindow()) + { + var compiled = AvaloniaRuntimeXamlLoader.LoadGroup(documents); + var window = Assert.IsType(compiled[1]); var border = window.FindControl("border"); var brush = (ISolidColorBrush)border.Background; @@ -284,13 +286,14 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions [Fact] public void DynamicResource_Can_Be_Assigned_To_Property_In_ControlTemplate_In_Styles_File() { - var styleXaml = @" + var documents = new[] + { + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Style.xaml"), @" #ff506070 - -"; - - using (StyledWindow(assets: ("test:style.xaml", styleXaml))) - { - var xaml = @" +"), + new RuntimeXamlLoaderDocument(@" - +