Skip to content

Look past comment placeholders when matching statement sequences - #4130

Merged
siegfriedpammer merged 2 commits into
masterfrom
fix/comment-placeholder-statements
Sep 10, 2026
Merged

Look past comment placeholders when matching statement sequences#4130
siegfriedpammer merged 2 commits into
masterfrom
fix/comment-placeholder-statements

Conversation

@siegfriedpammer

Copy link
Copy Markdown
Member

The decompiler emits its comments as an EmptyStatement carrying trivia: //IL_ warnings
(ExpressionBuilder.cs:2599, StatementBuilder.cs:1524), "Could not convert BlockContainer"
(ExpressionBuilder.cs:2870), "try-fault" (StatementBuilder.cs:510), and any Nop with a
Comment (StatementBuilder.cs:151, fed by YieldReturnDecompiler). The last one can land
anywhere in a statement sequence, not just at a block head.

Every C# AST transform that walks a statement sequence stops recognizing its pattern as soon as
one of those appears in it. Two of the consequences are output that does not compile:

  • TransformFieldAndConstructorInitializers.MoveConstructorInitializer matches the ctor call
    against Body.Statements.FirstOrDefault(). A leading placeholder fails the match and
    this(...) / base(...) stays in the body as an expression statement.
  • PatternStatementTransform.destructorPattern required the body to be exactly one
    TryCatchStatement. A placeholder anywhere in it leaves the method as
    protected override void Finalize() — CS0249.

The rest degrade quietly: constructor initializers are not moved, a constructor is misfiled as
not chained with this (which changes initializer placement for the whole type), and using var,
for and switch-section flattening are not introduced.

Changes

GetFirstNonEmptyStatementOrDefault() and GetNextNonEmptyStatement() in SyntaxExtensions,
applied at the nine sequence-walking sites in TransformFieldAndConstructorInitializers,
PatternStatementTransform and FlattenSwitchBlocks.

The destructor matcher is restructured into MatchDestructorBody, shared by TransformDestructor
and TransformDestructorBody so the two cannot drift. Strictness is unchanged — the try statement
must still be alone in the body, still have no catch clauses, and the finally block must still hold
nothing but base.Finalize(). Placeholders skipped there are moved to the front of the body that
replaces the old one, so the warning that caused the problem is not dropped with the statement
carrying it.

The first commit is separate: a deconstruction target of a backing-field store is accepted only for
a setter-less property. It assigns several members at once, so it can never become a property
initializer, and a property that kept a setter would invoke that setter instead of storing the
field — without the restriction the field declaration is removed while the store still references it.

Testing

ICSharpCode.Decompiler.Tests is green: 3606 total, 0 failed, 50 skipped (Windows-only and the
ILSpy-tests round-trip cases). Finalizers were checked by hand in Release and Debug builds,
including one with a user-written try/finally nested inside the compiler's.

Neither the destructor case nor the deconstruction guard has a regression test. Plain nop
opcodes are removed before the C# AST is built, and a settable property's constructor store goes
through the setter, so neither state is reachable from compiled C# — both would need hand-written
IL fixtures.

🤖 Generated with 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 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
@siegfriedpammer
siegfriedpammer merged commit dbf23c6 into master Sep 10, 2026
17 checks passed
@siegfriedpammer
siegfriedpammer deleted the fix/comment-placeholder-statements branch September 10, 2026 18:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant