Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 44 additions & 6 deletions src/System.Management.Automation/engine/CoreAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2757,8 +2757,8 @@ internal ParameterizedPropertyCacheEntry(List<PropertyInfo> properties)
this.propertyType = typeof(object);
}

// Get the public getter
MethodInfo propertyGetter = property.GetGetMethod();
// Get the public getter, which is inherited when an override defines only the setter
MethodInfo propertyGetter = property.GetGetMethod() ?? GetPropertyWithInheritedGetter(property).GetGetMethod();
StringBuilder definition = new StringBuilder();
StringBuilder extraDefinition = new StringBuilder();
if (propertyGetter != null)
Expand Down Expand Up @@ -2815,6 +2815,7 @@ internal class PropertyCacheEntry : CacheEntry
internal PropertyCacheEntry(PropertyInfo property)
{
this.member = property;
this.getterProperty = GetPropertyWithInheritedGetter(property);
this.propertyType = property.PropertyType;
// Generating code for fields/properties in ValueTypes is complex and will probably
// require different delegates
Expand All @@ -2828,13 +2829,13 @@ internal PropertyCacheEntry(PropertyInfo property)
propertyType.IsCOMObject)
{
this.readOnly = property.GetSetMethod() == null;
this.writeOnly = property.GetGetMethod() == null;
this.writeOnly = getterProperty.GetGetMethod() == null;
this.useReflection = true;
return;
}

// Get the public or protected getter
MethodInfo propertyGetter = property.GetGetMethod(true);
MethodInfo propertyGetter = getterProperty.GetGetMethod(true);
if (propertyGetter != null && (propertyGetter.IsPublic || propertyGetter.IsFamily || propertyGetter.IsFamilyOrAssembly))
{
this.isStatic = propertyGetter.IsStatic;
Expand Down Expand Up @@ -2924,7 +2925,7 @@ private void InitGetter()
return;
}

var property = (PropertyInfo)member;
var property = getterProperty;
var propertyGetter = property.GetGetMethod(true);

instance = this.isStatic ? null : parameter.Cast(propertyGetter.DeclaringType);
Expand Down Expand Up @@ -3011,6 +3012,10 @@ private void InitSetter()

internal MemberInfo member;

// The property whose getter is used. It differs from 'member' when 'member' is an override
// that defines only the setter, see GetPropertyWithInheritedGetter.
internal PropertyInfo getterProperty;

