Only vbc's On Error lowering produces this filter; structured
Catch...When filters call SetProjectError and never reach it. Roslyn
emits it from a single code path with System.Exception as the type, and
legacy vbc output has the same shape, so there is no chain of unknown
conjuncts to walk. The transform runs before the expression transforms
canonicalize comparisons, so the filter still compares with cgt against
ldnull and cgt.un against zero: the ldnull comparison is fixed up first,
and MatchCompUnsignedZero accepts both the unsigned and canonical form.
Assisted-by: Claude:claude-opus-5:Claude Code
vbc builds an exception filter as a single non-short-circuiting
expression: `isinst`, then the user's `when` conditions, combined with
`and`. DetectCatchWhenConditionBlocks only knew the branch chain csc
emits, so the type test stayed inside the filter and the handler kept the
`object` variable it has in IL:
catch (object obj) when ((obj is Exception) & (num2 != 0) & (num == 0))
{
ProjectData.SetProjectError((Exception)obj);
A catch type has to derive from Exception, so that does not compile.
The conjunction form is matched too now, lifting the test to the catch
type as the block form already does. The other conjuncts stop running for
a non-matching exception once the test moves, so the filter has to be
pure for this to be invisible; PropagateExceptionVariable then drops the
castclass in the handler:
catch (Exception ex) when ((num2 != 0) & (num == 0))
{
ProjectData.SetProjectError(ex);
Closes#3659
The WPF tree did this, and the Avalonia port kept the routine but never
wired it up: the row template's expander binds IsExpanded straight to the
node, so no expansion reached the control and HandleExpanding sat with no
callers. Expanding a row near the bottom of the pane left its children
off screen.
The rule copies the native Windows tree control: scroll far enough to
show the new children, but stop at the expanded node so it never leaves
the viewport, and do not move at all when the children already fit. The
reveal now hangs off user gestures only -- the expander's Click and the
keyboard cases -- because the paths that expand nodes programmatically
position the viewport themselves afterwards, which is what the removed
doNotScrollOnExpanding flag used to arrange.
Assisted-by: Claude:claude-opus-5:Claude Code
Sorting reorders the assembly list in place and reports it as a Move, which
nothing downstream could act on: the tree node's handler had cases for Add,
Remove and Reset only, and neither the child collection nor the flattener had a
move at all. The rows therefore kept their pre-sort order until something else
forced a rebuild. Moving the node rather than removing and re-inserting it keeps
its identity, so an expanded subtree stays expanded and its row is not rebuilt.
A run longer than one row has to be reported the way the consumer reads it:
Avalonia's VirtualizingStackPanel applies a ranged move by removing OldItems.Count
rows and re-inserting them at NewStartingIndex - (Count - 1), so a run reported by
its final start index lands short by its own length. For a single row - a
collapsed node, and every move a sort makes - the two readings coincide.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
When an IfInstruction was created with resultType=bool, because only
values 0 or 1 are possible, then we need to preserve this property in
transforms -- otherwise the added correctness test would fail.
A finding is recorded before the reference context exists, because the resolver
keeps discovering references until the assembly is done. An assembly that fails
in the type system returns without ever reporting its resolutions, so its keys
stayed pending and the next assembly stamped its own reference set onto them.
Naming the wrong references is worse than naming none, in a report that is read
precisely to tell a reference problem from a decompiler defect.
Assisted-by: Claude:claude-opus-5:Claude Code
A finding named the package, the assembly's file name and the type, which is
not enough to reproduce it: the assembly lives in a version-specific cache
directory, and the references it was decompiled against are the whole question
whenever a warning turns out to be a reference problem rather than a defect.
That judgement had to be made by rerunning the package under NUGETFUZZ_VERBOSE
and reading the console, which the ledger of a catalog sweep cannot offer at
all.
The reference set is captured once the assembly is finished rather than when a
finding is first hit. The resolver keeps discovering references for as long as
it decompiles, so a finding from the first type would otherwise record a set
that is mostly still empty.
Ledger lines written before this field existed deserialize with it empty and
render as they did.
Assisted-by: Claude:claude-opus-5:Claude Code
A sweep covers thousands of packages, and the same simple assembly name
ships in many of them and in several TFM folders of a single package, so
a finding keyed on the file name alone cannot be traced back to the
assembly it came from.
Assisted-by: Claude:claude-opus-5:Claude Code
Declining every type-parameter operand was too broad. The reason a type
parameter has no lambda spelling is that `v == other` is CS0019 and boxing
both operands compares box identity where the tree compares values - and both
only apply while the parameter may be a value type. A parameter constrained to
a reference type compares as a reference, which is what the tree asks for and
what `t == null` spells, so it converts like any other reference comparison.
IsReferenceType is the distinction the type system already makes here:
TypeUtils.GetStackType maps a type parameter to Obj or VT by the same
question. An unconstrained parameter answers null and keeps declining.
Assisted-by: Claude:claude-opus-5:Claude Code
The decompiler emits comments - //IL_ warnings, "Could not convert
BlockContainer", "try-fault", a Nop's comment - as an EmptyStatement in the
middle of a statement sequence. Every transform that walks such a sequence
then stops recognizing its pattern the moment one of those lands in it:
constructor initializers stay in the body, `using var` and `for` are not
introduced, and a finalizer keeps its `override Finalize` shape, which does
not compile at all.
The destructor matcher moves the placeholders it skipped into the body that
replaces the old one, so the warning that caused the problem is not dropped
along with the statement carrying it.
Assisted-by: Claude:claude-opus-5:Claude Code
A constructor store to an auto-property's backing field is expressible
after the field declaration is gone in one of two ways: ReplaceBackingFieldUsage
rewrites it to an assignment of a setter-less property, or
TransformFieldAndConstructorInitializers lifts it into a property initializer.
A deconstruction target assigns several members at once, so it can never take
the second route, and a property that kept a setter would invoke that setter
instead of storing the field. Without the restriction the declaration is
removed while the store keeps referencing it.
Assisted-by: Claude:claude-opus-5:Claude Code
The WPF host kept the name prompt open and said the name was taken; the port
kept the resource string but dropped the message, and the New / Clone / Rename
/ Add-preconfigured handlers only return early on a collision. Nothing tells
the user why the list they just named did not appear, which reads as the dialog
having accepted the name.
The check belongs in the prompt, where the name is entered: OK stays disabled
while the name collides, so no flow can be handed one. Rename passes the name
of the list being renamed as allowed, because that is a no-op rather than a
collision with itself.
Assisted-by: Claude:claude-opus-5:Claude Code
decompdiff counted the substring "<>" in the output text, which both missed
mangled names that do not contain it ("VB$AnonymousType_0", "<Main>$") and
counted every generic argument list ending in an identifier character. The
shape is matched lexically there because only text is available; nugetfuzz has
the syntax tree, so it applies the decompiler's own identifier rule
(EscapeInvalidIdentifiers.IsValid) to the tree's identifiers instead, and any
hit is output that does not compile. Findings collapse to the shape of the
name because the bracketed part and the digits vary per occurrence, so one
unfolded construct stays one finding rather than one per member it hit.
The report's kind list is also the render loop's only source of sections, so
the PDB bucket added with the PDB verification mode never reached the HTML.
Assisted-by: Claude:claude-opus-5:Claude Code
The async stepping blob's first field is the compiler-generated catch
handler's IL offset plus one, and 0 when there is nothing to record. ILSpy
wrote the raw offset, so a consumer decoding it as (value - 1) resolved an
address in the middle of an instruction: Mono.Cecil throws
ArgumentNullException while reading the body, which is why the reported
assembly opened in ILSpy but killed ILLink on every MoveNext it had.
CatchHandlerOffset now holds the offset it is named after, or -1 for "none",
and BuildBlob applies the bias - the same model the compiler uses.
It is recorded only where an escaping exception is unlikely to be observed:
an async void method, and an async entry point. Recording it more widely
would be worse than recording it nowhere, because an async Task method
returns its exception through the Task and the debugger would then break on
exceptions user code catches. Measured over csc 1.3.2 to 5.10: async void is
handler+1 and a normal async Task is 0 in every version; only async Main
changed, from 0 to handler+1 between 2.10 and 3.11.
The entry point token names the synchronous '<Main>' shim that exists because
the runtime will not take .entrypoint on an async method, so the method to
record is the one the shim calls. Reading that call is exact; matching the
shim's siblings by name is not, and gets a 'Main' overload beside the real
entry point wrong.
Both tests compare against the compiler's own PDB for the same assembly, so
the blobs describe the same IL and the field compares directly.
Assisted-by: Claude:claude-opus-5:Claude Code
The PDB writer had no coverage beyond hand-written fixtures whose sequence
points are compared to the compiler's, and nothing ever asked whether a
consumer can read what it emits. Issue #2823 is the consequence: a PDB that
loads fine in ILSpy kills ILLink, and it took a reporter's own tool to find
out.
--pdb reuses the corpus, download and reference-pack machinery already in
nugetfuzz and replaces the type sweep with two checks: Mono.Cecil - the
consumer ILLink uses - has to read every method body through the generated
PDB, and a lint has to find everything the PDB claims true of the assembly.
--pdb-lint exists because a lint is only worth its findings if it is silent
on correct input. It runs the same checks against a PDB the compiler wrote,
and three of the checks written here were wrong until it said so - including
the async one, which flagged the compiler's own PDB for any executable with
an async Main, a case 190 nuget packages could not contain because a library
has no entry point.
Assisted-by: Claude:claude-opus-5:Claude Code
Switching to another assembly list replaces the whole tree, but nothing
is removed from the outgoing list - the list itself goes away - so no
collection event announces it and the selection, the open tabs and the
navigation history all kept describing a list that was no longer on
screen. The switch now says so with the same Reset that clearing a list
raises, which is the one path that already discards all of it.
Emptying a tab kept its title, because the CurrentNodes setter only
recomputes the cached base title and StartDecompile returns early with
nothing to decompile. A tab showing an empty document went on naming the
member it used to show.
A removal that took the selected node with it left nothing selected,
even with assemblies still loaded: the tree view picks the nearest
survivor for its own Delete gesture, but a removal from anywhere else
did not. The selection is handed over once the tree has caught up.
Assisted-by: Claude:claude-opus-5:Claude Code
A member ID was resolved against the loaded assemblies with the
reference assemblies filtered out entirely, so the assembly set a
project's references make up - which is what the VS add-in passes -
left every target unresolved. The lookup now prefers assemblies that
carry a body and falls back to the reference assemblies, which do
declare the member; the banner already says what the reader is looking
at.
An ID that names nothing anywhere used to leave the tree untouched and
say nothing, which is indistinguishable from a jump to the wrong place.
It now names the target and the assemblies that were searched, rather
than selecting an arbitrary one of them. The report prefers the pane the
jump would have filled and falls back to a tab of its own, because
ShowText writes to the active decompiler tab and does nothing at all
when the active content is something else - a metadata table, or nothing
yet at startup, which is exactly when this report is written.
The test fixture is this project's own reference assembly: the compiler
writes one carrying the ReferenceAssembly attribute and the same members
as the output, which is the pair a targeting pack and its runtime form,
and keeps the test off machine-specific NuGet paths.
Assisted-by: Claude:claude-opus-5:Claude Code
Removing assemblies one at a time cleaned up the tabs that showed them,
but clearing the whole list did not: Clear() raises a Reset whose
OldItems is null, and the handler pruned the history and returned before
the loop that empties the main tab and closes the orphaned ones. The
last decompiled member stayed on screen over an empty list.
Sorting had to be fixed first. It rebuilt the collection through Clear()
plus AddRange, so it raised the same Reset and dropped the whole
navigation history as a side effect - and once Reset closes tabs, it
would have closed all of those too. It now reorders in place, and a Move
is ignored where a removal would be handled, because a sort removes
nothing.
Assisted-by: Claude:claude-opus-5:Claude Code
Reusing whatever Release build a checkout happened to carry made a run
measure code that neither side is on, and the only signal was a
timestamp in the header line that is easy to read past. Building is now
what happens unless --no-build asks for the fast path, which repeated
runs against unchanged sides still want; it says which dll it reused and
when that was built.
Assisted-by: Claude:claude-opus-5:Claude Code
A constant narrower than its stack type builds as a plain ldc.i4, which
infers as int, so a conditional whose other branch really is a char saw
two different types and the whole tree was left as the Expression calls
that built it - EF Core's StringCharConverter.ToChar is one. Bool and
enum constants were already wrapped for this reason; the wrap now
applies wherever the built value does not infer as the declared type.
Assisted-by: Claude:claude-opus-5:Claude Code
The builder returned by ConvertLambda hands back null when a nested
conversion declines, and the result was cast and dereferenced before
anything checked it, so a tree the transform cannot handle took down the
whole method with a NullReferenceException instead of being left alone.
EF Core's StringCharConverter.ToChar is such a tree: the conditional
spills the Expression.Call arguments into stack slots, which
MatchGetMethodFromHandle does not see through.
Assisted-by: Claude:claude-opus-5:Claude Code
A hand-built Expression.Equal whose operands are a type parameter has no
lambda to decompile to: `v == other` is CS0019 for a type parameter, and
boxing both operands compiles but compares box identity where the tree
compares values once the parameter is a value type. The conversion now
declines, leaving the Expression calls that built the tree.
Found in EF Core's BoolToTwoValuesConverter<TProvider>.ToBool, which
decompiled to code that does not compile.
Assisted-by: Claude:claude-opus-5:Claude Code