Make inherited protected internal instance members accessible in PowerShell class scope (#25245)

This commit is contained in:
Matthias Wolf
2025-05-12 14:30:12 -07:00
committed by GitHub
parent fcc1833963
commit eb1cc6ddcd
3 changed files with 217 additions and 9 deletions
@@ -2835,7 +2835,7 @@ namespace System.Management.Automation
// Get the public or protected getter
MethodInfo propertyGetter = property.GetGetMethod(true);
if (propertyGetter != null && (propertyGetter.IsPublic || propertyGetter.IsFamily))
if (propertyGetter != null && (propertyGetter.IsPublic || propertyGetter.IsFamily || propertyGetter.IsFamilyOrAssembly))
{
this.isStatic = propertyGetter.IsStatic;
// Delegate is initialized later to avoid jit if it's not called
@@ -2847,7 +2847,7 @@ namespace System.Management.Automation
// Get the public or protected setter
MethodInfo propertySetter = property.GetSetMethod(true);
if (propertySetter != null && (propertySetter.IsPublic || propertySetter.IsFamily))
if (propertySetter != null && (propertySetter.IsPublic || propertySetter.IsFamily || propertySetter.IsFamilyOrAssembly))
{
this.isStatic = propertySetter.IsStatic;
}
@@ -5298,7 +5298,8 @@ namespace System.Management.Automation.Language
var propertyAccessor = adapterData.member as PropertyInfo;
if (propertyAccessor != null)
{
if (propertyAccessor.GetMethod.IsFamily &&
var propertyGetter = propertyAccessor.GetMethod;
if ((propertyGetter.IsFamily || propertyGetter.IsFamilyOrAssembly) &&
(_classScope == null || !_classScope.IsSubclassOf(propertyAccessor.DeclaringType)))
{
return GenerateGetPropertyException(restrictions).WriteToDebugLog(this);
@@ -5758,8 +5759,8 @@ namespace System.Management.Automation.Language
var getMethod = propertyInfo.GetGetMethod(nonPublic: true);
var setMethod = propertyInfo.GetSetMethod(nonPublic: true);
if ((getMethod == null || getMethod.IsFamily || getMethod.IsPublic) &&
(setMethod == null || setMethod.IsFamily || setMethod.IsPublic))
if ((getMethod == null || getMethod.IsPublic || getMethod.IsFamily || getMethod.IsFamilyOrAssembly) &&
(setMethod == null || setMethod.IsPublic || setMethod.IsFamily || setMethod.IsFamilyOrAssembly))
{
memberInfo = new PSProperty(this.Name, PSObject.DotNetInstanceAdapter, target.Value, new DotNetAdapter.PropertyCacheEntry(propertyInfo));
}
@@ -5769,7 +5770,7 @@ namespace System.Management.Automation.Language
var fieldInfo = member as FieldInfo;
if (fieldInfo != null)
{
if (fieldInfo.IsFamily)
if (fieldInfo.IsFamily || fieldInfo.IsFamilyOrAssembly)
{
memberInfo = new PSProperty(this.Name, PSObject.DotNetInstanceAdapter, target.Value, new DotNetAdapter.PropertyCacheEntry(fieldInfo));
}
@@ -5777,7 +5778,7 @@ namespace System.Management.Automation.Language
else
{
var methodInfo = member as MethodInfo;
if (methodInfo != null && (methodInfo.IsPublic || methodInfo.IsFamily))
if (methodInfo != null && (methodInfo.IsPublic || methodInfo.IsFamily || methodInfo.IsFamilyOrAssembly))
{
candidateMethods ??= new List<MethodBase>();
@@ -6292,7 +6293,8 @@ namespace System.Management.Automation.Language
var targetExpr = _static ? null : PSGetMemberBinder.GetTargetExpr(target, data.member.DeclaringType);
if (propertyInfo != null)
{
if (propertyInfo.SetMethod.IsFamily &&
var propertySetter = propertyInfo.SetMethod;
if ((propertySetter.IsFamily || propertySetter.IsFamilyOrAssembly) &&
(_classScope == null || !_classScope.IsSubclassOf(propertyInfo.DeclaringType)))
{
return GeneratePropertyAssignmentException(restrictions).WriteToDebugLog(this);
@@ -7902,7 +7904,7 @@ namespace System.Management.Automation.Language
? BindingRestrictions.GetTypeRestriction(target.Expression, target.Value.GetType())
: target.PSGetTypeRestriction();
restrictions = args.Aggregate(restrictions, static (current, arg) => current.Merge(arg.PSGetMethodArgumentRestriction()));
var newConstructors = DotNetAdapter.GetMethodInformationArray(ctors.Where(static c => c.IsPublic || c.IsFamily).ToArray());
var newConstructors = DotNetAdapter.GetMethodInformationArray(ctors.Where(static c => c.IsPublic || c.IsFamily || c.IsFamilyOrAssembly).ToArray());
return PSInvokeMemberBinder.InvokeDotNetMethod(_callInfo, "new", _constraints, PSInvokeMemberBinder.MethodInvocationType.BaseCtor,
target, args, restrictions, newConstructors, typeof(MethodException));
}
@@ -715,3 +715,209 @@ Describe 'Base type has abstract properties' -Tags "CI" {
$failure.Exception.Message | Should -BeLike "*'get_Exists'*"
}
}
Describe 'Classes inheritance with protected and protected internal members in base class' -Tags 'CI' {
BeforeAll {
Set-StrictMode -Version 3
$c1DefinitionProtectedInternal = @'
public class C1ProtectedInternal
{
protected internal string InstanceField = "C1_InstanceField";
protected internal string InstanceProperty { get; set; } = "C1_InstanceProperty";
protected internal string InstanceMethod() { return "C1_InstanceMethod"; }
protected internal virtual string VirtualProperty1 { get; set; } = "C1_VirtualProperty1";
protected internal virtual string VirtualProperty2 { get; set; } = "C1_VirtualProperty2";
protected internal virtual string VirtualMethod1() { return "C1_VirtualMethod1"; }
protected internal virtual string VirtualMethod2() { return "C1_VirtualMethod2"; }
public string CtorUsed { get; set; }
public C1ProtectedInternal() { CtorUsed = "default ctor"; }
protected internal C1ProtectedInternal(string p1) { CtorUsed = "C1_ctor_1args:" + p1; }
}
'@
$c2DefinitionProtectedInternal = @'
class C2ProtectedInternal : C1ProtectedInternal {
C2ProtectedInternal() : base() { $this.VirtualProperty2 = 'C2_VirtualProperty2' }
C2ProtectedInternal([string]$p1) : base($p1) { $this.VirtualProperty2 = 'C2_VirtualProperty2' }
[string]GetInstanceField() { return $this.InstanceField }
[string]SetInstanceField([string]$value) { $this.InstanceField = $value; return $this.InstanceField }
[string]GetInstanceProperty() { return $this.InstanceProperty }
[string]SetInstanceProperty([string]$value) { $this.InstanceProperty = $value; return $this.InstanceProperty }
[string]CallInstanceMethod() { return $this.InstanceMethod() }
[string]GetVirtualProperty1() { return $this.VirtualProperty1 }
[string]SetVirtualProperty1([string]$value) { $this.VirtualProperty1 = $value; return $this.VirtualProperty1 }
[string]CallVirtualMethod1() { return $this.VirtualMethod1() }
[string]$VirtualProperty2
[string]VirtualMethod2() { return 'C2_VirtualMethod2' }
# Note: Overriding a virtual property in a derived PowerShell class prevents access to the
# base property via simple typecast ([base]$this).VirtualProperty2.
[string]GetVirtualProperty2() { return $this.VirtualProperty2 }
[string]SetVirtualProperty2([string]$value) { $this.VirtualProperty2 = $value; return $this.VirtualProperty2 }
[string]CallVirtualMethod2Base() { return ([C1ProtectedInternal]$this).VirtualMethod2() }
[string]CallVirtualMethod2Derived() { return $this.VirtualMethod2() }
[string]GetInstanceMemberDynamic([string]$name) { return $this.$name }
[string]SetInstanceMemberDynamic([string]$name, [string]$value) { $this.$name = $value; return $this.$name }
[string]CallInstanceMemberDynamic([string]$name) { return $this.$name() }
}
[C2ProtectedInternal]
'@
Add-Type -TypeDefinition $c1DefinitionProtectedInternal
Add-Type -TypeDefinition (($c1DefinitionProtectedInternal -creplace 'C1ProtectedInternal', 'C1Protected') -creplace 'protected internal', 'protected')
$testCases = @(
@{ accessType = 'protected'; derivedType = Invoke-Expression ($c2DefinitionProtectedInternal -creplace 'ProtectedInternal', 'Protected') }
@{ accessType = 'protected internal'; derivedType = Invoke-Expression $c2DefinitionProtectedInternal }
)
}
AfterAll {
Set-StrictMode -Off
}
Context 'Derived class can access instance base class members' {
It 'can call protected internal .NET method Object.MemberwiseClone()' {
class CNetMethod {
[string]$Foo
[object]CloneIt() { return $this.MemberwiseClone() }
}
$c1 = [CNetMethod]::new()
$c1.Foo = 'bar'
$c2 = $c1.CloneIt()
$c2.Foo | Should -Be 'bar'
}
It 'can call <accessType> base ctor' -TestCases $testCases {
param($derivedType)
$derivedType::new('foo').CtorUsed | Should -Be 'C1_ctor_1args:foo'
}
It 'can access <accessType> base field' -TestCases $testCases {
param($derivedType)
$c2 = $derivedType::new()
$c2.GetInstanceField() | Should -Be 'C1_InstanceField'
$c2.SetInstanceField('foo_InstanceField') | Should -Be 'foo_InstanceField'
}
It 'can access <accessType> base property' -TestCases $testCases {
param($derivedType)
$c2 = $derivedType::new()
$c2.GetInstanceProperty() | Should -Be 'C1_InstanceProperty'
$c2.SetInstanceProperty('foo_InstanceProperty') | Should -Be 'foo_InstanceProperty'
}
It 'can call <accessType> base method' -TestCases $testCases {
param($derivedType)
$derivedType::new().CallInstanceMethod() | Should -Be 'C1_InstanceMethod'
}
It 'can access <accessType> virtual base property' -TestCases $testCases {
param($derivedType)
$c2 = $derivedType::new()
$c2.GetVirtualProperty1() | Should -Be 'C1_VirtualProperty1'
$c2.SetVirtualProperty1('foo_VirtualProperty1') | Should -Be 'foo_VirtualProperty1'
}
It 'can call <accessType> virtual base method' -TestCases $testCases {
param($derivedType)
$derivedType::new().CallVirtualMethod1() | Should -Be 'C1_VirtualMethod1'
}
}
Context 'Derived class can override virtual base class members' {
It 'can override <accessType> virtual base property' -TestCases $testCases {
param($derivedType)
$c2 = $derivedType::new()
$c2.GetVirtualProperty2() | Should -Be 'C2_VirtualProperty2'
$c2.SetVirtualProperty2('foo_VirtualProperty2') | Should -Be 'foo_VirtualProperty2'
}
It 'can override <accessType> virtual base method' -TestCases $testCases {
param($derivedType)
$c2 = $derivedType::new()
$c2.CallVirtualMethod2Base() | Should -Be 'C1_VirtualMethod2'
$c2.CallVirtualMethod2Derived() | Should -Be 'C2_VirtualMethod2'
}
}
Context 'Derived class can access instance base class members dynamically' {
It 'can access <accessType> base fields and properties' -TestCases $testCases {
param($derivedType)
$c2 = $derivedType::new()
$c2.GetInstanceMemberDynamic('InstanceField') | Should -Be 'C1_InstanceField'
$c2.GetInstanceMemberDynamic('InstanceProperty') | Should -Be 'C1_InstanceProperty'
$c2.GetInstanceMemberDynamic('VirtualProperty1') | Should -Be 'C1_VirtualProperty1'
$c2.SetInstanceMemberDynamic('InstanceField', 'foo1') | Should -Be 'foo1'
$c2.SetInstanceMemberDynamic('InstanceProperty', 'foo2') | Should -Be 'foo2'
$c2.SetInstanceMemberDynamic('VirtualProperty1', 'foo3') | Should -Be 'foo3'
}
It 'can call <accessType> base methods' -TestCases $testCases {
param($derivedType)
$c2 = $derivedType::new()
$c2.CallInstanceMemberDynamic('InstanceMethod') | Should -Be 'C1_InstanceMethod'
$c2.CallInstanceMemberDynamic('VirtualMethod1') | Should -Be 'C1_VirtualMethod1'
}
}
Context 'Base class members are not accessible outside class scope' {
BeforeAll {
$instanceTest = {
$c2 = $derivedType::new()
{ $null = $c2.InstanceField } | Should -Throw -ErrorId 'PropertyNotFoundStrict'
{ $null = $c2.InstanceProperty } | Should -Throw -ErrorId 'PropertyNotFoundStrict'
{ $null = $c2.VirtualProperty1 } | Should -Throw -ErrorId 'PropertyNotFoundStrict'
{ $c2.InstanceField = 'foo' } | Should -Throw -ErrorId 'PropertyAssignmentException'
{ $c2.InstanceProperty = 'foo' } | Should -Throw -ErrorId 'PropertyAssignmentException'
{ $c2.VirtualProperty1 = 'foo' } | Should -Throw -ErrorId 'PropertyAssignmentException'
{ $derivedType::new().InstanceMethod() } | Should -Throw -ErrorId 'MethodNotFound'
{ $derivedType::new().VirtualMethod1() } | Should -Throw -ErrorId 'MethodNotFound'
foreach ($name in @('InstanceField', 'InstanceProperty', 'VirtualProperty1')) {
{ $null = $c2.$name } | Should -Throw -ErrorId 'PropertyNotFoundStrict'
{ $c2.$name = 'foo' } | Should -Throw -ErrorId 'PropertyAssignmentException'
}
foreach ($name in @('InstanceMethod', 'VirtualMethod1')) {
{ $c2.$name() } | Should -Throw -ErrorId 'MethodNotFound'
}
}
$c3UnrelatedType = Invoke-Expression @"
class C3Unrelated {
[void]RunInstanceTest([type]`$derivedType) { $instanceTest }
}
[C3Unrelated]
"@
$negativeTestCases = $testCases.ForEach({
$item = $_.Clone()
$item['scopeType'] = 'null scope'
$item['classScope'] = $null
$item
$item = $_.Clone()
$item['scopeType'] = 'unrelated class scope'
$item['classScope'] = $c3UnrelatedType
$item
})
}
It 'cannot access <accessType> instance base members in <scopeType>' -TestCases $negativeTestCases {
param($derivedType, $classScope)
if ($null -eq $classScope) {
$instanceTest.Invoke()
}
else {
$c3 = $classScope::new()
$c3.RunInstanceTest($derivedType)
}
}
}
}