Whether an element is a markup extension is decided by walking its base
types, and that walk leaves the assemblies the document's own assembly
references: WPFLocalizeExtension's LocExtension, in the report, derives
from a type in XAMLMarkupExtensions, which the application never
references itself. Only the assemblies named by the module were loaded,
so the base type resolved to nothing, the extension was not recognised,
and it went out as an element tree carrying the decompiler's own
placeholder namespace - https://github.com/icsharpcode/ILSpy - into the
XAML.
The reference closure is now followed transitively. It is built once per
assembly, and the cost is bounded: for a .NET 8 WPF application the type
system grows from 87 to 158 modules and from 92 to 118 ms, for a .NET
Framework one from 9 to 24 modules and 39 to 44 ms. Over the 1158 BAML
documents of a DevExpress theme assembly the output does not change at
all - this only decides cases that used to resolve to nothing.
Assisted-by: Claude:claude-opus-5:Claude Code
Resolving one assembly resolves its whole reference closure, and every
reference in it asked the same framework directories the same questions.
The worst of it was the scan for the closest version folder of a shared
framework: a directory listing plus a recursive file search, repeated per
reference and per runtime pack - 42 scans for two distinct answers when
decompiling ICSharpCode.ILSpyX.dll.
The scan result is only safe to keep for a bounded time: a runtime can be
installed or removed while ILSpy runs, and reloading an assembly list has
to see that. So it is kept for the length of an explicitly opened scope,
which the type system opens around the closure it resolves and closes
again afterwards; outside a scope the file system is read as before. The
scope owns what was read, so two of them on one resolver do not stack -
the first to end takes it, and the other reads the file system again.
BeginSnapshot is on IAssemblyResolver rather than an interface of its
own: it is core functionality of a resolver, and one implementation is
not an abstraction. This breaks the interface for implementors outside
this repository, who opt out by returning null - which is what the three
resolvers here that hold nothing do.
The remaining probes cost nothing to fix: the preferred runtime pack was
listed among the defaults it already belongs to, so its directory was
scanned twice for every reference that is not in it, and one package
folder was probed once per assembly the package contains.
Measured over 27 references with a fresh resolver each time: 3.3 ms per
assembly before, 3.1 ms without a scope, 1.1 ms with one.
Assisted-by: Claude:claude-opus-5:Claude Code
Two things #2253 costs an exported application, both found by decompiling
one that was built from published source.
StartupUri is written in App.xaml but never reaches the BAML: the markup
compiler turns the attribute into an assignment inside
InitializeComponent, and the project decompiler deletes the generated
members. The exported application then builds and opens no window. The
assignment is read back out of the generated method and written to the
document root again. Where the compiler emits no app.baml at all -
App.xaml carrying nothing but attributes - there is no document to write
it on, and the export has no ApplicationDefinition either; that is a
larger gap of its own.
x:Name is recorded as the runtime name property of an element, which is
FrameworkElement.Name for everything WPF - a property of the framework.
Any property called "Name" on a type of the assembly being decompiled was
written back as the directive, so <local:Helper Name="theName" /> came
back as x:Name: the name gets registered and the property stays unset,
which still compiles and quietly means something else. The directive is
now written only for a name the element does not declare itself.
Assisted-by: Claude:claude-opus-5:Claude Code
.NET ships a WindowsBase facade on every platform. It resolves under the
name BAML means, so no synthetic stand-in was substituted for it, and it
carries none of the WPF types, because those live in the WindowsDesktop
runtime pack. System.Windows.Point and Size then resolved to nothing and
the whole resource was lost with a NullReferenceException - ten of the
BAML entries in one DevExpress theme assembly, on any machine without
WPF, which is every Linux and macOS user and every CI run.
A well-known assembly now counts as resolved only if it defines a type it
is expected to have, so a facade gives way to the stand-in the way an
assembly that does not resolve at all does. Nothing else changes: over
1158 documents of that assembly the output is identical, with the ten
that used to be lost added back.
Assisted-by: Claude:claude-opus-5:Claude Code
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 Code
Obfuscators put arbitrary characters into BAML strings, and XML 1.0 has no
representation for most control characters - a numeric character reference is invalid
for them too. Writing such a document threw ArgumentException from XmlWriter, which
loses the resource on project export and shows an exception instead of the page in the
UI. The escapes are spelled the way the C# output spells them, so one convention covers
both languages. Namespace URIs have to be escaped where the XNamespace is created rather
than in the final pass: the URI is baked into every element name built from it, so
patching only the xmlns declaration would desync the two. Characters XML can carry stay
untouched, so ordinary documents decompile byte-identically.
Every BAML stream of an assembly lives in one .resources container, and the recovery
around resource writing sat outside the loop over its entries, so a single page that
could not be written discarded every other page sharing the container with it.
Assisted-by: Claude:claude-opus-5:Claude Code
An assembly can map one CLR namespace to several XML namespaces: PresentationFramework
maps its namespaces to both the winfx/2006 and the netfx/2007 presentation namespace. The
fallback used when the BAML xmlns records name no namespace for a type always preferred the
winfx/2006 one, so a document that binds the default prefix to netfx/2007 ended up with a
root start tag that declares one presentation namespace and needs the other for its own
name, which XmlWriter rejects. Only the root carries the xmlns declaration, which is why
skipping XClassRewritePass worked around it.
Assisted-by: Claude:claude-opus-5:Claude Code
Dock 12.1.0.6 removed the public JsonConverterFactoryList/JsonConverterList<T>
converters from Dock.Serializer.SystemTextJson that ILSpyDockJson used to
deserialize IList<T> into ObservableCollection<T>. Dock now does the same
substitution with an internal JsonTypeInfo modifier that swaps CreateObject
for IList<T> enumerables. ILSpyDockJson mirrors that technique in its own
modifier chain, which also removes the per-element JsonSerializer.Serialize
side effect the old converter had.
Also bumps Xaml.Behaviors.Avalonia, ProDataGrid, AwesomeAssertions, CliWrap,
NUnit3TestAdapter, and the decompiler minor version to 11.1.
Assisted-by: Claude:claude-fable-5:Claude Code
* Set v11 RTM
* Update features in README.md
* Remove the two 900-iteration process-list scroll tests
* Keep Svg.Controls.Skia.Avalonia at 12.0.0.13
* 10.0.11 and Roslyn for net11p7
* Fix module-scan test failing when PowerShell's NGen images are stale
* Opt Pack NuGets out of the MSBuild server to fix SBOM generation
The BAML tests were orphaned: in no solution, no CI, and missing their
ICSharpCode.BamlDecompiler reference. Revive them split by platform,
mirroring ILSpy.Tests / ILSpy.Tests.Windows: ILSpy.BamlDecompiler.Tests
(net11.0) holds platform-agnostic tests - including a regression test that
decompiles with the WPF assemblies hidden, covering the missing-assembly
fix - and ILSpy.BamlDecompiler.Tests.Windows (net11.0-windows) holds the
WPF round-trip cases that compile XAML into BAML. Single-TFM projects keep
normal lock files; the Windows one declares all RIDs so its lock is
generatable and verifiable on any host.
Wire the cross-platform project into ILSpy.sln, ILSpy.XPlat.slnf and
ILSpy.Desktop.slnf so it builds and runs on the Linux/macOS legs; the
Windows project stays in ILSpy.sln (its tests execute in the Windows leg).
The Windows job also archives that assembly, whose embedded BAML lets the
decompiler be exercised against real BAML on a host without WPF.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Browsing an embedded .baml resource runs it through BamlReader.ReadDocument,
whose post-parse defer pass walks the record list with NavigateTree. That walk
recursed on every nested StaticResourceStart/KeyElementStart record with no
depth cap and indexed the record list with no bound. A crafted resource could
therefore drive recursion into a StackOverflowException -- which is uncatchable
and kills the process despite the resource node's try/catch -- or walk the index
off the end of the list. Both are reachable from ordinary resource browsing, no
project export required.
Consolidate the duplicated defer walk into one bounded, depth-capped helper that
fails with a catchable InvalidDataException, so the existing UI catch turns a
malformed resource into a "BAML decompilation failed" message instead of a crash.
Reject an oversized signature length before it drives a multi-gigabyte allocation
(it is read before the MSBAML check), and resolve defer offsets with TryGetValue
so a bogus offset reports malformed data rather than escaping as a bare
KeyNotFoundException.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Replace the Windows-only .bat helpers (clean / debugbuild / releasebuild /
restore / updatedeps and BuildTools/format) with cross-platform pwsh
scripts at the repo root: restore.ps1, build.ps1 (-Configuration), clean.ps1,
updatedeps.ps1 and BuildTools/format.ps1, alongside the existing publish.ps1.
Enable a packages.lock.json for every project by hoisting
RestorePackagesWithLockFile into the root Directory.Build.props (the four
core libraries set it individually before) and commit the generated locks,
so restores are repeatable and CI can cache packages off them.
Cache the NuGet packages folder in the three setup-dotnet workflows
(build-ilspy, build-frontends, codeql-analysis), keyed on the lock files
per the setup-dotnet caching guidance.
Scope the Debug "Verify package contents" check to the *.filelist outputs
it actually generates. A project's packages.lock.json is keyed only by
(framework, RID), with no host-OS axis, so a lock produced on Linux
legitimately differs from one produced on Windows whenever an OS-conditional
PackageReference applies (Debug+Windows pulls Microsoft.DiaSymReader*). The
Windows restore then rewrites those locks; that churn must not fail a step
whose job is to police the VSIX/MSI file lists.
Also drop the dead ILSpy.BamlDecompiler publish line from
publishlocaldev.ps1, mirroring the earlier publish.ps1 fix.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Strip the WPF ILSpy/, ILSpy.Tests/, and ILSpy.BamlDecompiler/ projects
and their solution entries so this commit can serve as the base for
replaying the rewritten Avalonia history on top. The BAML plugin needed
UseWpf + AvalonEdit and had a ProjectReference to ILSpy/ILSpy.csproj;
once both are gone it cannot build and would block bisect for every
intermediate commit. The Avalonia main app will absorb its three exports
(factory, file handler, entry node) in the later "BAML to XAML
decompiler integration" commit.
* .NET 11 RC2 minimal changes
* Heuristic for transport feed Roslyn selection
* Microsoft.CodeAnalysis.NetAnalyzers from main NuGet feed
* Use the VS2026 image
* Switch all test projects to net11
* Extract constants
* Include vsix with plain nuget.config files
* Basics of net8.0. Breaking unit tests expected.
* Missed that TestRunner project was already upgraded to net7.0 (search and replace fail)
* Use Preview 6 locally
* Use .NET 8.0 RTM
* Final fixups
---------
Co-authored-by: Christoph Wille <christoph.wille@gmail.com>