diff --git a/src/tools/illink/src/linker/Linker.Steps/DiscoverCustomOperatorsHandler.cs b/src/tools/illink/src/linker/Linker.Steps/DiscoverCustomOperatorsHandler.cs new file mode 100644 index 0000000000000..e4b2458bddc4c --- /dev/null +++ b/src/tools/illink/src/linker/Linker.Steps/DiscoverCustomOperatorsHandler.cs @@ -0,0 +1,215 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Diagnostics; +using Mono.Cecil; + +namespace Mono.Linker.Steps +{ + public class DiscoverOperatorsHandler : IMarkHandler + { + LinkContext _context; + bool _seenLinqExpressions; + readonly HashSet _trackedTypesWithOperators; + Dictionary> _pendingOperatorsForType; + + Dictionary> PendingOperatorsForType { + get { + if (_pendingOperatorsForType == null) + _pendingOperatorsForType = new Dictionary> (); + return _pendingOperatorsForType; + } + } + + public DiscoverOperatorsHandler () + { + _trackedTypesWithOperators = new HashSet (); + } + + public void Initialize (LinkContext context, MarkContext markContext) + { + _context = context; + markContext.RegisterMarkTypeAction (ProcessType); + } + + void ProcessType (TypeDefinition type) + { + CheckForLinqExpressions (type); + + // Check for custom operators and either: + // - mark them, if Linq.Expressions was already marked, or + // - track them to be marked in case Linq.Expressions is marked later + var hasOperators = ProcessCustomOperators (type, mark: _seenLinqExpressions); + if (!_seenLinqExpressions) { + if (hasOperators) + _trackedTypesWithOperators.Add (type); + return; + } + + // Mark pending operators defined on other types that reference this type + // (these are only tracked if we have already seen Linq.Expressions) + if (PendingOperatorsForType.TryGetValue (type, out var pendingOperators)) { + foreach (var customOperator in pendingOperators) + MarkOperator (customOperator); + PendingOperatorsForType.Remove (type); + } + } + + void CheckForLinqExpressions (TypeDefinition type) + { + if (_seenLinqExpressions) + return; + + if (type.Namespace != "System.Linq.Expressions" || type.Name != "Expression") + return; + + _seenLinqExpressions = true; + + foreach (var markedType in _trackedTypesWithOperators) + ProcessCustomOperators (markedType, mark: true); + + _trackedTypesWithOperators.Clear (); + } + + void MarkOperator (MethodDefinition method) + { + _context.Annotations.Mark (method, new DependencyInfo (DependencyKind.PreservedOperator, method.DeclaringType)); + } + + bool ProcessCustomOperators (TypeDefinition type, bool mark) + { + if (!type.HasMethods) + return false; + + bool hasCustomOperators = false; + foreach (var method in type.Methods) { + if (!IsOperator (method, out var otherType)) + continue; + + if (!mark) + return true; + + Debug.Assert (_seenLinqExpressions); + hasCustomOperators = true; + + if (otherType == null || _context.Annotations.IsMarked (otherType)) { + MarkOperator (method); + continue; + } + + // Wait until otherType gets marked to mark the operator. + if (!PendingOperatorsForType.TryGetValue (otherType, out var pendingOperators)) { + pendingOperators = new List (); + PendingOperatorsForType.Add (otherType, pendingOperators); + } + pendingOperators.Add (method); + } + return hasCustomOperators; + } + + TypeDefinition _nullableOfT; + TypeDefinition NullableOfT { + get { + if (_nullableOfT == null) + _nullableOfT = BCL.FindPredefinedType ("System", "Nullable`1", _context); + return _nullableOfT; + } + } + + TypeDefinition NonNullableType (TypeReference type) + { + var typeDef = _context.TryResolve (type); + if (typeDef == null) + return null; + + if (!typeDef.IsValueType || typeDef != NullableOfT) + return typeDef; + + // Unwrap Nullable + Debug.Assert (typeDef.HasGenericParameters); + var nullableType = type as GenericInstanceType; + Debug.Assert (nullableType != null && nullableType.HasGenericArguments && nullableType.GenericArguments.Count == 1); + return _context.TryResolve (nullableType.GenericArguments[0]); + } + + bool IsOperator (MethodDefinition method, out TypeDefinition otherType) + { + otherType = null; + + if (!method.IsStatic || !method.IsPublic || !method.IsSpecialName || !method.Name.StartsWith ("op_")) + return false; + + var operatorName = method.Name.Substring (3); + var self = method.DeclaringType; + + switch (operatorName) { + // Unary operators + case "UnaryPlus": + case "UnaryNegation": + case "LogicalNot": + case "OnesComplement": + case "Increment": + case "Decrement": + case "True": + case "False": + // Parameter type of a unary operator must be the declaring type + if (method.Parameters.Count != 1 || NonNullableType (method.Parameters[0].ParameterType) != self) + return false; + // ++ and -- must return the declaring type + if (operatorName is "Increment" or "Decrement" && NonNullableType (method.ReturnType) != self) + return false; + return true; + // Binary operators + case "Addition": + case "Subtraction": + case "Multiply": + case "Division": + case "Modulus": + case "BitwiseAnd": + case "BitwiseOr": + case "ExclusiveOr": + case "LeftShift": + case "RightShift": + case "Equality": + case "Inequality": + case "LessThan": + case "GreaterThan": + case "LessThanOrEqual": + case "GreaterThanOrEqual": + if (method.Parameters.Count != 2) + return false; + var nnLeft = NonNullableType (method.Parameters[0].ParameterType); + var nnRight = NonNullableType (method.Parameters[1].ParameterType); + if (nnLeft == null || nnRight == null) + return false; + // << and >> must take the declaring type and int + if (operatorName is "LeftShift" or "RightShift" && (nnLeft != self || nnRight.MetadataType != MetadataType.Int32)) + return false; + // At least one argument must be the declaring type + if (nnLeft != self && nnRight != self) + return false; + if (nnLeft != self) + otherType = nnLeft; + if (nnRight != self) + otherType = nnRight; + return true; + // Conversion operators + case "Implicit": + case "Explicit": + if (method.Parameters.Count != 1) + return false; + var nnSource = NonNullableType (method.Parameters[0].ParameterType); + var nnTarget = NonNullableType (method.ReturnType); + // Exactly one of source/target must be the declaring type + if (nnSource == self == (nnTarget == self)) + return false; + otherType = nnSource == self ? nnTarget : nnSource; + return true; + default: + return false; + } + } + } +} \ No newline at end of file diff --git a/src/tools/illink/src/linker/Linker/DependencyInfo.cs b/src/tools/illink/src/linker/Linker/DependencyInfo.cs index c0f326e98bafa..5ebaa2d81975c 100644 --- a/src/tools/illink/src/linker/Linker/DependencyInfo.cs +++ b/src/tools/illink/src/linker/Linker/DependencyInfo.cs @@ -136,6 +136,8 @@ public enum DependencyKind XmlSerialized = 84, // entry type or member for XML serialization SerializedRecursiveType = 85, // recursive type kept due to serialization handling SerializedMember = 86, // field or property kept on a type for serialization + + PreservedOperator = 87 // operator method preserved on a type } public readonly struct DependencyInfo : IEquatable diff --git a/src/tools/illink/src/linker/Linker/Driver.cs b/src/tools/illink/src/linker/Linker/Driver.cs index 1ea5f0b968962..dd8cbac7dfb4d 100644 --- a/src/tools/illink/src/linker/Linker/Driver.cs +++ b/src/tools/illink/src/linker/Linker/Driver.cs @@ -355,6 +355,12 @@ protected int SetupContext (ILogger customLogger = null) continue; + case "--disable-operator-discovery": + if (!GetBoolParam (token, l => context.DisableOperatorDiscovery = l)) + return -1; + + continue; + case "--ignore-descriptors": if (!GetBoolParam (token, l => context.IgnoreDescriptors = l)) return -1; @@ -732,6 +738,9 @@ protected int SetupContext (ILogger customLogger = null) if (!context.DisableSerializationDiscovery) p.MarkHandlers.Add (new DiscoverSerializationHandler ()); + if (!context.DisableOperatorDiscovery) + p.MarkHandlers.Add (new DiscoverOperatorsHandler ()); + foreach (string custom_step in custom_steps) { if (!AddCustomStep (p, custom_step)) return -1; diff --git a/src/tools/illink/src/linker/Linker/LinkContext.cs b/src/tools/illink/src/linker/Linker/LinkContext.cs index 58e3e0499dec1..61a6b17e420b6 100644 --- a/src/tools/illink/src/linker/Linker/LinkContext.cs +++ b/src/tools/illink/src/linker/Linker/LinkContext.cs @@ -123,6 +123,8 @@ public class LinkContext : IMetadataResolver, IDisposable public bool DisableSerializationDiscovery { get; set; } + public bool DisableOperatorDiscovery { get; set; } + public bool IgnoreDescriptors { get; set; } public bool IgnoreSubstitutions { get; set; } diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/LinqExpressions/CanDisableOperatorDiscovery.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/LinqExpressions/CanDisableOperatorDiscovery.cs new file mode 100644 index 0000000000000..8a8bf6c8f5012 --- /dev/null +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/LinqExpressions/CanDisableOperatorDiscovery.cs @@ -0,0 +1,31 @@ +using Mono.Linker.Tests.Cases.Expectations.Assertions; +using Mono.Linker.Tests.Cases.Expectations.Metadata; + +namespace Mono.Linker.Tests.Cases.LinqExpressions +{ + [SetupLinkerArgument ("--disable-operator-discovery")] + public class CanDisableOperatorDiscovery + { + public static void Main () + { + var c = new CustomOperators (); + var expression = typeof (System.Linq.Expressions.Expression); + c = -c; + var t = typeof (TargetType); + } + + [KeptMember (".ctor()")] + class CustomOperators + { + [Kept] + public static CustomOperators operator - (CustomOperators c) => null; + + public static CustomOperators operator + (CustomOperators c) => null; + public static CustomOperators operator + (CustomOperators left, CustomOperators right) => null; + public static explicit operator TargetType (CustomOperators self) => null; + } + + [Kept] + class TargetType { } + } +} diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/LinqExpressions/CanPreserveCustomOperators.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/LinqExpressions/CanPreserveCustomOperators.cs new file mode 100644 index 0000000000000..d08b3265b15fb --- /dev/null +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/LinqExpressions/CanPreserveCustomOperators.cs @@ -0,0 +1,93 @@ +using Mono.Linker.Tests.Cases.Expectations.Assertions; +using Mono.Linker.Tests.Cases.Expectations.Metadata; + +namespace Mono.Linker.Tests.Cases.LinqExpressions +{ + public class CanPreserveCustomOperators + { + public static void Main () + { + var t = typeof (CustomOperators); + var expression = typeof (System.Linq.Expressions.Expression); + + var t3 = typeof (TargetTypeImplicit); + var t4 = typeof (SourceTypeImplicit); + var t5 = typeof (TargetTypeExplicit); + var t6 = typeof (SourceTypeExplicit); + } + + class CustomOperators + { + // Unary operators + [Kept] + public static CustomOperators operator + (CustomOperators c) => null; + [Kept] + public static CustomOperators operator - (CustomOperators c) => null; + [Kept] + public static CustomOperators operator ! (CustomOperators c) => null; + [Kept] + public static CustomOperators operator ~ (CustomOperators c) => null; + [Kept] + public static CustomOperators operator ++ (CustomOperators c) => null; + [Kept] + public static CustomOperators operator -- (CustomOperators c) => null; + [Kept] + public static bool operator true (CustomOperators c) => true; + [Kept] + public static bool operator false (CustomOperators c) => true; + + // Binary operators + [Kept] + public static CustomOperators operator + (CustomOperators left, CustomOperators right) => null; + [Kept] + public static CustomOperators operator - (CustomOperators left, CustomOperators right) => null; + [Kept] + public static CustomOperators operator * (CustomOperators left, CustomOperators right) => null; + [Kept] + public static CustomOperators operator / (CustomOperators left, CustomOperators right) => null; + [Kept] + public static CustomOperators operator % (CustomOperators left, CustomOperators right) => null; + [Kept] + public static CustomOperators operator & (CustomOperators left, CustomOperators right) => null; + [Kept] + public static CustomOperators operator | (CustomOperators left, CustomOperators right) => null; + [Kept] + public static CustomOperators operator ^ (CustomOperators left, CustomOperators right) => null; + [Kept] + public static CustomOperators operator << (CustomOperators value, int shift) => null; + [Kept] + public static CustomOperators operator >> (CustomOperators value, int shift) => null; + [Kept] + public static CustomOperators operator == (CustomOperators left, CustomOperators right) => null; + [Kept] + public static CustomOperators operator != (CustomOperators left, CustomOperators right) => null; + [Kept] + public static CustomOperators operator < (CustomOperators left, CustomOperators right) => null; + [Kept] + public static CustomOperators operator > (CustomOperators left, CustomOperators right) => null; + [Kept] + public static CustomOperators operator <= (CustomOperators left, CustomOperators right) => null; + [Kept] + public static CustomOperators operator >= (CustomOperators left, CustomOperators right) => null; + + // conversion operators + [Kept] + public static implicit operator TargetTypeImplicit (CustomOperators self) => null; + [Kept] + public static implicit operator CustomOperators (SourceTypeImplicit other) => null; + [Kept] + public static explicit operator TargetTypeExplicit (CustomOperators self) => null; + [Kept] + public static explicit operator CustomOperators (SourceTypeExplicit other) => null; + } + + [Kept] + class TargetTypeImplicit { } + [Kept] + class SourceTypeImplicit { } + [Kept] + class TargetTypeExplicit { } + [Kept] + class SourceTypeExplicit { } + } +} diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/LinqExpressions/CanPreserveNullableCustomOperators.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/LinqExpressions/CanPreserveNullableCustomOperators.cs new file mode 100644 index 0000000000000..efcdb747749cf --- /dev/null +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/LinqExpressions/CanPreserveNullableCustomOperators.cs @@ -0,0 +1,85 @@ +using Mono.Linker.Tests.Cases.Expectations.Assertions; +using Mono.Linker.Tests.Cases.Expectations.Metadata; + +namespace Mono.Linker.Tests.Cases.LinqExpressions +{ + [SetupLinkerArgument ("--used-attrs-only")] + public class CanPreserveNullableCustomOperators + { + public static void Main () + { + var expression = typeof (System.Linq.Expressions.Expression); + + var r = typeof (ReferenceTypeOperators); + var t1 = typeof (TargetReferenceType); + var t2 = typeof (SourceReferenceType); + + var s = typeof (ValueTypeOperators); + var t3 = typeof (AdditionValueType); + var t4 = typeof (TargetValueType); + var t5 = typeof (SourceValueType); + + var s2 = typeof (ValueTypeUnusedOperators); + } + + class ReferenceTypeOperators + { + [Kept] + public static ReferenceTypeOperators operator + (ReferenceTypeOperators? c) => null; + [Kept] + public static bool operator true (ReferenceTypeOperators? c) => true; + [Kept] + public static bool operator false (ReferenceTypeOperators? c) => true; + [Kept] + public static ReferenceTypeOperators? operator + (ReferenceTypeOperators? left, ReferenceTypeOperators? right) => null; + [Kept] + public static explicit operator TargetReferenceType (ReferenceTypeOperators? self) => null; + [Kept] + public static explicit operator ReferenceTypeOperators (SourceReferenceType? other) => null; + } + + [Kept] + class TargetReferenceType { } + [Kept] + class SourceReferenceType { } + + struct ValueTypeOperators + { + [Kept] + public static ValueTypeOperators operator + (ValueTypeOperators? c) => default (ValueTypeOperators); + [Kept] + public static ValueTypeOperators? operator - (ValueTypeOperators? c) => null; + [Kept] + public static bool operator true (ValueTypeOperators? c) => true; + [Kept] + public static bool operator false (ValueTypeOperators? c) => true; + [Kept] + public static ValueTypeOperators? operator + (ValueTypeOperators? left, ValueTypeOperators? right) => null; + [Kept] + public static ValueTypeOperators? operator + (ValueTypeOperators? left, AdditionValueType? right) => null; + [Kept] + public static explicit operator TargetValueType? (ValueTypeOperators? self) => null; + [Kept] + public static explicit operator ValueTypeOperators? (SourceValueType? other) => null; + } + + [Kept] + struct ValueTypeUnusedOperators + { + public static ValueTypeUnusedOperators? operator + (ValueTypeUnusedOperators? left, AdditionValueTypeUnused? right) => null; + public static explicit operator TargetValueTypeUnused? (ValueTypeUnusedOperators? self) => null; + public static explicit operator ValueTypeUnusedOperators? (SourceValueTypeUnused? other) => null; + } + + [Kept] + struct AdditionValueType { } + [Kept] + struct TargetValueType { } + [Kept] + struct SourceValueType { } + + struct AdditionValueTypeUnused { } + struct TargetValueTypeUnused { } + struct SourceValueTypeUnused { } + } +} diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/LinqExpressions/CanRemoveMethodsNamedLikeCustomOperators.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/LinqExpressions/CanRemoveMethodsNamedLikeCustomOperators.cs new file mode 100644 index 0000000000000..7196cfb8f44ba --- /dev/null +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/LinqExpressions/CanRemoveMethodsNamedLikeCustomOperators.cs @@ -0,0 +1,31 @@ +using Mono.Linker.Tests.Cases.Expectations.Assertions; + +namespace Mono.Linker.Tests.Cases.LinqExpressions +{ + public class CanRemoveMethodsNamedLikeCustomOperators + { + public static void Main () + { + var t = typeof (FakeOperators); + var expression = typeof (System.Linq.Expressions.Expression); + var t1 = typeof (SubtractionType); + var t2 = typeof (TargetType); + } + + public class FakeOperators + { + [Kept] + public static FakeOperators operator - (FakeOperators f) => null; + + public static FakeOperators op_UnaryPlus (FakeOperators f) => null; + public static FakeOperators op_Addition (FakeOperators left, FakeOperators right) => null; + public static FakeOperators op_Subtraction (FakeOperators left, SubtractionType right) => null; + public static TargetType op_Explicit (FakeOperators self) => null; + } + + [Kept] + public class SubtractionType { } + [Kept] + public class TargetType { } + } +} diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/LinqExpressions/CanRemoveOperatorsWhenNotUsingLinqExpressions.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/LinqExpressions/CanRemoveOperatorsWhenNotUsingLinqExpressions.cs new file mode 100644 index 0000000000000..ee4fc91f8d603 --- /dev/null +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/LinqExpressions/CanRemoveOperatorsWhenNotUsingLinqExpressions.cs @@ -0,0 +1,41 @@ +using Mono.Linker.Tests.Cases.Expectations.Assertions; + +namespace Mono.Linker.Tests.Cases.LinqExpressions +{ + public class CanRemoveOperatorsWhenNotUsingLinqExpressions + { + public static void Main () + { + var c = new CustomOperators (); + var c2 = +c; + var c3 = c + c2; + var t = (TargetType) c3; + } + + [Kept] + [KeptMember (".ctor()")] + class CustomOperators + { + [Kept] + public static CustomOperators operator + (CustomOperators c) => null; + public static CustomOperators operator - (CustomOperators c) => null; + + [Kept] + public static CustomOperators operator + (CustomOperators left, CustomOperators right) => null; + public static CustomOperators operator + (CustomOperators left, AdditionTypeUnused right) => null; + public static CustomOperators operator - (CustomOperators left, CustomOperators right) => null; + + [Kept] + public static explicit operator TargetType (CustomOperators self) => null; + + public static explicit operator CustomOperators (SourceTypeUnused other) => null; + } + + class AdditionTypeUnused { } + + [Kept] + class TargetType { } + + class SourceTypeUnused { } + } +} diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/LinqExpressions/CustomOperatorsWithUnusedArgumentTypes.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/LinqExpressions/CustomOperatorsWithUnusedArgumentTypes.cs new file mode 100644 index 0000000000000..15e96bc791b38 --- /dev/null +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/LinqExpressions/CustomOperatorsWithUnusedArgumentTypes.cs @@ -0,0 +1,57 @@ +using Mono.Linker.Tests.Cases.Expectations.Assertions; +using Mono.Linker.Tests.Cases.Expectations.Metadata; + +namespace Mono.Linker.Tests.Cases.LinqExpressions +{ + public class CustomOperatorsWithUnusedArgumentTypes + { + public static void Main () + { + var t = typeof (CustomOperators); + var expression = typeof (System.Linq.Expressions.Expression); + + var t1 = typeof (AdditionType); + var t2 = typeof (SubtractionType); + } + + public class CustomOperators + { + // simple cases are still kept + [Kept] + public static CustomOperators operator + (CustomOperators c) => null; + [Kept] + public static CustomOperators operator + (CustomOperators left, CustomOperators right) => null; + [Kept] + public static CustomOperators operator - (CustomOperators left, CustomOperators right) => null; + + // binary operators taking kept other types are still kept + [Kept] + public static CustomOperators operator + (CustomOperators left, AdditionType right) => null; + [Kept] + public static CustomOperators operator - (SubtractionType left, CustomOperators right) => null; + + // binary operators taking unused other types are removed + public static CustomOperators operator * (CustomOperators left, MultiplicationTypeUnused right) => null; + public static CustomOperators operator / (DivisionTypeUnused left, CustomOperators right) => null; + + // conversion operators to/from unused other types are removed + public static implicit operator TargetTypeImplicitUnused (CustomOperators self) => null; + public static implicit operator CustomOperators (SourceTypeImplicitUnused other) => null; + public static explicit operator TargetTypeExplicitUnused (CustomOperators self) => null; + public static explicit operator CustomOperators (SourceTypeExplicitUnused other) => null; + } + + [Kept] + public class AdditionType { } + [Kept] + public class SubtractionType { } + + public class MultiplicationTypeUnused { } + public class DivisionTypeUnused { } + + public class TargetTypeImplicitUnused { } + public class SourceTypeImplicitUnused { } + public class TargetTypeExplicitUnused { } + public class SourceTypeExplicitUnused { } + } +} diff --git a/src/tools/illink/test/Mono.Linker.Tests/TestCases/TestDatabase.cs b/src/tools/illink/test/Mono.Linker.Tests/TestCases/TestDatabase.cs index 58c0b9205b06c..8179e1d2c9e6e 100644 --- a/src/tools/illink/test/Mono.Linker.Tests/TestCases/TestDatabase.cs +++ b/src/tools/illink/test/Mono.Linker.Tests/TestCases/TestDatabase.cs @@ -216,6 +216,11 @@ public static IEnumerable XmlTests () return NUnitCasesBySuiteName ("LinkXml"); } + public static IEnumerable LinqExpressionsTests () + { + return NUnitCasesBySuiteName ("LinqExpressions"); + } + public static IEnumerable MetadataTests () { return NUnitCasesBySuiteName ("Metadata"); diff --git a/src/tools/illink/test/Mono.Linker.Tests/TestCases/TestSuites.cs b/src/tools/illink/test/Mono.Linker.Tests/TestCases/TestSuites.cs index 09de355e65a48..045d2a9e6acd1 100644 --- a/src/tools/illink/test/Mono.Linker.Tests/TestCases/TestSuites.cs +++ b/src/tools/illink/test/Mono.Linker.Tests/TestCases/TestSuites.cs @@ -259,6 +259,12 @@ public void XmlTests (TestCase testCase) Run (testCase); } + [TestCaseSource (typeof (TestDatabase), nameof (TestDatabase.LinqExpressionsTests))] + public void LinqExpressionsTests (TestCase testCase) + { + Run (testCase); + } + [TestCaseSource (typeof (TestDatabase), nameof (TestDatabase.MetadataTests))] public void MetadataTests (TestCase testCase) {