diff --git a/ICSharpCode.BamlDecompiler/Handlers/Records/PropertyHandler.cs b/ICSharpCode.BamlDecompiler/Handlers/Records/PropertyHandler.cs index a469bdf35..214557049 100644 --- a/ICSharpCode.BamlDecompiler/Handlers/Records/PropertyHandler.cs +++ b/ICSharpCode.BamlDecompiler/Handlers/Records/PropertyHandler.cs @@ -49,11 +49,37 @@ namespace ICSharpCode.BamlDecompiler.Handlers if (xamlProp.IsAttachedTo(elemType)) return new XAttribute(xamlProp.ToXName(ctx, parent.Xaml, true), value); - if (xamlProp.PropertyName == "Name" && elemType.ResolvedType.GetDefinition()?.ParentModule.IsMainModule == true) + if (IsRuntimeNameOfElement(xamlProp, elemType)) return new XAttribute(ctx.GetKnownNamespace("Name", XamlContext.KnownNamespace_Xaml), value); return new XAttribute(xamlProp.ToXName(ctx, parent.Xaml, false), value); } } + + /// + /// Whether is the name of as + /// x:Name means it, so that the directive can be written instead of the property. + /// + /// x:Name is recorded as the runtime name property of the element, which is + /// FrameworkElement.Name for everything WPF - a property of the framework, not of the + /// assembly being decompiled. A type of that assembly declaring a property of its own called + /// "Name" is an ordinary property: writing the directive for it registers a name and leaves + /// the property unset, which still compiles and silently means something else (issue #2253). + /// + /// + internal static bool IsRuntimeNameOfElement(XamlProperty property, XamlType elementType) + { + if (property.PropertyName != "Name") + return false; + if (elementType?.ResolvedType.GetDefinition()?.ParentModule.IsMainModule != true) + return false; + // The type that declares the property, not the one the document names as the owner of + // the attribute: a control of the assembly being decompiled inherits Name from the + // framework, and the document names the control. Only a Name the type declares itself + // is a property of its own rather than the runtime name. + var declaringType = property.ResolvedMember?.DeclaringTypeDefinition + ?? property.DeclaringType?.ResolvedType?.GetDefinition(); + return declaringType?.ParentModule.IsMainModule != true; + } } } \ No newline at end of file diff --git a/ICSharpCode.BamlDecompiler/Rewrite/StartupUriRewritePass.cs b/ICSharpCode.BamlDecompiler/Rewrite/StartupUriRewritePass.cs new file mode 100644 index 000000000..af5dd1c87 --- /dev/null +++ b/ICSharpCode.BamlDecompiler/Rewrite/StartupUriRewritePass.cs @@ -0,0 +1,130 @@ +// Copyright (c) 2026 Siegfried Pammer +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Xml.Linq; + +using ICSharpCode.Decompiler.Disassembler; +using ICSharpCode.Decompiler.TypeSystem; + +namespace ICSharpCode.BamlDecompiler.Rewrite +{ + /// + /// Recovers the StartupUri of an application from the code the markup compiler generated for it. + /// + /// StartupUri is written in App.xaml, but it does not reach the BAML: the markup compiler turns + /// the attribute into an assignment inside InitializeComponent. The project decompiler deletes + /// the generated members, so a document decompiled without this would build into an application + /// that starts and shows nothing. + /// + /// + internal class StartupUriRewritePass : IRewritePass + { + const string StartupUriPropertyName = "StartupUri"; + + public void Run(XamlContext ctx, XDocument document) + { + var root = document.Elements().FirstOrDefault()?.Elements().FirstOrDefault(); + if (root == null || root.Attribute(StartupUriPropertyName) != null) + return; + // The type of the document, which the x:Class pass has recorded by the time this runs. + if (ctx.XClassNames.FirstOrDefault() is not string className) + return; + var typeDefinition = ctx.TypeSystem.MainModule.GetTypeDefinition(new FullTypeName(className).TopLevelTypeName); + if (typeDefinition == null) + return; + + string startupUri = FindAssignedStartupUri(typeDefinition); + if (startupUri != null) + root.Add(new XAttribute(StartupUriPropertyName, startupUri)); + } + + /// + /// The string assigned to a StartupUri property in InitializeComponent, if there is one. + /// The generated code reads + /// StartupUri = new Uri("MainWindow.xaml", UriKind.Relative), so the string wanted is + /// the last one loaded before the call to the setter. + /// + static string FindAssignedStartupUri(ITypeDefinition typeDefinition) + { + var method = typeDefinition.Methods.FirstOrDefault( + m => m.Name == "InitializeComponent" && m.Parameters.Count == 0); + if (method?.MetadataToken.IsNil != false) + return null; + var module = typeDefinition.ParentModule?.MetadataFile; + if (module == null) + return null; + + try + { + var metadata = module.Metadata; + var methodDefinition = metadata.GetMethodDefinition((MethodDefinitionHandle)method.MetadataToken); + if (methodDefinition.RelativeVirtualAddress == 0) + return null; + var body = module.GetMethodBody(methodDefinition.RelativeVirtualAddress); + var reader = body.GetILReader(); + string lastLoadedString = null; + while (reader.RemainingBytes > 0) + { + var opCode = reader.DecodeOpCode(); + switch (opCode) + { + case ILOpCode.Ldstr: + lastLoadedString = metadata.GetUserString( + MetadataTokens.UserStringHandle(reader.ReadInt32())); + break; + case ILOpCode.Call: + case ILOpCode.Callvirt: + var target = MetadataTokens.EntityHandle(reader.ReadInt32()); + if (lastLoadedString != null && IsStartupUriSetter(metadata, target)) + return lastLoadedString; + break; + default: + ILParser.SkipOperand(ref reader, opCode); + break; + } + } + } + catch (BadImageFormatException) + { + // A method body nobody can read says nothing about the StartupUri. + } + return null; + } + + static bool IsStartupUriSetter(MetadataReader metadata, EntityHandle handle) + { + StringHandle name; + switch (handle.Kind) + { + case HandleKind.MethodDefinition: + name = metadata.GetMethodDefinition((MethodDefinitionHandle)handle).Name; + break; + case HandleKind.MemberReference: + name = metadata.GetMemberReference((MemberReferenceHandle)handle).Name; + break; + default: + return false; + } + return metadata.StringComparer.Equals(name, "set_" + StartupUriPropertyName); + } + } +} diff --git a/ICSharpCode.BamlDecompiler/XamlDecompiler.cs b/ICSharpCode.BamlDecompiler/XamlDecompiler.cs index 7a1e842c2..d0f37ad09 100644 --- a/ICSharpCode.BamlDecompiler/XamlDecompiler.cs +++ b/ICSharpCode.BamlDecompiler/XamlDecompiler.cs @@ -40,6 +40,8 @@ namespace ICSharpCode.BamlDecompiler { static readonly IRewritePass[] rewritePasses = new IRewritePass[] { new XClassRewritePass(), + // After the x:Class pass, which is what establishes the type of the document. + new StartupUriRewritePass(), new MarkupExtensionRewritePass(), new AttributeRewritePass(), new ConnectionIdRewritePass(), diff --git a/ILSpy.BamlDecompiler.Tests/ILSpy.BamlDecompiler.Tests.csproj b/ILSpy.BamlDecompiler.Tests/ILSpy.BamlDecompiler.Tests.csproj index 949c7f36c..97dde68e0 100644 --- a/ILSpy.BamlDecompiler.Tests/ILSpy.BamlDecompiler.Tests.csproj +++ b/ILSpy.BamlDecompiler.Tests/ILSpy.BamlDecompiler.Tests.csproj @@ -47,6 +47,8 @@ + + diff --git a/ILSpy.BamlDecompiler.Tests/RuntimeNamePropertyTests.cs b/ILSpy.BamlDecompiler.Tests/RuntimeNamePropertyTests.cs new file mode 100644 index 000000000..0ac542d4d --- /dev/null +++ b/ILSpy.BamlDecompiler.Tests/RuntimeNamePropertyTests.cs @@ -0,0 +1,131 @@ +// Copyright (c) 2026 Siegfried Pammer +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Reflection.PortableExecutable; + +using ICSharpCode.BamlDecompiler; +using ICSharpCode.BamlDecompiler.Handlers; +using ICSharpCode.BamlDecompiler.Xaml; +using ICSharpCode.Decompiler.Metadata; +using ICSharpCode.Decompiler.TypeSystem; + +using NUnit.Framework; + +namespace ILSpy.BamlDecompiler.Tests +{ + /// + /// A type of the assembly being decompiled with a CLR property of its own called "Name". + /// + public class HelperWithItsOwnName + { + public string Name { get; set; } + } + + /// + /// x:Name is the directive that registers a name for an element, and it is recorded as the + /// runtime name property of that element - FrameworkElement.Name for everything WPF, a property + /// of the framework rather than of the assembly being decompiled. Writing the directive for a + /// type's own property of the same name registers a name and leaves the property unset, which + /// still compiles and silently means something else (issue #2253). + /// + [TestFixture] + public class RuntimeNamePropertyTests + { + static ICompilation compilation; + + [OneTimeSetUp] + public void LoadTestAssembly() + { + string location = typeof(RuntimeNamePropertyTests).Assembly.Location; + using var fileStream = new FileStream(location, FileMode.Open, FileAccess.Read); + var file = new PEFile(location, fileStream, streamOptions: PEStreamOptions.PrefetchEntireImage); + var resolver = new UniversalAssemblyResolver(location, throwOnError: false, + file.DetectTargetFrameworkId(), file.DetectRuntimePack()); + compilation = new BamlDecompilerTypeSystem(file, resolver); + } + + static XamlType XamlTypeOf(Type type) + { + var definition = compilation.FindType(type).GetDefinition(); + return new XamlType(definition.ParentModule, definition.ParentModule.FullAssemblyName, + definition.Namespace, definition.Name) { + ResolvedType = definition + }; + } + + /// + /// A property of , the way the decompiler resolves one from + /// the type a BAML attribute record names as its owner. + /// + static XamlProperty PropertyOf(Type declaringType, string propertyName) + { + var declaring = XamlTypeOf(declaringType); + return new XamlProperty(declaring, propertyName) { + ResolvedMember = declaring.ResolvedType.GetDefinition() + .GetProperties(p => p.Name == propertyName).FirstOrDefault() + }; + } + + [Test] + public void ATypesOwnNamePropertyIsNotTheRuntimeName() + { + // : the property belongs to the assembly being + // decompiled, and the value has to reach it. + var property = PropertyOf(typeof(HelperWithItsOwnName), "Name"); + + Assert.That(PropertyHandler.IsRuntimeNameOfElement(property, XamlTypeOf(typeof(HelperWithItsOwnName))), + Is.False); + } + + [Test] + public void ANameInheritedFromTheFrameworkIsTheRuntimeName() + { + // : the document names the control as the owner of + // the attribute, because that is the element it sits on, but the property comes from + // the framework type the control derives from. + var property = new XamlProperty(XamlTypeOf(typeof(HelperWithItsOwnName)), "Name") { + ResolvedMember = compilation.FindType(typeof(MemberInfo)).GetDefinition() + .GetProperties(p => p.Name == "Name").First() + }; + + Assert.That(PropertyHandler.IsRuntimeNameOfElement(property, XamlTypeOf(typeof(HelperWithItsOwnName))), + Is.True); + } + + [Test] + public void AnElementFromOutsideTheAssemblyKeepsItsProperty() + { + var property = PropertyOf(typeof(Uri), "Name"); + + Assert.That(PropertyHandler.IsRuntimeNameOfElement(property, XamlTypeOf(typeof(Uri))), Is.False); + } + + [Test] + public void APropertyOfAnotherNameIsNeverTheRuntimeName() + { + var property = PropertyOf(typeof(Uri), "Title"); + + Assert.That(PropertyHandler.IsRuntimeNameOfElement(property, XamlTypeOf(typeof(HelperWithItsOwnName))), + Is.False); + } + } +} diff --git a/ILSpy.BamlDecompiler.Tests/StartupUriTests.cs b/ILSpy.BamlDecompiler.Tests/StartupUriTests.cs new file mode 100644 index 000000000..1e28c7b72 --- /dev/null +++ b/ILSpy.BamlDecompiler.Tests/StartupUriTests.cs @@ -0,0 +1,130 @@ +// Copyright (c) 2026 Siegfried Pammer +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.IO; +using System.Reflection.PortableExecutable; + +using ICSharpCode.BamlDecompiler; +using ICSharpCode.BamlDecompiler.Baml; +using ICSharpCode.Decompiler.Metadata; + +using NUnit.Framework; + +namespace ILSpy.BamlDecompiler.Tests +{ + /// + /// StartupUri is written in App.xaml but compiled into App.g.cs, not into the BAML: the markup + /// compiler turns the attribute into an assignment inside InitializeComponent. The project + /// exporter deletes the generated members, so without recovering the assignment the exported + /// application builds and then opens no window (issue #2253). + /// + public class TestApplication + { + public Uri StartupUri { get; set; } + } + + /// + /// What the markup compiler generates for an Application with a StartupUri. + /// + public class AppWithStartupUri : TestApplication + { + public void InitializeComponent() + { + StartupUri = new Uri("MainWindow.xaml", UriKind.Relative); + Uri resourceLocator = new Uri("/Demo;component/app.xaml", UriKind.Relative); + GC.KeepAlive(resourceLocator); + } + } + + /// + /// The same without one, which must stay without one. + /// + public class AppWithoutStartupUri : TestApplication + { + public void InitializeComponent() + { + Uri resourceLocator = new Uri("/Demo;component/app.xaml", UriKind.Relative); + GC.KeepAlive(resourceLocator); + } + } + + [TestFixture] + public class StartupUriTests + { + static MemoryStream CreateBaml(params BamlRecord[] records) + { + var version = new BamlDocument.BamlVersion { Major = 0, Minor = 0x60 }; + var document = new BamlDocument { + Signature = "MSBAML", + ReaderVersion = version, + UpdaterVersion = version, + WriterVersion = version + }; + document.Add(new DocumentStartRecord()); + document.AddRange(records); + document.Add(new DocumentEndRecord()); + + var stream = new MemoryStream(); + BamlWriter.WriteDocument(document, stream); + stream.Position = 0; + return stream; + } + + static string Decompile(Stream baml) + { + var location = typeof(StartupUriTests).Assembly.Location; + using var fileStream = new FileStream(location, FileMode.Open, FileAccess.Read); + var file = new PEFile(location, fileStream, streamOptions: PEStreamOptions.PrefetchEntireImage); + var resolver = new UniversalAssemblyResolver(location, throwOnError: false, + file.DetectTargetFrameworkId(), file.DetectRuntimePack()); + var decompiler = new XamlDecompiler(new BamlDecompilerTypeSystem(file, resolver), + new BamlDecompilerSettings()); + return decompiler.Decompile(baml).Xaml.ToString(); + } + + /// + /// A document whose root is of this assembly, which is what + /// makes the BAML decompiler treat it as the code-behind class of the document. + /// + static string DecompileDocumentOf(string typeName) + { + return Decompile(CreateBaml( + new AssemblyInfoRecord { AssemblyId = 0, AssemblyFullName = "ILSpy.BamlDecompiler.Tests" }, + new TypeInfoRecord { TypeId = 0, AssemblyId = 0, TypeFullName = "ILSpy.BamlDecompiler.Tests." + typeName }, + new ElementStartRecord { TypeId = 0 }, + new ElementEndRecord())); + } + + [Test] + public void TheStartupUriOfTheGeneratedCodeComesBackAsAnAttribute() + { + string xaml = DecompileDocumentOf(nameof(AppWithStartupUri)); + + Assert.That(xaml, Does.Contain(@"StartupUri=""MainWindow.xaml"""), xaml); + } + + [Test] + public void NoStartupUriIsInventedForADocumentThatHasNone() + { + string xaml = DecompileDocumentOf(nameof(AppWithoutStartupUri)); + + Assert.That(xaml, Does.Not.Contain("StartupUri=\""), xaml); + } + } +}