mirror of https://github.com/icsharpcode/ILSpy.git
Browse Source
Two of the defects reported on issue #2253 come from the decompiler writing text that means something else when it is read again. A markup extension is written as a single attribute value, and its grammar gives ',' '=' '{' '}' and the quote characters a meaning. Values went out unquoted, so an argument carrying any of them was read back as further name/value pairs: {DXBinding Expr='Price - Prev > 0 ? ...'}, the reported case, no longer compiles at all (MC3042, MC3045). Values without such a character stay unquoted, because quoting them would rewrite every document that never needed it. A clr-namespace declaration names the CLR namespace it maps, but nothing read that name out of it, so no lookup by namespace could match a declaration the document itself had made. Every type in such a namespace then got a second prefix declared on the element that used it. The assembly is the second half of the same lookup, and there the document records the name it was written against while a well-known type carries the assembly it resolves to now - "mscorlib" against "System.Private.CoreLib" - so the two are also accepted as the same when the recorded assembly forwards the type. Assisted-by: Claude:claude-opus-5:Claude Codepull/4108/head
12 changed files with 390 additions and 15 deletions
@ -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> |
<FrameworkElement.Style> |
||||||
<Style /> |
<Style /> |
||||||
</FrameworkElement.Style> |
</FrameworkElement.Style> |
||||||
|
|||||||
@ -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 "<Button Content="{StaticResource <value>}" />", 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}")); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -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); |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue