Skip to content

Commit

Permalink
JS translation: Dictionary.GetValueOrDefault
Browse files Browse the repository at this point in the history
  • Loading branch information
exyi committed Jan 13, 2024
1 parent 5b6b6a3 commit 07ad81f
Show file tree
Hide file tree
Showing 5 changed files with 55 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -59,7 +60,7 @@ public DirectiveCompilationService(CompiledAssemblyCache compiledAssemblyCache,

public object? ResolvePropertyInitializer(DothtmlDirectiveNode directive, Type propertyType, BindingParserNode? initializer, ImmutableList<NamespaceImport> imports)
{
if (initializer == null) { return CreateDefaultValue(propertyType); }
if (initializer == null) { return ReflectionUtils.GetDefaultValue(propertyType); }

var registry = RegisterImports(TypeRegistry.DirectivesDefault(compiledAssemblyCache), imports);

Expand All @@ -75,25 +76,16 @@ public DirectiveCompilationService(CompiledAssemblyCache compiledAssemblyCache,
var lambda = Expression.Lambda<Func<object?>>(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<NamespaceImport> imports)
{
TypeRegistry registry;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,20 @@ private void AddDefaultDictionaryTranslations()
AddMethodTranslator(() => default(IReadOnlyDictionary<Generic.T, Generic.T>)!.ContainsKey(null!), containsKey);
AddMethodTranslator(() => default(IDictionary<Generic.T, Generic.T>)!.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<Generic.T, Generic.T>)!.GetValueOrDefault(null!), getValueOrDefault);
AddMethodTranslator(() => default(IReadOnlyDictionary<Generic.T, Generic.T>)!.GetValueOrDefault(null!, null), getValueOrDefault);
#endif
AddMethodTranslator(() => default(IImmutableDictionary<Generic.T, Generic.T>)!.GetValueOrDefault(null!), getValueOrDefault);
AddMethodTranslator(() => default(IImmutableDictionary<Generic.T, Generic.T>)!.GetValueOrDefault(null!, null), getValueOrDefault);
AddMethodTranslator(() => FunctionalExtensions.GetValueOrDefault(default(IReadOnlyDictionary<Generic.T, Generic.T>)!, null!, null!, false), getValueOrDefault);
}

private bool IsDictionary(Type type) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@ export function containsKey<Key, Value>(dictionary: Dictionary<Key, Value>, iden
return getKeyValueIndex(dictionary, identifier) !== null;
}

export function getItem<Key, Value>(dictionary: Dictionary<Key, Value>, identifier: Key): Value {
export function getItem<Key, Value>(dictionary: Dictionary<Key, Value>, 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);
Expand Down
27 changes: 19 additions & 8 deletions src/Framework/Framework/Utils/ReflectionUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
using DotVVM.Framework.Routing;
using DotVVM.Framework.ViewModel;
using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace DotVVM.Framework.Utils
{
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

/// <summary> Returns the equivalent of default(T) in C#, null for reference and Nullable&lt;T>, zeroed object for structs. </summary>
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()))
Expand Down
12 changes: 12 additions & 0 deletions src/Tests/Binding/JavascriptCompilationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Check failure on line 547 in src/Tests/Binding/JavascriptCompilationTests.cs

View workflow job for this annotation

GitHub Actions / .NET unit tests (windows-2022)

JsTranslator_Dictionary_GetValueOrDefault (Dictionary)

Test method DotVVM.Framework.Tests.Binding.JavascriptCompilationTests.JsTranslator_Dictionary_GetValueOrDefault threw exception: System.InvalidOperationException: Could not find method overload nor extension method that matched 'GetValueOrDefault'.

Check failure on line 547 in src/Tests/Binding/JavascriptCompilationTests.cs

View workflow job for this annotation

GitHub Actions / .NET unit tests (windows-2022)

JsTranslator_Dictionary_GetValueOrDefault (ReadOnlyDictionary)

Test method DotVVM.Framework.Tests.Binding.JavascriptCompilationTests.JsTranslator_Dictionary_GetValueOrDefault threw exception: System.InvalidOperationException: Could not find method overload nor extension method that matched 'GetValueOrDefault'.
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()
{
Expand Down

0 comments on commit 07ad81f

Please sign in to comment.