From 07ad81fe6e6ede6a10224226d409cc71841e2133 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 13 Jan 2024 19:48:12 +0100 Subject: [PATCH] JS translation: Dictionary.GetValueOrDefault --- .../Resolved/DirectiveCompilationService.cs | 16 +++-------- .../JavascriptTranslatableMethodCollection.cs | 14 ++++++++++ .../Scripts/translations/dictionaryHelper.ts | 8 ++++-- .../Framework/Utils/ReflectionUtils.cs | 27 +++++++++++++------ .../Binding/JavascriptCompilationTests.cs | 12 +++++++++ 5 files changed, 55 insertions(+), 22 deletions(-) diff --git a/src/Framework/Framework/Compilation/ControlTree/Resolved/DirectiveCompilationService.cs b/src/Framework/Framework/Compilation/ControlTree/Resolved/DirectiveCompilationService.cs index b602c75505..3bc17d053c 100644 --- a/src/Framework/Framework/Compilation/ControlTree/Resolved/DirectiveCompilationService.cs +++ b/src/Framework/Framework/Compilation/ControlTree/Resolved/DirectiveCompilationService.cs @@ -6,6 +6,7 @@ using DotVVM.Framework.Compilation.Binding; using System.Collections.Immutable; using DotVVM.Framework.Configuration; +using DotVVM.Framework.Utils; namespace DotVVM.Framework.Compilation.ControlTree.Resolved { @@ -59,7 +60,7 @@ public DirectiveCompilationService(CompiledAssemblyCache compiledAssemblyCache, public object? ResolvePropertyInitializer(DothtmlDirectiveNode directive, Type propertyType, BindingParserNode? initializer, ImmutableList imports) { - if (initializer == null) { return CreateDefaultValue(propertyType); } + if (initializer == null) { return ReflectionUtils.GetDefaultValue(propertyType); } var registry = RegisterImports(TypeRegistry.DirectivesDefault(compiledAssemblyCache), imports); @@ -75,25 +76,16 @@ public DirectiveCompilationService(CompiledAssemblyCache compiledAssemblyCache, var lambda = Expression.Lambda>(Expression.Block(Expression.Convert(TypeConversion.EnsureImplicitConversion(initializerExpression, propertyType), typeof(object)))); var lambdaDelegate = lambda.Compile(true); - return lambdaDelegate.Invoke() ?? CreateDefaultValue(propertyType); + return lambdaDelegate.Invoke() ?? ReflectionUtils.GetDefaultValue(propertyType); } catch (Exception ex) { directive.AddError("Could not initialize property value."); directive.AddError(ex.Message); - return CreateDefaultValue(propertyType); + return ReflectionUtils.GetDefaultValue(propertyType); } } - private object? CreateDefaultValue(Type? type) - { - if (type != null && type.IsValueType) - { - return Activator.CreateInstance(type); - } - return null; - } - private Expression? CompileDirectiveExpression(DothtmlDirectiveNode directive, BindingParserNode expressionSyntax, ImmutableList imports) { TypeRegistry registry; diff --git a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs index 96f9f1e2a2..d53d1636ce 100644 --- a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs +++ b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs @@ -681,6 +681,20 @@ private void AddDefaultDictionaryTranslations() AddMethodTranslator(() => default(IReadOnlyDictionary)!.ContainsKey(null!), containsKey); AddMethodTranslator(() => default(IDictionary)!.Remove(null!), new GenericMethodCompiler(args => new JsIdentifierExpression("dotvvm").Member("translations").Member("dictionary").Member("remove").Invoke(args[0].WithAnnotation(ShouldBeObservableAnnotation.Instance), args[1]))); + + var getValueOrDefault = new GenericMethodCompiler((JsExpression[] args, MethodInfo method) => { + var defaultValue = + args.Length > 3 ? args[3] : + new JsLiteral(ReflectionUtils.GetDefaultValue(method.GetGenericArguments().Last())); + return new JsIdentifierExpression("dotvvm").Member("translations").Member("dictionary").Member("getItem").Invoke(args[1], args[2], defaultValue); + }); +#if DotNetCore + AddMethodTranslator(() => default(IReadOnlyDictionary)!.GetValueOrDefault(null!), getValueOrDefault); + AddMethodTranslator(() => default(IReadOnlyDictionary)!.GetValueOrDefault(null!, null), getValueOrDefault); +#endif + AddMethodTranslator(() => default(IImmutableDictionary)!.GetValueOrDefault(null!), getValueOrDefault); + AddMethodTranslator(() => default(IImmutableDictionary)!.GetValueOrDefault(null!, null), getValueOrDefault); + AddMethodTranslator(() => FunctionalExtensions.GetValueOrDefault(default(IReadOnlyDictionary)!, null!, null!, false), getValueOrDefault); } private bool IsDictionary(Type type) => diff --git a/src/Framework/Framework/Resources/Scripts/translations/dictionaryHelper.ts b/src/Framework/Framework/Resources/Scripts/translations/dictionaryHelper.ts index bc06aafd58..d19e7671b7 100644 --- a/src/Framework/Framework/Resources/Scripts/translations/dictionaryHelper.ts +++ b/src/Framework/Framework/Resources/Scripts/translations/dictionaryHelper.ts @@ -8,10 +8,14 @@ export function containsKey(dictionary: Dictionary, iden return getKeyValueIndex(dictionary, identifier) !== null; } -export function getItem(dictionary: Dictionary, identifier: Key): Value { +export function getItem(dictionary: Dictionary, identifier: Key, defaultValue?: Value): Value { const index = getKeyValueIndex(dictionary, identifier); if (index === null) { - throw Error("Provided key \"" + identifier + "\" is not present in the dictionary!"); + if (defaultValue !== undefined) { + return defaultValue; + } else { + throw Error("Provided key \"" + identifier + "\" is not present in the dictionary!"); + } } return ko.unwrap(ko.unwrap(dictionary[index]).Value); diff --git a/src/Framework/Framework/Utils/ReflectionUtils.cs b/src/Framework/Framework/Utils/ReflectionUtils.cs index bf5e8ea0ef..847090e336 100644 --- a/src/Framework/Framework/Utils/ReflectionUtils.cs +++ b/src/Framework/Framework/Utils/ReflectionUtils.cs @@ -27,6 +27,7 @@ using DotVVM.Framework.Routing; using DotVVM.Framework.ViewModel; using System.Diagnostics; +using System.Runtime.CompilerServices; namespace DotVVM.Framework.Utils { @@ -135,14 +136,7 @@ public static bool IsAssignableToGenericType(this Type givenType, Type genericTy // handle null values if (value == null) { - if (type == typeof(bool)) - return BoxingUtils.False; - else if (type == typeof(int)) - return BoxingUtils.Zero; - else if (type.IsValueType) - return Activator.CreateInstance(type); - else - return null; + return GetDefaultValue(type); } if (type.IsInstanceOfType(value)) return value; @@ -460,6 +454,23 @@ public static Type MakeNullableType(this Type type) return type.IsValueType && Nullable.GetUnderlyingType(type) == null && type != typeof(void) ? typeof(Nullable<>).MakeGenericType(type) : type; } + /// Returns the equivalent of default(T) in C#, null for reference and Nullable<T>, zeroed object for structs. + public static object? GetDefaultValue(Type type) + { + if (!type.IsValueType) + return null; + if (type.IsNullable()) + return null; + + if (type == typeof(bool)) + return BoxingUtils.False; + else if (type == typeof(int)) + return BoxingUtils.Zero; + // see https://github.com/dotnet/runtime/issues/90697 + // notably we can't use Activator.CreateInstance, because C# now allows default constructors in structs + return FormatterServices.GetUninitializedObject(type); + } + public static Type UnwrapTaskType(this Type type) { if (type.IsGenericType && typeof(Task<>).IsAssignableFrom(type.GetGenericTypeDefinition())) diff --git a/src/Tests/Binding/JavascriptCompilationTests.cs b/src/Tests/Binding/JavascriptCompilationTests.cs index 1c323b8337..561e1f7921 100644 --- a/src/Tests/Binding/JavascriptCompilationTests.cs +++ b/src/Tests/Binding/JavascriptCompilationTests.cs @@ -538,6 +538,18 @@ public void JsTranslator_ReadOnlyDictionaryIndexer_Get() Assert.AreEqual("dotvvm.translations.dictionary.getItem(ReadOnlyDictionary(),1)", result); } + [DataTestMethod] + [DataRow("Dictionary")] + [DataRow("ReadOnlyDictionary")] + public void JsTranslator_Dictionary_GetValueOrDefault(string property) + { + var imports = new NamespaceImport[] { new("System.Collections.Generic"), new("DotVVM.Framework.Utils") }; + var result = CompileBinding($"{property}.GetValueOrDefault(1)", imports, typeof(TestViewModel5)); + Assert.AreEqual($"dotvvm.translations.dictionary.getItem({property}(),1,0)", result); + var result2 = CompileBinding($"{property}.GetValueOrDefault(1, 1024)", imports, typeof(TestViewModel5)); + Assert.AreEqual($"dotvvm.translations.dictionary.getItem({property}(),1,1024)", result2); + } + [TestMethod] public void JsTranslator_DictionaryIndexer_Set() {