Browse Source

Merge pull request #4108 from icsharpcode/fix/2253-baml-project-export

Fix the parts of #2253 that still reproduce
pull/4110/head
Siegfried Pammer 2 weeks ago committed by GitHub
parent
commit
0ed559ab13
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 27
      ICSharpCode.BamlDecompiler/BamlDecompilerTypeSystem.cs
  2. 28
      ICSharpCode.BamlDecompiler/Handlers/Records/PropertyHandler.cs
  3. 12
      ICSharpCode.BamlDecompiler/Handlers/Records/XmlnsPropertyHandler.cs
  4. 130
      ICSharpCode.BamlDecompiler/Rewrite/StartupUriRewritePass.cs
  5. 35
      ICSharpCode.BamlDecompiler/Xaml/NamespaceMap.cs
  6. 2
      ICSharpCode.BamlDecompiler/Xaml/XamlExtension.cs
  7. 4
      ICSharpCode.BamlDecompiler/Xaml/XamlType.cs
  8. 79
      ICSharpCode.BamlDecompiler/Xaml/XamlUtils.cs
  9. 2
      ICSharpCode.BamlDecompiler/XamlContext.cs
  10. 2
      ICSharpCode.BamlDecompiler/XamlDecompiler.cs
  11. 10
      ICSharpCode.BamlDecompiler/XmlnsDictionary.cs
  12. 86
      ICSharpCode.Decompiler.Tests/ProjectDecompiler/ProjectFileWriterDefaultTests.cs
  13. 5
      ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterDefault.cs
  14. 53
      ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs
  15. 9
      ICSharpCode.Decompiler/PartialTypeInfo.cs
  16. 28
      ICSharpCode.ILSpyCmd.Tests/BamlFixtureTypes.cs
  17. 6
      ICSharpCode.ILSpyCmd.Tests/ICSharpCode.ILSpyCmd.Tests.csproj
  18. 120
      ICSharpCode.ILSpyCmd.Tests/ProjectExportBamlTests.cs
  19. BIN
      ICSharpCode.ILSpyCmd.Tests/fixtures/test.g.resources
  20. 6
      ICSharpCode.ILSpyCmd/BamlAwareWholeProjectDecompiler.cs
  21. 27
      ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs
  22. 8
      ICSharpCode.ILSpyCmd/README.md
  23. 6
      ILSpy.BamlDecompiler.Tests.Windows/Cases/EscapeSequence.xaml
  24. 2
      ILSpy.BamlDecompiler.Tests.Windows/Cases/MarkupExtension.xaml
  25. 91
      ILSpy.BamlDecompiler.Tests/FacadeAssemblyTests.cs
  26. 5
      ILSpy.BamlDecompiler.Tests/ILSpy.BamlDecompiler.Tests.csproj
  27. 133
      ILSpy.BamlDecompiler.Tests/MarkupExtensionQuotingTests.cs
  28. 131
      ILSpy.BamlDecompiler.Tests/RuntimeNamePropertyTests.cs
  29. 130
      ILSpy.BamlDecompiler.Tests/StartupUriTests.cs
  30. 118
      ILSpy.BamlDecompiler.Tests/XmlnsDeclarationPlacementTests.cs
  31. 11
      ILSpy/TreeNodes/BamlResourceNodeFactory.cs

27
ICSharpCode.BamlDecompiler/BamlDecompilerTypeSystem.cs

@ -40,6 +40,29 @@ namespace ICSharpCode.BamlDecompiler @@ -40,6 +40,29 @@ namespace ICSharpCode.BamlDecompiler
"System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
};
// A type each of these assemblies must define for the module resolved under its name to be
// the assembly BAML means by it. .NET ships a WindowsBase facade on every platform - it
// resolves everywhere and carries none of the WPF types, because those live in the
// WindowsDesktop runtime pack - and a module like that has to give way to the synthetic
// stand-in the way an assembly that does not resolve at all does. Without this, a document
// using System.Windows.Point or Size is lost outright on a machine without WPF.
static readonly Dictionary<string, TopLevelTypeName> wellKnownProbeTypes = new(StringComparer.OrdinalIgnoreCase) {
["WindowsBase"] = new TopLevelTypeName("System.Windows", "Point"),
["PresentationCore"] = new TopLevelTypeName("System.Windows.Media", "Brush"),
["PresentationFramework"] = new TopLevelTypeName("System.Windows.Controls", "Button")
};
/// <summary>
/// Whether <paramref name="file"/> is the assembly its name claims, rather than a facade
/// standing where it should be.
/// </summary>
static bool IsTheAssemblyItIsNamedAfter(MetadataFile file)
{
if (!wellKnownProbeTypes.TryGetValue(file.Name, out var probeType))
return true;
return !file.GetTypeDefinition(probeType).IsNil;
}
// The WPF assemblies whose types serialize under the presentation XML namespace. When one of
// these has to be synthesized (e.g. inspecting a WPF binary on a non-Windows machine), the
// synthetic module reproduces its XmlnsDefinitionAttribute mapping so known types still emit
@ -120,6 +143,10 @@ namespace ICSharpCode.BamlDecompiler @@ -120,6 +143,10 @@ namespace ICSharpCode.BamlDecompiler
}
}
}
// A facade standing in for a well-known assembly is worse than nothing: it satisfies the
// name, so no stand-in is synthesized, and then every type BAML expects from it is
// missing. Drop it and let the stand-in below take its place.
referencedAssemblies.RemoveAll(file => !IsTheAssemblyItIsNamedAfter(file));
var mainModuleWithOptions = mainModule.WithOptions(TypeSystemOptions.Default);
var referencedAssembliesWithOptions = referencedAssemblies.Select(file => file.WithOptions(TypeSystemOptions.Default));
// Substitute a synthetic stand-in for every well-known BAML assembly that could not be

28
ICSharpCode.BamlDecompiler/Handlers/Records/PropertyHandler.cs

@ -49,11 +49,37 @@ namespace ICSharpCode.BamlDecompiler.Handlers @@ -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);
}
}
/// <summary>
/// Whether <paramref name="property"/> is the name of <paramref name="elementType"/> as
/// x:Name means it, so that the directive can be written instead of the property.
/// <para>
/// 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).
/// </para>
/// </summary>
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;
}
}
}

12
ICSharpCode.BamlDecompiler/Handlers/Records/XmlnsPropertyHandler.cs

