Browse Source

Emit parameterized-property accessors as ordinary methods

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
pull/3925/head
Siegfried Pammer 2 months ago committed by Siegfried Pammer
parent
commit
832090c08d
  1. 16
      ICSharpCode.Decompiler.Tests/TestCases/ILPretty/Issue1325.cs
  2. 17
      ICSharpCode.Decompiler.Tests/TestCases/VBPretty/ParameterizedProperties.cs
  3. 84
      ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
  4. 6
      ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs
  5. 11
      ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs

16
ICSharpCode.Decompiler.Tests/TestCases/ILPretty/Issue1325.cs

@ -35,13 +35,15 @@ namespace Issue1325 @@ -35,13 +35,15 @@ namespace Issue1325
internal class Test
{
public string Parameterized {
get {
throw new NotImplementedException();
}
set {
throw new NotImplementedException();
}
// C# has no syntax for parameterized property 'Parameterized'.
public string get_Parameterized(int i)
{
throw new NotImplementedException();
}
public void set_Parameterized(int i, string value)
{
throw new NotImplementedException();
}
public string Unparameterized { get; set; }
}

17
ICSharpCode.Decompiler.Tests/TestCases/VBPretty/ParameterizedProperties.cs

@ -3,8 +3,7 @@ using System; @@ -3,8 +3,7 @@ using System;
public interface IParameterized
{
// C# has no syntax for parameterized properties; the accessors of
// property 'IndexedValue' are emitted as ordinary methods.
// C# has no syntax for parameterized property 'IndexedValue'.
int get_IndexedValue(int index);
void set_IndexedValue(int index, int Value);
}
@ -13,8 +12,7 @@ public class ParameterizedProperties : IParameterized @@ -13,8 +12,7 @@ public class ParameterizedProperties : IParameterized
{
private int _field;
// C# has no syntax for parameterized properties; the accessors of
// property 'SharedProp' are emitted as ordinary methods.
// C# has no syntax for parameterized property 'SharedProp'.
public static int get_SharedProp(int index)
{
return index;
@ -24,8 +22,7 @@ public class ParameterizedProperties : IParameterized @@ -24,8 +22,7 @@ public class ParameterizedProperties : IParameterized
{
}
// C# has no syntax for parameterized properties; the accessors of
// property 'IndexedValue' are emitted as ordinary methods.
// C# has no syntax for parameterized property 'IndexedValue'.
public int get_IndexedValue(int index)
{
return _field;
@ -36,16 +33,14 @@ public class ParameterizedProperties : IParameterized @@ -36,16 +33,14 @@ public class ParameterizedProperties : IParameterized
_field = value;
}
// C# has no syntax for parameterized properties; the accessors of
// property 'ReadOnlyProp' are emitted as ordinary methods.
// C# has no syntax for parameterized property 'ReadOnlyProp'.
public int get_ReadOnlyProp(int index)
{
return index;
}
// C# has no syntax for parameterized properties; the accessors of
// property 'Attributed' are emitted as ordinary methods. The property's
// attributes are kept below under the inert 'property:' target (CS0657).
// 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)
{

84
ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs

@ -1338,7 +1338,14 @@ namespace ICSharpCode.Decompiler.CSharp @@ -1338,7 +1338,14 @@ namespace ICSharpCode.Decompiler.CSharp
case HandleKind.PropertyDefinition:
IProperty property = module.GetDefinition((PropertyDefinitionHandle)entity);
parentExtensionInfo = property.ResolveExtensionInfo();
syntaxTree.Members.Add(DoDecompile(property, decompileRun, new SimpleTypeResolveContext(property), parentExtensionInfo));
if (property.IsParameterizedProperty())
{
syntaxTree.Members.AddRange(DecompileParameterizedProperty(property, decompileRun, new SimpleTypeResolveContext(property), parentExtensionInfo));
}
else
{
syntaxTree.Members.Add(DoDecompile(property, decompileRun, new SimpleTypeResolveContext(property), parentExtensionInfo));
}
if (first)
{
parentTypeDef = property.DeclaringTypeDefinition;
@ -1869,6 +1876,15 @@ namespace ICSharpCode.Decompiler.CSharp @@ -1869,6 +1876,15 @@ namespace ICSharpCode.Decompiler.CSharp
{
return;
}
if (property.IsParameterizedProperty())
{
foreach (var accessorDecl in DecompileParameterizedProperty(property, decompileRun, decompilationContext, null))
{
entityMap.Add(property, accessorDecl);
EnqueueReferencedMembers(accessorDecl);
}
return;
}
entityDecl = DoDecompile(property, decompileRun, decompilationContext.WithCurrentMember(property), null);
entityMap.Add(property, entityDecl);
break;
@ -1897,19 +1913,24 @@ namespace ICSharpCode.Decompiler.CSharp @@ -1897,19 +1913,24 @@ namespace ICSharpCode.Decompiler.CSharp
throw new ArgumentOutOfRangeException("Unexpected member type");
}
foreach (var node in entityDecl.Descendants)
EnqueueReferencedMembers(entityDecl);
void EnqueueReferencedMembers(EntityDeclaration decl)
{
var rr = node.GetResolveResult();
if (rr is MemberResolveResult mrr
&& mrr.Member.DeclaringTypeDefinition == typeDef
&& !(mrr.Member is IMethod { IsLocalFunction: true }))
foreach (var node in decl.Descendants)
{
workList.Enqueue(mrr.Member);
}
else if (rr is TypeResolveResult trr
&& trr.Type.GetDefinition()?.DeclaringTypeDefinition == typeDef)
{
workList.Enqueue(trr.Type.GetDefinition()!);
var rr = node.GetResolveResult();
if (rr is MemberResolveResult mrr
&& mrr.Member.DeclaringTypeDefinition == typeDef
&& !(mrr.Member is IMethod { IsLocalFunction: true }))
{
workList.Enqueue(mrr.Member);
}
else if (rr is TypeResolveResult trr
&& trr.Type.GetDefinition()?.DeclaringTypeDefinition == typeDef)
{
workList.Enqueue(trr.Type.GetDefinition()!);
}
}
}
}
@ -2047,7 +2068,10 @@ namespace ICSharpCode.Decompiler.CSharp @@ -2047,7 +2068,10 @@ namespace ICSharpCode.Decompiler.CSharp
{
methodDecl.Modifiers |= Modifiers.Extern;
}
if (method.SymbolKind == SymbolKind.Method && !method.IsExplicitInterfaceImplementation
// Accessors qualify only when they are emitted as ordinary methods (parameterized
// properties); an Accessor node cannot carry the 'new' modifier.
if ((method.SymbolKind == SymbolKind.Method || (method.SymbolKind == SymbolKind.Accessor && methodDecl is MethodDeclaration))
&& !method.IsExplicitInterfaceImplementation
&& methodDefinition.HasFlag(System.Reflection.MethodAttributes.Virtual) == methodDefinition.HasFlag(System.Reflection.MethodAttributes.NewSlot))
{
SetNewModifier(methodDecl);
@ -2456,6 +2480,40 @@ namespace ICSharpCode.Decompiler.CSharp @@ -2456,6 +2480,40 @@ namespace ICSharpCode.Decompiler.CSharp
return false;
}
/// <summary>
/// Decompiles a parameterized property (a named property with parameters, e.g. from
/// VB.NET) into declarations of its accessor methods, because C# has no syntax for
/// such properties. The property-level attributes are placed on the first accessor
/// under the 'property:' attribute target, which is not valid for methods and is
/// therefore ignored by the C# compiler (CS0657): recompilation neither loses the
/// attributes from the source nor misapplies them to the accessor method.
/// </summary>
List<EntityDeclaration> DecompileParameterizedProperty(IProperty property, DecompileRun decompileRun, ITypeResolveContext decompilationContext, ExtensionInfo? extensionInfo)
{
var result = new List<EntityDeclaration>(2);
var typeSystemAstBuilder = CreateAstBuilder(decompileRun.Settings);
foreach (var accessor in new[] { property.Getter, property.Setter })
{
if (accessor == null)
continue;
var accessorDecl = DoDecompile(accessor, decompileRun, decompilationContext.WithCurrentMember(accessor), extensionInfo);
if (result.Count == 0)
{
accessorDecl.AddLeadingTrivia(new Comment($" C# has no syntax for parameterized property '{property.Name}'."));
var attributes = property.GetAttributes().Select(typeSystemAstBuilder.ConvertAttribute).ToList();
if (attributes.Count > 0)
{
var attrSection = new AttributeSection { AttributeTarget = "property" };
attrSection.Attributes.AddRange(attributes);
accessorDecl.Attributes.InsertAfter(null, attrSection);
accessorDecl.AddLeadingTrivia(new Comment(" Its 'property:' attributes below are ignored by the compiler (CS0657)."));
}
}
result.Add(accessorDecl);
}
return result;
}
EntityDeclaration DoDecompile(IProperty property, DecompileRun decompileRun, ITypeResolveContext decompilationContext, ExtensionInfo? extensionInfo)
{
Debug.Assert(decompilationContext.CurrentMember == property);

6
ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs

@ -1871,6 +1871,12 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax @@ -1871,6 +1871,12 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
return ConvertDestructor((IMethod)entity);
case SymbolKind.Accessor:
IMethod accessor = (IMethod)entity;
if (accessor.AccessorOwner is IProperty owner && owner.IsParameterizedProperty())
{
// C# cannot represent the parameterized property itself; its accessors
// are declared as ordinary methods.
return ConvertMethod(accessor);
}
Accessibility ownerAccessibility = accessor.AccessorOwner?.Accessibility ?? Accessibility.None;
return ConvertAccessor(accessor, accessor.AccessorKind, ownerAccessibility, false)!;
default:

11
ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs

@ -171,6 +171,17 @@ namespace ICSharpCode.Decompiler.TypeSystem @@ -171,6 +171,17 @@ namespace ICSharpCode.Decompiler.TypeSystem
}
}
/// <summary>
/// Gets whether the property is a parameterized property that is not an indexer,
/// i.e. a named property with parameters (a VB.NET parameterized property or a
/// C++/CLI indexed property). C# has no syntax for declaring or using such a
/// property; only its accessor methods can be represented.
/// </summary>
public static bool IsParameterizedProperty(this IProperty property)
{
return property.SymbolKind == SymbolKind.Property && property.Parameters.Count > 0;
}
/// <summary>
/// Gets whether the type is an open type (contains type parameters).
/// </summary>

Loading…
Cancel
Save