Skip to content

Commit 0e0c11a

Browse files
authored
Merge pull request #1909 from riganti/not-assign-init-only
Disallow asigning to init-only properties in bindings
2 parents e247317 + d9387b2 commit 0e0c11a

File tree

4 files changed

+65
-0
lines changed

4 files changed

+65
-0
lines changed

src/Framework/Framework/Compilation/Binding/GeneralBindingPropertyResolvers.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ public Expression<BindingDelegate> CompileToDelegate(
5858
{
5959
var replacementVisitor = new BindingCompiler.ParameterReplacementVisitor(dataContext);
6060
var expr = replacementVisitor.Visit(expression.Expression);
61+
var initOnlyPropertyChecker = binding is IStaticCommandBinding
62+
? InitOnlyPropertyCheckingVisitor.InstanceDynamicError // allow this client-side
63+
: InitOnlyPropertyCheckingVisitor.Instance;
64+
expr = initOnlyPropertyChecker.Visit(expr);
6165
expr = new ExpressionNullPropagationVisitor(e => true).Visit(expr);
6266
expr = ExpressionUtils.ConvertToObject(expr);
6367
expr = replacementVisitor.WrapExpression(expr, contextObject: binding);
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using System;
2+
using System.Linq.Expressions;
3+
using System.Reflection;
4+
using System.Runtime.CompilerServices;
5+
using DotVVM.Framework.Utils;
6+
7+
namespace DotVVM.Framework.Compilation.Binding
8+
{
9+
/// <summary> Checks that assigned properties have setters without the [IsExternalInit] attribute.
10+
/// <paramref name="staticError"/> specifies if the error should be thrown immediately, or only when actually executed at runtime. </summary>
11+
public class InitOnlyPropertyCheckingVisitor(bool staticError = true): ExpressionVisitor
12+
{
13+
public static InitOnlyPropertyCheckingVisitor Instance { get; } = new InitOnlyPropertyCheckingVisitor(true);
14+
public static InitOnlyPropertyCheckingVisitor InstanceDynamicError { get; } = new InitOnlyPropertyCheckingVisitor(false);
15+
16+
protected override Expression VisitBinary(BinaryExpression node)
17+
{
18+
if (node is { NodeType: ExpressionType.Assign, Left: MemberExpression { Member: PropertyInfo assignedProperty } } &&
19+
assignedProperty.IsInitOnly())
20+
{
21+
var message = $"Property '{assignedProperty.DeclaringType!.Name}.{assignedProperty.Name}' is init-only and cannot be assigned to in bindings executed server-side. You can only assign to such properties in staticCommand bindings executed on the client.";
22+
if (staticError)
23+
{
24+
throw new Exception(message);
25+
}
26+
else
27+
{
28+
return Expression.Throw(Expression.New(typeof(Exception).GetConstructor([typeof(string)])!, [Expression.Constant(message)]));
29+
}
30+
}
31+
32+
return base.VisitBinary(node);
33+
}
34+
}
35+
}

src/Tests/Binding/BindingCompilationTests.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1354,6 +1354,17 @@ public void BindingCompiler_AmbiguousMatches()
13541354
var result = ExecuteBinding("Strings.SomeResource", new[] { new NamespaceImport("DotVVM.Framework.Tests.Ambiguous"), new NamespaceImport("DotVVM.Framework.Tests") }, new TestViewModel());
13551355
Assert.AreEqual("hello", result);
13561356
}
1357+
1358+
[TestMethod]
1359+
public void BindingCompiler_InitOnlyPropertyCannotBeAssigned()
1360+
{
1361+
var vm = new TestViewModelWithInitOnlyProperties() { MyProperty = 999 };
1362+
1363+
var exception = XAssert.ThrowsAny<Exception>(() => ExecuteBinding("_this.MyProperty = 1", vm));
1364+
XAssert.Contains("Property 'TestViewModelWithInitOnlyProperties.MyProperty' is init-only", exception.Message);
1365+
1366+
Assert.AreEqual(999, vm.MyProperty);
1367+
}
13571368
}
13581369
public class TestViewModel
13591370
{
@@ -1574,6 +1585,11 @@ public class TestViewModel5
15741585
public int[] Array { get; set; } = new int[] { 1, 2, 3 };
15751586
}
15761587

1588+
public class TestViewModelWithInitOnlyProperties
1589+
{
1590+
public int MyProperty { get; init; }
1591+
}
1592+
15771593
public struct TestStruct
15781594
{
15791595
public int Int { get; set; }

src/Tests/Binding/StaticCommandCompilationTests.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,16 @@ public void StaticCommandCompilation_FailReasonablyOnInvalidMethod()
384384
Assert.AreEqual("Member 'Int32 GetCharCode(Char)' is not static.", result.GetBaseException().Message);
385385
}
386386

387+
388+
[TestMethod]
389+
public void StaticCommandCompilation_InitOnlyPropertyCanBeAssigned()
390+
{
391+
// on client, we allow this (we don't support with nor calling constructor, so it would be very annoying without this)
392+
var result = CompileBinding("_this.MyProperty = 1", niceMode: true, new[] { typeof(TestViewModelWithInitOnlyProperties) }, typeof(Command));
393+
Console.WriteLine(result);
394+
AreEqual("{options.viewModel.MyProperty(1);}", result);
395+
}
396+
387397
public void AreEqual(string expected, string actual)
388398
=> Assert.AreEqual(RemoveWhitespaces(expected), RemoveWhitespaces(actual));
389399

0 commit comments

Comments
 (0)