@ -58,12 +58,20 @@ namespace ICSharpCode.BamlDecompiler.Handlers @@ -58,12 +58,20 @@ namespace ICSharpCode.BamlDecompiler.Handlers
foreach (var asmId in record.AssemblyIds)
{
var assembly = ctx.Baml.ResolveAssembly(asmId);
ctx.XmlNs.Add(new NamespaceMap(record.Prefix, assembly.FullAssemblyName, record.XmlNamespace));
// A clr-namespace declaration names its CLR namespace itself. Leaving that unread
// means no lookup by namespace can ever find the declaration the document made,
// and every type in it gets a second prefix of its own (issue #2253).
XamlUtils.TryParseClrNamespace(record.XmlNamespace, out string declaredClrNamespace);
ctx.XmlNs.Add(new NamespaceMap(record.Prefix, assembly.FullAssemblyName, record.XmlNamespace, declaredClrNamespace) {
Assembly = assembly.Assembly
});
if (assembly.Assembly?.IsMainModule == true)
{
foreach (var clrNs in ResolveCLRNamespaces(assembly.Assembly, record.XmlNamespace))
ctx.XmlNs.Add(new NamespaceMap(record.Prefix, assembly.FullAssemblyName, record.XmlNamespace, clrNs));
ctx.XmlNs.Add(new NamespaceMap(record.Prefix, assembly.FullAssemblyName, record.XmlNamespace, clrNs) {
Assembly = assembly.Assembly
});
}
}

130
ICSharpCode.BamlDecompiler/Rewrite/StartupUriRewritePass.cs

@ -0,0 +1,130 @@ @@ -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
{
/// <summary>
/// Recovers the StartupUri of an application from the code the markup compiler generated for it.
/// <para>
/// 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.
/// </para>
/// </summary>
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));
}
/// <summary>
/// The string assigned to a StartupUri property in InitializeComponent, if there is one.
/// The generated code reads
/// <c>StartupUri = new Uri("MainWindow.xaml", UriKind.Relative)</c>, so the string wanted is
/// the last one loaded before the call to the setter.
/// </summary>
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);
}
}
}

35
ICSharpCode.BamlDecompiler/Xaml/NamespaceMap.cs

@ -31,6 +31,13 @@ namespace ICSharpCode.BamlDecompiler.Xaml @@ -31,6 +31,13 @@ namespace ICSharpCode.BamlDecompiler.Xaml
{
public string XmlnsPrefix { get; set; }
public string FullAssemblyName { get; set; }
/// <summary>
/// The assembly <see cref="FullAssemblyName"/> resolves to, where it could be resolved.
/// The name is the one the document was written against, which is not always the name of
/// the assembly the types actually come from.
/// </summary>
public IModule Assembly { get; set; }
public string XMLNamespace { get; set; }
public string CLRNamespace { get; set; }
@ -47,6 +54,34 @@ namespace ICSharpCode.BamlDecompiler.Xaml @@ -47,6 +54,34 @@ namespace ICSharpCode.BamlDecompiler.Xaml
CLRNamespace = clrNs;
}
/// <summary>
/// Whether <paramref name="map"/> is the declaration to use for a type named
/// <paramref name="typeName"/> in <paramref name="clrNs"/> of
/// <paramref name="fullAssemblyName"/>.
/// </summary>
public static bool Matches(NamespaceMap map, string fullAssemblyName, string clrNs, string typeName)
{
if (map.CLRNamespace != clrNs)
return false;
if (map.FullAssemblyName == fullAssemblyName)
return true;
// The document records the assembly it was written against, while a well-known type
// carries the assembly it resolves to now - "mscorlib" against "System.Private.CoreLib"
// on .NET, say. The two name the same type when the recorded assembly forwards it, and
// then the declaration the document made is the one to use.
return typeName != null && ForwardsOrDeclares(map.Assembly, clrNs, typeName);
}
static bool ForwardsOrDeclares(IModule assembly, string clrNs, string typeName)
{
if (assembly == null)
return false;
var name = new TopLevelTypeName(clrNs, typeName);
if (assembly.GetTypeDefinition(name) != null)
return true;
return assembly.MetadataFile?.GetTypeForwarder(new FullTypeName(name)).IsNil == false;
}
public override string ToString() => $"{XmlnsPrefix}:[{FullAssemblyName}|{CLRNamespace ?? XMLNamespace}]";
}
}

2
ICSharpCode.BamlDecompiler/Xaml/XamlExtension.cs

@ -43,7 +43,7 @@ namespace ICSharpCode.BamlDecompiler.Xaml @@ -43,7 +43,7 @@ namespace ICSharpCode.BamlDecompiler.Xaml
if (value is XamlExtension)
sb.Append(((XamlExtension)value).ToString(ctx, ctxElement));
else
sb.Append(value.ToString());
sb.Append(XamlUtils.QuoteMarkupExtensionValue(value.ToString()));
}
public string ToString(XamlContext ctx, XElement ctxElement)

4
ICSharpCode.BamlDecompiler/Xaml/XamlType.cs

@ -64,9 +64,9 @@ namespace ICSharpCode.BamlDecompiler.Xaml @@ -64,9 +64,9 @@ namespace ICSharpCode.BamlDecompiler.Xaml
string xmlNs = null;
if (elem.Annotation<XmlnsScope>() != null)
xmlNs = elem.Annotation<XmlnsScope>().LookupXmlns(FullAssemblyName, TypeNamespace);
xmlNs = elem.Annotation<XmlnsScope>().LookupXmlns(FullAssemblyName, TypeNamespace, TypeName);
if (xmlNs == null)
xmlNs = ctx.XmlNs.LookupXmlns(FullAssemblyName, TypeNamespace);
xmlNs = ctx.XmlNs.LookupXmlns(FullAssemblyName, TypeNamespace, TypeName);
// Sometimes there's no reference to System.Xaml even if x:Type is used
if (xmlNs == null)
xmlNs = XamlContext.TryGetXmlNamespace(Assembly, TypeNamespace, elem);

79
ICSharpCode.BamlDecompiler/Xaml/XamlUtils.cs

