diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f8592ff5..a4ee07d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,10 +25,10 @@ jobs: run: dotnet test -c Release ValveKeyValue/ValveKeyValue.Test/ValveKeyValue.Test.csproj /p:CollectCoverage=true /p:CoverletOutputFormat=lcov /p:CoverletOutput='./lcov.info' /p:Include="[ValveKeyValue*]*" - name: Upload test coverage - uses: coverallsapp/github-action@v1.1.1 + uses: coverallsapp/github-action@1.1.3 with: github-token: ${{ secrets.GITHUB_TOKEN }} - path-to-lcov: ./ValveKeyValue/ValveKeyValue.Test/lcov.info + path-to-lcov: ./ValveKeyValue/ValveKeyValue.Test/lcov.net5.0.info flag-name: run-${{ matrix.test_number }} parallel: true @@ -37,7 +37,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Coveralls Finished - uses: coverallsapp/github-action@v1.1.1 + uses: coverallsapp/github-action@1.1.3 with: github-token: ${{ secrets.github_token }} parallel-finished: true diff --git a/Directory.Build.props b/Directory.Build.props index 2304ba2b..891dd95a 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -13,8 +13,6 @@ embedded true en - false - false true diff --git a/ValveKeyValue/ValveKeyValue.Test/ValveKeyValue.Test.csproj b/ValveKeyValue/ValveKeyValue.Test/ValveKeyValue.Test.csproj index 8521931d..df8c9d6a 100644 --- a/ValveKeyValue/ValveKeyValue.Test/ValveKeyValue.Test.csproj +++ b/ValveKeyValue/ValveKeyValue.Test/ValveKeyValue.Test.csproj @@ -1,7 +1,7 @@  Exe - netcoreapp3.1 + netcoreapp3.1;net5.0 ValveKeyValue.Test Valve KeyValue Library - Unit Tests Unit Tests for library to parse and write Valve KeyValue formats diff --git a/ValveKeyValue/ValveKeyValue/DefaultObjectReflector.cs b/ValveKeyValue/ValveKeyValue/DefaultObjectReflector.cs index 1ed55e9a..c25c162a 100644 --- a/ValveKeyValue/ValveKeyValue/DefaultObjectReflector.cs +++ b/ValveKeyValue/ValveKeyValue/DefaultObjectReflector.cs @@ -1,15 +1,29 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Reflection; namespace ValveKeyValue { sealed class DefaultObjectReflector : IObjectReflector { - IEnumerable IObjectReflector.GetMembers(object @object) + IEnumerable IObjectReflector.GetMembers( +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(Trimming.Properties)] +#endif + Type type, + object @object) { + Require.NotNull(type, nameof(type)); Require.NotNull(@object, nameof(@object)); - var properties = @object.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + var properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + return GetMembersFromProperties(@object, properties); + + } + + static IEnumerable GetMembersFromProperties(object @object, PropertyInfo[] properties) + { foreach (var property in properties) { if (property.GetCustomAttribute() != null) diff --git a/ValveKeyValue/ValveKeyValue/IObjectReflector.cs b/ValveKeyValue/ValveKeyValue/IObjectReflector.cs index a03e7f7c..db4f2f16 100644 --- a/ValveKeyValue/ValveKeyValue/IObjectReflector.cs +++ b/ValveKeyValue/ValveKeyValue/IObjectReflector.cs @@ -1,9 +1,16 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; namespace ValveKeyValue { interface IObjectReflector { - IEnumerable GetMembers(object @object); + IEnumerable GetMembers( +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(Trimming.Properties)] +#endif + Type type, + object @object); } } diff --git a/ValveKeyValue/ValveKeyValue/KVSerializer.cs b/ValveKeyValue/ValveKeyValue/KVSerializer.cs index 35c1dcd0..bdd37646 100644 --- a/ValveKeyValue/ValveKeyValue/KVSerializer.cs +++ b/ValveKeyValue/ValveKeyValue/KVSerializer.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using ValveKeyValue.Abstraction; using ValveKeyValue.Deserialization; @@ -52,7 +53,11 @@ public KVObject Deserialize(Stream stream, KVSerializerOptions options = null) /// Options to use that can influence the deserialization process. /// A instance representing the KeyValues structure in the stream. /// The type of object to deserialize.; +#if NET5_0_OR_GREATER + public TObject Deserialize<[DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] TObject>(Stream stream, KVSerializerOptions options = null) +#else public TObject Deserialize(Stream stream, KVSerializerOptions options = null) +#endif { Require.NotNull(stream, nameof(stream)); @@ -84,7 +89,11 @@ public void Serialize(Stream stream, KVObject data, KVSerializerOptions options /// The top-level object name /// Options to use that can influence the serialization process. /// The type of object to serialize. +#if NET5_0_OR_GREATER + public void Serialize<[DynamicallyAccessedMembers(Trimming.Properties)] TData>(Stream stream, TData data, string name, KVSerializerOptions options = null) +#else public void Serialize(Stream stream, TData data, string name, KVSerializerOptions options = null) +#endif { Require.NotNull(stream, nameof(stream)); diff --git a/ValveKeyValue/ValveKeyValue/ObjectCopier.cs b/ValveKeyValue/ValveKeyValue/ObjectCopier.cs index ba5d4744..4c91fa84 100644 --- a/ValveKeyValue/ValveKeyValue/ObjectCopier.cs +++ b/ValveKeyValue/ValveKeyValue/ObjectCopier.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Runtime.ExceptionServices; @@ -12,13 +13,26 @@ namespace ValveKeyValue // TODO: Migrate to IVisitationListener static class ObjectCopier { +#if NET5_0_OR_GREATER + public static TObject MakeObject<[DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] TObject>(KVObject keyValueObject) +#else public static TObject MakeObject(KVObject keyValueObject) +#endif => MakeObject(keyValueObject, new DefaultObjectReflector()); - public static object MakeObject(Type objectType, KVObject keyValueObject, IObjectReflector reflector) + public static object MakeObject( +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] +#endif + Type objectType, KVObject keyValueObject, IObjectReflector reflector) => InvokeGeneric(nameof(MakeObject), objectType, new object[] { keyValueObject, reflector }); +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2062", Justification = "If the lookup value type exists at runtime then it should have enough for us to introspect.")] + public static TObject MakeObject<[DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] TObject>(KVObject keyValueObject, IObjectReflector reflector) +#else public static TObject MakeObject(KVObject keyValueObject, IObjectReflector reflector) +#endif { Require.NotNull(keyValueObject, nameof(keyValueObject)); Require.NotNull(reflector, nameof(reflector)); @@ -60,10 +74,24 @@ public static TObject MakeObject(KVObject keyValueObject, IObjectReflec } } - public static KVObject FromObject(Type objectType, object managedObject, string topLevelName) + public static KVObject FromObject( +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(Trimming.Properties)] +#endif + Type objectType, + object managedObject, + string topLevelName) => FromObjectCore(objectType, managedObject, topLevelName, new DefaultObjectReflector(), new HashSet()); - static KVObject FromObjectCore(Type objectType, object managedObject, string topLevelName, IObjectReflector reflector, HashSet visitedObjects) + static KVObject FromObjectCore( +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(Trimming.Properties)] +#endif + Type objectType, + object managedObject, + string topLevelName, + IObjectReflector reflector, + HashSet visitedObjects) { if (managedObject == null) { @@ -78,7 +106,17 @@ static KVObject FromObjectCore(Type objectType, object managedObject, string top return new KVObject(topLevelName, transformedValue); } - static KVValue ConvertObjectToValue(Type objectType, object managedObject, IObjectReflector reflector, HashSet visitedObjects) +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072", Justification = "If the IDictionary's value object already exists at runtime then its properties will too.")] +#endif + static KVValue ConvertObjectToValue( +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(Trimming.Properties)] +#endif + Type objectType, + object managedObject, + IObjectReflector reflector, + HashSet visitedObjects) { if (!objectType.IsValueType && objectType != typeof(string) && !visitedObjects.Add(managedObject)) { @@ -109,7 +147,7 @@ static KVValue ConvertObjectToValue(Type objectType, object managedObject, IObje var counter = 0; foreach (var child in (IEnumerable)managedObject) { - var childKVObject = CopyObject(child, counter.ToString(), reflector, visitedObjects); + var childKVObject = FromObjectCore(child.GetType(), child, counter.ToString(), reflector, visitedObjects); childObjects.Add(childKVObject); counter++; @@ -117,7 +155,7 @@ static KVValue ConvertObjectToValue(Type objectType, object managedObject, IObje } else { - foreach (var member in reflector.GetMembers(managedObject).OrderBy(p => p.Name, StringComparer.InvariantCulture)) + foreach (var member in reflector.GetMembers(objectType, managedObject).OrderBy(p => p.Name, StringComparer.InvariantCulture)) { if (!member.MemberType.IsValueType && member.Value is null) { @@ -130,7 +168,7 @@ static KVValue ConvertObjectToValue(Type objectType, object managedObject, IObje } else { - childObjects.Add(CopyObject(member.Value, member.Name, reflector, visitedObjects)); + childObjects.Add(FromObjectCore(member.Value.GetType(), member.Value, member.Name, reflector, visitedObjects)); } } } @@ -138,10 +176,12 @@ static KVValue ConvertObjectToValue(Type objectType, object managedObject, IObje return childObjects; } - static KVObject CopyObject(object @object, string name, IObjectReflector reflector, HashSet visitedObjects) - => FromObjectCore(@object.GetType(), @object, name, reflector, visitedObjects); - +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072", Justification = "Cannot annotate IObjectMember, but analysis performed earlier in flow should be sufficient.")] + static void CopyObject<[DynamicallyAccessedMembers(Trimming.Properties)] TObject>(KVObject kv, TObject obj, IObjectReflector reflector) +#else static void CopyObject(KVObject kv, TObject obj, IObjectReflector reflector) +#endif { Require.NotNull(kv, nameof(kv)); @@ -153,7 +193,7 @@ static void CopyObject(KVObject kv, TObject obj, IObjectReflector refle Require.NotNull(reflector, nameof(reflector)); - var members = reflector.GetMembers(obj).ToDictionary(m => m.Name, m => m, StringComparer.OrdinalIgnoreCase); + var members = reflector.GetMembers(typeof(TObject), obj).ToDictionary(m => m.Name, m => m, StringComparer.OrdinalIgnoreCase); foreach (var item in kv.Children) { @@ -223,11 +263,28 @@ static bool IsLookupWithStringKey(Type type, out Type valueType) return true; } - static object MakeLookup(Type valueType, IEnumerable items, IObjectReflector reflector) + static object MakeLookup( +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] +#endif + Type valueType, + IEnumerable items, + IObjectReflector reflector) => InvokeGeneric(nameof(MakeLookupCore), valueType, new object[] { items, reflector }); +#if NET5_0_OR_GREATER + static ILookup MakeLookupCore<[DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] TValue>(IEnumerable items, IObjectReflector reflector) +#else static ILookup MakeLookupCore(IEnumerable items, IObjectReflector reflector) - => items.ToLookup(kv => kv.Name, kv => ConvertValue(kv.Value, reflector)); +#endif + { +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2091", Justification = "Analysis gets lost in compiler-rewritten IL for lambdas.")] +#endif + TValue valueConversionFunc(KVObject kv) => ConvertValue(kv.Value, reflector); + + return items.ToLookup(kv => kv.Name, valueConversionFunc); + } static readonly Dictionary> EnumerableBuilders = new Dictionary> { @@ -238,7 +295,14 @@ static ILookup MakeLookupCore(IEnumerable item [typeof(ObservableCollection<>)] = (type, values, reflector) => InvokeGeneric(nameof(MakeObservableCollection), type.GetGenericArguments()[0], new object[] { values, reflector }), }; - static bool ConstructTypedEnumerable(Type type, object[] values, IObjectReflector reflector, out object typedEnumerable) +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072", Justification = "If our T[] array exists then so much the element T.")] +#endif + static bool ConstructTypedEnumerable( + Type type, + object[] values, + IObjectReflector reflector, + out object typedEnumerable) { object listObject = null; @@ -290,6 +354,10 @@ static bool IsConstructibleEnumerableType(Type type) return false; } +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2060", Justification = "Analysis cannot follow MakeGenericMethod. All callers validated manually.")] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2111", Justification = "Analysis cannot follow MakeGenericMethod. All callers validated manually.")] +#endif static object InvokeGeneric(string methodName, Type genericType, params object[] parameters) { var method = typeof(ObjectCopier) @@ -308,18 +376,34 @@ static object InvokeGeneric(string methodName, Type genericType, params object[] } } +#if NET5_0_OR_GREATER + static List MakeList<[DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] TElement>(object[] items, IObjectReflector reflector) +#else static List MakeList(object[] items, IObjectReflector reflector) +#endif { - return items.Select(i => ConvertValue(i, reflector)) - .ToList(); + var list = new List(capacity: items.Length); + foreach (var item in items) + { + list.Add(ConvertValue(item, reflector)); + } + return list; } +#if NET5_0_OR_GREATER + static Collection MakeCollection<[DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] TElement>(object[] items, IObjectReflector reflector) +#else static Collection MakeCollection(object[] items, IObjectReflector reflector) +#endif { return new Collection(MakeList(items, reflector)); } +#if NET5_0_OR_GREATER + static ObservableCollection MakeObservableCollection<[DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] TElement>(object[] items, IObjectReflector reflector) +#else static ObservableCollection MakeObservableCollection(object[] items, IObjectReflector reflector) +#endif { return new ObservableCollection(MakeList(items, reflector)); } @@ -351,7 +435,17 @@ static bool IsDictionary(Type type) return true; } - static object MakeDictionary(Type type, KVObject kv, IObjectReflector reflector) +#if NET5_0_OR_GREATER + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2060", Justification = "Analysis cannot follow MakeGenericMethod but we should be clear by here anyway.")] +#endif + static object MakeDictionary( +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] +#endif + Type type, + KVObject kv, + IObjectReflector reflector) { var dictionary = Activator.CreateInstance(type); var genericArguments = type.GetGenericArguments(); @@ -364,7 +458,11 @@ static object MakeDictionary(Type type, KVObject kv, IObjectReflector reflector) return dictionary; } +#if NET5_0_OR_GREATER + static void FillDictionary<[DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] TKey, [DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] TValue>(Dictionary dictionary, KVObject kv, IObjectReflector reflector) +#else static void FillDictionary(Dictionary dictionary, KVObject kv, IObjectReflector reflector) +#endif { foreach (var item in kv.Children) { @@ -375,9 +473,20 @@ static void FillDictionary(Dictionary dictionary, KV } } - static TValue ConvertValue(object value, IObjectReflector reflector) => (TValue)ConvertValue(value, typeof(TValue), reflector); - - static object ConvertValue(object value, Type valueType, IObjectReflector reflector) +#if NET5_0_OR_GREATER + static TValue ConvertValue<[DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] TValue>(object value, IObjectReflector reflector) +#else + static TValue ConvertValue(object value, IObjectReflector reflector) +#endif + => (TValue)ConvertValue(value, typeof(TValue), reflector); + + static object ConvertValue( + object value, +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] +#endif + Type valueType, + IObjectReflector reflector) { if (value is KVCollectionValue collectionValue) { diff --git a/ValveKeyValue/ValveKeyValue/Trimming.cs b/ValveKeyValue/ValveKeyValue/Trimming.cs new file mode 100644 index 00000000..6eff21a5 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue/Trimming.cs @@ -0,0 +1,12 @@ +#if NET5_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; + +namespace ValveKeyValue +{ + internal static class Trimming + { + public const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; + public const DynamicallyAccessedMemberTypes Properties = DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties; + } +} +#endif diff --git a/ValveKeyValue/ValveKeyValue/ValveKeyValue.csproj b/ValveKeyValue/ValveKeyValue/ValveKeyValue.csproj index 01d6cb90..6aa6df0e 100644 --- a/ValveKeyValue/ValveKeyValue/ValveKeyValue.csproj +++ b/ValveKeyValue/ValveKeyValue/ValveKeyValue.csproj @@ -1,6 +1,6 @@ - netstandard2.1 + netstandard2.1;net5.0 Valve KeyValue Library Library to parse and write Valve KeyValue formats View release notes at https://github.com/SteamDatabase/ValveKeyValue/releases @@ -8,6 +8,7 @@ steam valve keyvalues keyvalue kv kv3 csgo dota2 tf2 true ValveKeyValue.snk + true