diff --git a/packages/Avalonia/AvaloniaBuildTasks.targets b/packages/Avalonia/AvaloniaBuildTasks.targets index 8fe77a095d..d3750935f9 100644 --- a/packages/Avalonia/AvaloniaBuildTasks.targets +++ b/packages/Avalonia/AvaloniaBuildTasks.targets @@ -134,6 +134,8 @@ false false false + true + false @@ -162,6 +164,7 @@ DelaySign="$(DelaySign)" SkipXamlCompilation="$(_AvaloniaSkipXamlCompilation)" DebuggerLaunch="$(AvaloniaXamlIlDebuggerLaunch)" + CreateSourceInfo="$(AvaloniaXamlCreateSourceInfo)" DefaultCompileBindings="$(AvaloniaUseCompiledBindingsByDefault)" VerboseExceptions="$(AvaloniaXamlVerboseExceptions)" AnalyzerConfigFiles="@(EditorConfigFiles)"/> diff --git a/packages/Avalonia/AvaloniaRules.Project.xml b/packages/Avalonia/AvaloniaRules.Project.xml index b69ea6de17..0a5c1b8243 100644 --- a/packages/Avalonia/AvaloniaRules.Project.xml +++ b/packages/Avalonia/AvaloniaRules.Project.xml @@ -31,6 +31,11 @@ Description="Allow debug XAML compilation" Category="Debug" /> + + (); @@ -363,7 +364,7 @@ namespace Avalonia.Markup.Xaml.XamlIl } var parsed = compiler.Parse(xaml, overrideType); - parsed.Document = "runtimexaml:" + parsedDocuments.Count; + parsed.Document = document.Document ?? ("runtimexaml" + parsedDocuments.Count); compiler.Transform(parsed); var xamlName = GetSafeUriIdentifier(document.BaseUri) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index e0225a24f7..5b00ea0bea 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using Avalonia.Markup.Xaml.Loader.CompilerExtensions.Transformers; using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.GroupTransformers; using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; using XamlX; @@ -20,6 +21,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions private readonly IXamlType _contextType = null!; private readonly AvaloniaXamlIlDesignPropertiesTransformer _designTransformer; private readonly AvaloniaBindingExtensionTransformer _bindingTransformer; + private readonly AvaloniaXamlIlAddSourceInfoTransformer _addSourceInfoTransformer; + private readonly AvaloniaXamlResourceTransformer _resourceTransformer; private AvaloniaXamlIlCompiler(TransformerConfiguration configuration, XamlLanguageEmitMappings emitMappings) : base(configuration, emitMappings, true) @@ -47,6 +50,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions new AvaloniaXamlIlResolveClassesPropertiesTransformer(), new AvaloniaXamlIlTransformInstanceAttachedProperties(), new AvaloniaXamlIlTransformSyntheticCompiledBindingMembers()); + + InsertAfter( new AvaloniaXamlIlAvaloniaPropertyResolver(), new AvaloniaXamlIlReorderClassesPropertiesTransformer(), @@ -85,7 +90,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions ); InsertBeforeMany(new [] { typeof(DeferredContentTransformer), typeof(AvaloniaXamlIlCompiledBindingsMetadataRemover) }, - new AvaloniaXamlIlDeferredResourceTransformer()); + _resourceTransformer = new AvaloniaXamlResourceTransformer()); InsertBefore(new AvaloniaXamlIlTransformRoutedEvent()); @@ -94,6 +99,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions Transformers.Add(new AvaloniaXamlIlEnsureResourceDictionaryCapacityTransformer()); Transformers.Add(new AvaloniaXamlIlRootObjectScope()); + Transformers.Add(_addSourceInfoTransformer = new AvaloniaXamlIlAddSourceInfoTransformer()); + Emitters.Add(new AvaloniaNameScopeRegistrationXamlIlNodeEmitter()); Emitters.Add(new AvaloniaXamlIlRootObjectScope.Emitter()); @@ -122,6 +129,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions public const string PopulateName = "__AvaloniaXamlIlPopulate"; public const string BuildName = "__AvaloniaXamlIlBuild"; + public bool CreateSourceInfo + { + get => _addSourceInfoTransformer.CreateSourceInfo || _resourceTransformer.CreateSourceInfo; + set => _addSourceInfoTransformer.CreateSourceInfo = _resourceTransformer.CreateSourceInfo = value; + } + public bool IsDesignMode { get => _designTransformer.IsDesignMode; 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 766c6d9e02..9ee893e0d4 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs @@ -20,8 +20,12 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer { public IXamlAstNode Transform(AstGroupTransformationContext context, IXamlAstNode node) { - if (node is not XamlValueWithManipulationNode valueNode - || valueNode.Value is not XamlAstNewClrObjectNode objectNode + // Filter object initialization nodes like: + // > XamlValueWithManipulationNode + // > > XamlAstNewClrObjectNode // StyleInclude or ResourceInclude, can be nested in another XamlValueWithManipulationNode + // > > XamlObjectInitializationNode + if (node is not XamlValueWithManipulationNode { Manipulation: XamlObjectInitializationNode initializationNode } valueNode + || valueNode.UnwrapValue() is not { } objectNode || (objectNode.Type.GetClrType() != context.GetAvaloniaTypes().StyleInclude && objectNode.Type.GetClrType() != context.GetAvaloniaTypes().ResourceInclude)) { @@ -36,11 +40,6 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer throw new InvalidOperationException($"\"{nodeTypeName}\".Loaded property is expected to be defined"); } - if (valueNode.Manipulation is not XamlObjectInitializationNode initializationNode) - { - throw new InvalidOperationException($"Invalid \"{nodeTypeName}\" node initialization."); - } - var additionalProperties = new List(); if (initializationNode.Manipulation is not XamlPropertyAssignmentNode { Property: { Name: "Source" } } sourceProperty) { @@ -176,9 +175,25 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer strictSourceValueType ? XamlDiagnosticSeverity.Error : XamlDiagnosticSeverity.Warning, $"\"{nodeTypeName}.Source\" supports only \"avares://\" absolute or relative uri. This {nodeTypeName} will be resolved in runtime instead.", 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 + if (sourceProperty.Values.Count != 1) + { + OnInvalidSource(sourceProperty); + return (null, null); + } + + // `new Uri` can be wrapped in manipulation node if source info or another manipulation was applied. + var sourceUriNodeWrapped = sourceProperty.Values.Single(); + var sourceUriNode = sourceUriNodeWrapped switch + { + XamlAstNewClrObjectNode newObj => newObj, + XamlValueWithManipulationNode manipulation => manipulation.UnwrapValue(), + _ => null + }; + + // Validate Uri type and constant arguments. + if (sourceUriNode is null || sourceUriNode.Type.GetClrType() != context.GetAvaloniaTypes().Uri || sourceUriNode.Arguments.FirstOrDefault() is not XamlConstantNode { Constant: string originalAssetPath } || sourceUriNode.Arguments.Skip(1).FirstOrDefault() is not XamlConstantNode { Constant: int uriKind }) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlAddSourceInfoTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlAddSourceInfoTransformer.cs new file mode 100644 index 0000000000..1ce6e456d6 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlAddSourceInfoTransformer.cs @@ -0,0 +1,69 @@ +using System.Linq; +using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; +using XamlX.Ast; +using XamlX.Emit; +using XamlX.IL; +using XamlX.Transform; + +namespace Avalonia.Markup.Xaml.Loader.CompilerExtensions.Transformers +{ + /// + /// An XAMLIL AST transformer that injects metadata into the generated XAML code. + /// + /// + /// This transformer wraps object creation nodes with a manipulation node that adds source information. + /// This source information includes line number, position, and document name, which can be useful for debugging and diagnostics. + /// Note: ResourceDictionary source info is handled separately in . + /// + internal class AvaloniaXamlIlAddSourceInfoTransformer : IXamlAstTransformer + { + /// + /// Gets or sets a value indicating whether source information should be generated + /// and injected into the compiled XAML output. + /// + public bool CreateSourceInfo { get; set; } + + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (CreateSourceInfo + && node is XamlAstNewClrObjectNode objNode + && context.ParentNodes().FirstOrDefault() is not XamlValueWithManipulationNode { Manipulation: XamlSourceInfoValueManipulation } + && !objNode.Type.GetClrType().IsValueType) + { + var avaloniaTypes = context.GetAvaloniaTypes(); + + return new XamlValueWithManipulationNode( + objNode, objNode, + new XamlSourceInfoValueManipulation(avaloniaTypes, objNode, context.Document)); + } + + return node; + } + + private class XamlSourceInfoValueManipulation( + AvaloniaXamlIlWellKnownTypes avaloniaTypes, + XamlAstNewClrObjectNode objNode, string? document) + : XamlAstNode(objNode), IXamlAstManipulationNode, IXamlAstILEmitableNode + { + public XamlILNodeEmitResult Emit(XamlEmitContext context, IXamlILEmitter codeGen) + { + // Target object is already on stack. + + // var info = new XamlSourceInfo(Line, Position, Document); + codeGen.Ldc_I4(Line); + codeGen.Ldc_I4(Position); + if (document is not null) + codeGen.Ldstr(document); + else + codeGen.Ldnull(); + codeGen.Newobj(avaloniaTypes.XamlSourceInfoConstructor); + + // Set the XamlSourceInfo property on the current object + // XamlSourceInfo.SetValue(@this, info); + codeGen.EmitCall(avaloniaTypes.XamlSourceInfoSetter); + + return XamlILNodeEmitResult.Void(1); + } + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDeferredResourceTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDeferredResourceTransformer.cs deleted file mode 100644 index 81a174c6e2..0000000000 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDeferredResourceTransformer.cs +++ /dev/null @@ -1,191 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Visitors; -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 AvaloniaXamlIlDeferredResourceTransformer : IXamlAstTransformer - { - public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) - { - if (!(node is XamlPropertyAssignmentNode pa) || pa.Values.Count != 2) - return node; - - var types = context.GetAvaloniaTypes(); - - if (pa.Property.DeclaringType == types.ResourceDictionary && pa.Property.Name == "Content" - && ShouldBeDeferred(pa.Values[1])) - { - IXamlMethod addMethod = TryGetSharedValue(pa.Values[1], out var isShared) && !isShared - ? types.ResourceDictionaryNotSharedDeferredAdd - : types.ResourceDictionaryDeferredAdd; - - pa.Values[1] = new XamlDeferredContentNode(pa.Values[1], types.XamlIlTypes.Object, context.Configuration); - pa.PossibleSetters = new List - { - new XamlDirectCallPropertySetter(addMethod), - }; - } - else if (pa.Property.Name == "Resources" && pa.Property.Getter?.ReturnType.Equals(types.IResourceDictionary) == true - && ShouldBeDeferred(pa.Values[1])) - { - IXamlMethod addMethod = TryGetSharedValue(pa.Values[1], out var isShared) && !isShared - ? types.ResourceDictionaryNotSharedDeferredAdd - : types.ResourceDictionaryDeferredAdd; - - pa.Values[1] = new XamlDeferredContentNode(pa.Values[1], types.XamlIlTypes.Object, context.Configuration); - pa.PossibleSetters = new List - { - new AdderSetter(pa.Property.Getter, addMethod), - }; - } - - return node; - - bool TryGetSharedValue(IXamlAstValueNode valueNode, out bool value) - { - value = default; - if (valueNode is XamlAstConstructableObjectNode co) - { - // Try find x:Share directive - if (co.Children.Find(d => d is XamlAstXmlDirective { Namespace: XamlNamespaces.Xaml2006, Name: "Shared" }) is XamlAstXmlDirective sharedDirective) - { - if (sharedDirective.Values.Count == 1 && sharedDirective.Values[0] is XamlAstTextNode text) - { - if (bool.TryParse(text.Text, out var parseValue)) - { - // If the parser succeeds, remove the x:Share directive - co.Children.Remove(sharedDirective); - return true; - } - else - { - context.ReportTransformError("Invalid argument type for x:Shared directive.", node); - } - } - else - { - context.ReportTransformError("Invalid number of arguments for x:Shared directive.", node); - } - } - } - return false; - } - } - - private static bool ShouldBeDeferred(IXamlAstValueNode node) - { - var clrType = node.Type.GetClrType(); - - // XAML compiler is currently strict about value types, allowing them to be created only through converters. - // At the moment it should be safe to not defer structs. - if (clrType.IsValueType) - { - return false; - } - - // Never defer strings. - if (clrType.FullName == "System.String") - { - return false; - } - - // Do not defer resources, if it has any x:Name registration, as it cannot be delayed. - // This visitor will count x:Name registrations, ignoring nested NestedScopeMetadataNode scopes. - // We set target scope level to 0, assuming that this resource node is a scope of itself. - var nameRegistrationsVisitor = new NameScopeRegistrationVisitor( - targetMetadataScopeLevel: 0); - node.Visit(nameRegistrationsVisitor); - if (nameRegistrationsVisitor.Count > 0) - { - return false; - } - - return true; - } - - class AdderSetter : IXamlILOptimizedEmitablePropertySetter, IEquatable - { - private readonly IXamlMethod _getter; - private readonly IXamlMethod _adder; - - public AdderSetter(IXamlMethod getter, IXamlMethod adder) - { - _getter = getter; - _adder = adder; - TargetType = getter.DeclaringType; - Parameters = adder.ParametersWithThis().Skip(1).ToList(); - - bool allowNull = Parameters.Last().AcceptsNull(); - BinderParameters = new PropertySetterBinderParameters - { - AllowMultiple = true, - AllowXNull = allowNull, - AllowRuntimeNull = allowNull, - AllowAttributeSyntax = false, - }; - } - - public IXamlType TargetType { get; } - - public PropertySetterBinderParameters BinderParameters { get; } - - public IReadOnlyList Parameters { get; } - public IReadOnlyList CustomAttributes => _adder.CustomAttributes; - - public void Emit(IXamlILEmitter emitter) - { - var locals = new Stack(); - // Save all "setter" parameters - for (var c = Parameters.Count - 1; c >= 0; c--) - { - var loc = emitter.LocalsPool.GetLocal(Parameters[c]); - locals.Push(loc); - emitter.Stloc(loc.Local); - } - - emitter.EmitCall(_getter); - while (locals.Count>0) - using (var loc = locals.Pop()) - emitter.Ldloc(loc.Local); - emitter.EmitCall(_adder, true); - } - - public void EmitWithArguments( - XamlEmitContextWithLocals context, - IXamlILEmitter emitter, - IReadOnlyList arguments) - { - emitter.EmitCall(_getter); - - for (var i = 0; i < arguments.Count; ++i) - context.Emit(arguments[i], emitter, Parameters[i]); - - emitter.EmitCall(_adder, true); - } - - public bool Equals(AdderSetter? other) - { - if (ReferenceEquals(null, other)) - return false; - if (ReferenceEquals(this, other)) - return true; - - return _getter.Equals(other._getter) && _adder.Equals(other._adder); - } - - public override bool Equals(object? obj) - => Equals(obj as AdderSetter); - - public override int GetHashCode() - => (_getter.GetHashCode() * 397) ^ _adder.GetHashCode(); - } - } -} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlResourceTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlResourceTransformer.cs new file mode 100644 index 0000000000..4e2458696e --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlResourceTransformer.cs @@ -0,0 +1,343 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Visitors; +using XamlX; +using XamlX.Ast; +using XamlX.Emit; +using XamlX.IL; +using XamlX.Transform; +using XamlX.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + /// + /// Transforms ResourceDictionary and IResourceDictionary property assignments + /// to use Add method calls with deferred content where applicable. + /// Additionally, handles x:Shared on assignments and injects XamlSourceInfo. + /// + internal class AvaloniaXamlResourceTransformer : IXamlAstTransformer + { + /// + /// Gets or sets a value indicating whether source information should be generated + /// and injected into the compiled XAML output. + /// + public bool CreateSourceInfo { get; set; } = true; + + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (!(node is XamlPropertyAssignmentNode pa) || pa.Values.Count != 2) + return node; + + var types = context.GetAvaloniaTypes(); + var document = context.Document; + + if (pa.Property.DeclaringType == types.ResourceDictionary && pa.Property.Name == "Content") + { + var value = pa.Values[1]; + (var adder, value) = ResolveAdderAndValue(value); + + pa.Values[1] = value; + pa.PossibleSetters = new List + { + new AdderSetter(adder, CreateSourceInfo, types, value.Line, value.Position, document), + }; + } + else if (pa.Property.Name == "Resources" && pa.Property.Getter?.ReturnType.Equals(types.IResourceDictionary) == true) + { + var value = pa.Values[1]; + (var adder, value) = ResolveAdderAndValue(value); + + pa.Values[1] = value; + pa.PossibleSetters = new List + { + new AdderSetter(pa.Property.Getter, adder, CreateSourceInfo, types, value.Line, value.Position, document), + }; + } + + return node; + + (IXamlMethod adder, IXamlAstValueNode newValue) ResolveAdderAndValue(IXamlAstValueNode valueNode) + { + if (ShouldBeDeferred(valueNode)) + { + var adder = TryGetSharedValue(valueNode, out var isShared) && !isShared + ? types.ResourceDictionaryNotSharedDeferredAdd + : types.ResourceDictionaryDeferredAdd; + var deferredNode = new XamlDeferredContentNode(valueNode, types.XamlIlTypes.Object, context.Configuration); + return (adder, deferredNode); + } + else + { + var adder = XamlTransformHelpers.FindPossibleAdders(context, types.IResourceDictionary) + .FirstOrDefault() ?? throw new XamlTransformException("No suitable Add method found for IResourceDictionary.", node); + return (adder, valueNode); + } + } + + bool TryGetSharedValue(IXamlAstValueNode valueNode, out bool value) + { + value = default; + if (valueNode is XamlAstConstructableObjectNode co) + { + // Try find x:Share directive + if (co.Children.Find(d => d is XamlAstXmlDirective { Namespace: XamlNamespaces.Xaml2006, Name: "Shared" }) is XamlAstXmlDirective sharedDirective) + { + if (sharedDirective.Values.Count == 1 && sharedDirective.Values[0] is XamlAstTextNode text) + { + if (bool.TryParse(text.Text, out var parseValue)) + { + // If the parser succeeds, remove the x:Share directive + co.Children.Remove(sharedDirective); + return true; + } + else + { + context.ReportTransformError("Invalid argument type for x:Shared directive.", node); + } + } + else + { + context.ReportTransformError("Invalid number of arguments for x:Shared directive.", node); + } + } + } + return false; + } + } + + private static bool ShouldBeDeferred(IXamlAstValueNode node) + { + var clrType = node.Type.GetClrType(); + + // XAML compiler is currently strict about value types, allowing them to be created only through converters. + // At the moment it should be safe to not defer structs. + if (clrType.IsValueType) + { + return false; + } + + // Never defer strings. + if (clrType.FullName == "System.String") + { + return false; + } + + // Do not defer resources, if it has any x:Name registration, as it cannot be delayed. + // This visitor will count x:Name registrations, ignoring nested NestedScopeMetadataNode scopes. + // We set target scope level to 0, assuming that this resource node is a scope of itself. + var nameRegistrationsVisitor = new NameScopeRegistrationVisitor( + targetMetadataScopeLevel: 0); + node.Visit(nameRegistrationsVisitor); + if (nameRegistrationsVisitor.Count > 0) + { + return false; + } + + return true; + } + + class AdderSetter : IXamlILOptimizedEmitablePropertySetter, IEquatable + { + private readonly IXamlMethod? _getter; + private readonly IXamlMethod _adder; + private readonly bool _emitSourceInfo; + private readonly AvaloniaXamlIlWellKnownTypes _avaloniaTypes; + private readonly string? _document; + private readonly int _line, _position; + + /// + /// Creates an adder-only setter. Target is assumed to be already on the stack before emit. + /// For example: + /// var resourceDictionary = ... + /// resourceDictionary.Add(key, value); + /// resourceDictionary.Add(key2, value2); + /// + public AdderSetter( + IXamlMethod adder, + bool emitSourceInfo, + AvaloniaXamlIlWellKnownTypes avaloniaTypes, + int line, int position, string? document) + { + _adder = adder; + _emitSourceInfo = emitSourceInfo; + _avaloniaTypes = avaloniaTypes; + _line = line; + _position = position; + _document = document; + + TargetType = adder.ThisOrFirstParameter(); + Parameters = adder.ParametersWithThis().Skip(1).ToList(); + bool allowNull = Parameters.Last().AcceptsNull(); + BinderParameters = new PropertySetterBinderParameters + { + AllowMultiple = true, + AllowXNull = allowNull, + AllowRuntimeNull = allowNull + }; + } + + /// + /// Explicit target getter - target will be obtained by calling the getter first. + /// + /// + public AdderSetter( + IXamlMethod getter, IXamlMethod adder, + bool emitSourceInfo, + AvaloniaXamlIlWellKnownTypes avaloniaTypes, + int line, int position, string? document) + : this(adder, emitSourceInfo, avaloniaTypes, line, position, document) + { + _getter = getter; + TargetType = getter.DeclaringType; + BinderParameters.AllowMultiple = false; + BinderParameters.AllowAttributeSyntax = false; + } + + public IXamlType TargetType { get; } + + public PropertySetterBinderParameters BinderParameters { get; } + + public IReadOnlyList Parameters { get; } + public IReadOnlyList CustomAttributes => _adder.CustomAttributes; + + /// + /// Emits the setter with arguments already on the stack. + /// + /// + /// If _getter is null - assume target is already on the stack. + /// In this case, we can just call Emit. Unless _emitSourceInfo is true. + /// + /// If _emitSourceInfo is true - we need to make sure that target and key are on the stack for XamlSourceInfo setting, + /// so we need to store parameters to locals first regardless. + /// + public void Emit(IXamlILEmitter emitter) + { + using var keyLocal = emitter.LocalsPool.GetLocal(Parameters[0]); + + if (_getter is not null || _emitSourceInfo) + { + var locals = new Stack(); + // Save all "setter" parameters + for (var c = Parameters.Count - 1; c >= 0; c--) + { + var loc = emitter.LocalsPool.GetLocal(Parameters[c]); + locals.Push(loc); + emitter.Stloc(loc.Local); + + if (c == 0 && _emitSourceInfo) + { + // Store the key argument for XamlSourceInfo later + emitter.Ldloc(loc.Local); + emitter.Stloc(keyLocal.Local); + } + } + + if (_getter is not null) + { + emitter.EmitCall(_getter); + } + + // Duplicate the target object on stack for setting XamlSourceInfo later + emitter.Dup(); + + while (locals.Count > 0) + using (var loc = locals.Pop()) + emitter.Ldloc(loc.Local); + } + + emitter.EmitCall(_adder, true); + + if (_emitSourceInfo) + { + // Target is already on stack (dup) + // Load the key argument from local + emitter.Ldloc(keyLocal.Local); + EmitSetSourceInfo(emitter); + } + } + + /// + /// Emits the setter with provided arguments that are not yet on the stack. + /// + /// + /// If _getter is null - assume target is already on the stack. + /// If _emitSourceInfo is true - we need to make sure that target and key are on the stack for XamlSourceInfo setting. + /// + public void EmitWithArguments( + XamlEmitContextWithLocals context, + IXamlILEmitter emitter, + IReadOnlyList arguments) + { + using var keyLocal = _emitSourceInfo ? emitter.LocalsPool.GetLocal(Parameters[0]) : null; + + if (_getter is not null) + { + emitter.EmitCall(_getter); + } + + if (_emitSourceInfo) + { + // Duplicate the target object on stack for setting XamlSourceInfo later + emitter.Dup(); + } + + for (var i = 0; i < arguments.Count; ++i) + { + context.Emit(arguments[i], emitter, Parameters[i]); + + // Store the key argument for XamlSourceInfo later + if (i == 0 && _emitSourceInfo) + { + emitter.Stloc(keyLocal!.Local); + emitter.Ldloc(keyLocal.Local); + } + } + + emitter.EmitCall(_adder, true); + + if (_emitSourceInfo) + { + // Target is already on stack (dub) + // Load the key argument from local + emitter.Ldloc(keyLocal!.Local); + + EmitSetSourceInfo(emitter); + } + } + + private void EmitSetSourceInfo(IXamlILEmitter emitter) + { + // Assumes the target object and key are already on the stack + + emitter.Ldc_I4(_line); + emitter.Ldc_I4(_position); + if (_document is not null) + emitter.Ldstr(_document); + else + emitter.Ldnull(); + emitter.Newobj(_avaloniaTypes.XamlSourceInfoConstructor); + + // Set the XamlSourceInfo property on the current object + // XamlSourceInfo.SetXamlSourceInfo(@this, key, info); + emitter.EmitCall(_avaloniaTypes.XamlSourceInfoDictionarySetter); + } + + public bool Equals(AdderSetter? other) + { + if (ReferenceEquals(null, other)) + return false; + if (ReferenceEquals(this, other)) + return true; + + return _getter?.Equals(other._getter) == true && _adder.Equals(other._adder); + } + + public override bool Equals(object? obj) + => Equals(obj as AdderSetter); + + public override int GetHashCode() + => (_getter, _adder).GetHashCode(); + } + } +} 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 8659eb1299..df54e71108 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -134,6 +134,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType ControlTemplate { get; } public IXamlType EventHandlerT { get; } public IXamlMethod GetClassProperty { get; } + public IXamlConstructor XamlSourceInfoConstructor { get; } + public IXamlMethod XamlSourceInfoSetter { get; } + public IXamlMethod XamlSourceInfoDictionarySetter { get; } sealed internal class InteractivityWellKnownTypes { @@ -343,6 +346,15 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers allowDowncast:false, cfg.WellKnownTypes.String ); + + var xamlSourceInfo = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Diagnostics.XamlSourceInfo"); + XamlSourceInfoConstructor = xamlSourceInfo.GetConstructor([ + XamlIlTypes.Int32, XamlIlTypes.Int32, XamlIlTypes.String + ]); + XamlSourceInfoSetter = + xamlSourceInfo.GetMethod("SetXamlSourceInfo", XamlIlTypes.Void, false, XamlIlTypes.Object, xamlSourceInfo); + XamlSourceInfoDictionarySetter = + xamlSourceInfo.GetMethod("SetXamlSourceInfo", XamlIlTypes.Void, false, IResourceDictionary, XamlIlTypes.Object, xamlSourceInfo); } } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlAstNewClrObjectHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlAstNewClrObjectHelper.cs new file mode 100644 index 0000000000..612f675124 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlAstNewClrObjectHelper.cs @@ -0,0 +1,26 @@ +using XamlX.Ast; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions; + +internal static class XamlAstNewClrObjectHelper +{ + /// + /// Tries to resolve the underlying value of a , + /// unwrapping any nested instances. + /// + public static TXamlAstValueNode? UnwrapValue(this XamlValueWithManipulationNode node) + where TXamlAstValueNode : class, IXamlAstValueNode + { + var current = node.Value; + while (current is XamlValueWithManipulationNode valueWithManipulation) + { + current = valueWithManipulation.Value; + if (current is TXamlAstValueNode typedValue) + { + return typedValue; + } + } + + return current as TXamlAstValueNode; + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index e6186bbea6..6dd2690414 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -38,6 +38,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/Diagnostics/XamlSourceInfo.cs b/src/Markup/Avalonia.Markup.Xaml/Diagnostics/XamlSourceInfo.cs new file mode 100644 index 0000000000..1bb5f1c98c --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/Diagnostics/XamlSourceInfo.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Avalonia.Controls; + +namespace Avalonia.Markup.Xaml.Diagnostics +{ + /// + /// Represents source location information for an element within a XAML or code file. + /// + // ReSharper disable once ClassNeverInstantiated.Global //This class is instantiated through the XAML compiler. + public record XamlSourceInfo + { + private static readonly AttachedProperty s_xamlSourceInfo = + AvaloniaProperty.RegisterAttached( + "XamlSourceInfo", typeof(XamlSourceInfo)); + + private static readonly ConditionalWeakTable s_sourceInfo = []; + private static readonly ConditionalWeakTable> s_keyedSourceInfo = []; + + /// + /// Gets the full path of the source file containing the element, or null if unavailable. + /// + public Uri? SourceUri { get; } + + /// + /// Gets the 1-based line number in the source file where the element is defined. + /// + public int LineNumber { get; } + + /// + /// Gets the 1-based column number in the source file where the element is defined. + /// + public int LinePosition { get; } + + /// + /// Initializes a new instance of the class + /// with a specified line, column, and file path. + /// + /// The line number of the source element. + /// The column number of the source element. + /// The full path of the source file. + public XamlSourceInfo(int line, int column, string? filePath) + { + LineNumber = line; + LinePosition = column; + SourceUri = filePath is not null ? new UriBuilder("file", "") { Path = filePath }.Uri : null; + } + + /// + /// Associates XAML source information with the specified object for debugging or diagnostic purposes. + /// + /// This method is typically used to enable enhanced debugging or diagnostics by tracking + /// the origin of XAML elements at runtime. If the same object is passed multiple times, the most recent source + /// information will overwrite any previous value. + /// The object to associate with the XAML source information. Cannot be null. + /// The XAML source information to associate with the object, or null to remove any existing association. + public static void SetXamlSourceInfo(object obj, XamlSourceInfo? info) + { + if (obj is null) + throw new ArgumentNullException(nameof(obj)); + + if (obj is AvaloniaObject avaloniaObject) + { + avaloniaObject.SetValue(s_xamlSourceInfo, info); + } + else + { + s_sourceInfo.AddOrUpdate(obj, info); + } + } + + /// + /// Associates XAML source information with the specified key in the given resource dictionary. + /// + /// The resource dictionary to associate with the XAML source information. + /// The key associated with the source info. + /// The XAML source information to associate with the object, or null to remove any existing association. + public static void SetXamlSourceInfo(IResourceDictionary dictionary, object key, XamlSourceInfo? info) + { + if (dictionary is null) + throw new ArgumentNullException(nameof(dictionary)); + + var dict = s_keyedSourceInfo.GetOrCreateValue(dictionary); + if (info == null) + { + _ = dict.Remove(key); + } + else + { + dict[key] = info; + } + } + + /// + /// Retrieves the XAML source information associated with the specified object, if available. + /// + /// The object for which to obtain XAML source information. Cannot be null. + /// A instance containing the XAML source information for the specified object, or + /// if no source information is available. + public static XamlSourceInfo? GetXamlSourceInfo(object obj) + { + if (obj is null) + throw new ArgumentNullException(nameof(obj)); + + if (obj is AvaloniaObject avaloniaObject) + { + return avaloniaObject.GetValue(s_xamlSourceInfo); + } + else + { + s_sourceInfo.TryGetValue(obj, out var info); + return info; + } + } + + /// + /// Retrieves the XAML source information associated with the specified key in the given resource dictionary, if available. + /// + /// The resource dictionary associated with the XAML source information. + /// The key associated with the source info. + /// A instance containing the XAML source information for the specified key, or + /// if no source information is available. + public static XamlSourceInfo? GetXamlSourceInfo(IResourceDictionary dictionary, object key) + { + if (dictionary is null) + throw new ArgumentNullException(nameof(dictionary)); + + if (s_keyedSourceInfo.TryGetValue(dictionary, out var dict) + && dict.TryGetValue(key, out var info)) + { + return info; + } + + return null; + } + + /// + /// Returns a string that represents the current . + /// + /// + /// A formatted string in the form "FilePath:Line,Column", + /// or "(unknown):Line,Column" if the file path is not set. + /// + public override string ToString() + { + var filePath = SourceUri?.LocalPath ?? "(unknown)"; + return $"{filePath}:{LineNumber},{LinePosition}"; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/RuntimeXamlLoaderConfiguration.cs b/src/Markup/Avalonia.Markup.Xaml/RuntimeXamlLoaderConfiguration.cs index 8723ff4f90..c9c421b766 100644 --- a/src/Markup/Avalonia.Markup.Xaml/RuntimeXamlLoaderConfiguration.cs +++ b/src/Markup/Avalonia.Markup.Xaml/RuntimeXamlLoaderConfiguration.cs @@ -22,6 +22,12 @@ public class RuntimeXamlLoaderConfiguration /// public bool DesignMode { get; set; } = false; + /// + /// When enabled, the XAML compiler embeds SourceInfo metadata (file path, line, and column) into generated code. + /// Default is 'false'. + /// + public bool CreateSourceInfo { get; set; } = false; + /// /// XAML diagnostics handler. /// diff --git a/src/Markup/Avalonia.Markup.Xaml/RuntimeXamlLoaderDocument.cs b/src/Markup/Avalonia.Markup.Xaml/RuntimeXamlLoaderDocument.cs index 5fabc6ee35..937f64f14e 100644 --- a/src/Markup/Avalonia.Markup.Xaml/RuntimeXamlLoaderDocument.cs +++ b/src/Markup/Avalonia.Markup.Xaml/RuntimeXamlLoaderDocument.cs @@ -58,6 +58,11 @@ public class RuntimeXamlLoaderDocument /// public Uri? BaseUri { get; set; } + /// + /// Path to the XAML document being loaded. + /// + public string? Document { get; set; } + /// /// The optional instance into which the XAML should be loaded. /// diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs index 624e14899b..8949df2eb4 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs @@ -12,8 +12,10 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions { public class ResourceIncludeTests : XamlTestBase { - [Fact] - public void ResourceInclude_Loads_ResourceDictionary() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ResourceInclude_Loads_ResourceDictionary(bool createSourceInfo) { var documents = new[] { @@ -37,9 +39,11 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions ") }; + var config = new RuntimeXamlLoaderConfiguration { CreateSourceInfo = createSourceInfo }; + using (StartWithResources()) { - var compiled = AvaloniaRuntimeXamlLoader.LoadGroup(documents); + var compiled = AvaloniaRuntimeXamlLoader.LoadGroup(documents, config); var userControl = Assert.IsType(compiled[1]); var border = userControl.GetControl("border"); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs index 608e10f739..c805619f44 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs @@ -18,8 +18,10 @@ public class MergeResourceIncludeTests : XamlTestBase RuntimeHelpers.RunClassConstructor(typeof(RelativeSource).TypeHandle); } - [Fact] - public void MergeResourceInclude_Works_With_Single_Resource() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void MergeResourceInclude_Works_With_Single_Resource(bool createSourceInfo) { var documents = new[] { @@ -41,8 +43,9 @@ public class MergeResourceIncludeTests : XamlTestBase ") }; - - var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents); + + var config = new RuntimeXamlLoaderConfiguration { CreateSourceInfo = createSourceInfo }; + var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents, config); var contentControl = Assert.IsType(objects[1]); var resources = Assert.IsType(contentControl.Resources); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs index b28e049191..1fdbd9bcc3 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs @@ -265,6 +265,8 @@ public class StyleIncludeTests : XamlTestBase [Fact] public void StyleInclude_Should_Be_Replaced_With_Direct_Call() { + using var _ = UnitTestApplication.Start(TestServices.StyledWindow); + var control = (ContentControl)AvaloniaRuntimeXamlLoader.Load(@" (); var control = (ContentControl)AvaloniaRuntimeXamlLoader.Load(new RuntimeXamlLoaderDocument(@" + + """) + { + Document = document + }; + + var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xamlDocument, s_configuration); + + var sourceInfo = XamlSourceInfo.GetXamlSourceInfo(userControl); + + Assert.NotNull(sourceInfo); + Assert.Equal("file", sourceInfo.SourceUri!.Scheme); + Assert.True(sourceInfo.SourceUri!.IsAbsoluteUri); + Assert.Equal(new UriBuilder("file", "") {Path = document}.Uri, sourceInfo.SourceUri); + } + + [Fact] + public void Root_UserControl_Gets_XamlSourceInfo_Set() + { + var xaml = new RuntimeXamlLoaderDocument(@" + +"); + + var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml, s_configuration); + + var sourceInfo = XamlSourceInfo.GetXamlSourceInfo(userControl); + + Assert.NotNull(sourceInfo); + } + + [Fact] + public void Nested_Controls_All_Get_XamlSourceInfo_Set() + { + var xaml = new RuntimeXamlLoaderDocument(@" + + +