From 1412ee76430e8c2d4319e418ab46bfb1af5b839f Mon Sep 17 00:00:00 2001 From: Vitek Karas <10670590+vitek-karas@users.noreply.github.com> Date: Wed, 30 Nov 2022 10:29:55 -0800 Subject: [PATCH] Enable basic Kept validation in NativeAOT running linker tests (#78828) This implements basic Type and Method Kept attribute validation. It adds a `KeptBy` property on all `KeptAttribute` which uses the same `ProducedBy` enum to specify exceptions for each target product. (Eventually after the source merge we should rename `ProducedBy` enum to just `Tool` or `Platform`, but it's not important and I didn't want to do it here as it would be a giant diff). The validation is done by going over all `ConstructedEETypeNode` and `IMethodBodyNode` nodes in the final graph and remembers that list. Then it compares that list to the expected kept members as per the attributes in tests. There are some tweaks: * Filters out all compiler added types/members * Doesn't track generic instantiations - only remember definition * If a method body is kept, then it behaves as if the type is also kept even though there's no `ConstructedEETypeNode` - this is technically wrong (since NativeAOT can remove the type if for example only static methods are used from it), but it would require major changes to the linker tests (since in IL this is a necessity). If we ever need precise validation of this, we would probably add a new attribute to check just for this. I also copied all of the "other assemblies" kept validation code from ResultChecker (were it lives in linker) to AssemblyChecker where it will need to be to actually work (and where it should probably be anyway). That code is not used yet. Left lot of TODO/commented out code in the AssemblyChecker - lots of other validation to enable eventually. Fixed/adapted all of the tests which were already ported to NativeAOT to pass with this new validation. Removed a test for unresolved members, since it actually fails NativeAOT compilation, so it doesn't test anything really. One tiny product change: Display names now consistently include all parent types when writing out nested type names. ILLink already does this and NativeAOT was a bit inconsistent. Since the display names are used only in diagnostics, this brings the diagnostics experience closer between the two tools. The added benefit is that we can use display names to compare members between Cecil and Internal.TypeSystem more precisely. Co-authored-by: Tlakaelel Axayakatl Ceja --- .../Common/Compiler/DisplayNameHelpers.cs | 12 +- .../Compiler/Dataflow/Origin.cs | 2 + .../DataflowAnalyzedMethodNode.cs | 2 +- .../Compiler/UsageBasedMetadataManager.cs | 1 + .../aot/ILCompiler/ILCompilerRootCommand.cs | 1 - .../Assertions/KeptAttribute.cs | 6 + .../DataFlow/ApplyTypeAnnotations.cs | 28 +- .../DataFlow/ComplexTypeHandling.cs | 60 +- .../DataFlow/IReflectDataflow.cs | 17 +- .../DataFlow/MemberTypesAllOnCopyAssembly.cs | 4 +- .../DataFlow/UnresolvedMembers.cs | 135 --- .../Extensions/CecilExtensions.cs | 2 + .../TestCasesRunner/AssemblyChecker.cs | 818 +++++++++++++++++- .../TestCasesRunner/ILCompilerDriver.cs | 11 +- .../TestCasesRunner/ILCompilerOptions.cs | 1 + .../ILCompilerOptionsBuilder.cs | 5 + .../ILCompilerTestCaseResult.cs | 5 +- .../TestCasesRunner/NameUtils.cs | 84 ++ .../TestCasesRunner/ResultChecker.cs | 202 +++-- .../TestCasesRunner/TestRunner.cs | 4 +- 20 files changed, 1116 insertions(+), 284 deletions(-) delete mode 100644 src/coreclr/tools/aot/Mono.Linker.Tests.Cases/DataFlow/UnresolvedMembers.cs create mode 100644 src/coreclr/tools/aot/Mono.Linker.Tests/TestCasesRunner/NameUtils.cs diff --git a/src/coreclr/tools/Common/Compiler/DisplayNameHelpers.cs b/src/coreclr/tools/Common/Compiler/DisplayNameHelpers.cs index fde8c756edf87..df62b988f20e1 100644 --- a/src/coreclr/tools/Common/Compiler/DisplayNameHelpers.cs +++ b/src/coreclr/tools/Common/Compiler/DisplayNameHelpers.cs @@ -34,9 +34,9 @@ public static string GetDisplayName(this MethodDesc method) sb.Append(method.OwningType.GetDisplayName()); sb.Append('.'); - if (method.IsConstructor) + if (method.IsConstructor && method.OwningType is DefType defType) { - sb.Append(method.OwningType.GetDisplayNameWithoutNamespace()); + sb.Append(defType.Name); } #if !READYTORUN else if (method.GetPropertyForAccessor() is PropertyPseudoDesc property) @@ -238,12 +238,8 @@ protected override Unit AppendNameForNamespaceType(StringBuilder sb, DefType typ protected override Unit AppendNameForNestedType(StringBuilder sb, DefType nestedType, DefType containingType, FormatOptions options) { - if ((options & FormatOptions.NamespaceQualify) != 0) - { - AppendName(sb, containingType, options); - sb.Append('.'); - } - + AppendName(sb, containingType, options); + sb.Append('.'); sb.Append(nestedType.Name); return default; diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/Dataflow/Origin.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/Dataflow/Origin.cs index 13b6cf85447e8..bf8142ab85302 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/Dataflow/Origin.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/Dataflow/Origin.cs @@ -81,5 +81,7 @@ public GenericParameterOrigin(GenericParameterDesc genericParam) { GenericParameter = genericParam; } + + public string GetDisplayName() => GenericParameter.GetDisplayName(); } } diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/DataflowAnalyzedMethodNode.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/DataflowAnalyzedMethodNode.cs index 07f34d58a1829..1b73665245936 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/DataflowAnalyzedMethodNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/DataflowAnalyzedMethodNode.cs @@ -44,7 +44,7 @@ public override IEnumerable GetStaticDependencies(NodeFacto protected override string GetName(NodeFactory factory) { - return "Dataflow analysis for " + factory.NameMangler.GetMangledMethodName(_methodIL.OwningMethod).ToString(); + return "Dataflow analysis for " + _methodIL.OwningMethod.ToString(); } public override bool InterestingForDynamicDependencyAnalysis => false; diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/UsageBasedMetadataManager.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/UsageBasedMetadataManager.cs index 19b9c9c5594cd..8144079a62041 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/UsageBasedMetadataManager.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/UsageBasedMetadataManager.cs @@ -305,6 +305,7 @@ protected override void GetMetadataDependenciesDueToReflectability(ref Dependenc bool fullyRoot; string reason; + // https://github.com/dotnet/runtime/issues/78752 // Compat with https://github.com/dotnet/linker/issues/1541 IL Linker bug: // Asking to root an assembly with entrypoint will not actually root things in the assembly. // We need to emulate this because the SDK injects a root for the entrypoint assembly right now diff --git a/src/coreclr/tools/aot/ILCompiler/ILCompilerRootCommand.cs b/src/coreclr/tools/aot/ILCompiler/ILCompilerRootCommand.cs index d85c275c791da..2e3000f40f5b5 100644 --- a/src/coreclr/tools/aot/ILCompiler/ILCompilerRootCommand.cs +++ b/src/coreclr/tools/aot/ILCompiler/ILCompilerRootCommand.cs @@ -7,7 +7,6 @@ using System.CommandLine.Help; using System.CommandLine.Parsing; using System.IO; -using System.Runtime.InteropServices; using Internal.TypeSystem; diff --git a/src/coreclr/tools/aot/Mono.Linker.Tests.Cases.Expectations/Assertions/KeptAttribute.cs b/src/coreclr/tools/aot/Mono.Linker.Tests.Cases.Expectations/Assertions/KeptAttribute.cs index 179ea77e5d7bd..c6f87ceb27a8c 100644 --- a/src/coreclr/tools/aot/Mono.Linker.Tests.Cases.Expectations/Assertions/KeptAttribute.cs +++ b/src/coreclr/tools/aot/Mono.Linker.Tests.Cases.Expectations/Assertions/KeptAttribute.cs @@ -8,5 +8,11 @@ namespace Mono.Linker.Tests.Cases.Expectations.Assertions [AttributeUsage (AttributeTargets.All, Inherited = false)] public class KeptAttribute : BaseExpectedLinkedBehaviorAttribute { + /// + /// By default the target should be kept by all platforms + /// This property can override that by setting only the platforms + /// which are expected to keep the target. + /// + public ProducedBy By { get; set; } = ProducedBy.TrimmerAnalyzerAndNativeAot; } } diff --git a/src/coreclr/tools/aot/Mono.Linker.Tests.Cases/DataFlow/ApplyTypeAnnotations.cs b/src/coreclr/tools/aot/Mono.Linker.Tests.Cases/DataFlow/ApplyTypeAnnotations.cs index 4576ba2baa776..4791b8f938038 100644 --- a/src/coreclr/tools/aot/Mono.Linker.Tests.Cases/DataFlow/ApplyTypeAnnotations.cs +++ b/src/coreclr/tools/aot/Mono.Linker.Tests.Cases/DataFlow/ApplyTypeAnnotations.cs @@ -128,7 +128,9 @@ private static void RequireCombinationOnString ( { } - [Kept] + // https://github.com/dotnet/runtime/issues/72833 + // NativeAOT doesn't implement full type name parser yet + [Kept (By = ProducedBy.Trimmer)] class FromStringConstantWithGenericInner { } @@ -141,10 +143,12 @@ class FromStringConstantWithGeneric public T GetValue () { return default (T); } } - [Kept] + // https://github.com/dotnet/runtime/issues/72833 + // NativeAOT doesn't implement full type name parser yet + [Kept (By = ProducedBy.Trimmer)] class FromStringConstantWithGenericInnerInner { - [Kept] + [Kept (By = ProducedBy.Trimmer)] public void Method () { } @@ -152,15 +156,17 @@ public void Method () int unusedField; } - [Kept] + [Kept (By = ProducedBy.Trimmer)] class FromStringConstantWithGenericInnerOne< [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] - [KeptAttributeAttribute (typeof (DynamicallyAccessedMembersAttribute))] + [KeptAttributeAttribute (typeof (DynamicallyAccessedMembersAttribute), By = ProducedBy.Trimmer)] T> { } - [Kept] + // https://github.com/dotnet/runtime/issues/72833 + // NativeAOT doesn't implement full type name parser yet + [Kept (By = ProducedBy.Trimmer)] class FromStringConstantWithGenericInnerTwo { void UnusedMethod () @@ -168,14 +174,22 @@ void UnusedMethod () } } - [Kept] + // https://github.com/dotnet/runtime/issues/72833 + // NativeAOT doesn't implement full type name parser yet + [Kept (By = ProducedBy.Trimmer)] class FromStringConstantWitGenericInnerMultiDimArray { } + // https://github.com/dotnet/runtime/issues/72833 + // NativeAOT actually preserves this, but for a slightly wrong reason - it completely ignores the array notations [Kept] + [KeptMember (".ctor()", By = ProducedBy.NativeAot)] class FromStringConstantWithMultiDimArray { + // https://github.com/dotnet/runtime/issues/72833 + // NativeAOT actually preserves this, but for a slightly wrong reason - it completely ignores the array notations + [Kept (By = ProducedBy.NativeAot)] public void UnusedMethod () { } } diff --git a/src/coreclr/tools/aot/Mono.Linker.Tests.Cases/DataFlow/ComplexTypeHandling.cs b/src/coreclr/tools/aot/Mono.Linker.Tests.Cases/DataFlow/ComplexTypeHandling.cs index 2e071edd796dc..2ecaae4a2fe58 100644 --- a/src/coreclr/tools/aot/Mono.Linker.Tests.Cases/DataFlow/ComplexTypeHandling.cs +++ b/src/coreclr/tools/aot/Mono.Linker.Tests.Cases/DataFlow/ComplexTypeHandling.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Reflection; using System.Text; using System.Threading.Tasks; using Mono.Linker.Tests.Cases.Expectations.Assertions; @@ -24,9 +25,12 @@ public static void Main () TestArrayTypeGetType (); TestArrayCreateInstanceByName (); TestArrayInAttributeParameter (); + TestArrayInAttributeParameter_ViaReflection (); } - [Kept] + // NativeAOT: No need to preserve the element type if it's never instantiated + // There will be a reflection record about it, but we don't validate that yet + [Kept (By = ProducedBy.Trimmer)] class ArrayElementType { public ArrayElementType () { } @@ -54,7 +58,9 @@ static void RequirePublicMethodsOnArrayOfGeneric () RequirePublicMethods (typeof (T[])); } - [Kept] + // NativeAOT: No need to preserve the element type if it's never instantiated + // There will be a reflection record about it, but we don't validate that yet + [Kept (By = ProducedBy.Trimmer)] class ArrayElementInGenericType { public ArrayElementInGenericType () { } @@ -91,7 +97,9 @@ static void RequirePublicMethodsOnArrayOfGenericParameter () _ = new RequirePublicMethodsGeneric (); } - [Kept] + // NativeAOT: No need to preserve the element type if it's never instantiated + // There will be a reflection record about it, but we don't validate that yet + [Kept (By = ProducedBy.Trimmer)] sealed class ArrayGetTypeFromMethodParamElement { // This method should not be marked, instead Array.* should be marked @@ -110,7 +118,9 @@ static void TestArrayGetTypeFromMethodParam () TestArrayGetTypeFromMethodParamHelper (null); } - [Kept] + // NativeAOT: No need to preserve the element type if it's never instantiated + // There will be a reflection record about it, but we don't validate that yet + [Kept (By = ProducedBy.Trimmer)] sealed class ArrayGetTypeFromFieldElement { // This method should not be marked, instead Array.* should be marked @@ -126,9 +136,15 @@ static void TestArrayGetTypeFromField () RequirePublicMethods (_arrayGetTypeFromField.GetType ()); } + + // https://github.com/dotnet/runtime/issues/72833 + // NativeAOT doesn't implement full type name parser yet - it ignores the [] and thus sees this as a direct type reference [Kept] sealed class ArrayTypeGetTypeElement { + // https://github.com/dotnet/runtime/issues/72833 + // NativeAOT doesn't implement full type name parser yet - it ignores the [] and thus sees this as a direct type reference + [Kept (By = ProducedBy.NativeAot)] // This method should not be marked, instead Array.* should be marked public void PublicMethod () { } } @@ -139,11 +155,13 @@ static void TestArrayTypeGetType () RequirePublicMethods (Type.GetType ("Mono.Linker.Tests.Cases.DataFlow.ComplexTypeHandling+ArrayTypeGetTypeElement[]")); } - // Technically there's no reason to mark this type since it's only used as an array element type and CreateInstance + // ILLink: Technically there's no reason to mark this type since it's only used as an array element type and CreateInstance // doesn't work on arrays, but the currently implementation will preserve it anyway due to how it processes // string -> Type resolution. This will only impact code which would have failed at runtime, so very unlikely to // actually occur in real apps (and even if it does happen, it just increases size, doesn't break behavior). - [Kept] + // NativeAOT: https://github.com/dotnet/runtime/issues/72833 + // NativeAOT doesn't implement full type name parser yet - it ignores the [] and thus sees this as a direct type reference + [Kept (By = ProducedBy.Trimmer)] class ArrayCreateInstanceByNameElement { public ArrayCreateInstanceByNameElement () @@ -157,7 +175,8 @@ static void TestArrayCreateInstanceByName () Activator.CreateInstance ("test", "Mono.Linker.Tests.Cases.DataFlow.ComplexTypeHandling+ArrayCreateInstanceByNameElement[]"); } - [Kept] + // NativeAOT doesn't keep attributes on non-reflectable methods + [Kept (By = ProducedBy.Trimmer)] class ArrayInAttributeParamElement { // This method should not be marked, instead Array.* should be marked @@ -165,12 +184,37 @@ public void PublicMethod () { } } [Kept] - [KeptAttributeAttribute (typeof (RequiresPublicMethodAttribute))] + // NativeAOT doesn't keep attributes on non-reflectable methods + [KeptAttributeAttribute (typeof (RequiresPublicMethodAttribute), By = ProducedBy.Trimmer)] [RequiresPublicMethod (typeof (ArrayInAttributeParamElement[]))] static void TestArrayInAttributeParameter () { } + // The usage of a type in attribute parameter is not enough to create NativeAOT EEType + // which is what the test infra looks for right now, so the type is not kept. + // There should be a reflection record of the type though (we just don't validate that yet). + [Kept (By = ProducedBy.Trimmer)] + class ArrayInAttributeParamElement_ViaReflection + { + // This method should not be marked, instead Array.* should be marked + public void PublicMethod () { } + } + + [Kept] + [KeptAttributeAttribute (typeof (RequiresPublicMethodAttribute))] + [RequiresPublicMethod (typeof (ArrayInAttributeParamElement_ViaReflection[]))] + static void TestArrayInAttributeParameter_ViaReflection_Inner () + { + } + + [Kept] + static void TestArrayInAttributeParameter_ViaReflection () + { + typeof (ComplexTypeHandling) + .GetMethod (nameof (TestArrayInAttributeParameter_ViaReflection_Inner), BindingFlags.NonPublic) + .Invoke (null, new object[] { }); + } [Kept] private static void RequirePublicMethods ( diff --git a/src/coreclr/tools/aot/Mono.Linker.Tests.Cases/DataFlow/IReflectDataflow.cs b/src/coreclr/tools/aot/Mono.Linker.Tests.Cases/DataFlow/IReflectDataflow.cs index 54ced51d123ca..230b67dfb5215 100644 --- a/src/coreclr/tools/aot/Mono.Linker.Tests.Cases/DataFlow/IReflectDataflow.cs +++ b/src/coreclr/tools/aot/Mono.Linker.Tests.Cases/DataFlow/IReflectDataflow.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.Reflection; using Mono.Linker.Tests.Cases.Expectations.Assertions; +using Mono.Linker.Tests.Cases.Expectations.Helpers; namespace Mono.Linker.Tests.Cases.DataFlow { @@ -117,8 +118,10 @@ class MyReflect : IReflect public object InvokeMember (string name, BindingFlags invokeAttr, Binder binder, object target, object[] args, ParameterModifier[] modifiers, CultureInfo culture, string[] namedParameters) => throw new NotImplementedException (); } - [Kept] - [KeptBaseType (typeof (MyReflect))] + // NativeAOT: Doesn't preserve this type because there's no need. The type itself is never instantiated + // there's only a field of that type and accessed to that field can be made without knowing it's type (just memory address access) + [Kept (By = ProducedBy.Trimmer)] + [KeptBaseType (typeof (MyReflect), By = ProducedBy.Trimmer)] class MyReflectDerived : MyReflect { } @@ -167,7 +170,15 @@ class TestType [Kept] public static void Test () { - new MyReflectOverType (typeof (TestType)).GetFields (BindingFlags.Instance | BindingFlags.Public); + var i = new MyReflectOverType (typeof (TestType)); + i.GetFields (BindingFlags.Instance | BindingFlags.Public); + +#if NATIVEAOT + // In Native AOT the test infra doesn't setup the compiler in a way where it will force preserve + // all external types. Like here, it will actually track usage of methods on IReflect + // and remove any which are not used. We don't want that for this test. + typeof (IReflect).RequiresAll (); +#endif } } } diff --git a/src/coreclr/tools/aot/Mono.Linker.Tests.Cases/DataFlow/MemberTypesAllOnCopyAssembly.cs b/src/coreclr/tools/aot/Mono.Linker.Tests.Cases/DataFlow/MemberTypesAllOnCopyAssembly.cs index 775bfd2681aea..8bc0975216495 100644 --- a/src/coreclr/tools/aot/Mono.Linker.Tests.Cases/DataFlow/MemberTypesAllOnCopyAssembly.cs +++ b/src/coreclr/tools/aot/Mono.Linker.Tests.Cases/DataFlow/MemberTypesAllOnCopyAssembly.cs @@ -65,7 +65,9 @@ namespace Mono.Linker.Tests.Cases.DataFlow [KeptTypeInAssembly ("base.dll", "Mono.Linker.Tests.Cases.DataFlow.Dependencies.MemberTypesAllBaseType/PrivateNestedType")] [KeptMemberInAssembly ("base.dll", "Mono.Linker.Tests.Cases.DataFlow.Dependencies.MemberTypesAllBaseType/PrivateNestedType", new string[] { "PrivateMethod()" })] - [KeptMember (".ctor()")] + // NativeAOT: https://github.com/dotnet/runtime/issues/78752 + // Once the above issue is fixed the ctor should be preserved even in NativeAOT + [KeptMember (".ctor()", By = ProducedBy.Trimmer)] public class MemberTypesAllOnCopyAssembly { public static void Main () diff --git a/src/coreclr/tools/aot/Mono.Linker.Tests.Cases/DataFlow/UnresolvedMembers.cs b/src/coreclr/tools/aot/Mono.Linker.Tests.Cases/DataFlow/UnresolvedMembers.cs deleted file mode 100644 index 400dd9af052bd..0000000000000 --- a/src/coreclr/tools/aot/Mono.Linker.Tests.Cases/DataFlow/UnresolvedMembers.cs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Mono.Linker.Tests.Cases.Expectations.Assertions; -using Mono.Linker.Tests.Cases.Expectations.Metadata; - -namespace Mono.Linker.Tests.Cases.DataFlow -{ - // NativeAOT will not compile a method with unresolved types in it - it will instead replace it with a throwing method body - // So it doesn't produce any of these warnings - which is also correct, because the code at runtime would never get there - // it would fail to JIT/run anyway. - - [SkipPeVerify] - [SetupLinkerArgument ("--skip-unresolved", "true")] - [SetupCompileBefore ("UnresolvedLibrary.dll", new[] { "Dependencies/UnresolvedLibrary.cs" }, removeFromLinkerInput: true)] - [ExpectedNoWarnings] - class UnresolvedMembers - { - public static void Main () - { - UnresolvedGenericArgument (); - UnresolvedAttributeArgument (); - UnresolvedAttributePropertyValue (); - UnresolvedAttributeFieldValue (); - UnresolvedObjectGetType (); - UnresolvedMethodParameter (); - } - - [Kept] - [KeptMember (".ctor()")] - class TypeWithUnresolvedGenericArgument< - [KeptAttributeAttribute (typeof (DynamicallyAccessedMembersAttribute))] - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] T> - { - } - - [Kept] - static void MethodWithUnresolvedGenericArgument< - [KeptAttributeAttribute (typeof (DynamicallyAccessedMembersAttribute))] - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] T> () - { } - - [Kept] - [ExpectedWarning ("IL2066", "TypeWithUnresolvedGenericArgument", ProducedBy = ProducedBy.Trimmer | ProducedBy.Analyzer)] // Local variable type - [ExpectedWarning ("IL2066", "TypeWithUnresolvedGenericArgument", ProducedBy = ProducedBy.Trimmer | ProducedBy.Analyzer)] // Called method declaring type - [ExpectedWarning ("IL2066", nameof (MethodWithUnresolvedGenericArgument), ProducedBy = ProducedBy.Trimmer | ProducedBy.Analyzer)] - static void UnresolvedGenericArgument () - { - var a = new TypeWithUnresolvedGenericArgument (); - MethodWithUnresolvedGenericArgument (); - } - - [Kept] - [KeptBaseType (typeof (Attribute))] - class AttributeWithRequirements : Attribute - { - [Kept] - public AttributeWithRequirements ( - [KeptAttributeAttribute (typeof (DynamicallyAccessedMembersAttribute))] - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type) - { } - - [Kept] - [KeptBackingField] - [KeptAttributeAttribute (typeof (DynamicallyAccessedMembersAttribute))] - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] - public Type PropertyWithRequirements { get; [Kept] set; } - - [Kept] - [KeptAttributeAttribute (typeof (DynamicallyAccessedMembersAttribute))] - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] - public Type FieldWithRequirements; - } - - [Kept] - [ExpectedWarning ("IL2062", nameof (AttributeWithRequirements), ProducedBy = ProducedBy.Trimmer | ProducedBy.Analyzer)] - [KeptAttributeAttribute (typeof (AttributeWithRequirements))] - [AttributeWithRequirements (typeof (Dependencies.UnresolvedType))] - static void UnresolvedAttributeArgument () - { - } - - [Kept] - [ExpectedWarning ("IL2062", nameof (AttributeWithRequirements.PropertyWithRequirements), ProducedBy = ProducedBy.Trimmer | ProducedBy.Analyzer)] - [KeptAttributeAttribute (typeof (AttributeWithRequirements))] - [AttributeWithRequirements (typeof (EmptyType), PropertyWithRequirements = typeof (Dependencies.UnresolvedType))] - static void UnresolvedAttributePropertyValue () - { - } - - [Kept] - [ExpectedWarning ("IL2064", nameof (AttributeWithRequirements.FieldWithRequirements), ProducedBy = ProducedBy.Trimmer | ProducedBy.Analyzer)] - [KeptAttributeAttribute (typeof (AttributeWithRequirements))] - [AttributeWithRequirements (typeof (EmptyType), FieldWithRequirements = typeof (Dependencies.UnresolvedType))] - static void UnresolvedAttributeFieldValue () - { - } - - [Kept] - static Dependencies.UnresolvedType _unresolvedField; - - [Kept] - [ExpectedWarning ("IL2072", nameof (Object.GetType), ProducedBy = ProducedBy.Trimmer | ProducedBy.Analyzer)] - static void UnresolvedObjectGetType () - { - RequirePublicMethods (_unresolvedField.GetType ()); - } - - [Kept] - [ExpectedWarning ("IL2072", nameof (Object.GetType), ProducedBy = ProducedBy.Trimmer | ProducedBy.Analyzer)] - static void UnresolvedMethodParameter () - { - RequirePublicMethods (typeof (Dependencies.UnresolvedType)); - } - - [Kept] - class EmptyType - { - } - - [Kept] - static void RequirePublicMethods ( - [KeptAttributeAttribute (typeof (DynamicallyAccessedMembersAttribute))] - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] - Type t) - { - } - } -} diff --git a/src/coreclr/tools/aot/Mono.Linker.Tests/Extensions/CecilExtensions.cs b/src/coreclr/tools/aot/Mono.Linker.Tests/Extensions/CecilExtensions.cs index 763cec9c4c776..5e4dc5ffa1e13 100644 --- a/src/coreclr/tools/aot/Mono.Linker.Tests/Extensions/CecilExtensions.cs +++ b/src/coreclr/tools/aot/Mono.Linker.Tests/Extensions/CecilExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text; using Mono.Cecil; @@ -310,6 +311,7 @@ public static StringBuilder GetDisplayNameWithoutNamespace (this TypeReference t break; } + type = type.GetElementType (); if (type.DeclaringType is not TypeReference declaringType) break; diff --git a/src/coreclr/tools/aot/Mono.Linker.Tests/TestCasesRunner/AssemblyChecker.cs b/src/coreclr/tools/aot/Mono.Linker.Tests/TestCasesRunner/AssemblyChecker.cs index 1e2836d080e28..8d3e8efe8a09e 100644 --- a/src/coreclr/tools/aot/Mono.Linker.Tests/TestCasesRunner/AssemblyChecker.cs +++ b/src/coreclr/tools/aot/Mono.Linker.Tests/TestCasesRunner/AssemblyChecker.cs @@ -3,30 +3,44 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; using FluentAssertions; +using Internal.TypeSystem; +using Internal.TypeSystem.Ecma; using Mono.Cecil; using Mono.Cecil.Cil; using Mono.Linker.Tests.Cases.Expectations.Assertions; using Mono.Linker.Tests.Extensions; using Xunit; +using MetadataType = Internal.TypeSystem.MetadataType; namespace Mono.Linker.Tests.TestCasesRunner { public class AssemblyChecker { - private readonly AssemblyDefinition originalAssembly, linkedAssembly; - private HashSet linkedMembers; + private readonly BaseAssemblyResolver originalsResolver; + private readonly ReaderParameters originalReaderParameters; + private readonly AssemblyDefinition originalAssembly; + private readonly ILCompilerTestCaseResult testResult; + + private readonly Dictionary linkedMembers; private readonly HashSet verifiedGeneratedFields = new HashSet (); private readonly HashSet verifiedEventMethods = new HashSet (); private readonly HashSet verifiedGeneratedTypes = new HashSet (); private bool checkNames; - public AssemblyChecker (AssemblyDefinition original, AssemblyDefinition linked) + public AssemblyChecker ( + BaseAssemblyResolver originalsResolver, + ReaderParameters originalReaderParameters, + AssemblyDefinition original, + ILCompilerTestCaseResult testResult) { + this.originalsResolver = originalsResolver; + this.originalReaderParameters = originalReaderParameters; this.originalAssembly = original; - this.linkedAssembly = linked; + this.testResult = testResult; this.linkedMembers = new (StringComparer.Ordinal); checkNames = original.MainModule.GetTypeReferences ().Any (attr => @@ -35,20 +49,37 @@ public AssemblyChecker (AssemblyDefinition original, AssemblyDefinition linked) public void Verify () { - VerifyExportedTypes (originalAssembly, linkedAssembly); + // There are no type forwarders left after compilation in Native AOT + // VerifyExportedTypes (originalAssembly, linkedAssembly); - VerifyCustomAttributes (originalAssembly, linkedAssembly); - VerifySecurityAttributes (originalAssembly, linkedAssembly); + // TODO + // VerifyCustomAttributes (originalAssembly, linkedAssembly); + // VerifySecurityAttributes (originalAssembly, linkedAssembly); - foreach (var originalModule in originalAssembly.Modules) - VerifyModule (originalModule, linkedAssembly.Modules.FirstOrDefault (m => m.Name == originalModule.Name)); + // TODO - this is mostly attribute verification + // foreach (var originalModule in originalAssembly.Modules) + // VerifyModule (originalModule, linkedAssembly.Modules.FirstOrDefault (m => m.Name == originalModule.Name)); - VerifyResources (originalAssembly, linkedAssembly); - VerifyReferences (originalAssembly, linkedAssembly); + // TODO + // VerifyResources (originalAssembly, linkedAssembly); - foreach (var s in linkedAssembly.MainModule.AllMembers ().Select (s => s.FullName)) { - this.linkedMembers.Add (s); - } + // There are no assembly reference in Native AOT + // VerifyReferences (originalAssembly, linkedAssembly); + + PopulateLinkedMembers (); + + // Workaround for compiler injected attribute to describe the language version + linkedMembers.Remove ("Microsoft.CodeAnalysis.EmbeddedAttribute.EmbeddedAttribute()"); + linkedMembers.Remove ("System.Runtime.CompilerServices.RefSafetyRulesAttribute.Version"); + linkedMembers.Remove ("System.Runtime.CompilerServices.RefSafetyRulesAttribute.RefSafetyRulesAttribute(Int32)"); + + // Workaround for NativeAOT injected members + linkedMembers.Remove (".StartupCodeMain(Int32,IntPtr)"); + linkedMembers.Remove (".MainMethodWrapper()"); + + // Workaround for compiler injected attribute to describe the language version + verifiedGeneratedTypes.Add ("Microsoft.CodeAnalysis.EmbeddedAttribute"); + verifiedGeneratedTypes.Add ("System.Runtime.CompilerServices.RefSafetyRulesAttribute"); var membersToAssert = originalAssembly.MainModule.Types; foreach (var originalMember in membersToAssert) { @@ -58,8 +89,10 @@ public void Verify () continue; } - TypeDefinition linkedType = linkedAssembly.MainModule.GetType (originalMember.FullName); - VerifyTypeDefinition (td, linkedType); + linkedMembers.TryGetValue ( + NameUtils.GetExpectedOriginDisplayName (originalMember), + out TypeSystemEntity? linkedMember); + VerifyTypeDefinition (td, linkedMember as TypeDesc); linkedMembers.Remove (td.FullName); continue; @@ -68,7 +101,102 @@ public void Verify () throw new NotImplementedException ($"Don't know how to check member of type {originalMember.GetType ()}"); } - Assert.Empty (linkedMembers); + // Filter out all members which are not from the main assembly + // The Kept attributes are "optional" for non-main assemblies + string mainModuleName = originalAssembly.Name.Name; + List externalMembers = linkedMembers.Where (m => GetModuleName (m.Value) != mainModuleName).Select (m => m.Key).ToList (); + foreach (var externalMember in externalMembers) { + linkedMembers.Remove (externalMember); + } + + if (linkedMembers.Count != 0) + Assert.True ( + false, + "Linked output includes unexpected member:\n " + + string.Join ("\n ", linkedMembers.Keys)); + } + + private void PopulateLinkedMembers () + { + foreach (TypeDesc? constructedType in testResult.TrimmingResults.ConstructedEETypes) { + AddType (constructedType); + } + + foreach (MethodDesc? compiledMethod in testResult.TrimmingResults.CompiledMethodBodies) { + AddMethod (compiledMethod); + } + + void AddMethod (MethodDesc method) + { + MethodDesc methodDef = method.GetTypicalMethodDefinition (); + + if (!ShouldIncludeType (methodDef.OwningType)) + return; + + if (!AddMember (methodDef)) + return; + + if (methodDef.OwningType is { } owningType) + AddType (owningType); + } + + void AddType (TypeDesc type) + { + TypeDesc typeDef = type.GetTypeDefinition (); + + if (!ShouldIncludeType (typeDef)) + return; + + if (!AddMember (typeDef)) + return; + + if (typeDef is MetadataType { ContainingType: { } containingType }) { + AddType (containingType); + } + } + + bool AddMember (TypeSystemEntity entity) + { + if (NameUtils.GetActualOriginDisplayName (entity) is string fullName && + !linkedMembers.ContainsKey (fullName)) { + + linkedMembers.Add (fullName, entity); + return true; + } + + return false; + } + + bool ShouldIncludeType (TypeDesc type) + { + if (type is MetadataType metadataType) { + if (metadataType.ContainingType is { } containingType) { + if (!ShouldIncludeType (containingType)) + return false; + } + + if (metadataType.Namespace.StartsWith ("Internal")) + return false; + + // Simple way to filter out system assemblies - the best way would be to get a list + // of input/reference assemblies and filter on that, but it's tricky and this should work for basically everything + if (metadataType.Namespace.StartsWith ("System")) + return false; + + return true; + } + + return false; + } + } + + private static string? GetModuleName (TypeSystemEntity entity) + { + return entity switch { + MetadataType type => type.Module.ToString (), + MethodDesc { OwningType: MetadataType owningType } => owningType.Module.ToString (), + _ => null + }; } protected virtual void VerifyModule (ModuleDefinition original, ModuleDefinition? linked) @@ -92,12 +220,12 @@ protected virtual void VerifyModule (ModuleDefinition original, ModuleDefinition VerifyCustomAttributes (original, linked); } - protected virtual void VerifyTypeDefinition (TypeDefinition original, TypeDefinition? linked) + protected virtual void VerifyTypeDefinition (TypeDefinition original, TypeDesc? linked) { - if (linked != null && verifiedGeneratedTypes.Contains (linked.FullName)) + if (linked != null && NameUtils.GetActualOriginDisplayName (linked) is string linkedDisplayName && verifiedGeneratedTypes.Contains (linkedDisplayName)) return; - ModuleDefinition? linkedModule = linked?.Module; + EcmaModule? linkedModule = (linked as MetadataType)?.Module as EcmaModule; // // Little bit complex check to allow easier test writing to match @@ -106,9 +234,9 @@ protected virtual void VerifyTypeDefinition (TypeDefinition original, TypeDefini // - It contains at least one member which has [Kept] attribute (not recursive) // bool expectedKept = - original.HasAttributeDerivedFrom (nameof (KeptAttribute)) || - (linked != null && linkedModule!.Assembly.EntryPoint?.DeclaringType == linked) || - original.AllMembers ().Any (l => l.HasAttribute (nameof (KeptAttribute))); + HasActiveKeptDerivedAttribute (original) || + (linked != null && linkedModule?.EntryPoint?.OwningType == linked) || + original.AllMembers ().Any (HasActiveKeptDerivedAttribute); if (!expectedKept) { if (linked != null) @@ -128,19 +256,23 @@ protected virtual void VerifyTypeDefinition (TypeDefinition original, TypeDefini foreach (var attr in original.CustomAttributes.Where (l => l.AttributeType.Name == nameof (CreatedMemberAttribute))) { var newName = original.FullName + "::" + attr.ConstructorArguments[0].Value.ToString (); - if (linkedMembers!.RemoveWhere (l => l.Contains (newName)) != 1) + var linkedMemberName = linkedMembers.Keys.FirstOrDefault (l => l.Contains (newName)); + if (linkedMemberName == null) Assert.True (false, $"Newly created member '{newName}' was not found"); + + linkedMembers.Remove (linkedMemberName); } } } - protected virtual void VerifyTypeDefinitionKept (TypeDefinition original, TypeDefinition? linked) + protected virtual void VerifyTypeDefinitionKept (TypeDefinition original, TypeDesc? linked) { if (linked == null) { Assert.True (false, $"Type `{original}' should have been kept"); return; } +#if false if (!original.IsInterface) VerifyBaseType (original, linked); @@ -151,13 +283,20 @@ protected virtual void VerifyTypeDefinitionKept (TypeDefinition original, TypeDe VerifySecurityAttributes (original, linked); VerifyFixedBufferFields (original, linked); +#endif foreach (var td in original.NestedTypes) { - VerifyTypeDefinition (td, linked?.NestedTypes.FirstOrDefault (l => td.FullName == l.FullName)); - linkedMembers.Remove (td.FullName); + string originalFullName = NameUtils.GetExpectedOriginDisplayName (td); + linkedMembers.TryGetValue ( + originalFullName, + out TypeSystemEntity? linkedMember); + + VerifyTypeDefinition (td, linkedMember as TypeDesc); + linkedMembers.Remove (originalFullName); } - // Need to check properties before fields so that the KeptBackingFieldAttribute is handled correctly +#if false + //// Need to check properties before fields so that the KeptBackingFieldAttribute is handled correctly foreach (var p in original.Properties) { VerifyProperty (p, linked.Properties.FirstOrDefault (l => p.Name == l.Name), linked); linkedMembers.Remove (p.FullName); @@ -177,13 +316,19 @@ protected virtual void VerifyTypeDefinitionKept (TypeDefinition original, TypeDe VerifyField (f, linked.Fields.FirstOrDefault (l => f.Name == l.Name)); linkedMembers.Remove (f.FullName); } +#endif foreach (var m in original.Methods) { if (verifiedEventMethods.Contains (m.FullName)) continue; - var msign = m.GetSignature (); - VerifyMethod (m, linked.Methods.FirstOrDefault (l => msign == l.GetSignature ())); - linkedMembers.Remove (m.FullName); + + string originalFullName = NameUtils.GetExpectedOriginDisplayName (m); + linkedMembers.TryGetValue ( + originalFullName, + out TypeSystemEntity? linkedMember); + + VerifyMethod (m, linkedMember as MethodDesc); + linkedMembers.Remove (originalFullName); } } @@ -319,13 +464,13 @@ private void VerifyEvent (EventDefinition src, EventDefinition? linked, TypeDefi } if (src.CustomAttributes.Any (attr => attr.AttributeType.Name == nameof (KeptEventAddMethodAttribute))) { - VerifyMethodInternal (src.AddMethod, linked.AddMethod, true); + //VerifyMethodInternal (src.AddMethod, linked.AddMethod, true); verifiedEventMethods.Add (src.AddMethod.FullName); linkedMembers.Remove (src.AddMethod.FullName); } if (src.CustomAttributes.Any (attr => attr.AttributeType.Name == nameof (KeptEventRemoveMethodAttribute))) { - VerifyMethodInternal (src.RemoveMethod, linked.RemoveMethod, true); + //VerifyMethodInternal (src.RemoveMethod, linked.RemoveMethod, true); verifiedEventMethods.Add (src.RemoveMethod.FullName); linkedMembers.Remove (src.RemoveMethod.FullName); } @@ -334,17 +479,17 @@ private void VerifyEvent (EventDefinition src, EventDefinition? linked, TypeDefi VerifyCustomAttributes (src, linked); } - private void VerifyMethod (MethodDefinition src, MethodDefinition? linked) + private void VerifyMethod (MethodDefinition src, MethodDesc? linked) { bool expectedKept = ShouldMethodBeKept (src); VerifyMethodInternal (src, linked, expectedKept); } - private void VerifyMethodInternal (MethodDefinition src, MethodDefinition? linked, bool expectedKept) + private void VerifyMethodInternal (MethodDefinition src, MethodDesc? linked, bool expectedKept) { if (!expectedKept) { if (linked != null) - Assert.True (false, $"Method `{src.FullName}' should have been removed"); + Assert.True (false, $"Method `{NameUtils.GetExpectedOriginDisplayName (src)}' should have been removed"); return; } @@ -381,13 +526,14 @@ private void VerifyMemberBackingField (IMemberDefinition src, TypeDefinition lin linkedMembers.Remove (srcField.FullName); } - protected virtual void VerifyMethodKept (MethodDefinition src, MethodDefinition? linked) + protected virtual void VerifyMethodKept (MethodDefinition src, MethodDesc? linked) { if (linked == null) { - Assert.True (false, $"Method `{src.FullName}' should have been kept"); + Assert.True (false, $"Method `{NameUtils.GetExpectedOriginDisplayName (src)}' should have been kept"); return; } +#if false VerifyPseudoAttributes (src, linked); VerifyGenericParameters (src, linked); VerifyCustomAttributes (src, linked); @@ -395,7 +541,10 @@ protected virtual void VerifyMethodKept (MethodDefinition src, MethodDefinition? VerifyParameters (src, linked); VerifySecurityAttributes (src, linked); VerifyArrayInitializers (src, linked); + + // Method bodies are not very different in Native AOT VerifyMethodBody (src, linked); +#endif } protected virtual void VerifyMethodBody (MethodDefinition src, MethodDefinition linked) @@ -735,7 +884,7 @@ private void VerifyInitializerField (FieldDefinition src, FieldDefinition? linke VerifyFieldKept (src, linked); verifiedGeneratedFields.Add (linked!.FullName); linkedMembers.Remove (linked.FullName); - VerifyTypeDefinitionKept (src.FieldType.Resolve (), linked.FieldType.Resolve ()); + //VerifyTypeDefinitionKept (src.FieldType.Resolve (), linked.FieldType.Resolve ()); linkedMembers.Remove (linked.FieldType.FullName); linkedMembers.Remove (linked.DeclaringType.FullName); verifiedGeneratedTypes.Add (linked.DeclaringType.FullName); @@ -840,7 +989,7 @@ private void VerifyFixedBufferFields (TypeDefinition src, TypeDefinition linked) verifiedGeneratedFields.Add (originalElementField.FullName); linkedMembers.Remove (linkedField!.FullName); - VerifyTypeDefinitionKept (originalCompilerGeneratedBufferType, linkedCompilerGeneratedBufferType); + //VerifyTypeDefinitionKept (originalCompilerGeneratedBufferType, linkedCompilerGeneratedBufferType); verifiedGeneratedTypes.Add (originalCompilerGeneratedBufferType.FullName); } } @@ -915,14 +1064,20 @@ protected virtual bool ShouldMethodBeKept (MethodDefinition method) protected virtual bool ShouldBeKept (T member, string? signature = null) where T : MemberReference, ICustomAttributeProvider { - if (member.HasAttribute (nameof (KeptAttribute))) + if (HasActiveKeptAttribute (member)) return true; ICustomAttributeProvider cap = (ICustomAttributeProvider) member.DeclaringType; if (cap == null) return false; - return GetCustomAttributeCtorValues (cap, nameof (KeptMemberAttribute)).Any (a => a == (signature ?? member.Name)); + return GetActiveKeptAttributes (cap, nameof (KeptMemberAttribute)).Any (ca => { + if (ca.Constructor.Parameters.Count != 1 || + ca.ConstructorArguments[0].Value is not string a) + return false; + + return a == (signature ?? member.Name); + }); } protected static uint GetExpectedPseudoAttributeValue (ICustomAttributeProvider provider, uint sourceValue) @@ -955,5 +1110,584 @@ protected static IEnumerable GetStringOrTypeArrayAttributeValue (CustomA { return ((CustomAttributeArgument[]) attribute.ConstructorArguments[0].Value)?.Select (arg => arg.Value.ToString ()!); } + + private static IEnumerable GetActiveKeptAttributes (ICustomAttributeProvider provider, string attributeName) + { + return provider.CustomAttributes.Where (ca => { + if (ca.AttributeType.Name != attributeName) { + return false; + } + + object? keptBy = ca.GetPropertyValue (nameof (KeptAttribute.By)); + return keptBy is null ? true : ((ProducedBy) keptBy).HasFlag (ProducedBy.NativeAot); + }); + } + + private static bool HasActiveKeptAttribute (ICustomAttributeProvider provider) + { + return GetActiveKeptAttributes (provider, nameof (KeptAttribute)).Any (); + } + + private static IEnumerable GetActiveKeptDerivedAttributes (ICustomAttributeProvider provider) + { + return provider.CustomAttributes.Where (ca => { + if (!ca.AttributeType.Resolve ().DerivesFrom (nameof (KeptAttribute))) { + return false; + } + + object? keptBy = ca.GetPropertyValue (nameof (KeptAttribute.By)); + return keptBy is null ? true : ((ProducedBy) keptBy).HasFlag (ProducedBy.NativeAot); + }); + } + + + private static bool HasActiveKeptDerivedAttribute (ICustomAttributeProvider provider) + { + return GetActiveKeptDerivedAttributes (provider).Any (); + } + + private void VerifyLinkingOfOtherAssemblies (AssemblyDefinition original) + { + var checks = BuildOtherAssemblyCheckTable (original); + + // TODO + // For now disable the code below by simply removing all checks + checks.Clear (); + + try { + foreach (var assemblyName in checks.Keys) { + var linkedAssembly = ResolveLinkedAssembly (assemblyName); + foreach (var checkAttrInAssembly in checks[assemblyName]) { + var attributeTypeName = checkAttrInAssembly.AttributeType.Name; + + switch (attributeTypeName) { + case nameof (KeptAllTypesAndMembersInAssemblyAttribute): + VerifyKeptAllTypesAndMembersInAssembly (linkedAssembly); + continue; + case nameof (KeptAttributeInAssemblyAttribute): + VerifyKeptAttributeInAssembly (checkAttrInAssembly, linkedAssembly); + continue; + case nameof (RemovedAttributeInAssembly): + VerifyRemovedAttributeInAssembly (checkAttrInAssembly, linkedAssembly); + continue; + default: + break; + } + + var expectedTypeName = checkAttrInAssembly.ConstructorArguments[1].Value.ToString ()!; + TypeDefinition? linkedType = linkedAssembly.MainModule.GetType (expectedTypeName); + + if (linkedType == null && linkedAssembly.MainModule.HasExportedTypes) { + ExportedType? exportedType = linkedAssembly.MainModule.ExportedTypes + .FirstOrDefault (exported => exported.FullName == expectedTypeName); + + // Note that copied assemblies could have dangling references. + if (exportedType != null && original.EntryPoint.DeclaringType.CustomAttributes.FirstOrDefault ( + ca => ca.AttributeType.Name == nameof (RemovedAssemblyAttribute) + && ca.ConstructorArguments[0].Value.ToString () == exportedType.Scope.Name + ".dll") != null) + continue; + + linkedType = exportedType?.Resolve (); + } + + switch (attributeTypeName) { + case nameof (RemovedTypeInAssemblyAttribute): + if (linkedType != null) + Assert.Fail ($"Type `{expectedTypeName}' should have been removed from assembly {assemblyName}"); + GetOriginalTypeFromInAssemblyAttribute (checkAttrInAssembly); + break; + case nameof (KeptTypeInAssemblyAttribute): + if (linkedType == null) + Assert.Fail ($"Type `{expectedTypeName}' should have been kept in assembly {assemblyName}"); + break; + case nameof (RemovedInterfaceOnTypeInAssemblyAttribute): + if (linkedType == null) + Assert.Fail ($"Type `{expectedTypeName}' should have been kept in assembly {assemblyName}"); + VerifyRemovedInterfaceOnTypeInAssembly (checkAttrInAssembly, linkedType); + break; + case nameof (KeptInterfaceOnTypeInAssemblyAttribute): + if (linkedType == null) + Assert.Fail ($"Type `{expectedTypeName}' should have been kept in assembly {assemblyName}"); + VerifyKeptInterfaceOnTypeInAssembly (checkAttrInAssembly, linkedType); + break; + case nameof (RemovedMemberInAssemblyAttribute): + if (linkedType == null) + continue; + + VerifyRemovedMemberInAssembly (checkAttrInAssembly, linkedType); + break; + case nameof (KeptBaseOnTypeInAssemblyAttribute): + if (linkedType == null) + Assert.Fail ($"Type `{expectedTypeName}' should have been kept in assembly {assemblyName}"); + VerifyKeptBaseOnTypeInAssembly (checkAttrInAssembly, linkedType); + break; + case nameof (KeptMemberInAssemblyAttribute): + if (linkedType == null) + Assert.Fail ($"Type `{expectedTypeName}' should have been kept in assembly {assemblyName}"); + + VerifyKeptMemberInAssembly (checkAttrInAssembly, linkedType); + break; + case nameof (RemovedForwarderAttribute): + if (linkedAssembly.MainModule.ExportedTypes.Any (l => l.Name == expectedTypeName)) + Assert.Fail ($"Forwarder `{expectedTypeName}' should have been removed from assembly {assemblyName}"); + + break; + + case nameof (RemovedAssemblyReferenceAttribute): + Assert.False (linkedAssembly.MainModule.AssemblyReferences.Any (l => l.Name == expectedTypeName), + $"AssemblyRef '{expectedTypeName}' should have been removed from assembly {assemblyName}"); + break; + + case nameof (KeptResourceInAssemblyAttribute): + VerifyKeptResourceInAssembly (checkAttrInAssembly); + break; + case nameof (RemovedResourceInAssemblyAttribute): + VerifyRemovedResourceInAssembly (checkAttrInAssembly); + break; + case nameof (KeptReferencesInAssemblyAttribute): + VerifyKeptReferencesInAssembly (checkAttrInAssembly); + break; + case nameof (ExpectedInstructionSequenceOnMemberInAssemblyAttribute): + if (linkedType == null) + Assert.Fail ($"Type `{expectedTypeName}` should have been kept in assembly {assemblyName}"); + VerifyExpectedInstructionSequenceOnMemberInAssembly (checkAttrInAssembly, linkedType); + break; + default: + UnhandledOtherAssemblyAssertion (expectedTypeName, checkAttrInAssembly, linkedType); + break; + } + } + } + } catch (AssemblyResolutionException e) { + Assert.Fail ($"Failed to resolve linked assembly `{e.AssemblyReference.Name}`. It must not exist in the output."); + } + } + + private void VerifyKeptAttributeInAssembly (CustomAttribute inAssemblyAttribute, AssemblyDefinition linkedAssembly) + { + VerifyAttributeInAssembly (inAssemblyAttribute, linkedAssembly, VerifyCustomAttributeKept); + } + + private void VerifyRemovedAttributeInAssembly (CustomAttribute inAssemblyAttribute, AssemblyDefinition linkedAssembly) + { + VerifyAttributeInAssembly (inAssemblyAttribute, linkedAssembly, VerifyCustomAttributeRemoved); + } + + private void VerifyAttributeInAssembly (CustomAttribute inAssemblyAttribute, AssemblyDefinition linkedAssembly, Action assertExpectedAttribute) + { + var assemblyName = (string) inAssemblyAttribute.ConstructorArguments[0].Value!; + string expectedAttributeTypeName; + var attributeTypeOrTypeName = inAssemblyAttribute.ConstructorArguments[1].Value!; + if (attributeTypeOrTypeName is TypeReference typeReference) { + expectedAttributeTypeName = typeReference.FullName; + } else { + expectedAttributeTypeName = attributeTypeOrTypeName.ToString ()!; + } + + if (inAssemblyAttribute.ConstructorArguments.Count == 2) { + // Assembly + assertExpectedAttribute (linkedAssembly, expectedAttributeTypeName); + return; + } + + // We are asserting on type or member + var typeOrTypeName = inAssemblyAttribute.ConstructorArguments[2].Value; + var originalType = GetOriginalTypeFromInAssemblyAttribute (inAssemblyAttribute.ConstructorArguments[0].Value.ToString ()!, typeOrTypeName); + if (originalType == null) + Assert.Fail ($"Invalid test assertion. The original `{assemblyName}` does not contain a type `{typeOrTypeName}`"); + + var linkedType = linkedAssembly.MainModule.GetType (originalType.FullName); + if (linkedType == null) + Assert.Fail ($"Missing expected type `{typeOrTypeName}` in `{assemblyName}`"); + + if (inAssemblyAttribute.ConstructorArguments.Count == 3) { + assertExpectedAttribute (linkedType, expectedAttributeTypeName); + return; + } + + // we are asserting on a member + string memberName = (string) inAssemblyAttribute.ConstructorArguments[3].Value; + + // We will find the matching type from the original assembly first that way we can confirm + // that the name defined in the attribute corresponds to a member that actually existed + var originalFieldMember = originalType.Fields.FirstOrDefault (m => m.Name == memberName); + if (originalFieldMember != null) { + var linkedField = linkedType.Fields.FirstOrDefault (m => m.Name == memberName); + if (linkedField == null) + Assert.Fail ($"Field `{memberName}` on Type `{originalType}` should have been kept"); + + assertExpectedAttribute (linkedField, expectedAttributeTypeName); + return; + } + + var originalPropertyMember = originalType.Properties.FirstOrDefault (m => m.Name == memberName); + if (originalPropertyMember != null) { + var linkedProperty = linkedType.Properties.FirstOrDefault (m => m.Name == memberName); + if (linkedProperty == null) + Assert.Fail ($"Property `{memberName}` on Type `{originalType}` should have been kept"); + + assertExpectedAttribute (linkedProperty, expectedAttributeTypeName); + return; + } + + var originalMethodMember = originalType.Methods.FirstOrDefault (m => m.GetSignature () == memberName); + if (originalMethodMember != null) { + var linkedMethod = linkedType.Methods.FirstOrDefault (m => m.GetSignature () == memberName); + if (linkedMethod == null) + Assert.Fail ($"Method `{memberName}` on Type `{originalType}` should have been kept"); + + assertExpectedAttribute (linkedMethod, expectedAttributeTypeName); + return; + } + + Assert.Fail ($"Invalid test assertion. No member named `{memberName}` exists on the original type `{originalType}`"); + } + + private static void VerifyCopyAssemblyIsKeptUnmodified (NPath outputDirectory, string assemblyName) + { + string inputAssemblyPath = Path.Combine (Directory.GetParent (outputDirectory)!.ToString (), "input", assemblyName); + string outputAssemblyPath = Path.Combine (outputDirectory, assemblyName); + Assert.True (File.ReadAllBytes (inputAssemblyPath).SequenceEqual (File.ReadAllBytes (outputAssemblyPath)), + $"Expected assemblies\n" + + $"\t{inputAssemblyPath}\n" + + $"\t{outputAssemblyPath}\n" + + $"binaries to be equal, since the input assembly has copy action."); + } + + private void VerifyCustomAttributeKept (ICustomAttributeProvider provider, string expectedAttributeTypeName) + { + var match = provider.CustomAttributes.FirstOrDefault (attr => attr.AttributeType.FullName == expectedAttributeTypeName); + if (match == null) + Assert.Fail ($"Expected `{provider}` to have an attribute of type `{expectedAttributeTypeName}`"); + } + + private void VerifyCustomAttributeRemoved (ICustomAttributeProvider provider, string expectedAttributeTypeName) + { + var match = provider.CustomAttributes.FirstOrDefault (attr => attr.AttributeType.FullName == expectedAttributeTypeName); + if (match != null) + Assert.Fail ($"Expected `{provider}` to no longer have an attribute of type `{expectedAttributeTypeName}`"); + } + + private void VerifyRemovedInterfaceOnTypeInAssembly (CustomAttribute inAssemblyAttribute, TypeDefinition linkedType) + { + var originalType = GetOriginalTypeFromInAssemblyAttribute (inAssemblyAttribute); + + var interfaceAssemblyName = inAssemblyAttribute.ConstructorArguments[2].Value.ToString ()!; + var interfaceType = inAssemblyAttribute.ConstructorArguments[3].Value; + + var originalInterface = GetOriginalTypeFromInAssemblyAttribute (interfaceAssemblyName, interfaceType); + if (!originalType.HasInterfaces) + Assert.Fail ("Invalid assertion. Original type does not have any interfaces"); + + var originalInterfaceImpl = GetMatchingInterfaceImplementationOnType (originalType, originalInterface.FullName); + if (originalInterfaceImpl == null) + Assert.Fail ($"Invalid assertion. Original type never had an interface of type `{originalInterface}`"); + + var linkedInterfaceImpl = GetMatchingInterfaceImplementationOnType (linkedType, originalInterface.FullName); + if (linkedInterfaceImpl != null) + Assert.Fail ($"Expected `{linkedType}` to no longer have an interface of type {originalInterface.FullName}"); + } + + private void VerifyKeptInterfaceOnTypeInAssembly (CustomAttribute inAssemblyAttribute, TypeDefinition linkedType) + { + var originalType = GetOriginalTypeFromInAssemblyAttribute (inAssemblyAttribute); + + var interfaceAssemblyName = inAssemblyAttribute.ConstructorArguments[2].Value.ToString ()!; + var interfaceType = inAssemblyAttribute.ConstructorArguments[3].Value; + + var originalInterface = GetOriginalTypeFromInAssemblyAttribute (interfaceAssemblyName, interfaceType); + if (!originalType.HasInterfaces) + Assert.Fail ("Invalid assertion. Original type does not have any interfaces"); + + var originalInterfaceImpl = GetMatchingInterfaceImplementationOnType (originalType, originalInterface.FullName); + if (originalInterfaceImpl == null) + Assert.Fail ($"Invalid assertion. Original type never had an interface of type `{originalInterface}`"); + + var linkedInterfaceImpl = GetMatchingInterfaceImplementationOnType (linkedType, originalInterface.FullName); + if (linkedInterfaceImpl == null) + Assert.Fail ($"Expected `{linkedType}` to have interface of type {originalInterface.FullName}"); + } + + private void VerifyKeptBaseOnTypeInAssembly (CustomAttribute inAssemblyAttribute, TypeDefinition linkedType) + { + var originalType = GetOriginalTypeFromInAssemblyAttribute (inAssemblyAttribute); + + var baseAssemblyName = inAssemblyAttribute.ConstructorArguments[2].Value.ToString ()!; + var baseType = inAssemblyAttribute.ConstructorArguments[3].Value; + + var originalBase = GetOriginalTypeFromInAssemblyAttribute (baseAssemblyName, baseType); + if (originalType.BaseType.Resolve () != originalBase) + Assert.Fail ("Invalid assertion. Original type's base does not match the expected base"); + + Assert.True (originalBase.FullName == linkedType.BaseType.FullName, + $"Incorrect base on `{linkedType.FullName}`. Expected `{originalBase.FullName}` but was `{linkedType.BaseType.FullName}`"); + } + + private static InterfaceImplementation? GetMatchingInterfaceImplementationOnType (TypeDefinition type, string expectedInterfaceTypeName) + { + return type.Interfaces.FirstOrDefault (impl => { + var resolvedImpl = impl.InterfaceType.Resolve (); + + if (resolvedImpl == null) + Assert.Fail ($"Failed to resolve interface : `{impl.InterfaceType}` on `{type}`"); + + return resolvedImpl.FullName == expectedInterfaceTypeName; + }); + } + + private void VerifyRemovedMemberInAssembly (CustomAttribute inAssemblyAttribute, TypeDefinition linkedType) + { + var originalType = GetOriginalTypeFromInAssemblyAttribute (inAssemblyAttribute); + foreach (var memberNameAttr in (CustomAttributeArgument[]) inAssemblyAttribute.ConstructorArguments[2].Value) { + string memberName = (string) memberNameAttr.Value; + + // We will find the matching type from the original assembly first that way we can confirm + // that the name defined in the attribute corresponds to a member that actually existed + var originalFieldMember = originalType.Fields.FirstOrDefault (m => m.Name == memberName); + if (originalFieldMember != null) { + var linkedField = linkedType.Fields.FirstOrDefault (m => m.Name == memberName); + if (linkedField != null) + Assert.Fail ($"Field `{memberName}` on Type `{originalType}` should have been removed"); + + continue; + } + + var originalPropertyMember = originalType.Properties.FirstOrDefault (m => m.Name == memberName); + if (originalPropertyMember != null) { + var linkedProperty = linkedType.Properties.FirstOrDefault (m => m.Name == memberName); + if (linkedProperty != null) + Assert.Fail ($"Property `{memberName}` on Type `{originalType}` should have been removed"); + + continue; + } + + var originalMethodMember = originalType.Methods.FirstOrDefault (m => m.GetSignature () == memberName); + if (originalMethodMember != null) { + var linkedMethod = linkedType.Methods.FirstOrDefault (m => m.GetSignature () == memberName); + if (linkedMethod != null) + Assert.Fail ($"Method `{memberName}` on Type `{originalType}` should have been removed"); + + continue; + } + + Assert.Fail ($"Invalid test assertion. No member named `{memberName}` exists on the original type `{originalType}`"); + } + } + + private void VerifyKeptMemberInAssembly (CustomAttribute inAssemblyAttribute, TypeDefinition linkedType) + { + var originalType = GetOriginalTypeFromInAssemblyAttribute (inAssemblyAttribute); + var memberNames = (CustomAttributeArgument[]) inAssemblyAttribute.ConstructorArguments[2].Value; + Assert.True (memberNames.Length > 0, "Invalid KeptMemberInAssemblyAttribute. Expected member names."); + foreach (var memberNameAttr in memberNames) { + string memberName = (string) memberNameAttr.Value; + + // We will find the matching type from the original assembly first that way we can confirm + // that the name defined in the attribute corresponds to a member that actually existed + + if (TryVerifyKeptMemberInAssemblyAsField (memberName, originalType, linkedType)) + continue; + + if (TryVerifyKeptMemberInAssemblyAsProperty (memberName, originalType, linkedType)) + continue; + + if (TryVerifyKeptMemberInAssemblyAsMethod (memberName, originalType, linkedType)) + continue; + + Assert.Fail ($"Invalid test assertion. No member named `{memberName}` exists on the original type `{originalType}`"); + } + } + + protected virtual bool TryVerifyKeptMemberInAssemblyAsField (string memberName, TypeDefinition originalType, TypeDefinition linkedType) + { + var originalFieldMember = originalType.Fields.FirstOrDefault (m => m.Name == memberName); + if (originalFieldMember != null) { + var linkedField = linkedType.Fields.FirstOrDefault (m => m.Name == memberName); + if (linkedField == null) + Assert.Fail ($"Field `{memberName}` on Type `{originalType}` should have been kept"); + + return true; + } + + return false; + } + + protected virtual bool TryVerifyKeptMemberInAssemblyAsProperty (string memberName, TypeDefinition originalType, TypeDefinition linkedType) + { + var originalPropertyMember = originalType.Properties.FirstOrDefault (m => m.Name == memberName); + if (originalPropertyMember != null) { + var linkedProperty = linkedType.Properties.FirstOrDefault (m => m.Name == memberName); + if (linkedProperty == null) + Assert.Fail ($"Property `{memberName}` on Type `{originalType}` should have been kept"); + + return true; + } + + return false; + } + + protected virtual bool TryVerifyKeptMemberInAssemblyAsMethod (string memberName, TypeDefinition originalType, TypeDefinition linkedType) + { + return TryVerifyKeptMemberInAssemblyAsMethod (memberName, originalType, linkedType, out _, out _); + } + + protected virtual bool TryVerifyKeptMemberInAssemblyAsMethod (string memberName, TypeDefinition originalType, TypeDefinition linkedType, out MethodDefinition? originalMethod, out MethodDefinition? linkedMethod) + { + originalMethod = originalType.Methods.FirstOrDefault (m => m.GetSignature () == memberName); + if (originalMethod != null) { + linkedMethod = linkedType.Methods.FirstOrDefault (m => m.GetSignature () == memberName); + if (linkedMethod == null) + Assert.Fail ($"Method `{memberName}` on Type `{originalType}` should have been kept"); + + return true; + } + + linkedMethod = null; + return false; + } + + private void VerifyKeptReferencesInAssembly (CustomAttribute inAssemblyAttribute) + { + var assembly = ResolveLinkedAssembly (inAssemblyAttribute.ConstructorArguments[0].Value.ToString ()!); + var expectedReferenceNames = ((CustomAttributeArgument[]) inAssemblyAttribute.ConstructorArguments[1].Value).Select (attr => (string) attr.Value).ToList (); + for (int i = 0; i < expectedReferenceNames.Count; i++) + if (expectedReferenceNames[i].EndsWith (".dll")) + expectedReferenceNames[i] = expectedReferenceNames[i].Substring (0, expectedReferenceNames[i].LastIndexOf (".")); + + Assert.Equal (assembly.MainModule.AssemblyReferences.Select (asm => asm.Name), expectedReferenceNames); + } + + private void VerifyKeptResourceInAssembly (CustomAttribute inAssemblyAttribute) + { + var assembly = ResolveLinkedAssembly (inAssemblyAttribute.ConstructorArguments[0].Value.ToString ()!); + var resourceName = inAssemblyAttribute.ConstructorArguments[1].Value.ToString (); + + Assert.Contains (resourceName, assembly.MainModule.Resources.Select (r => r.Name)); + } + + private void VerifyRemovedResourceInAssembly (CustomAttribute inAssemblyAttribute) + { + var assembly = ResolveLinkedAssembly (inAssemblyAttribute.ConstructorArguments[0].Value.ToString ()!); + var resourceName = inAssemblyAttribute.ConstructorArguments[1].Value.ToString (); + + Assert.DoesNotContain (resourceName, assembly.MainModule.Resources.Select (r => r.Name)); + } + + private void VerifyKeptAllTypesAndMembersInAssembly (AssemblyDefinition linked) + { + var original = ResolveOriginalsAssembly (linked.MainModule.Assembly.Name.Name); + + if (original == null) + Assert.Fail ($"Failed to resolve original assembly {linked.MainModule.Assembly.Name.Name}"); + + var originalTypes = original.AllDefinedTypes ().ToDictionary (t => t.FullName); + var linkedTypes = linked.AllDefinedTypes ().ToDictionary (t => t.FullName); + + var missingInLinked = originalTypes.Keys.Except (linkedTypes.Keys); + + Assert.True (missingInLinked.Any (), $"Expected all types to exist in the linked assembly, but one or more were missing"); + + foreach (var originalKvp in originalTypes) { + var linkedType = linkedTypes[originalKvp.Key]; + + var originalMembers = originalKvp.Value.AllMembers ().Select (m => m.FullName); + var linkedMembers = linkedType.AllMembers ().Select (m => m.FullName); + + var missingMembersInLinked = originalMembers.Except (linkedMembers); + + Assert.True (missingMembersInLinked.Any (), $"Expected all members of `{originalKvp.Key}`to exist in the linked assembly, but one or more were missing"); + } + } + + private TypeDefinition GetOriginalTypeFromInAssemblyAttribute (CustomAttribute inAssemblyAttribute) + { + string assemblyName; + if (inAssemblyAttribute.HasProperties && inAssemblyAttribute.Properties[0].Name == "ExpectationAssemblyName") + assemblyName = inAssemblyAttribute.Properties[0].Argument.Value.ToString ()!; + else + assemblyName = inAssemblyAttribute.ConstructorArguments[0].Value.ToString ()!; + + return GetOriginalTypeFromInAssemblyAttribute (assemblyName, inAssemblyAttribute.ConstructorArguments[1].Value); + } + + private TypeDefinition GetOriginalTypeFromInAssemblyAttribute (string assemblyName, object typeOrTypeName) + { + if (typeOrTypeName is TypeReference attributeValueAsTypeReference) + return attributeValueAsTypeReference.Resolve (); + + var assembly = ResolveOriginalsAssembly (assemblyName); + + var expectedTypeName = typeOrTypeName.ToString (); + var originalType = assembly.MainModule.GetType (expectedTypeName); + if (originalType == null) + Assert.Fail ($"Invalid test assertion. Unable to locate the original type `{expectedTypeName}.`"); + return originalType; + } + + private static Dictionary> BuildOtherAssemblyCheckTable (AssemblyDefinition original) + { + var checks = new Dictionary> (); + + foreach (var typeWithRemoveInAssembly in original.AllDefinedTypes ()) { + foreach (var attr in typeWithRemoveInAssembly.CustomAttributes.Where (IsTypeInOtherAssemblyAssertion)) { + var assemblyName = (string) attr.ConstructorArguments[0].Value; + if (!checks.TryGetValue (assemblyName, out List? checksForAssembly)) + checks[assemblyName] = checksForAssembly = new List (); + + checksForAssembly.Add (attr); + } + } + + return checks; + } + + protected AssemblyDefinition ResolveLinkedAssembly (string assemblyName) + { + //var cleanAssemblyName = assemblyName; + //if (assemblyName.EndsWith (".exe") || assemblyName.EndsWith (".dll")) + //cleanAssemblyName = System.IO.Path.GetFileNameWithoutExtension (assemblyName); + //return _linkedResolver.Resolve (new AssemblyNameReference (cleanAssemblyName, null), _linkedReaderParameters); + // TODO - adapt to Native AOT + return ResolveOriginalsAssembly (assemblyName); + } + + protected AssemblyDefinition ResolveOriginalsAssembly (string assemblyName) + { + var cleanAssemblyName = assemblyName; + if (assemblyName.EndsWith (".exe") || assemblyName.EndsWith (".dll")) + cleanAssemblyName = Path.GetFileNameWithoutExtension (assemblyName); + return originalsResolver.Resolve (new AssemblyNameReference (cleanAssemblyName, null), originalReaderParameters); + } + + private static bool IsTypeInOtherAssemblyAssertion (CustomAttribute attr) + { + return attr.AttributeType.Resolve ()?.DerivesFrom (nameof (BaseInAssemblyAttribute)) ?? false; + } + + private void VerifyExpectedInstructionSequenceOnMemberInAssembly (CustomAttribute inAssemblyAttribute, TypeDefinition linkedType) + { + var originalType = GetOriginalTypeFromInAssemblyAttribute (inAssemblyAttribute); + var memberName = (string) inAssemblyAttribute.ConstructorArguments[2].Value; + + if (TryVerifyKeptMemberInAssemblyAsMethod (memberName, originalType, linkedType, out MethodDefinition? originalMethod, out MethodDefinition? linkedMethod)) { + static string[] valueCollector (MethodDefinition m) => AssemblyChecker.FormatMethodBody (m.Body); + var linkedValues = valueCollector (linkedMethod!); + var srcValues = valueCollector (originalMethod!); + + var expected = ((CustomAttributeArgument[]) inAssemblyAttribute.ConstructorArguments[3].Value)?.Select (arg => arg.Value.ToString ()).ToArray (); + Assert.Equal ( + linkedValues, + expected); + + return; + } + + Assert.Fail ($"Invalid test assertion. No method named `{memberName}` exists on the original type `{originalType}`"); + } + + protected virtual void UnhandledOtherAssemblyAssertion (string expectedTypeName, CustomAttribute checkAttrInAssembly, TypeDefinition? linkedType) + { + throw new NotImplementedException ($"Type {expectedTypeName}, has an unknown other assembly attribute of type {checkAttrInAssembly.AttributeType}"); + } } } diff --git a/src/coreclr/tools/aot/Mono.Linker.Tests/TestCasesRunner/ILCompilerDriver.cs b/src/coreclr/tools/aot/Mono.Linker.Tests/TestCasesRunner/ILCompilerDriver.cs index e70aca84829c3..f9a5dd676aaaf 100644 --- a/src/coreclr/tools/aot/Mono.Linker.Tests/TestCasesRunner/ILCompilerDriver.cs +++ b/src/coreclr/tools/aot/Mono.Linker.Tests/TestCasesRunner/ILCompilerDriver.cs @@ -18,7 +18,7 @@ public class ILCompilerDriver { private const string DefaultSystemModule = "System.Private.CoreLib"; - public void Trim (ILCompilerOptions options, ILogWriter logWriter) + public ILScanResults Trim (ILCompilerOptions options, ILogWriter logWriter) { ComputeDefaultOptions (out var targetOS, out var targetArchitecture); var targetDetails = new TargetDetails (targetArchitecture, targetOS, TargetAbi.NativeAot); @@ -35,6 +35,11 @@ public void Trim (ILCompilerOptions options, ILogWriter logWriter) inputModules.Add (module); } + foreach (var trimAssembly in options.TrimAssemblies) { + EcmaModule module = typeSystemContext.GetModuleFromPath (trimAssembly); + inputModules.Add (module); + } + CompilationModuleGroup compilationGroup = new TestInfraMultiFileSharedCompilationModuleGroup (typeSystemContext, inputModules); List compilationRoots = new List (); @@ -74,7 +79,7 @@ public void Trim (ILCompilerOptions options, ILogWriter logWriter) logger, Array.Empty> (), Array.Empty (), - Array.Empty (), + options.AdditionalRootAssemblies.ToArray (), options.TrimAssemblies.ToArray ()); CompilationBuilder builder = new RyuJitCompilationBuilder (typeSystemContext, compilationGroup) @@ -87,7 +92,7 @@ public void Trim (ILCompilerOptions options, ILogWriter logWriter) .UseParallelism (System.Diagnostics.Debugger.IsAttached ? 1 : -1) .ToILScanner (); - _ = scanner.Scan (); + return scanner.Scan (); } public static void ComputeDefaultOptions (out TargetOS os, out TargetArchitecture arch) diff --git a/src/coreclr/tools/aot/Mono.Linker.Tests/TestCasesRunner/ILCompilerOptions.cs b/src/coreclr/tools/aot/Mono.Linker.Tests/TestCasesRunner/ILCompilerOptions.cs index 7bba7ae4de334..777f506b14d32 100644 --- a/src/coreclr/tools/aot/Mono.Linker.Tests/TestCasesRunner/ILCompilerOptions.cs +++ b/src/coreclr/tools/aot/Mono.Linker.Tests/TestCasesRunner/ILCompilerOptions.cs @@ -11,6 +11,7 @@ public class ILCompilerOptions public Dictionary ReferenceFilePaths = new Dictionary (); public List InitAssemblies = new List (); public List TrimAssemblies = new List (); + public List AdditionalRootAssemblies = new List (); public Dictionary FeatureSwitches = new Dictionary (); } } diff --git a/src/coreclr/tools/aot/Mono.Linker.Tests/TestCasesRunner/ILCompilerOptionsBuilder.cs b/src/coreclr/tools/aot/Mono.Linker.Tests/TestCasesRunner/ILCompilerOptionsBuilder.cs index a370eff817d26..a865e1d0ebb2c 100644 --- a/src/coreclr/tools/aot/Mono.Linker.Tests/TestCasesRunner/ILCompilerOptionsBuilder.cs +++ b/src/coreclr/tools/aot/Mono.Linker.Tests/TestCasesRunner/ILCompilerOptionsBuilder.cs @@ -119,6 +119,11 @@ public virtual void AddKeepDebugMembers (string value) public virtual void AddAssemblyAction (string action, string assembly) { + switch (action) { + case "copy": + Options.AdditionalRootAssemblies.Add (assembly); + break; + } } public virtual void AddSkipUnresolved (bool skipUnresolved) diff --git a/src/coreclr/tools/aot/Mono.Linker.Tests/TestCasesRunner/ILCompilerTestCaseResult.cs b/src/coreclr/tools/aot/Mono.Linker.Tests/TestCasesRunner/ILCompilerTestCaseResult.cs index 20039ead239a3..5229431b282e6 100644 --- a/src/coreclr/tools/aot/Mono.Linker.Tests/TestCasesRunner/ILCompilerTestCaseResult.cs +++ b/src/coreclr/tools/aot/Mono.Linker.Tests/TestCasesRunner/ILCompilerTestCaseResult.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using ILCompiler; using Mono.Linker.Tests.Extensions; using Mono.Linker.Tests.TestCases; @@ -14,9 +15,10 @@ public class ILCompilerTestCaseResult public readonly TestCaseSandbox Sandbox; public readonly TestCaseMetadataProvider MetadataProvider; public readonly ManagedCompilationResult CompilationResult; + public readonly ILScanResults TrimmingResults; public readonly TestLogWriter LogWriter; - public ILCompilerTestCaseResult (TestCase testCase, NPath inputAssemblyPath, NPath expectationsAssemblyPath, TestCaseSandbox sandbox, TestCaseMetadataProvider metadataProvider, ManagedCompilationResult compilationResult, TestLogWriter logWriter) + public ILCompilerTestCaseResult (TestCase testCase, NPath inputAssemblyPath, NPath expectationsAssemblyPath, TestCaseSandbox sandbox, TestCaseMetadataProvider metadataProvider, ManagedCompilationResult compilationResult, ILScanResults trimmingResults, TestLogWriter logWriter) { TestCase = testCase; InputAssemblyPath = inputAssemblyPath; @@ -24,6 +26,7 @@ public ILCompilerTestCaseResult (TestCase testCase, NPath inputAssemblyPath, NPa Sandbox = sandbox; MetadataProvider = metadataProvider; CompilationResult = compilationResult; + TrimmingResults = trimmingResults; LogWriter = logWriter; } } diff --git a/src/coreclr/tools/aot/Mono.Linker.Tests/TestCasesRunner/NameUtils.cs b/src/coreclr/tools/aot/Mono.Linker.Tests/TestCasesRunner/NameUtils.cs new file mode 100644 index 0000000000000..68c4006eb6530 --- /dev/null +++ b/src/coreclr/tools/aot/Mono.Linker.Tests/TestCasesRunner/NameUtils.cs @@ -0,0 +1,84 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using System.Text; +using ILCompiler; +using Internal.TypeSystem; +using Mono.Cecil; +using Mono.Linker.Tests.Extensions; + +namespace Mono.Linker.Tests.TestCasesRunner +{ + internal static class NameUtils + { + internal static string? GetActualOriginDisplayName (TypeSystemEntity? entity) => entity switch { + DefType defType => TrimAssemblyNamePrefix (defType.GetDisplayName ()), + MethodDesc method => TrimAssemblyNamePrefix (method.GetDisplayName ()), + FieldDesc field => TrimAssemblyNamePrefix (field.ToString ()), + ModuleDesc module => module.Assembly.GetName ().Name, + _ => null + }; + + private static string? TrimAssemblyNamePrefix (string? name) + { + if (name == null) + return null; + + if (name.StartsWith ('[')) { + int i = name.IndexOf (']'); + if (i > 0) { + return name.Substring (i + 1); + } + } + + return name; + } + + internal static string GetExpectedOriginDisplayName (ICustomAttributeProvider provider) => + ConvertSignatureToIlcFormat (provider switch { + MethodDefinition method => method.GetDisplayName (), + FieldDefinition field => field.GetDisplayName (), + TypeDefinition type => type.GetDisplayName (), + IMemberDefinition member => member.FullName, + AssemblyDefinition asm => asm.Name.Name, + _ => throw new NotImplementedException () + }); + + internal static string ConvertSignatureToIlcFormat (string value) + { + if (value.Contains ('(') || value.Contains ('<')) { + value = value.Replace (", ", ","); + } + + if (value.Contains ('/')) { + value = value.Replace ('/', '+'); + } + + // Split it into . separated parts and if one is ending with > rewrite it to `1 format + // ILC folows the reflection format which doesn't actually use generic instantiations on anything but the last type + // in nested hierarchy - it's difficult to replicate this with Cecil as it has different representation so just strip that info + var parts = value.Split ('.'); + StringBuilder sb = new StringBuilder (); + foreach (var part in parts) { + if (sb.Length > 0) + sb.Append ('.'); + + if (part.EndsWith ('>')) { + int i = part.LastIndexOf ('<'); + if (i >= 0) { + sb.Append (part.AsSpan (0, i)); + sb.Append ('`'); + sb.Append (part.Substring (i + 1).Where (c => c == ',').Count () + 1); + continue; + } + } + + sb.Append (part); + } + + return sb.ToString (); + } + } +} diff --git a/src/coreclr/tools/aot/Mono.Linker.Tests/TestCasesRunner/ResultChecker.cs b/src/coreclr/tools/aot/Mono.Linker.Tests/TestCasesRunner/ResultChecker.cs index 89cfe82017281..090a3a1b039b9 100644 --- a/src/coreclr/tools/aot/Mono.Linker.Tests/TestCasesRunner/ResultChecker.cs +++ b/src/coreclr/tools/aot/Mono.Linker.Tests/TestCasesRunner/ResultChecker.cs @@ -4,17 +4,17 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; -using System.Text; using System.Text.RegularExpressions; using FluentAssertions; -using ILCompiler; using ILCompiler.Logging; using Internal.TypeSystem; using Mono.Cecil; using Mono.Cecil.Cil; using Mono.Linker.Tests.Cases.Expectations.Assertions; +using Mono.Linker.Tests.Cases.Expectations.Metadata; using Mono.Linker.Tests.Extensions; using Xunit; @@ -45,18 +45,57 @@ public ResultChecker (BaseAssemblyResolver originalsResolver, _linkedReaderParameters = linkedReaderParameters; } - public virtual void Check (ILCompilerTestCaseResult trimmedResult) + private static bool ShouldValidateIL (AssemblyDefinition inputAssembly) { - InitializeResolvers (trimmedResult); + if (HasAttribute (inputAssembly, nameof (SkipPeVerifyAttribute))) + return false; + + var caaIsUnsafeFlag = (CustomAttributeArgument caa) => + (caa.Type.Name == "String" && caa.Type.Namespace == "System") + && (string) caa.Value == "/unsafe"; + var customAttributeHasUnsafeFlag = (CustomAttribute ca) => ca.ConstructorArguments.Any (caaIsUnsafeFlag); + if (GetCustomAttributes (inputAssembly, nameof (SetupCompileArgumentAttribute)) + .Any (customAttributeHasUnsafeFlag)) + return false; + + return true; + } + + public virtual void Check (ILCompilerTestCaseResult testResult) + { + InitializeResolvers (testResult); try { - var original = ResolveOriginalsAssembly (trimmedResult.ExpectationsAssemblyPath.FileNameWithoutExtension); - AdditionalChecking (trimmedResult, original); + var original = ResolveOriginalsAssembly (testResult.ExpectationsAssemblyPath.FileNameWithoutExtension); + + if (!HasAttribute (original, nameof (NoLinkedOutputAttribute))) { + // TODO Validate presence of the main assembly - if it makes sense (reflection only somehow) + + // IL verification is impossible for NativeAOT since there's no IL output + // if (ShouldValidateIL (original)) + // VerifyIL (); + + InitialChecking (testResult, original); + + PerformOutputAssemblyChecks (original, testResult); + PerformOutputSymbolChecks (original, testResult); + + if (!HasAttribute (original.MainModule.GetType (testResult.TestCase.ReconstructedFullTypeName), nameof (SkipKeptItemsValidationAttribute))) { + CreateAssemblyChecker (original, testResult).Verify (); + } + } + + AdditionalChecking (testResult, original); } finally { _originalsResolver.Dispose (); } } + protected virtual AssemblyChecker CreateAssemblyChecker (AssemblyDefinition original, ILCompilerTestCaseResult testResult) + { + return new AssemblyChecker (_originalsResolver, _originalReaderParameters, original, testResult); + } + private void InitializeResolvers (ILCompilerTestCaseResult linkedResult) { _originalsResolver.AddSearchDirectory (linkedResult.ExpectationsAssemblyPath.Parent.ToString ()); @@ -70,6 +109,56 @@ protected AssemblyDefinition ResolveOriginalsAssembly (string assemblyName) return _originalsResolver.Resolve (new AssemblyNameReference (cleanAssemblyName, null), _originalReaderParameters); } + private static void PerformOutputAssemblyChecks (AssemblyDefinition original, ILCompilerTestCaseResult testResult) + { + var assembliesToCheck = original.MainModule.Types.SelectMany (t => t.CustomAttributes).Where (ExpectationsProvider.IsAssemblyAssertion); + var actionAssemblies = new HashSet (); + //bool trimModeIsCopy = false; + + foreach (var assemblyAttr in assembliesToCheck) { + var name = (string) assemblyAttr.ConstructorArguments.First ().Value; + name = Path.GetFileNameWithoutExtension (name); + +#if false + if (assemblyAttr.AttributeType.Name == nameof (RemovedAssemblyAttribute)) + Assert.IsFalse (expectedPath.FileExists (), $"Expected the assembly {name} to not exist in {outputDirectory}, but it did"); + else if (assemblyAttr.AttributeType.Name == nameof (KeptAssemblyAttribute)) + Assert.IsTrue (expectedPath.FileExists (), $"Expected the assembly {name} to exist in {outputDirectory}, but it did not"); + else if (assemblyAttr.AttributeType.Name == nameof (SetupLinkerActionAttribute)) { + string assemblyName = (string) assemblyAttr.ConstructorArguments[1].Value; + if ((string) assemblyAttr.ConstructorArguments[0].Value == "copy") { + VerifyCopyAssemblyIsKeptUnmodified (outputDirectory, assemblyName + (assemblyName == "test" ? ".exe" : ".dll")); + } + + actionAssemblies.Add (assemblyName); + } else if (assemblyAttr.AttributeType.Name == nameof (SetupLinkerTrimModeAttribute)) { + // We delay checking that everything was copied after processing all assemblies + // with a specific action, since assembly action wins over trim mode. + if ((string) assemblyAttr.ConstructorArguments[0].Value == "copy") + trimModeIsCopy = true; + } else + throw new NotImplementedException ($"Unknown assembly assertion of type {assemblyAttr.AttributeType}"); +#endif + } + +#if false + if (trimModeIsCopy) { + foreach (string assemblyName in Directory.GetFiles (Directory.GetParent (outputDirectory).ToString (), "input")) { + var fileInfo = new FileInfo (assemblyName); + if (fileInfo.Extension == ".dll" && !actionAssemblies.Contains (assemblyName)) + VerifyCopyAssemblyIsKeptUnmodified (outputDirectory, assemblyName + (assemblyName == "test" ? ".exe" : ".dll")); + } + } +#endif + } + +#pragma warning disable IDE0060 // Remove unused parameter + private static void PerformOutputSymbolChecks (AssemblyDefinition original, ILCompilerTestCaseResult testResult) +#pragma warning restore IDE0060 // Remove unused parameter + { + // While NativeAOT has symbols, verifying them is rather difficult + } + protected virtual void AdditionalChecking (ILCompilerTestCaseResult linkResult, AssemblyDefinition original) { bool checkRemainingErrors = !HasAttribute (original.MainModule.GetType (linkResult.TestCase.ReconstructedFullTypeName), nameof (SkipRemainingErrorsValidationAttribute)); @@ -97,6 +186,11 @@ private static IEnumerable GetAttributeProviders (Asse yield return assembly; } + protected virtual void InitialChecking (ILCompilerTestCaseResult testResult, AssemblyDefinition original) + { + // PE verifier is done here in ILLinker, but that's not possible with NativeAOT + } + private void VerifyLoggedMessages (AssemblyDefinition original, TestLogWriter logger, bool checkRemainingErrors) { List loggedMessages = logger.GetLoggedMessages (); @@ -207,8 +301,8 @@ private void VerifyLoggedMessages (AssemblyDefinition original, TestLogWriter lo if (attrProvider is not IMemberDefinition expectedMember) continue; - string? actualName = GetActualOriginDisplayName (methodDesc); - string expectedTypeName = ConvertSignatureToIlcFormat (GetExpectedOriginDisplayName (expectedMember.DeclaringType)); + string? actualName = NameUtils.GetActualOriginDisplayName (methodDesc); + string expectedTypeName = NameUtils.GetExpectedOriginDisplayName (expectedMember.DeclaringType); if (actualName?.Contains (expectedTypeName) == true && actualName?.Contains ("<" + expectedMember.Name + ">") == true) { expectedWarningFound = true; @@ -245,7 +339,7 @@ private void VerifyLoggedMessages (AssemblyDefinition original, TestLogWriter lo } var expectedOriginString = fileName == null - ? GetExpectedOriginDisplayName (attrProvider) + ": " + ? NameUtils.GetExpectedOriginDisplayName (attrProvider) + ": " : ""; Assert.True (expectedWarningFound, @@ -304,7 +398,7 @@ static bool LogMessageHasSameOriginMember (MessageContainer mc, ICustomAttribute { var origin = mc.Origin; Debug.Assert (origin != null); - if (GetActualOriginDisplayName (origin?.MemberDefinition) == ConvertSignatureToIlcFormat (GetExpectedOriginDisplayName (expectedOriginProvider))) + if (NameUtils.GetActualOriginDisplayName (origin?.MemberDefinition) == NameUtils.GetExpectedOriginDisplayName (expectedOriginProvider)) return true; var actualMember = origin!.Value.MemberDefinition; @@ -331,87 +425,51 @@ static bool LogMessageHasSameOriginMember (MessageContainer mc, ICustomAttribute _ => null }; - static string? GetActualOriginDisplayName (TypeSystemEntity? entity) => entity switch { - DefType defType => TrimAssemblyNamePrefix (defType.ToString ()), - MethodDesc method => TrimAssemblyNamePrefix (method.GetDisplayName ()), - FieldDesc field => TrimAssemblyNamePrefix (field.ToString ()), - ModuleDesc module => module.Assembly.GetName ().Name, - _ => null - }; - - static string TrimAssemblyNamePrefix (string name) - { - if (name.StartsWith ('[')) { - int i = name.IndexOf (']'); - if (i > 0) { - return name.Substring (i + 1); - } - } - - return name; - } - - static string GetExpectedOriginDisplayName (ICustomAttributeProvider provider) => - provider switch { - MethodDefinition method => method.GetDisplayName (), - FieldDefinition field => field.GetDisplayName (), - TypeDefinition type => type.GetDisplayName (), - IMemberDefinition member => member.FullName, - AssemblyDefinition asm => asm.Name.Name, - _ => throw new NotImplementedException () - }; - static bool MessageTextContains (string message, string value) { // This is a workaround for different formatting of methods between ilc and linker/analyzer // Sometimes they're written with a space after comma and sometimes without // Method(String,String) - ilc // Method(String, String) - linker/analyzer - return message.Contains (value) || message.Contains (ConvertSignatureToIlcFormat (value)); + return message.Contains (value) || message.Contains (NameUtils.ConvertSignatureToIlcFormat (value)); } + } - static string ConvertSignatureToIlcFormat (string value) - { - if (value.Contains ('(') || value.Contains ('<')) { - value = value.Replace (", ", ","); - } - - // Split it into . separated parts and if one is ending with > rewrite it to `1 format - // ILC folows the reflection format which doesn't actually use generic instantiations on anything but the last type - // in nested hierarchy - it's difficult to replicate this with Cecil as it has different representation so just strip that info - var parts = value.Split ('.'); - StringBuilder sb = new StringBuilder (); - foreach (var part in parts) { - if (sb.Length > 0) - sb.Append ('.'); - - if (part.EndsWith ('>')) { - int i = part.LastIndexOf ('<'); - if (i >= 0) { - sb.Append (part.AsSpan (0, i)); - sb.Append ('`'); - sb.Append (part.Substring (i + 1).Where (c => c == ',').Count () + 1); - continue; - } - } + private static bool HasAttribute (ICustomAttributeProvider caProvider, string attributeName) + { + return TryGetCustomAttribute (caProvider, attributeName, out var _); + } - sb.Append (part); - } +#nullable enable + private static bool TryGetCustomAttribute (ICustomAttributeProvider caProvider, string attributeName, [NotNullWhen (true)] out CustomAttribute? customAttribute) + { + if (caProvider is AssemblyDefinition assembly && assembly.EntryPoint != null) { + customAttribute = assembly.EntryPoint.DeclaringType.CustomAttributes + .FirstOrDefault (attr => attr!.AttributeType.Name == attributeName, null); + return customAttribute is not null; + } - return sb.ToString (); + if (caProvider is TypeDefinition type) { + customAttribute = type.CustomAttributes + .FirstOrDefault (attr => attr!.AttributeType.Name == attributeName, null); + return customAttribute is not null; } + customAttribute = null; + return false; } - private static bool HasAttribute (ICustomAttributeProvider caProvider, string attributeName) + private static IEnumerable GetCustomAttributes (ICustomAttributeProvider caProvider, string attributeName) { if (caProvider is AssemblyDefinition assembly && assembly.EntryPoint != null) return assembly.EntryPoint.DeclaringType.CustomAttributes - .Any (attr => attr.AttributeType.Name == attributeName); + .Where (attr => attr!.AttributeType.Name == attributeName); if (caProvider is TypeDefinition type) - return type.CustomAttributes.Any (attr => attr.AttributeType.Name == attributeName); + return type.CustomAttributes + .Where (attr => attr!.AttributeType.Name == attributeName); - return false; + return Enumerable.Empty (); } +#nullable restore } } diff --git a/src/coreclr/tools/aot/Mono.Linker.Tests/TestCasesRunner/TestRunner.cs b/src/coreclr/tools/aot/Mono.Linker.Tests/TestCasesRunner/TestRunner.cs index 6911201b8581a..50442019768c0 100644 --- a/src/coreclr/tools/aot/Mono.Linker.Tests/TestCasesRunner/TestRunner.cs +++ b/src/coreclr/tools/aot/Mono.Linker.Tests/TestCasesRunner/TestRunner.cs @@ -112,9 +112,9 @@ private ILCompilerTestCaseResult Link (TestCase testCase, TestCaseSandbox sandbo AddLinkOptions (sandbox, compilationResult, builder, metadataProvider); var logWriter = new TestLogWriter (); - trimmer.Trim (builder.Options, logWriter); + var trimmingResults = trimmer.Trim (builder.Options, logWriter); - return new ILCompilerTestCaseResult (testCase, compilationResult.InputAssemblyPath, compilationResult.ExpectationsAssemblyPath, sandbox, metadataProvider, compilationResult, logWriter); + return new ILCompilerTestCaseResult (testCase, compilationResult.InputAssemblyPath, compilationResult.ExpectationsAssemblyPath, sandbox, metadataProvider, compilationResult, trimmingResults, logWriter); } protected virtual void AddLinkOptions (TestCaseSandbox sandbox, ManagedCompilationResult compilationResult, ILCompilerOptionsBuilder builder, TestCaseMetadataProvider metadataProvider)