internal GetterDelegate getterDelegate
{
get
Expand Down Expand Up @@ -3339,6 +3344,39 @@ private static bool PropertyAlreadyPresent(List<PropertyInfo> previousProperties
return returnValue;
}

/// <summary>
/// An override can redefine only the setter of a virtual property and inherit the getter.
/// Reflection then returns a property that has only the setter. In that case, return the property
/// from the type that introduced the virtual property, so that its getter can be used. The getter
/// is virtual, so a call through it still reaches a getter override in a type in between.
/// </summary>
private static PropertyInfo GetPropertyWithInheritedGetter(PropertyInfo property)
{
MethodInfo setter = property.SetMethod;
if (property.GetMethod != null || setter == null)
{
return property;
}

MethodInfo baseSetter = setter.GetBaseDefinition();
if (baseSetter.DeclaringType == setter.DeclaringType)
{
// Not an override.
return property;
}

foreach (PropertyInfo baseProperty in baseSetter.DeclaringType.GetProperties(
BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
{
if (baseProperty.SetMethod == baseSetter && string.Equals(baseProperty.Name, property.Name, StringComparison.Ordinal))
{
return baseProperty.GetMethod != null ? baseProperty : property;
}
}

return property;
}

/// <summary>
/// Called from GetPropertyReflectionTable within a lock to fill the
/// property cache table.
Expand Down Expand Up @@ -4053,7 +4091,7 @@ protected override object PropertyGet(PSProperty property)

if (adapterData.useReflection)
{
return propertyInfo.GetValue(property.baseObject, null);
return adapterData.getterProperty.GetValue(property.baseObject, null);
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5295,7 +5295,7 @@ public override DynamicMetaObject FallbackGetMember(DynamicMetaObject target, Dy
// For static property access, the target expr must be null. For non-static, we must convert
// because target.Expression is typeof(object) because this is a dynamic site.
var targetExpr = _static ? null : GetTargetExpr(target, adapterData.member.DeclaringType);
var propertyAccessor = adapterData.member as PropertyInfo;
var propertyAccessor = adapterData.getterProperty;
if (propertyAccessor != null)
{
var propertyGetter = propertyAccessor.GetMethod;
Expand Down
112 changes: 112 additions & 0 deletions test/powershell/engine/ETS/Adapter.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -396,3 +396,115 @@ namespace BaseMethodCallTest.OnXmlDocument {
$child.ChildNodes.Count | Should -Be 1
}
}

Describe "Property override that redefines only the setter" -tags "CI" {
BeforeAll {
$code = @'
namespace SetterOnlyOverrideTest {
public class Base {
protected int _value;
public virtual int Value { get => _value; set => _value = value; }
public virtual int this[int index] { get => _value + index; set => _value = value; }
// Without the getter of the int indexer, Item(1) would call this overload.
public virtual int this[string key] { get => -1; set { } }
protected virtual int ProtectedValue { get => _value; set => _value = value; }
}

public class SetterOverride : Base {
public override int Value { set => _value = value + 1; }
public override int this[int index] { set => _value = value + 1; }
protected override int ProtectedValue { set => _value = value + 1; }
}

public class GetterOverride : Base {
public override int Value { get => _value * 10; }
}

public class SetterOverrideOfGetterOverride : GetterOverride {
public override int Value { set => _value = value + 1; }
}

public class OptionalIndexBase {
public int Column;
public virtual int this[int row, int column = 0] { get => row * 100 + column; set => Column = column; }
}

public class OptionalIndexSetterOverride : OptionalIndexBase {
public override int this[int row, int column = 1] { set => Column = column; }
}

public class GenericBase<T> {
protected T _value;
public virtual T Value { get => _value; set => _value = value; }
protected virtual int ProtectedValue { get; set; }
}

public class GenericSetterOverride : GenericBase<int> {
public override int Value { set => _value = value + 1; }
protected override int ProtectedValue { set => base.ProtectedValue = value + 1; }
}

public class GenericTypeSetterOverride<T> : GenericBase<T> {
public override T Value { set => _value = value; }
}
}
'@
try {
$null = [SetterOnlyOverrideTest.Base]
} catch {
Add-Type -TypeDefinition $code
}
}

It "Can get a property whose override defines only the setter" {
$obj = [SetterOnlyOverrideTest.SetterOverride]::new()
$obj.Value = 1
$obj.Value | Should -Be 2
$obj.psobject.Properties['Value'].Value | Should -Be 2
$obj.psobject.Properties['Value'].Value = 2
$obj.Value | Should -Be 3
(Get-Member -InputObject $obj -Name Value).Definition | Should -BeExactly 'int Value {get;set;}'
}

It "Calls the getter override of an intermediate type" {
$obj = [SetterOnlyOverrideTest.SetterOverrideOfGetterOverride]::new()
$obj.Value = 1
$obj.Value | Should -Be 20
}

It "Can get a parameterized property whose override defines only the setter" {
$obj = [SetterOnlyOverrideTest.SetterOverride]::new()
$obj[0] = 1
$obj.Item(1) | Should -Be 3
}

It "Keeps the optional argument default of an indexer override that defines only the setter" {
$obj = [SetterOnlyOverrideTest.OptionalIndexSetterOverride]::new()
$obj.psobject.Members['Item'].InvokeSet(42, 5)
$obj.Column | Should -Be 1
$obj.Item(5, 2) | Should -Be 502
}

It "Can get a property whose override from a generic base type defines only the setter" {
$obj = [SetterOnlyOverrideTest.GenericSetterOverride]::new()
$obj.Value = 1
$obj.Value | Should -Be 2
}

It "Can get a property whose override in a generic type defines only the setter" {
$obj = [SetterOnlyOverrideTest.GenericTypeSetterOverride[int]]::new()
$obj.Value = 5
$obj.Value | Should -Be 5
$obj.psobject.Properties['Value'].Value | Should -Be 5
}

It "Can get a protected property whose override defines only the setter from a derived class" {
$obj = Invoke-Expression 'class SetterOnlyOverrideTestClass : SetterOnlyOverrideTest.SetterOverride { [int] GetProtectedValue() { $this.ProtectedValue = 1; return $this.ProtectedValue } } [SetterOnlyOverrideTestClass]::new()'
$obj.GetProtectedValue() | Should -Be 2
}

It "Can get a protected property of a generic base type whose override defines only the setter from a derived class" {
$obj = Invoke-Expression 'class GenericSetterOnlyOverrideTestClass : SetterOnlyOverrideTest.GenericSetterOverride { [int] GetProtectedValue() { $this.ProtectedValue = 1; return $this.ProtectedValue } } [GenericSetterOnlyOverrideTestClass]::new()'
$obj.GetProtectedValue() | Should -Be 2
}
}