mirror of https://github.com/icsharpcode/ILSpy.git
Browse Source
Adds ICSharpCode.BamlDecompiler as a ProjectReference, ports IResourceFileHandler + ResourceFileHandlerContext from WPF, and plumbs both through: Assisted-by: Claude:claude-opus-4-7:Claude Codepull/3755/head
7 changed files with 473 additions and 1 deletions
@ -0,0 +1,138 @@
@@ -0,0 +1,138 @@
|
||||
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// 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.Linq; |
||||
|
||||
using Avalonia.Headless.NUnit; |
||||
|
||||
using AwesomeAssertions; |
||||
|
||||
using ICSharpCode.Decompiler; |
||||
using ICSharpCode.Decompiler.Metadata; |
||||
using ICSharpCode.ILSpyX.Abstractions; |
||||
|
||||
using ILSpy; |
||||
using ILSpy.AppEnv; |
||||
using ILSpy.Baml; |
||||
using ILSpy.Languages; |
||||
using ILSpy.TreeNodes; |
||||
using ILSpy.Views; |
||||
|
||||
using NUnit.Framework; |
||||
|
||||
using IResourceFileHandler = ILSpy.Languages.IResourceFileHandler; |
||||
using ResourceFileHandlerContext = ILSpy.Languages.ResourceFileHandlerContext; |
||||
|
||||
namespace ICSharpCode.ILSpy.Tests; |
||||
|
||||
[TestFixture] |
||||
public class BamlResourceTests |
||||
{ |
||||
// Boot composition once per test so AppComposition.Current is live for the MEF lookups
|
||||
// the factory/handler depend on. Resolving the MainWindow type forces App.Initialize.
|
||||
static void EnsureComposition() => AppComposition.Current.GetExport<MainWindow>(); |
||||
|
||||
[AvaloniaTest] |
||||
public void Factory_Creates_BamlResourceEntryNode_For_Baml_Resource() |
||||
{ |
||||
// The MEF-discovered BamlResourceNodeFactory must claim any `.baml`-named resource
|
||||
// and wrap it in a BamlResourceEntryNode, regardless of the bytes inside the stream.
|
||||
// Routing is name-based, mirroring the XML / image / cursor factories — actual BAML
|
||||
// parsing happens lazily when the user selects the node.
|
||||
|
||||
// Arrange — boot composition; build a dummy .baml-named resource (bytes don't matter
|
||||
// for the routing decision).
|
||||
EnsureComposition(); |
||||
var resource = new ByteArrayResource("Pages/MyWindow.baml", new byte[] { 0, 1, 2 }); |
||||
|
||||
// Act — dispatch through the central resource factory pipeline.
|
||||
var node = ResourceEntryNode.Create(resource); |
||||
|
||||
// Assert — comes back as a BamlResourceEntryNode, not the generic fallback.
|
||||
node.GetType().Should().Be(typeof(BamlResourceEntryNode)); |
||||
} |
||||
|
||||
[AvaloniaTest] |
||||
public void Factory_Ignores_Non_Baml_Resource_Names() |
||||
{ |
||||
// The BAML factory must only claim `.baml` resources. Anything else flows through
|
||||
// to other factories or the generic ResourceTreeNode fallback. A misfire here would
|
||||
// hijack arbitrary streams and crash on first selection (XamlDecompiler.Decompile
|
||||
// throws on non-BAML input).
|
||||
|
||||
// Arrange + Act — feed a non-baml name straight to the BAML factory.
|
||||
var factory = new BamlResourceNodeFactory(); |
||||
var node = factory.CreateNode(new ByteArrayResource("readme.txt", new byte[] { 0, 1, 2 })); |
||||
|
||||
// Assert — factory yields null so the dispatcher tries the next candidate.
|
||||
node.Should().BeNull(); |
||||
} |
||||
|
||||
[AvaloniaTest] |
||||
public void File_Handler_Claims_Baml_Extension_Case_Insensitive() |
||||
{ |
||||
// During project export the handler is consulted via CanHandle; first claim wins.
|
||||
// The match is case-insensitive on the extension (`.BAML` from an obfuscated assembly
|
||||
// must still produce a XAML output).
|
||||
|
||||
// Arrange — boot composition; build a context with a default DecompilationOptions
|
||||
// (the handler doesn't touch the options for CanHandle).
|
||||
EnsureComposition(); |
||||
var handler = new BamlResourceFileHandler(); |
||||
var context = new ResourceFileHandlerContext(new DecompilationOptions()); |
||||
|
||||
// Act + Assert — three positive variants and two rejections.
|
||||
handler.CanHandle("MainWindow.baml", context).Should().BeTrue(); |
||||
handler.CanHandle("themes/Generic.BAML", context).Should().BeTrue(); |
||||
handler.CanHandle("Resources/Strings.baml", context).Should().BeTrue(); |
||||
handler.CanHandle("readme.txt", context).Should().BeFalse(); |
||||
handler.CanHandle("Window.xaml", context).Should().BeFalse(); |
||||
} |
||||
|
||||
[AvaloniaTest] |
||||
public void File_Handler_Reports_Page_Entry_Type() |
||||
{ |
||||
// EntryType drives the MSBuild item group the produced file lands in. BAML→XAML
|
||||
// must go under `<Page>` so MSBuild's XAML compiler picks it up and regenerates the
|
||||
// matching .g.cs partial on rebuild — same as the WPF side.
|
||||
|
||||
// Arrange + Act + Assert — exact string match (case-sensitive; MSBuild element names
|
||||
// are case-insensitive in practice but the canonical form is "Page").
|
||||
new BamlResourceFileHandler().EntryType.Should().Be("Page"); |
||||
} |
||||
|
||||
[AvaloniaTest] |
||||
public void Mef_Discovers_Baml_Factory_And_File_Handler() |
||||
{ |
||||
// Both exports must surface through AppComposition. If `[Export]` / `[Shared]` got
|
||||
// dropped during a future refactor the pane would silently dump raw bytes for .baml
|
||||
// and project export would emit a bare `EmbeddedResource` instead of a `<Page>` —
|
||||
// detect that regression here.
|
||||
|
||||
// Arrange — boot composition.
|
||||
EnsureComposition(); |
||||
|
||||
// Act — pull every resource-node-factory and every file-handler from the container.
|
||||
var factories = AppComposition.Current.GetExports<IResourceNodeFactory>().ToList(); |
||||
var handlers = AppComposition.Current.GetExports<IResourceFileHandler>().ToList(); |
||||
|
||||
// Assert — at least one of each is our BAML implementation.
|
||||
factories.Should().ContainItemsAssignableTo<BamlResourceNodeFactory>(); |
||||
handlers.Should().ContainItemsAssignableTo<BamlResourceFileHandler>(); |
||||
} |
||||
} |
||||
@ -0,0 +1,81 @@
@@ -0,0 +1,81 @@
|
||||
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// 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.Collections.Generic; |
||||
using System.IO; |
||||
|
||||
using ICSharpCode.Decompiler; |
||||
using ICSharpCode.ILSpyX; |
||||
|
||||
namespace ILSpy.Languages |
||||
{ |
||||
/// <summary>
|
||||
/// Plug-in contract for converting a resource entry from its raw byte form into a
|
||||
/// language-specific source file during project export. Implementations are MEF-discovered
|
||||
/// via <c>[Export(typeof(IResourceFileHandler))]</c> and consulted by
|
||||
/// <see cref="CSharpLanguage"/>'s project-export path; the first one to <see cref="CanHandle"/>
|
||||
/// the resource wins. The handler returns the on-disk file name it emitted so the project
|
||||
/// file picks the right MSBuild item group + Generator / SubType properties.
|
||||
/// </summary>
|
||||
public interface IResourceFileHandler |
||||
{ |
||||
/// <summary>
|
||||
/// MSBuild item type for the produced project entry — <c>"Page"</c> for BAML→XAML,
|
||||
/// <c>"EmbeddedResource"</c> for the default raw fallback, etc.
|
||||
/// </summary>
|
||||
string EntryType { get; } |
||||
|
||||
/// <summary>Returns true when this handler can process the named resource.</summary>
|
||||
bool CanHandle(string name, ResourceFileHandlerContext context); |
||||
|
||||
/// <summary>
|
||||
/// Writes the decoded resource into the project directory (taken from
|
||||
/// <see cref="ResourceFileHandlerContext.DecompilationOptions"/>) and returns the
|
||||
/// resulting file name (relative to that directory). May add partial-type info to
|
||||
/// <paramref name="context"/> for downstream WPF page x:Class binding.
|
||||
/// </summary>
|
||||
string WriteResourceToFile(LoadedAssembly assembly, string fileName, Stream stream, ResourceFileHandlerContext context); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Per-project-export context shared across every <see cref="IResourceFileHandler"/>
|
||||
/// invocation in a single export run. Collects partial-type info and additional MSBuild
|
||||
/// properties; <see cref="CSharpLanguage"/>'s ILSpyWholeProjectDecompiler stitches them
|
||||
/// into the produced .csproj.
|
||||
/// </summary>
|
||||
public class ResourceFileHandlerContext |
||||
{ |
||||
readonly List<ICSharpCode.Decompiler.PartialTypeInfo> partialTypes = new(); |
||||
internal List<ICSharpCode.Decompiler.PartialTypeInfo> PartialTypes => partialTypes; |
||||
|
||||
readonly Dictionary<string, string> additionalProperties = new(); |
||||
public Dictionary<string, string> AdditionalProperties => additionalProperties; |
||||
|
||||
public DecompilationOptions DecompilationOptions { get; } |
||||
|
||||
public ResourceFileHandlerContext(DecompilationOptions options) |
||||
{ |
||||
this.DecompilationOptions = options; |
||||
} |
||||
|
||||
public void AddPartialTypeInfo(ICSharpCode.Decompiler.PartialTypeInfo info) |
||||
{ |
||||
this.PartialTypes.Add(info); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,80 @@
@@ -0,0 +1,80 @@
|
||||
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// 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 ICSharpCode.BamlDecompiler; |
||||
using ICSharpCode.Decompiler; |
||||
|
||||
using ILSpy.Languages; |
||||
using ILSpy.TextView; |
||||
using ILSpy.TreeNodes; |
||||
|
||||
namespace ILSpy.Baml |
||||
{ |
||||
/// <summary>
|
||||
/// Tree node for a single <c>.baml</c> resource entry inside a WPF/UWP assembly's
|
||||
/// resource file. Selecting the node decompiles the BAML stream into XAML on a
|
||||
/// background thread and emits the resulting XML into the editor (XML highlighting
|
||||
/// kicks in via <c>SyntaxExtensionOverride</c>).
|
||||
/// </summary>
|
||||
public sealed class BamlResourceEntryNode : ResourceEntryNode |
||||
{ |
||||
public BamlResourceEntryNode(string key, Func<Stream> openStream) |
||||
: base(key, openStream) |
||||
{ |
||||
} |
||||
|
||||
public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) |
||||
{ |
||||
try |
||||
{ |
||||
LoadBaml(output, options); |
||||
// Surface the rendered XAML as XML in the editor so the highlighter kicks in
|
||||
// and the foldings strategy collapses nested elements. The smart-output sink
|
||||
// reads this override before falling back to the language's FileExtension.
|
||||
if (output is AvaloniaEditTextOutput edit) |
||||
edit.SyntaxExtensionOverride = ".xml"; |
||||
} |
||||
catch (Exception ex) |
||||
{ |
||||
language.WriteCommentLine(output, "BAML decompilation failed:"); |
||||
output.WriteLine(ex.ToString()); |
||||
} |
||||
} |
||||
|
||||
void LoadBaml(ITextOutput output, DecompilationOptions options) |
||||
{ |
||||
var assemblyNode = this.Ancestors().OfType<AssemblyTreeNode>().First(); |
||||
var assembly = assemblyNode.LoadedAssembly; |
||||
using var data = OpenStream(); |
||||
var typeSystem = new BamlDecompilerTypeSystem( |
||||
assembly.GetMetadataFileOrNull()!, |
||||
assembly.GetAssemblyResolver()); |
||||
var decompiler = new XamlDecompiler(typeSystem, new BamlDecompilerSettings { |
||||
ThrowOnAssemblyResolveErrors = options.DecompilerSettings.ThrowOnAssemblyResolveErrors, |
||||
}) { |
||||
CancellationToken = options.CancellationToken, |
||||
}; |
||||
var result = decompiler.Decompile(data); |
||||
output.Write(result.Xaml.ToString()); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,108 @@
@@ -0,0 +1,108 @@
|
||||
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// 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.Composition; |
||||
using System.IO; |
||||
|
||||
using ICSharpCode.BamlDecompiler; |
||||
using ICSharpCode.Decompiler; |
||||
using ICSharpCode.Decompiler.CSharp.ProjectDecompiler; |
||||
using ICSharpCode.Decompiler.Metadata; |
||||
using ICSharpCode.ILSpyX; |
||||
using ICSharpCode.ILSpyX.Abstractions; |
||||
|
||||
using ILSpy.Languages; |
||||
using ILSpy.TreeNodes; |
||||
|
||||
using IResourceFileHandler = ILSpy.Languages.IResourceFileHandler; |
||||
using ResourceFileHandlerContext = ILSpy.Languages.ResourceFileHandlerContext; |
||||
|
||||
namespace ILSpy.Baml |
||||
{ |
||||
/// <summary>
|
||||
/// Tree-node factory that claims <c>.baml</c> resource entries inside an embedded
|
||||
/// <c>.resources</c> file and wraps them in a <see cref="BamlResourceEntryNode"/>.
|
||||
/// Discovered by MEF via <see cref="ILSpyTreeNode.ResourceNodeFactories"/>.
|
||||
/// </summary>
|
||||
[Export(typeof(IResourceNodeFactory))] |
||||
[Shared] |
||||
public sealed class BamlResourceNodeFactory : IResourceNodeFactory |
||||
{ |
||||
public ITreeNode? CreateNode(Resource resource) |
||||
{ |
||||
if (resource.Name.EndsWith(".baml", StringComparison.OrdinalIgnoreCase)) |
||||
return new BamlResourceEntryNode(resource.Name, () => resource.TryOpenStream() ?? Stream.Null); |
||||
return null; |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Project-export resource handler that intercepts <c>.baml</c> entries during
|
||||
/// "Save Code" / project export, decompiles them to XAML, and writes them as
|
||||
/// MSBuild <c><Page></c> items. The accompanying generated code-behind
|
||||
/// partial-class members are surfaced via <see cref="PartialTypeInfo"/> so the
|
||||
/// CSharp project decompiler emits matching <c>InitializeComponent</c> stubs.
|
||||
/// </summary>
|
||||
[Export(typeof(IResourceFileHandler))] |
||||
[Shared] |
||||
public sealed class BamlResourceFileHandler : IResourceFileHandler |
||||
{ |
||||
public string EntryType => "Page"; |
||||
|
||||
public bool CanHandle(string name, ResourceFileHandlerContext context) |
||||
=> name.EndsWith(".baml", StringComparison.OrdinalIgnoreCase); |
||||
|
||||
public string WriteResourceToFile(LoadedAssembly assembly, string fileName, Stream stream, ResourceFileHandlerContext context) |
||||
{ |
||||
var typeSystem = new BamlDecompilerTypeSystem( |
||||
assembly.GetMetadataFileOrNull()!, |
||||
assembly.GetAssemblyResolver()); |
||||
var decompiler = new XamlDecompiler(typeSystem, new BamlDecompilerSettings { |
||||
ThrowOnAssemblyResolveErrors = context.DecompilationOptions.DecompilerSettings.ThrowOnAssemblyResolveErrors, |
||||
}) { |
||||
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.
|
||||
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); |
||||
foreach (var member in result.GeneratedMembers) |
||||
partialTypeInfo.AddDeclaredMember(member); |
||||
context.AddPartialTypeInfo(partialTypeInfo); |
||||
} |
||||
else |
||||
{ |
||||
fileName = Path.ChangeExtension(fileName, ".xaml"); |
||||
} |
||||
context.AdditionalProperties.Add("Generator", "MSBuild:Compile"); |
||||
context.AdditionalProperties.Add("SubType", "Designer"); |
||||
var saveFileName = Path.Combine(context.DecompilationOptions.SaveAsProjectDirectory!, fileName); |
||||
Directory.CreateDirectory(Path.GetDirectoryName(saveFileName)!); |
||||
result.Xaml.Save(saveFileName); |
||||
return fileName; |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue