Skip to content

Represent VB.NET parameterized properties as accessor methods in C# output - #3917

Merged
siegfriedpammer merged 6 commits into
masterfrom
parameterized-properties
Jul 27, 2026
Merged

Represent VB.NET parameterized properties as accessor methods in C# output#3917
siegfriedpammer merged 6 commits into
masterfrom
parameterized-properties

Conversation

@siegfriedpammer

@siegfriedpammer siegfriedpammer commented Jul 27, 2026

Copy link
Copy Markdown
Member

Addresses #1089 (and its sub-issues #1644, #2003; also the declaration half of the old #339/#1560 reports).

CLI metadata allows named properties with arbitrary parameter lists (VB.NET parameterized properties, C++/CLI indexed properties, COM PIAs); C# has syntax only for the default member. ILSpy previously emitted a parameterless property whose accessor bodies referenced the vanished parameters -- uncompilable output (see the Issue1325 fixture diff in this PR for a before/after).

Example -- given this VB.NET input:

<Obsolete("read-write parameterized property")>
Public Property Attributed(index As Integer) As Integer
    Get
        Return index
    End Get
    Set(value As Integer)
    End Set
End Property

ILSpy previously produced (uncompilable -- index is not declared anywhere):

[Obsolete("read-write parameterized property")]
public int Attributed {
    get {
        return index;
    }
    set {
    }
}

and now produces:

// C# has no syntax for parameterized property 'Attributed'.
// Its 'property:' attributes below are ignored by the compiler (CS0657).
[property: Obsolete("read-write parameterized property")]
public int get_Attributed(int index)
{
    return index;
}

public void set_Attributed(int index, int value)
{
}

with call sites reading x.set_Attributed(1, 42); / int v = x.get_Attributed(1);, and XML documentation (a P: entry) shown on the first accessor and in tooltips.

