diff --git a/src/System.Management.Automation/engine/parser/PSType.cs b/src/System.Management.Automation/engine/parser/PSType.cs index 06f978a51ce..91e19d553da 100644 --- a/src/System.Management.Automation/engine/parser/PSType.cs +++ b/src/System.Management.Automation/engine/parser/PSType.cs @@ -9,6 +9,8 @@ using System.Management.Automation.Internal; using System.Reflection; using System.Reflection.Emit; +using System.Security.Cryptography; +using System.Text; using System.Threading; using Microsoft.PowerShell; @@ -277,7 +279,9 @@ private sealed class DefineTypeHelper internal readonly TypeBuilder _staticHelpersTypeBuilder; private readonly Dictionary _definedProperties; private readonly Dictionary>> _definedMethods; + private HashSet _interfaces; private Dictionary, PropertyInfo> _abstractProperties; + private Dictionary, MethodInfo> _interfaceMethods; internal readonly List<(string fieldName, IParameterMetadataProvider bodyAst, bool isStatic)> _fieldsToInitForMemberFunctions; private bool _baseClassHasDefaultCtor; @@ -449,20 +453,9 @@ private bool ShouldImplementProperty(string name, Type type, [NotNullWhen(true)] { if (_abstractProperties == null) { - _abstractProperties = new Dictionary, PropertyInfo>(); - var allInterfaces = new HashSet(); - - // TypeBuilder.GetInterfaces() returns only the interfaces that was explicitly passed to its constructor. - // During compilation the interface hierarchy is flattened, so we only need to resolve one level of ancestral interfaces. - foreach (var interfaceType in _typeBuilder.GetInterfaces()) - { - foreach (var parentInterface in interfaceType.GetInterfaces()) - { - allInterfaces.Add(parentInterface); - } - allInterfaces.Add(interfaceType); - } + _abstractProperties = new Dictionary, PropertyInfo>(); + var allInterfaces = GetImplementingInterfaces(); foreach (var interfaceType in allInterfaces) { @@ -487,6 +480,74 @@ private bool ShouldImplementProperty(string name, Type type, [NotNullWhen(true)] return _abstractProperties.TryGetValue(Tuple.Create(name, type), out interfaceProperty); } + private bool ShouldImplementMethod( + string name, + Type returnType, + Type[] parameterTypes, + [NotNullWhen(true)] out MethodInfo interfaceMethod) + { + if (_interfaceMethods == null) + { + _interfaceMethods = new Dictionary, MethodInfo>(); + var allInterfaces = GetImplementingInterfaces(); + + // We include NonPublic so we can also get protected interface methods. + BindingFlags methodFlags = BindingFlags.Public | + BindingFlags.NonPublic | + BindingFlags.Instance | + BindingFlags.Static; + + foreach (var interfaceType in allInterfaces) + { + foreach (var method in interfaceType.GetMethods(methodFlags)) + { + if (!(method.IsFamily || method.IsFamilyOrAssembly || method.IsPublic)) + { + // We want public and protected only, no internal or private. + continue; + } + + Type[] methodParameters = method.GetParameters().Select(p => p.ParameterType).ToArray(); + string methodParametersId = GetTypeArrayId(methodParameters); + _interfaceMethods.Add(Tuple.Create(method.Name, method.ReturnType, methodParametersId), method); + } + } + } + + string parameterTypeId = GetTypeArrayId(parameterTypes); + return _interfaceMethods.TryGetValue(Tuple.Create(name, returnType, parameterTypeId), out interfaceMethod); + } + + private HashSet GetImplementingInterfaces() + { + if (_interfaces == null) + { + _interfaces = new HashSet(); + + // TypeBuilder.GetInterfaces() returns only the interfaces that was explicitly passed to its constructor. + // During compilation the interface hierarchy is flattened, so we only need to resolve one level of ancestral interfaces. + foreach (var interfaceType in _typeBuilder.GetInterfaces()) + { + foreach (var parentInterface in interfaceType.GetInterfaces()) + { + _interfaces.Add(parentInterface); + } + + _interfaces.Add(interfaceType); + } + } + + return _interfaces; + } + + private static string GetTypeArrayId(Type[] types) + { + string typeId = string.Join(string.Empty, types.Select(t => t.AssemblyQualifiedName)); + byte[] typeHash = SHA256.HashData(Encoding.UTF8.GetBytes(typeId)); + + return Convert.ToHexString(typeHash); + } + public void DefineMembers() { // If user didn't provide any instance ctors or static ctor we will generate default ctor or static ctor respectively. @@ -884,12 +945,20 @@ private void DefineMethod(FunctionMemberAst functionMemberAst) return; } + var returnType = functionMemberAst.GetReturnType(); var attributes = functionMemberAst.IsPublic ? Reflection.MethodAttributes.Public : Reflection.MethodAttributes.Private; + MethodInfo interfaceBaseMethod = null; if (functionMemberAst.IsStatic) { attributes |= Reflection.MethodAttributes.Static; + + ShouldImplementMethod( + functionMemberAst.Name, + returnType, + parameterTypes, + out interfaceBaseMethod); } else { @@ -902,7 +971,6 @@ private void DefineMethod(FunctionMemberAst functionMemberAst) attributes |= Reflection.MethodAttributes.Virtual; } - var returnType = functionMemberAst.GetReturnType(); if (returnType == null) { _parser.ReportError(functionMemberAst.ReturnType.Extent, @@ -922,6 +990,11 @@ private void DefineMethod(FunctionMemberAst functionMemberAst) var ilGenerator = method.GetILGenerator(); DefineMethodBody(functionMemberAst, ilGenerator, GetMetaDataName(method.Name, parameterTypes.Length), functionMemberAst.IsStatic, parameterTypes, returnType, (i, n) => method.DefineParameter(i, ParameterAttributes.None, n)); + + if (interfaceBaseMethod != null) + { + _typeBuilder.DefineMethodOverride(method, interfaceBaseMethod); + } } private void DefineConstructor(IParameterMetadataProvider ipmp, ReadOnlyCollection attributeAsts, bool isHidden, Reflection.MethodAttributes methodAttributes, Type[] parameterTypes) diff --git a/test/powershell/Language/Classes/scripting.Classes.inheritance.tests.ps1 b/test/powershell/Language/Classes/scripting.Classes.inheritance.tests.ps1 index 391375b0011..4eb8e5f9687 100644 --- a/test/powershell/Language/Classes/scripting.Classes.inheritance.tests.ps1 +++ b/test/powershell/Language/Classes/scripting.Classes.inheritance.tests.ps1 @@ -123,6 +123,103 @@ class ClassWithStaticAbstractInterface : IInterfaceWithStaticAbstractProperty { [InterfaceStaticAbstractPropertyTest]::GetSetter[ClassWithStaticAbstractInterface]() | Should -Be 4 } + It 'can implement .NET interface static methods' { + Add-Type -TypeDefinition @' +public interface IInterfaceWithStaticAbstractMethod +{ + static abstract int GetNoParameters(); + static abstract int GetOneParameter(string value); + static abstract int GetTwoParameters(string value, int multiplier); +} + +public static class InterfaceStaticAbstractMethodTest +{ + public static int GetNoParameters() where T : IInterfaceWithStaticAbstractMethod + => T.GetNoParameters(); + + public static int GetOneParameter(string value) where T : IInterfaceWithStaticAbstractMethod + => T.GetOneParameter(value); + + public static int GetTwoParameters(string value, int multiplier) where T : IInterfaceWithStaticAbstractMethod + => T.GetTwoParameters(value, multiplier); +} +'@ + + $C1 = Invoke-Expression @' +class ClassWithStaticAbstractMethodInterface : IInterfaceWithStaticAbstractMethod { + static [int] GetNoParameters() { return 1 } + static [int] GetOneParameter([string]$Value) { return [int]$Value } + static [int] GetTwoParameters([string]$Value, [int]$Multiplier) { return ([int]$Value) * $Multiplier } + # Tests that the override uses the parameters when matching against the interface + static [int] GetTwoParameters([string]$Value, [string]$Multiplier) { return -1 } +} +[ClassWithStaticAbstractMethodInterface] +'@ + + $v = $C1::GetNoParameters() + $v | Should -Be 1 + $v | Should -BeOfType ([int]) + + $v = $C1::GetOneParameter("2") + $v | Should -Be 2 + $v | Should -BeOfType ([int]) + + $v = $C1::GetTwoParameters("3", 3) + $v | Should -Be 9 + $v | Should -BeOfType ([int]) + + $v = $C1::GetTwoParameters("3", "3") + $v | Should -Be -1 + $v | Should -BeOfType ([int]) + + [InterfaceStaticAbstractMethodTest]::GetNoParameters[ClassWithStaticAbstractMethodInterface]() | Should -Be 1 + [InterfaceStaticAbstractMethodTest]::GetOneParameter[ClassWithStaticAbstractMethodInterface]("2") | Should -Be 2 + [InterfaceStaticAbstractMethodTest]::GetTwoParameters[ClassWithStaticAbstractMethodInterface]("2", 2) | Should -Be 4 + } + + It 'can implement .NET interface static protected methods' { + Add-Type -TypeDefinition @' +public interface IInterfaceWithStaticAbstractProtectedMethod +{ + protected static abstract int ProtectedMeth(); + protected internal static abstract int ProtectedInternalMeth(string value); +} +'@ + + $C1 = Invoke-Expression @' +class ClassWithStaticAbstractProtectedMethodInterface : IInterfaceWithStaticAbstractProtectedMethod { + static [int] ProtectedMeth() { return 1 } + static [int] ProtectedInternalMeth([string]$Value) { return [int]$Value } +} +[ClassWithStaticAbstractProtectedMethodInterface] +'@ + + $v = $C1::ProtectedMeth() + $v | Should -Be 1 + $v | Should -BeOfType ([int]) + + $v = $C1::ProtectedInternalMeth("2") + $v | Should -Be 2 + $v | Should -BeOfType ([int]) + } + + It 'fails to implement .NET interface static internal methods' { + Add-Type -TypeDefinition @' +public interface IInterfaceWithStaticAbstractInternalMethod +{ + internal static abstract int Test(); +} +'@ + + { + Invoke-Expression @' +class ClassWithStaticAbstractInternalMethodInterface : IInterfaceWithStaticAbstractInternalMethod { + static [int] Test() { return 1 } +} +'@ + } | Should -Throw -ExceptionType ([System.Management.Automation.ParseException]) + } + It 'allows use of defined later type as a property type' { class A { static [B]$b } class B : A {}