diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs index 64fdfe155d..925bf0a4fa 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs @@ -274,7 +274,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions { var uriText = text.Trim(); - var kind = ((!uriText?.StartsWith("/") == true) ? UriKind.Absolute : UriKind.Relative); + var kind = ((!uriText?.StartsWith("/") == true) ? UriKind.RelativeOrAbsolute : UriKind.Relative); if (string.IsNullOrWhiteSpace(uriText) || !Uri.TryCreate(uriText, kind, out var _)) { diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs index 09a65c9d64..cc29d5ccb5 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs @@ -28,6 +28,13 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer } var nodeTypeName = objectNode.Type.GetClrType().Name; + var expectedLoadedType = objectNode.Type.GetClrType().GetAllProperties() + .FirstOrDefault(p => p.Name == "Loaded")?.PropertyType; + if (expectedLoadedType is null) + { + throw new InvalidOperationException($"\"{nodeTypeName}\".Loaded property is expected to be defined"); + } + if (valueNode.Manipulation is not XamlObjectInitializationNode { Manipulation: XamlPropertyAssignmentNode { Property: { Name: "Source" } } sourceProperty @@ -36,91 +43,109 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer return context.ParseError($"Source property must be set on the \"{nodeTypeName}\" node.", node); } + // We expect that AvaloniaXamlIlLanguageParseIntrinsics has already parsed the Uri and created node like: `new Uri(assetPath, uriKind)`. if (sourceProperty.Values.OfType().FirstOrDefault() is not { } sourceUriNode || sourceUriNode.Type.GetClrType() != context.GetAvaloniaTypes().Uri - || sourceUriNode.Arguments.FirstOrDefault() is not XamlConstantNode { Constant: string originalAssetPath }) + || sourceUriNode.Arguments.FirstOrDefault() is not XamlConstantNode { Constant: string originalAssetPath } + || sourceUriNode.Arguments.Skip(1).FirstOrDefault() is not XamlConstantNode { Constant: int uriKind }) { // 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("/")) + var uriPath = new Uri(originalAssetPath, (UriKind)uriKind); + if (!uriPath.IsAbsoluteUri) { var baseUrl = context.CurrentDocument.Uri ?? throw new InvalidOperationException("CurrentDocument URI is null."); - originalAssetPath = baseUrl.Substring(0, baseUrl.LastIndexOf('/')) + originalAssetPath; + uriPath = new Uri(new Uri(baseUrl, UriKind.Absolute), uriPath); } - else if (!originalAssetPath.StartsWith("avares://")) + else if (!uriPath.Scheme.Equals("avares", StringComparison.CurrentCultureIgnoreCase)) { return context.ParseError( - $"Avalonia supports only \"avares://\" sources or relative sources starting with \"/\" on the \"{nodeTypeName}\" node.", - node); + $"\"{nodeTypeName}.Source\" supports only \"avares://\" absolute or relative uri.", + sourceUriNode, node); } - originalAssetPath = Uri.UnescapeDataString(new Uri(originalAssetPath).AbsoluteUri); - var assetPath = originalAssetPath.Replace("avares://", ""); + var assetPathUri = Uri.UnescapeDataString(uriPath.AbsoluteUri); + var assetPath = assetPathUri.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) + // Search file in the current assembly among other XAML resources. + if (context.Documents.FirstOrDefault(d => string.Equals(d.Uri, assetPathUri, StringComparison.InvariantCultureIgnoreCase)) is {} targetDocument) { - if (targetDocument.ClassType is not null) + if (targetDocument.BuildMethod is not null) { - return FromType(context, targetDocument.ClassType, node); + return FromMethod(context, targetDocument.BuildMethod, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly); } - if (targetDocument.BuildMethod is null) + if (targetDocument.ClassType is not null) { - return context.ParseError($"\"{originalAssetPath}\" cannot be instantiated.", node); + return FromType(context, targetDocument.ClassType, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly); } - return FromMethod(context, targetDocument.BuildMethod, node); + return context.ParseError( + $"Unable to resolve XAML resource \"{assetPathUri}\" in the current assembly.", + sourceUriNode, node); } - + // If resource wasn't found in the current assembly, search in the others. if (context.Configuration.TypeSystem.FindAssembly(assembly) is not { } assetAssembly) { - return context.ParseError($"Assembly \"{assembly}\" was not found from the \"{originalAssetPath}\" source.", node); + return context.ParseError($"Assembly \"{assembly}\" was not found from the \"{assetPathUri}\" source.", sourceUriNode, node); } - if (assetAssembly.FindType(fullTypeName) is { } type - && type.FindMethod(m => m.Name == "!XamlIlPopulate") is not null) + var avaResType = assetAssembly.FindType("CompiledAvaloniaXaml.!AvaloniaResources"); + if (avaResType is null) { - return FromType(context, type, node); + return context.ParseError( + $"Unable to resolve \"!AvaloniaResources\" type on \"{assembly}\" assembly.", sourceUriNode, 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); + var relativeName = "Build:" + assetPath.Substring(assemblyNameSeparator); + var buildMethod = avaResType.FindMethod(m => m.Name == relativeName); + if (buildMethod is not null) + { + return FromMethod(context, buildMethod, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly); + } + else if (assetAssembly.FindType(fullTypeName) is { } type) + { + return FromType(context, type, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly); } - } - private static IXamlAstNode FromType(AstTransformationContext context, IXamlType type, IXamlLineInfo li) + return context.ParseError( + $"Unable to resolve XAML resource \"{assetPathUri}\" in the \"{assembly}\" assembly.", + sourceUriNode, node); + } + + private static IXamlAstNode FromType(AstTransformationContext context, IXamlType type, IXamlAstNode li, + IXamlType expectedLoadedType, IXamlAstNode fallbackNode, string assetPathUri, string assembly) { + if (!expectedLoadedType.IsAssignableFrom(type)) + { + return context.ParseError( + $"Resource \"{assetPathUri}\" is defined as \"{type}\" type in the \"{assembly}\" assembly, but expected \"{expectedLoadedType}\".", + li, fallbackNode); + } + 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) + private static IXamlAstNode FromMethod(AstTransformationContext context, IXamlMethod method, IXamlAstNode li, + IXamlType expectedLoadedType, IXamlAstNode fallbackNode, string assetPathUri, string assembly) { + if (!expectedLoadedType.IsAssignableFrom(method.ReturnType)) + { + return context.ParseError( + $"Resource \"{assetPathUri}\" is defined as \"{method.ReturnType}\" type in the \"{assembly}\" assembly, but expected \"{expectedLoadedType}\".", + li, fallbackNode); + } + var sp = context.Configuration.TypeMappings.ServiceProvider; return new XamlStaticOrTargetedReturnMethodCallNode(li, method, new[] { new NewServiceProviderNode(sp, li) }); diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaUriTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaUriTypeConverter.cs index 3c3a718213..54c57d8b7b 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaUriTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaUriTypeConverter.cs @@ -17,7 +17,7 @@ namespace Avalonia.Markup.Xaml.Converters if (s == null) return null; //On Unix Uri tries to interpret paths starting with "/" as file Uris - var kind = s.StartsWith("/") ? UriKind.Relative : UriKind.Absolute; + var kind = s.StartsWith("/") ? UriKind.Relative : UriKind.RelativeOrAbsolute; if (!Uri.TryCreate(s, kind, out var res)) throw new ArgumentException("Unable to parse URI: " + s); return res; diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs index 01db2c081f..d7eb328715 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs @@ -42,7 +42,8 @@ namespace Avalonia.Markup.Xaml.Styling if (_loaded == null) { _isLoading = true; - _loaded = (IResourceDictionary)AvaloniaXamlLoader.Load(Source, _baseUri); + var source = Source ?? throw new InvalidOperationException("ResourceInclude.Source must be set."); + _loaded = (IResourceDictionary)AvaloniaXamlLoader.Load(source, _baseUri); _isLoading = false; } diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index 8af49b5480..2b75e0dd54 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -51,7 +51,8 @@ namespace Avalonia.Markup.Xaml.Styling if (_loaded == null) { _isLoading = true; - var loaded = (IStyle)AvaloniaXamlLoader.Load(Source, _baseUri); + var source = Source ?? throw new InvalidOperationException("StyleInclude.Source must be set."); + var loaded = (IStyle)AvaloniaXamlLoader.Load(source, _baseUri); _loaded = new[] { loaded }; _isLoading = false; } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs index 4ff4405109..8eed5013a2 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs @@ -79,7 +79,35 @@ public class StyleIncludeTests } [Fact] - public void Relative_StyleInclude_Is_Resolved_With_Two_Files() + public void Relative_Back_StyleInclude_Is_Resolved_With_Two_Files() + { + var documents = new[] + { + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Subfolder/Style.xaml"), @" +"), + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Subfolder/Folder/Root.xaml"), @" + + + + +") + }; + + var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents); + var style = Assert.IsType"), + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Folder/Root.xaml"), @" + + + + +") + }; + + var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents); + var style = Assert.IsType"), + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Folder/Root.xaml"), @" + + + ") };