@ -20,6 +20,7 @@ @@ -20,6 +20,7 @@
THE SOFTWARE.
*/
using System;
using System.IO;
using System.Text;
using System.Xml;
@ -29,6 +30,84 @@ namespace ICSharpCode.BamlDecompiler.Xaml @@ -29,6 +30,84 @@ namespace ICSharpCode.BamlDecompiler.Xaml
{
internal static class XamlUtils
{
static readonly char[] markupExtensionSpecialCharacters = { ',', '=', '\'', '"', '\\' };
/// <summary>
/// Quotes an argument of a markup extension if the parser reading the document again would
/// take part of it for grammar: ',' and '=' separate arguments from one another, a quote
/// character starts a quoted value, '\' escapes whatever follows it, and whitespace at
/// either end is dropped. A value carrying none of those is left as it is, because quoting
/// every value would rewrite every document that never needed it.
/// <para>
/// Braces are grammar only where they are unbalanced: a stray '{' opens an extension and a
/// stray '}' closes the surrounding one, while a matched pair inside a value ("{0:C}",
/// "Element[{ns}Name]") is read as text and stays unquoted. A value beginning with '{' is
/// a nested extension that is already written as one, so it is left alone; the "{}" that
/// escapes a leading brace is not, because inside an extension it would open one.
/// </para>
/// </summary>
public static string QuoteMarkupExtensionValue(string value)
{
if (value == null)
return null;
if (value.StartsWith("{", StringComparison.Ordinal) && !value.StartsWith("{}", StringComparison.Ordinal))
{
return value; // a nested markup extension, already written as one
}
if (value.Length > 0
&& !value.StartsWith("{}", StringComparison.Ordinal)
&& value.IndexOfAny(markupExtensionSpecialCharacters) < 0
&& BracesAreBalanced(value)
&& !char.IsWhiteSpace(value[0])
&& !char.IsWhiteSpace(value[value.Length - 1]))
{
return value;
}
var quoted = new StringBuilder(value.Length + 2);
quoted.Append('\'');
foreach (char c in value)
{
if (c == '\'' || c == '\\')
quoted.Append('\\');
quoted.Append(c);
}
quoted.Append('\'');
return quoted.ToString();
}
static bool BracesAreBalanced(string value)
{
int depth = 0;
foreach (char c in value)
{
if (c == '{')
depth++;
else if (c == '}' && --depth < 0)
return false;
}
return depth == 0;
}
/// <summary>
/// Reads the CLR namespace out of a "clr-namespace:Some.Namespace;assembly=Some.Assembly"
/// declaration. Such a declaration names its CLR namespace itself; the other form of XML
/// namespace ("http://...") maps to CLR namespaces through XmlnsDefinition attributes
/// instead, and has none of its own.
/// </summary>
public static bool TryParseClrNamespace(string xmlNamespace, out string clrNamespace)
{
const string prefix = "clr-namespace:";
clrNamespace = null;
if (xmlNamespace == null || !xmlNamespace.StartsWith(prefix, StringComparison.Ordinal))
return false;
clrNamespace = xmlNamespace.Substring(prefix.Length);
int assembly = clrNamespace.IndexOf(';');
if (assembly >= 0)
clrNamespace = clrNamespace.Substring(0, assembly);
return true;
}
public static string Escape(string value)
{
if (value.Length == 0)

2
ICSharpCode.BamlDecompiler/XamlContext.cs

@ -126,7 +126,7 @@ namespace ICSharpCode.BamlDecompiler @@ -126,7 +126,7 @@ namespace ICSharpCode.BamlDecompiler
}
var clrNs = type.Namespace;
var xmlNs = XmlNs.LookupXmlns(fullAssemblyName, clrNs);
var xmlNs = XmlNs.LookupXmlns(fullAssemblyName, clrNs, type.Name);
typeMap[id] = xamlType = new XamlType(assembly, fullAssemblyName, clrNs, type.Name, GetXmlNamespace(xmlNs)) {
ResolvedType = type

2
ICSharpCode.BamlDecompiler/XamlDecompiler.cs

@ -40,6 +40,8 @@ namespace ICSharpCode.BamlDecompiler @@ -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(),

10
ICSharpCode.BamlDecompiler/XmlnsDictionary.cs

@ -40,11 +40,11 @@ namespace ICSharpCode.BamlDecompiler @@ -40,11 +40,11 @@ namespace ICSharpCode.BamlDecompiler
Element = elem;
}
public string LookupXmlns(string fullAssemblyName, string clrNs)
public string LookupXmlns(string fullAssemblyName, string clrNs, string typeName = null)
{
foreach (var ns in this)
{
if (fullAssemblyName == ns.FullAssemblyName && ns.CLRNamespace == clrNs)
if (NamespaceMap.Matches(ns, fullAssemblyName, clrNs, typeName))
return ns.XMLNamespace;
}
@ -119,11 +119,11 @@ namespace ICSharpCode.BamlDecompiler @@ -119,11 +119,11 @@ namespace ICSharpCode.BamlDecompiler
return null;
}
public string LookupXmlns(string fullAssemblyName, string clrNs)
public string LookupXmlns(string fullAssemblyName, string clrNs, string typeName = null)
{
foreach (var map in piMappings)
{
if (fullAssemblyName == map.Value.FullAssemblyName && map.Value.CLRNamespace == clrNs)
if (NamespaceMap.Matches(map.Value, fullAssemblyName, clrNs, typeName))
return map.Key;
}
@ -132,7 +132,7 @@ namespace ICSharpCode.BamlDecompiler @@ -132,7 +132,7 @@ namespace ICSharpCode.BamlDecompiler
{
foreach (var ns in scope)
{
if (fullAssemblyName == ns.FullAssemblyName && ns.CLRNamespace == clrNs)
if (NamespaceMap.Matches(ns, fullAssemblyName, clrNs, typeName))
return ns.XMLNamespace;
}

86
ICSharpCode.Decompiler.Tests/ProjectDecompiler/ProjectFileWriterDefaultTests.cs

@ -0,0 +1,86 @@ @@ -0,0 +1,86 @@
// 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 ICSharpCode.Decompiler.CSharp;
using ICSharpCode.Decompiler.CSharp.ProjectDecompiler;
using ICSharpCode.Decompiler.Metadata;
using NUnit.Framework;
namespace ICSharpCode.Decompiler.Tests.ProjectDecompiler;
[TestFixture]
public sealed class ProjectFileWriterDefaultTests
{
/// <summary>
/// Item metadata as attributes is MSBuild 15 syntax. The non-SDK format exists for the
/// toolchains that came before it, and those reject an unknown attribute on an item element,
/// so metadata has to be written the way every non-SDK project writes it: as child elements.
/// </summary>
[Test]
public void ItemMetadataIsWrittenAsChildElements()
{
ProjectItemInfo[] files = [
new ProjectItemInfo("Page", "Themes/Generic.xaml")
.With("Generator", "MSBuild:Compile")
.With("SubType", "Designer"),
];
string project = WriteProjectFile(files);
using (Assert.EnterMultipleScope())
{
Assert.That(project, Does.Contain(@"<Page Include=""Themes/Generic.xaml"">"), project);
Assert.That(project, Does.Contain(@"<Generator>MSBuild:Compile</Generator>"), project);
Assert.That(project, Does.Contain(@"<SubType>Designer</SubType>"), project);
Assert.That(project, Does.Not.Contain(@"Generator="""), "metadata does not belong in an attribute");
}
}
[Test]
public void AnItemWithoutMetadataStaysOnOneLine()
{
ProjectItemInfo[] files = [new ProjectItemInfo("Compile", "Program.cs")];
string project = WriteProjectFile(files);
Assert.That(project, Does.Contain(@"<Compile Include=""Program.cs"" />"), project);
}
static string WriteProjectFile(ProjectItemInfo[] files)
{
StringWriter output = new();
ProjectFileWriterDefault.Instance.Write(output, new TestProjectInfoProvider(), files,
new PEFile("ICSharpCode.Decompiler.dll"));
return output.ToString();
}
sealed class TestProjectInfoProvider : IProjectInfoProvider
{
public IAssemblyResolver AssemblyResolver { get; } = new UniversalAssemblyResolver(null, false, null);
public IAssemblyReferenceClassifier AssemblyReferenceClassifier { get; } = new AssemblyReferenceClassifier();
public LanguageVersion LanguageVersion => LanguageVersion.Latest;
public bool CheckForOverflowUnderflow => false;
public Guid ProjectGuid { get; } = Guid.NewGuid();
public string TargetDirectory { get; } = Environment.CurrentDirectory;
public string StrongNameKeyFile => null;
}
}

5
ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterDefault.cs

@ -173,8 +173,11 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler @@ -173,8 +173,11 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler
w.WriteAttributeString("Include", item.FileName);
if (item.AdditionalProperties != null)
{
// Item metadata as attributes is MSBuild 15 syntax. This format is what an
// export targets when the toolchain predates that, so the metadata goes where
// every non-SDK project keeps it: in child elements.
foreach (var (key, value) in item.AdditionalProperties)
w.WriteAttributeString(key, value);
w.WriteElementString(key, value);
}
w.WriteEndElement();
}

53
ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs

@ -387,23 +387,26 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler @@ -387,23 +387,26 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler
string GetFileFileNameForHandle(TypeDefinitionHandle h)
{
var type = metadata.GetTypeDefinition(h);
string file = CleanUpFileName(metadata.GetString(type.Name), ".cs");
string ns = metadata.GetString(type.Namespace);
if (string.IsNullOrEmpty(ns))
{
return file;
}
else
// A code-behind class belongs to the document it completes: WPF tooling expects
// MainWindow.xaml.cs beside MainWindow.xaml, and treats a stray MainWindow.cs
// elsewhere in the tree as an unrelated file.
foreach (var partialType in partialTypes)
{
string dir = Settings.UseNestedDirectoriesForNamespaces ? CleanUpPath(ns) : CleanUpDirectoryName(ns);
if (directories.Add(dir))
if (partialType.DeclaringTypeDefinitionHandle == h && partialType.CompanionFileName != null)
{
var path = Path.Combine(TargetDirectory, dir);
CreateDirectory(path);
string companionDirectory = Path.GetDirectoryName(partialType.CompanionFileName)!;
if (!string.IsNullOrEmpty(companionDirectory) && directories.Add(companionDirectory))
CreateDirectory(Path.Combine(TargetDirectory, companionDirectory));
return partialType.CompanionFileName + ".cs";
}
return Path.Combine(dir, file);
}
var type = metadata.GetTypeDefinition(h);
string fileName = GetFileNameForType(metadata.GetString(type.Namespace), metadata.GetString(type.Name), ".cs");
string directory = Path.GetDirectoryName(fileName)!;
if (!string.IsNullOrEmpty(directory) && directories.Add(directory))
CreateDirectory(Path.Combine(TargetDirectory, directory));
return fileName;
}
void ProcessFiles(List<IGrouping<string, TypeDefinitionHandle>> files)
@ -972,6 +975,30 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler @@ -972,6 +975,30 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler
/// Removes invalid characters from file names and reduces their length,
/// but keeps file extensions and path structure intact.
/// </summary>
/// <summary>
/// The path of a file belonging to a type: the namespace becomes directories or one
/// flattened directory name, depending on
/// <see cref="DecompilerSettings.UseNestedDirectoriesForNamespaces"/>. Everything a type
/// owns - its C# file and the XAML document it is the code-behind of - goes here, so the
/// two end up next to each other.
/// </summary>
public static string GetFileNameForType(string @namespace, string typeName, string extension,
bool useNestedDirectoriesForNamespaces)
{
string file = CleanUpFileName(typeName, extension);
if (string.IsNullOrEmpty(@namespace))
return file;
string directory = useNestedDirectoriesForNamespaces
? CleanUpPath(@namespace)
: CleanUpDirectoryName(@namespace);
return Path.Combine(directory, file);
}
protected string GetFileNameForType(string @namespace, string typeName, string extension)
{
return GetFileNameForType(@namespace, typeName, extension, Settings.UseNestedDirectoriesForNamespaces);
}
public static string SanitizeFileName(string fileName)
{
return CleanUpName(fileName, separateAtDots: false, treatAsFileName: true, treatAsPath: true);

9
ICSharpCode.Decompiler/PartialTypeInfo.cs

@ -16,6 +16,8 @@ @@ -16,6 +16,8 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
@ -43,6 +45,13 @@ namespace ICSharpCode.Decompiler @@ -43,6 +45,13 @@ namespace ICSharpCode.Decompiler
public TypeDefinitionHandle DeclaringTypeDefinitionHandle { get; }
/// <summary>
/// The document this type is the code-behind of, as a project-relative path
/// ("Views/MainWindow.xaml"), where there is one. The project decompiler names the type's
/// C# file after it, so that the two sit next to each other the way the tooling expects.
/// </summary>
public string? CompanionFileName { get; set; }
public void AddDeclaredMember(IMember member)
{
declaredMembers.Add(member.MetadataToken);

28
ICSharpCode.ILSpyCmd.Tests/BamlFixtureTypes.cs

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
// 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.
namespace ICSharpCode.ILSpyCmd.Tests.Views
{
/// <summary>
/// Plays the role of a WPF code-behind class: the BAML fixture "views/deeppage.baml" names it
/// as its root, which is how the BAML decompiler recognises an x:Class type.
/// </summary>
public class DeepPage
{
}
}

6
ICSharpCode.ILSpyCmd.Tests/ICSharpCode.ILSpyCmd.Tests.csproj

@ -24,4 +24,10 @@ @@ -24,4 +24,10 @@
<ProjectReference Include="..\ICSharpCode.ILSpyCmd\ICSharpCode.ILSpyCmd.csproj" />
</ItemGroup>
<ItemGroup>
<!-- A BAML stream inside a .g.resources container, the way the WPF build puts one into an
assembly, so that exporting this assembly as a project has one to convert. -->
<EmbeddedResource Include="fixtures\test.g.resources" LogicalName="ICSharpCode.ILSpyCmd.Tests.g.resources" />
</ItemGroup>
</Project>

120
ICSharpCode.ILSpyCmd.Tests/ProjectExportBamlTests.cs

@ -0,0 +1,120 @@ @@ -0,0 +1,120 @@
// 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.IO;
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using static ICSharpCode.ILSpyCmd.Tests.CliTestRunner;
namespace ICSharpCode.ILSpyCmd.Tests
{
/// <summary>
/// This assembly carries "mainwindow.baml" as an embedded resource, so exporting it as a
/// project exercises what happens to a BAML stream on the way into the project.
/// </summary>
[TestFixture]
public class ProjectExportBamlTests
{
static readonly string testAssemblyPath = typeof(ProjectExportBamlTests).Assembly.Location;
string outputDirectory;
[SetUp]
public void SetUp()
{
outputDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
Directory.CreateDirectory(outputDirectory);
}
[TearDown]
public void TearDown()
{
if (Directory.Exists(outputDirectory))
Directory.Delete(outputDirectory, recursive: true);
}
string ProjectFileContent()
{
string projectFile = Directory.EnumerateFiles(outputDirectory, "*.csproj").Single();
return File.ReadAllText(projectFile);
}
[Test]
public async Task BamlBecomesXamlWithoutAskingForIt()
{
var result = await RunAsync(testAssemblyPath, "--disable-updatecheck", "-p", "-o", outputDirectory);
Assert.That(result.ExitCode, Is.EqualTo(0), result.Error);
string xamlFile = Path.Combine(outputDirectory, "mainwindow.xaml");
Assert.That(File.Exists(xamlFile), Is.True, "the BAML resource is exported as XAML");
Assert.That(File.ReadAllText(xamlFile), Does.Contain("Hello from BAML"));
}
[Test]
public async Task TheProjectNamesTheXamlAsAPage()
{
await RunAsync(testAssemblyPath, "--disable-updatecheck", "-p", "-o", outputDirectory);
string project = ProjectFileContent();
Assert.Multiple(() => {
Assert.That(project, Does.Contain("<Page Include=\"mainwindow.xaml\""));
Assert.That(project, Does.Not.Contain("mainwindow.baml"), "the raw stream is not carried along as well");
});
}
[Test]
public async Task TheCodeBehindSitsNextToItsDocument()
{
// WPF tooling pairs MainWindow.xaml with MainWindow.xaml.cs by name and location; a
// code-behind anywhere else is an unrelated file as far as the project is concerned.
await RunAsync(testAssemblyPath, "--disable-updatecheck", "-p", "-o", outputDirectory);
string documentDirectory = Path.Combine(outputDirectory, "ICSharpCode.ILSpyCmd.Tests.Views");
Assert.Multiple(() => {
Assert.That(File.Exists(Path.Combine(documentDirectory, "DeepPage.xaml")), Is.True, "the document");
Assert.That(File.Exists(Path.Combine(documentDirectory, "DeepPage.xaml.cs")), Is.True, "its code-behind");
});
}
[Test]
public async Task DocumentsFollowTheNamespaceDirectoriesToo()
{
await RunAsync(testAssemblyPath, "--disable-updatecheck", "-p", "--nested-directories", "-o", outputDirectory);
string documentDirectory = Path.Combine(outputDirectory, "ICSharpCode", "ILSpyCmd", "Tests", "Views");
Assert.Multiple(() => {
Assert.That(File.Exists(Path.Combine(documentDirectory, "DeepPage.xaml")), Is.True, "the document");
Assert.That(File.Exists(Path.Combine(documentDirectory, "DeepPage.xaml.cs")), Is.True, "its code-behind");
});
}
[Test]
public async Task TheOldOptInFlagStillWorks()
{
// It is documented and scripted against; asking for what is now the default has to
// keep meaning the same thing.
var result = await RunAsync(testAssemblyPath, "--disable-updatecheck", "-p", "-o", outputDirectory, "--decompile-baml");
Assert.That(result.ExitCode, Is.EqualTo(0), result.Error);
Assert.That(File.Exists(Path.Combine(outputDirectory, "mainwindow.xaml")), Is.True);
}
}
}

BIN
ICSharpCode.ILSpyCmd.Tests/fixtures/test.g.resources vendored

Binary file not shown.

6
ICSharpCode.ILSpyCmd/BamlAwareWholeProjectDecompiler.cs

@ -67,8 +67,10 @@ namespace ICSharpCode.ILSpyCmd @@ -67,8 +67,10 @@ namespace ICSharpCode.ILSpyCmd
: null;
if (typeDefinition != null)
{
xamlFileName = SanitizeFileName(typeDefinition.ReflectionName + ".xaml");
partialTypeInfo = new PartialTypeInfo(typeDefinition);
// Next to where the type's own C# file goes, so that the code-behind can be named
// after the document and land beside it.
xamlFileName = GetFileNameForType(typeDefinition.Namespace, typeDefinition.Name, ".xaml");
partialTypeInfo = new PartialTypeInfo(typeDefinition) { CompanionFileName = xamlFileName };
foreach (var member in result.GeneratedMembers)
partialTypeInfo.AddDeclaredMember(member);
}

27
ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs

@ -82,8 +82,8 @@ Examples: @@ -82,8 +82,8 @@ Examples:
Extract a single resource. If the name ends with .baml, the output is decompiled XAML; otherwise raw bytes.
ilspycmd sample.dll --resource sample.g.resources/mainwindow.baml -o c:\decompiled
Decompile assembly as a compilable project and convert all BAML resources to XAML Page items.
ilspycmd sample.dll -p -o c:\decompiled --decompile-baml
Decompile assembly as a compilable project. BAML resources become XAML Page items.
ilspycmd sample.dll -p -o c:\decompiled
")]
[HelpOption("-h|--help")]
[ProjectOptionRequiresOutputDirectoryValidation]
@ -145,7 +145,7 @@ Examples: @@ -145,7 +145,7 @@ Examples:
[Option("--resource <name>", "Extract a single resource by name (as printed by --list-resources). Resources whose name ends with '.baml' are decompiled to XAML.", CommandOptionType.SingleValue)]
public string ResourceName { get; }
[Option("--decompile-baml", "When used with -p, decompile BAML resources to XAML files (Page items) instead of leaving them as raw byte streams.", CommandOptionType.NoValue)]
[Option("--decompile-baml", "Deprecated: -p decompiles BAML resources to XAML files (Page items) on its own. Accepted so that existing scripts keep working.", CommandOptionType.NoValue)]
public bool DecompileBamlFlag { get; }
[Option("--dump-table <table>", "Dump a metadata table: prints RID, token, names, heap offsets and coded indexes of every row. <table> is the ECMA-335 table name (e.g. TypeDef, Property, MethodSemantics; case-insensitive) or table number (decimal or 0x-prefixed hex, e.g. 0x17).", CommandOptionType.SingleValue)]
@ -795,19 +795,14 @@ Examples: @@ -795,19 +795,14 @@ Examples:
}
var settings = GetSettings(module);
var debugInfo = TryLoadPDB(module);
WholeProjectDecompiler decompiler;
if (DecompileBamlFlag)
{
var bamlTypeSystem = new BamlDecompilerTypeSystem(module, resolver);
var bamlSettings = new BamlDecompilerSettings {
ThrowOnAssemblyResolveErrors = settings.ThrowOnAssemblyResolveErrors
};
decompiler = new BamlAwareWholeProjectDecompiler(settings, resolver, resolver, debugInfo, bamlTypeSystem, bamlSettings);
}
else
{
decompiler = new WholeProjectDecompiler(settings, resolver, null, resolver, debugInfo);
}
// A WPF assembly keeps its XAML as BAML, so a project exported without converting it
// back is missing every window and page it is made of.
var bamlTypeSystem = new BamlDecompilerTypeSystem(module, resolver);
var bamlSettings = new BamlDecompilerSettings {
ThrowOnAssemblyResolveErrors = settings.ThrowOnAssemblyResolveErrors
};
WholeProjectDecompiler decompiler = new BamlAwareWholeProjectDecompiler(settings, resolver, resolver,
debugInfo, bamlTypeSystem, bamlSettings);
ProjectId projectId;
using (var projectFileWriter = new StreamWriter(File.Create(projectFileName)))
projectId = decompiler.DecompileProject(module, Path.GetDirectoryName(projectFileName), projectFileWriter);

8
ICSharpCode.ILSpyCmd/README.md

@ -39,8 +39,8 @@ Options: @@ -39,8 +39,8 @@ Options:
containers are listed individually as '<container>/<entry>'.
--resource <name> Extract a single resource by name (as printed by --list-resources). Resources
whose name ends with '.baml' are decompiled to XAML.
--decompile-baml When used with -p, decompile BAML resources to XAML files (Page items) instead
of leaving them as raw byte streams.
--decompile-baml Deprecated: -p decompiles BAML resources to XAML files (Page items) on its
own. Accepted so that existing scripts keep working.
--dump-table <table> Dump a metadata table: prints RID, token, names, heap offsets and coded
indexes of every row. <table> is the ECMA-335 table name (e.g. TypeDef,
Property, MethodSemantics; case-insensitive) or table number (decimal or
@ -121,8 +121,8 @@ Examples: @@ -121,8 +121,8 @@ Examples:
Extract a single resource. If the name ends with .baml, the output is decompiled XAML; otherwise raw bytes.
ilspycmd sample.dll --resource sample.g.resources/mainwindow.baml -o c:\decompiled
Decompile assembly as a compilable project and convert all BAML resources to XAML Page items.
ilspycmd sample.dll -p -o c:\decompiled --decompile-baml
Decompile assembly as a compilable project. BAML resources become XAML Page items.
ilspycmd sample.dll -p -o c:\decompiled
```
## Generate HTML diagrammers

6
ILSpy.BamlDecompiler.Tests.Windows/Cases/EscapeSequence.xaml

@ -7,17 +7,17 @@ @@ -7,17 +7,17 @@
<TextBlock Width="100" Text="{Binding Path=Element[{http://planetsNS}DiameterKM].Value}" />
<TextBlock Width="100" Text="{Binding Path=Attribute[Name].Value}" />
<TextBlock Text="{Binding Path=Element[{http://planetsNS}Details].Value}" />
<TextBlock Text="{Binding Source={x:Static system:DateTime.Now}, StringFormat=Date: {0:dddd, MMMM dd}}" />
<TextBlock Text="{Binding Source={x:Static system:DateTime.Now}, StringFormat='Date: {0:dddd, MMMM dd}'}" />
<TextBlock Text="{Binding Source={x:Static system:DateTime.Now}, StringFormat=Time: {0:HH:mm}}" />
</StackPanel>
</DataTemplate>
</ResourceDictionary>
</FrameworkElement.Resources>
<TextBlock Text="{Binding Path=ActualWidth, StringFormat=Window width: {0:#,#.0}}" />
<TextBlock Text="{Binding Path=ActualWidth, StringFormat='Window width: {0:#,#.0}'}" />
<TextBlock Text="{Binding Path=ActualHeight, StringFormat=Window height: {0:C}}" />
<WrapPanel Margin="10">
<TextBlock Text="Width: " />
<TextBlock Text="{Binding ActualWidth, StringFormat={}{0:#,#.0}}" />
<TextBlock Text="{Binding ActualWidth, StringFormat='{}{0:#,#.0}'}" />
<StackPanel Margin="10">
<TextBlock Text="{Binding Source={x:Static system:DateTime.Now}, ConverterCulture=de-DE, StringFormat=German date: {0:D}}" />
<TextBlock Text="{Binding Source={x:Static system:DateTime.Now}, ConverterCulture=en-US, StringFormat=American date: {0:D}}" />

2
ILSpy.BamlDecompiler.Tests.Windows/Cases/MarkupExtension.xaml

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
<Label xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" DataContext="{Binding Blub}" Content="{Binding Path=Blah, StringFormat={}{0} items}">
<Label xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" DataContext="{Binding Blub}" Content="{Binding Path=Blah, StringFormat='{}{0} items'}">
<FrameworkElement.Style>
<Style />
</FrameworkElement.Style>

91
ILSpy.BamlDecompiler.Tests/FacadeAssemblyTests.cs

@ -0,0 +1,91 @@ @@ -0,0 +1,91 @@
// 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.IO;
using System.Reflection.PortableExecutable;
using ICSharpCode.BamlDecompiler;
using ICSharpCode.BamlDecompiler.Baml;
using ICSharpCode.Decompiler.Metadata;
using NUnit.Framework;
namespace ILSpy.BamlDecompiler.Tests
{
/// <summary>
/// .NET ships a WindowsBase facade on every platform, so the assembly resolves everywhere - but
/// it carries none of the types BAML means by it, because those live in the WindowsDesktop
/// runtime pack. A well-known type that only exists in the real assembly then resolves to
/// nothing, and the whole resource is lost: ten of the BAML entries in a DevExpress theme
/// assembly are unreadable on a machine without WPF for exactly this reason.
/// </summary>
[TestFixture]
public class FacadeAssemblyTests
{
static ushort TypeId(KnownTypes type) => unchecked((ushort)-(short)type);
static ushort MemberId(KnownMembers member) => unchecked((ushort)-(short)member);
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(FacadeAssemblyTests).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();
}
[Test]
public void ATypeOfTheRealWindowsBaseStillDecompiles()
{
// System.Windows.Size is a well-known BAML type of WindowsBase, and one the facade does
// not have.
string xaml = Decompile(CreateBaml(
new ElementStartRecord { TypeId = TypeId(KnownTypes.Button) },
new PropertyComplexStartRecord { AttributeId = MemberId(KnownMembers.Button_Content) },
new ElementStartRecord { TypeId = TypeId(KnownTypes.Size) },
new ElementEndRecord(),
new PropertyComplexEndRecord(),
new ElementEndRecord()));
Assert.That(xaml, Does.Contain("Size"), xaml);
}
}
}

5
ILSpy.BamlDecompiler.Tests/ILSpy.BamlDecompiler.Tests.csproj

@ -43,9 +43,14 @@ @@ -43,9 +43,14 @@
</ItemGroup>
<ItemGroup>
<Compile Include="FacadeAssemblyTests.cs" />
<Compile Include="InvalidXmlCharacterTests.cs" />
<Compile Include="MissingReferencesTests.cs" />
<Compile Include="MarkupExtensionQuotingTests.cs" />
<Compile Include="RuntimeNamePropertyTests.cs" />
<Compile Include="StartupUriTests.cs" />
<Compile Include="XmlNamespaceResolutionTests.cs" />
<Compile Include="XmlnsDeclarationPlacementTests.cs" />
</ItemGroup>
</Project>

133
ILSpy.BamlDecompiler.Tests/MarkupExtensionQuotingTests.cs

@ -0,0 +1,133 @@ @@ -0,0 +1,133 @@
// 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.IO;
using System.Reflection.PortableExecutable;
using ICSharpCode.BamlDecompiler;
using ICSharpCode.BamlDecompiler.Baml;
using ICSharpCode.BamlDecompiler.Xaml;
using ICSharpCode.Decompiler.Metadata;
using NUnit.Framework;
namespace ILSpy.BamlDecompiler.Tests
{
/// <summary>
/// A markup extension is written back as text that the XAML parser reads again, and its
/// grammar gives ',' '=' '{' '}' and the quote characters a meaning. A value carrying one of
/// them has to be quoted, or the parser splits it into name/value pairs that do not exist -
/// the reported case is DevExpress' {DXBinding Expr='...'}, whose expressions are full of
/// commas and equals signs.
/// </summary>
[TestFixture]
public class MarkupExtensionQuotingTests
{
static ushort TypeId(KnownTypes type) => unchecked((ushort)-(short)type);
static ushort MemberId(KnownMembers member) => unchecked((ushort)-(short)member);
[TestCase("plain", ExpectedResult = "plain", TestName = "AnOrdinaryValueIsLeftAlone")]
[TestCase("with space", ExpectedResult = "with space", TestName = "InteriorWhitespaceNeedsNoQuotes")]
[TestCase("a, b", ExpectedResult = "'a, b'", TestName = "CommaSeparatesArguments")]
[TestCase("a = b", ExpectedResult = "'a = b'", TestName = "EqualsStartsANamedArgument")]
[TestCase("{}{0} items", ExpectedResult = "'{}{0} items'", TestName = "AnEscapedLeadingBraceIsQuoted")]
[TestCase("{x:Static local:Thing.Value}", ExpectedResult = "{x:Static local:Thing.Value}", TestName = "ANestedExtensionStaysOne")]
[TestCase("Element[{http://ns}Name].Value", ExpectedResult = "Element[{http://ns}Name].Value", TestName = "BracesInsideAValueAreNotGrammar")]
[TestCase("Date: {0:dddd, MMMM dd}", ExpectedResult = "'Date: {0:dddd, MMMM dd}'", TestName = "ACommaInsideBracesStillSeparates")]
[TestCase("a}b", ExpectedResult = "'a}b'", TestName = "AStrayClosingBraceWouldEndTheExtension")]
[TestCase("a{b", ExpectedResult = "'a{b'", TestName = "AStrayOpeningBraceWouldStartAnExtension")]
[TestCase(" padded ", ExpectedResult = "' padded '", TestName = "EdgeWhitespaceWouldBeTrimmed")]
[TestCase("", ExpectedResult = "''", TestName = "AnEmptyValueNeedsToStayEmpty")]
[TestCase("it's", ExpectedResult = @"'it\'s'", TestName = "TheQuoteCharacterIsEscaped")]
[TestCase(@"back\slash", ExpectedResult = @"'back\\slash'", TestName = "TheEscapeCharacterIsEscaped")]
public string QuotingRules(string value)
{
return XamlUtils.QuoteMarkupExtensionValue(value);
}
/// <summary>
/// Decompiles against this test assembly as the main module: the BAML built here refers
/// only to well-known WPF types, which the decompiler resolves without it.
/// </summary>
static string Decompile(Stream baml)
{
var location = typeof(MarkupExtensionQuotingTests).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();
}
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;
}
/// <summary>
/// Builds "&lt;Button Content="{StaticResource &lt;value&gt;}" /&gt;", whose resource key is
/// the single argument of the extension.
/// </summary>
static string DecompileExtensionArgument(string value)
{
return Decompile(CreateBaml(
new ElementStartRecord { TypeId = TypeId(KnownTypes.Button) },
new StringInfoRecord { StringId = 0, Value = value },
new PropertyWithExtensionRecord {
AttributeId = MemberId(KnownMembers.Button_Content),
Flags = (ushort)KnownTypes.StaticResourceExtension,
ValueId = 0
},
new ElementEndRecord()));
}
[Test]
public void AnArgumentCarryingACommaIsQuoted()
{
string xaml = DecompileExtensionArgument("ctor, arg = with comma");
Assert.That(xaml, Does.Contain("{StaticResource 'ctor, arg = with comma'}"));
}
[Test]
public void AnOrdinaryArgumentIsNotQuoted()
{
// Quoting everything would change every document ILSpy prints today.
string xaml = DecompileExtensionArgument("MyResourceKey");
Assert.That(xaml, Does.Contain("{StaticResource MyResourceKey}"));
}
}
}

131
ILSpy.BamlDecompiler.Tests/RuntimeNamePropertyTests.cs

@ -0,0 +1,131 @@ @@ -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
{
/// <summary>
/// A type of the assembly being decompiled with a CLR property of its own called "Name".
/// </summary>
public class HelperWithItsOwnName
{
public string Name { get; set; }
}
/// <summary>
/// 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).
/// </summary>
[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
};
}
/// <summary>
/// A property of <paramref name="declaringType"/>, the way the decompiler resolves one from
/// the type a BAML attribute record names as its owner.
/// </summary>
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()
{
// <local:Helper Name="theName" />: 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()
{
// <local:MyControl x:Name="theName" />: 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);
}
}
}

130
ILSpy.BamlDecompiler.Tests/StartupUriTests.cs

@ -0,0 +1,130 @@ @@ -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
{
/// <summary>
/// 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).
/// </summary>
public class TestApplication
{
public Uri StartupUri { get; set; }
}
/// <summary>
/// What the markup compiler generates for an Application with a StartupUri.
/// </summary>
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);
}
}
/// <summary>
/// The same without one, which must stay without one.
/// </summary>
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();
}
/// <summary>
/// A document whose root is <paramref name="typeName"/> of this assembly, which is what
/// makes the BAML decompiler treat it as the code-behind class of the document.
/// </summary>
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);
}
}
}

118
ILSpy.BamlDecompiler.Tests/XmlnsDeclarationPlacementTests.cs

@ -0,0 +1,118 @@ @@ -0,0 +1,118 @@
// 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.IO;
using System.Reflection.PortableExecutable;
using ICSharpCode.BamlDecompiler;
using ICSharpCode.BamlDecompiler.Baml;
using ICSharpCode.Decompiler.Metadata;
using NUnit.Framework;
namespace ILSpy.BamlDecompiler.Tests
{
/// <summary>
/// A document that already binds a prefix to a CLR namespace must keep using it. The document
/// records the assembly the way it was written - "mscorlib" - while a well-known type carries
/// the assembly it actually resolves to, which is the implementation assembly of whatever
/// runtime ILSpy runs on. Comparing the two by name never matches, and every use of such a
/// type then declared a second prefix for a namespace the root already had (issue #2253).
/// </summary>
[TestFixture]
public class XmlnsDeclarationPlacementTests
{
const string MscorlibFullName = "mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089";
const string SystemNamespaceXmlns = "clr-namespace:System;assembly=mscorlib";
static ushort TypeId(KnownTypes type) => unchecked((ushort)-(short)type);
static ushort MemberId(KnownMembers member) => unchecked((ushort)-(short)member);
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(XmlnsDeclarationPlacementTests).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();
}
/// <summary>
/// Builds a document whose root binds "sys" to the System namespace of mscorlib and then
/// puts a System.String into the tree.
/// </summary>
static string DecompileDocumentUsingStringUnderAPrefixedRoot()
{
return Decompile(CreateBaml(
new AssemblyInfoRecord { AssemblyId = 0, AssemblyFullName = MscorlibFullName },
new ElementStartRecord { TypeId = TypeId(KnownTypes.Button) },
new XmlnsPropertyRecord {
Prefix = "sys",
XmlNamespace = SystemNamespaceXmlns,
AssemblyIds = new ushort[] { 0 }
},
new PropertyComplexStartRecord { AttributeId = MemberId(KnownMembers.Button_Content) },
new ElementStartRecord { TypeId = TypeId(KnownTypes.String) },
new ElementEndRecord(),
new PropertyComplexEndRecord(),
new ElementEndRecord()));
}
[Test]
public void ThePrefixTheDocumentDeclaredIsTheOneThatGetsUsed()
{
string xaml = DecompileDocumentUsingStringUnderAPrefixedRoot();
Assert.That(xaml, Does.Contain("<sys:String"), xaml);
}
[Test]
public void NoSecondPrefixIsDeclaredForANamespaceTheRootAlreadyBinds()
{
string xaml = DecompileDocumentUsingStringUnderAPrefixedRoot();
Assert.Multiple(() => {
Assert.That(xaml, Does.Not.Contain("xmlns:system="), xaml);
Assert.That(xaml, Does.Not.Contain("System.Private.CoreLib"), xaml);
});
}
}
}

11
ILSpy/TreeNodes/BamlResourceNodeFactory.cs

@ -79,16 +79,17 @@ namespace ICSharpCode.ILSpy.Baml @@ -79,16 +79,17 @@ namespace ICSharpCode.ILSpy.Baml
CancellationToken = context.DecompilationOptions.CancellationToken,
};
var result = decompiler.Decompile(stream);
// If the BAML root names a CLR partial-class type, prefer the type's reflection name
// for the .xaml file so it lines up with the matching .xaml.cs the C# project writer
// emits. Otherwise just swap extensions on the existing resource name.
// If the BAML root names a CLR partial-class type, the document goes where that type's
// own C# file goes, and the code-behind is then named after the document and lands
// beside it. Otherwise just swap extensions on the existing resource name.
var typeDefinition = result.TypeName.HasValue
? typeSystem.MainModule.GetTypeDefinition(result.TypeName.Value.TopLevelTypeName)
: null;
if (typeDefinition != null)
{
fileName = WholeProjectDecompiler.SanitizeFileName(typeDefinition.ReflectionName + ".xaml");
var partialTypeInfo = new PartialTypeInfo(typeDefinition);
fileName = WholeProjectDecompiler.GetFileNameForType(typeDefinition.Namespace, typeDefinition.Name, ".xaml",
context.DecompilationOptions.DecompilerSettings.UseNestedDirectoriesForNamespaces);
var partialTypeInfo = new PartialTypeInfo(typeDefinition) { CompanionFileName = fileName };
foreach (var member in result.GeneratedMembers)
partialTypeInfo.AddDeclaredMember(member);
context.AddPartialTypeInfo(partialTypeInfo);

Loading…
Cancel
Save