diff --git a/src/System.Management.Automation/engine/CoreAdapter.cs b/src/System.Management.Automation/engine/CoreAdapter.cs index 6183f98a0a..907060bfe4 100644 --- a/src/System.Management.Automation/engine/CoreAdapter.cs +++ b/src/System.Management.Automation/engine/CoreAdapter.cs @@ -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; } diff --git a/src/System.Management.Automation/engine/runtime/Binding/Binders.cs b/src/System.Management.Automation/engine/runtime/Binding/Binders.cs index 54b23ee412..634d8b5965 100644 --- a/src/System.Management.Automation/engine/runtime/Binding/Binders.cs +++ b/src/System.Management.Automation/engine/runtime/Binding/Binders.cs @@ -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(); @@ -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)); } diff --git a/test/powershell/Language/Classes/scripting.Classes.inheritance.tests.ps1 b/test/powershell/Language/Classes/scripting.Classes.inheritance.tests.ps1 index 391375b001..6b24b8b6ce 100644 --- a/test/powershell/Language/Classes/scripting.Classes.inheritance.tests.ps1 +++ b/test/powershell/Language/Classes/scripting.Classes.inheritance.tests.ps1 @@ -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 base ctor' -TestCases $testCases { + param($derivedType) + $derivedType::new('foo').CtorUsed | Should -Be 'C1_ctor_1args:foo' + } + + It 'can access 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 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 base method' -TestCases $testCases { + param($derivedType) + $derivedType::new().CallInstanceMethod() | Should -Be 'C1_InstanceMethod' + } + + It 'can access 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 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 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 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 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 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 instance base members in ' -TestCases $negativeTestCases { + param($derivedType, $classScope) + if ($null -eq $classScope) { + $instanceTest.Invoke() + } + else { + $c3 = $classScope::new() + $c3.RunInstanceTest($derivedType) + } + } + } +}