Declarations (this PR): a parameterized non-indexer property now decompiles to its accessors as ordinary methods, with

  • a comment documenting the deliberate deviation,
  • the property-level attributes preserved on the first accessor under the property: attribute target. That target is not valid on method declarations, so the compiler ignores the attribute block with warning CS0657: the attributes stay visible and greppable in source, and recompilation neither drops them silently nor misapplies them to the accessor method. (For comparison, Visual Studio's metadata-as-source view renders such properties as bare accessor signatures and drops the property attributes entirely.)

Call sites already lower to direct accessor calls (t.set_Named(1, 2, value)) on master; the new fixture pins that behavior. This is the sanctioned C# consumption pattern: Roslyn exposes the accessors of properties whose shape C# cannot bind as regular callable methods (no CS0571), the same speakable-accessor pattern C# 14 made user-facing for extension-member disambiguation. Verified by compiling C# against a VB assembly using get_/set_ calls, including implicit and explicit interface implementations of the accessors.

Why not indexer syntax + [IndexerName]: all indexers of a type must share one name, so it fails for types with several differently-named indexed properties (the common COM-PIA shape), static parameterized properties, and explicit interface implementations -- and it would create a default member the original type does not have, while making direct accessor calls in sibling decompiled files illegal (CS0571). Not general enough to be worth the per-form call-site coupling.

Documentation: the compiler-generated XML documentation file has a P: entry for the property, which no emitted member id matches anymore; the decompiled output places the property's documentation on the first accessor, and the text-view tooltip shows it for either accessor.

Signature surfaces: the tree/tooltip/search rendering of parameterized properties had lost its parameter list at some point long before the Avalonia rewrite (the ambience read parameters from the converted AST node, which C# property syntax cannot carry); this PR restores it, rendering Data(int) : int with parentheses to match VB.NET usage syntax and distinguish these from indexers.

Known limitation (inherent to any C# projection): recompiling the output produces plain methods without property metadata, so VB.NET consumers of a recompiled assembly lose property-access syntax. Whole-project output currently surfaces CS0657 warnings; the generated projects define no NoWarn mechanism today, so this PR does not introduce one.

Tests: new VBPretty/ParameterizedProperties fixture (interface + implicit/explicit implementation, static, read-only, attributed, virtual/override, overloaded, and renamed-Implements properties -- the last generating the same explicit-interface-implementation forwarder stubs as the ordinary method path -- plus consuming code), green across all 12 VB compiler configurations on Linux; ILPretty/Issue1325 expectation updated from the old uncompilable output; full decompiler suite green (2517 passed / 0 failed / 38 skipped).

This PR was prepared by an AI agent (Claude) on behalf of @siegfriedpammer.

🤖 Generated with Claude Code

C# has no syntax for named properties with parameters, so the expected
output declares the accessors as ordinary methods, keeps the
property-level attributes under the inert 'property:' attribute target
(ignored by csc with CS0657), and consumes the properties through
direct accessor calls, which Roslyn permits exactly for properties
whose shape C# cannot bind. Red against the current decompiler, which
emits a parameterless property whose body references the vanished
parameters.

Assisted-by: Claude:claude-fable-5:Claude Code

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves ILSpy’s C# decompilation for named, parameterized properties (e.g., VB.NET parameterized properties / C++/CLI indexed properties) by representing them as ordinary get_*/set_* accessor methods, avoiding uncompilable C# output and restoring accurate signature rendering and documentation behavior.

Changes:

  • Decompile non-indexer parameterized properties to accessor method declarations (with explanatory comments and preserved property-level attributes via property: target).
  • Restore parameter lists for parameterized properties in signature surfaces (tree/tooltip/search) and improve XML doc propagation to accessor-method representations.
  • Add/adjust fixtures and tests covering parameterized properties and corrupt “no accessor” property metadata.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated no comments.

Show a summary per file
File Description
ILSpy/TextView/DecompilerTextView.axaml.cs Tooltip XML doc fallback: show owning property docs when hovering accessor-method representations.
ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs Adds IsParameterizedProperty() helper to identify named parameterized (non-indexer) properties.
ICSharpCode.Decompiler/CSharp/Transforms/AddXmlDocumentationTransform.cs Applies property XML docs to the first accessor when parameterized properties are emitted as methods.
ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs Converts parameterized-property accessors to MethodDeclaration instead of C# accessor syntax.
ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs Ensures signature surfaces show parameter lists for parameterized properties.
ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs Emits accessor methods for parameterized property declarations; preserves property attributes; updates referenced-member enqueue logic.
ICSharpCode.Decompiler.Tests/VBPrettyTestRunner.cs Adds VBPretty runner test for ParameterizedProperties.
ICSharpCode.Decompiler.Tests/TestCases/VBPretty/ParameterizedProperties.vb New VB fixture input covering multiple parameterized-property shapes and call sites.
ICSharpCode.Decompiler.Tests/TestCases/VBPretty/ParameterizedProperties.cs New expected C# output fixture validating accessor-method representation and call-site lowering.
ICSharpCode.Decompiler.Tests/TestCases/ILPretty/NoAccessorProperties.il New IL fixture covering corrupt properties with no accessors (must not crash).
ICSharpCode.Decompiler.Tests/TestCases/ILPretty/NoAccessorProperties.cs Expected C# output for the “no accessor” property metadata case.
ICSharpCode.Decompiler.Tests/TestCases/ILPretty/Issue1325.cs Updates expected output to avoid uncompilable parameterized-property C#.
ICSharpCode.Decompiler.Tests/Output/CSharpAmbienceTests.cs Adds unit test validating parameterized-property signature formatting.
ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs Adds ILPretty runner test for NoAccessorProperties.
ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj Includes new fixtures in the test project items.
Comments suppressed due to low confidence (1)

ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs:2075

  • The expanded SetNewModifier condition now runs for SymbolKind.Accessor unconditionally. When an accessor is decompiled as an Accessor node (e.g., decompiling get_*/set_* directly), SetNewModifier can add Modifiers.New, and the output visitor will print new get / new set, which is not valid C# syntax. Restrict SetNewModifier to accessors that are being emitted as ordinary MethodDeclarations (parameterized-property path), not Accessor nodes.
				if (method.SymbolKind is SymbolKind.Method or SymbolKind.Accessor && !method.IsExplicitInterfaceImplementation
					&& methodDefinition.HasFlag(System.Reflection.MethodAttributes.Virtual) == methodDefinition.HasFlag(System.Reflection.MethodAttributes.NewSlot))
				{
					SetNewModifier(methodDecl);
				}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

C# cannot declare a named property with parameters: only the type's
default member gets indexer syntax, and reusing it via [IndexerName]
collapses for types with several differently-named indexed properties,
static properties, or explicit interface implementations. Emitting the
accessors as ordinary methods is the only fully general compilable
form, matches how C# consumes such properties (Roslyn exposes the
accessors of properties it cannot bind as regular methods, the same
pattern C# 14 made user-facing for extension-member disambiguation),
and round-trips call sites to identical IL. Call sites already lower
to direct accessor calls.

The property-level attributes are kept on the first accessor under the
'property:' attribute target: it is not valid on methods, so csc emits
nothing for it (CS0657 warning) and recompilation neither loses the
attributes from the source nor misapplies them to the accessor. A
comment on the first accessor documents the deliberate deviation.
Visual Studio's metadata-as-source view drops such properties'
attributes entirely.

The assembly tree and tooltips are unaffected: they keep rendering the
property node with its parameter list. Known limitation, inherent to
any C# projection: recompiling the output produces plain methods, so
VB.NET consumers of the recompiled assembly lose property syntax.

Assisted-by: Claude:claude-fable-5:Claude Code
The compiler-generated documentation file contains a P: entry for a
parameterized property, but its accessors are emitted as ordinary
methods, whose M: id has no documentation entry. Fall back to the
owning property's documentation both when inserting XML documentation
into decompiled output (on the first accessor only, to avoid
duplicating it) and in the text view's tooltip (for either accessor).

Assisted-by: Claude:claude-fable-5:Claude Code
The main tree, tooltips, and search results once showed the parameter
list of a parameterized property; the ambience lost that when property
rendering went through the converted AST node, whose C# property syntax
cannot carry parameters. Take the parameter list from the symbol
instead and render it in parentheses (matching VB.NET usage syntax and
distinguishing these properties from indexers).

Assisted-by: Claude:claude-fable-5:Claude Code
…rties

The renamed-Implements case exposed a gap: the accessor-method
declarations did not include the explicit-interface-implementation
forwarders generated from .override directives, so decompiled types did
not implement their interfaces and same-name implementations lost their
interface mapping. DecompileParameterizedProperty now emits the same
forwarder stubs as the ordinary method path.

Assisted-by: Claude:claude-fable-5:Claude Code
Corrupt or hand-written metadata can contain a property with no
MethodSemantics rows at all. MetadataTypeDefinition.Properties already
skips properties with neither a visible getter nor setter, so neither
the parameterized-property path nor the ordinary one ever sees them;
the accessor-method emission also tolerates a missing accessor by
construction. Pin that with an ILPretty case containing both a
parameterized and an ordinary accessor-less property.

Assisted-by: Claude:claude-fable-5:Claude Code
@siegfriedpammer
siegfriedpammer force-pushed the parameterized-properties branch from 9292fd9 to d624a28 Compare July 27, 2026 13:19
@siegfriedpammer
siegfriedpammer merged commit c0aa2db into master Jul 27, 2026
13 checks passed
@siegfriedpammer
siegfriedpammer deleted the parameterized-properties branch July 27, 2026 14:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants