From ac2aaad85c50cb55c01777e9220a29d0a2a03dc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milan=20Miku=C5=A1?= Date: Thu, 2 Nov 2023 00:05:46 +0100 Subject: [PATCH 1/6] resolving half written attributes for the extension --- .../PropertyDeclarationDirectiveCompiler.cs | 73 +++++++++++-------- .../Parser/Binding/BindingParserTests.cs | 16 ++++ .../PropertyDirectiveTests.cs | 27 +++++++ 3 files changed, 86 insertions(+), 30 deletions(-) create mode 100644 src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/PropertyDirectiveTests.cs diff --git a/src/Framework/Framework/Compilation/Directives/PropertyDeclarationDirectiveCompiler.cs b/src/Framework/Framework/Compilation/Directives/PropertyDeclarationDirectiveCompiler.cs index 90910a6dc0..302a1c472f 100644 --- a/src/Framework/Framework/Compilation/Directives/PropertyDeclarationDirectiveCompiler.cs +++ b/src/Framework/Framework/Compilation/Directives/PropertyDeclarationDirectiveCompiler.cs @@ -53,44 +53,55 @@ protected override IAbstractPropertyDeclarationDirective Resolve(DothtmlDirectiv var attributeSyntaxes = (declaration?.Attributes ?? new List()); var resolvedAttributes = ProcessPropertyDirectiveAttributeReference(directiveNode, attributeSyntaxes) - .Select(a => TreeBuilder.BuildPropertyDeclarationAttributeReference(directiveNode, a.name, a.type, a.initializer, imports)) + .Select(a => TreeBuilder.BuildPropertyDeclarationAttributeReference(directiveNode, a.Name, a.Type, a.Initializer, imports)) .ToList(); return TreeBuilder.BuildPropertyDeclarationDirective(directiveNode, type, name, declaration?.Initializer, resolvedAttributes, valueSyntaxRoot, imports); } - private List<(TypeReferenceBindingParserNode type, IdentifierNameBindingParserNode name, LiteralExpressionBindingParserNode initializer)> ProcessPropertyDirectiveAttributeReference(DothtmlDirectiveNode directiveNode, List attributeReferences) + private List ProcessPropertyDirectiveAttributeReference(DothtmlDirectiveNode directiveNode, List attributeReferences) { - var result = new List<(TypeReferenceBindingParserNode, IdentifierNameBindingParserNode, LiteralExpressionBindingParserNode)>(); - foreach (var attributeReference in attributeReferences) + return attributeReferences.Select(attr => GetAttributeInfo(attr, directiveNode)).ToList(); + } + + private AttributeInfo GetAttributeInfo(BindingParserNode attributeReference, DothtmlDirectiveNode directiveNode) + { + if (attributeReference is not BinaryOperatorBindingParserNode { Operator: BindingTokenType.AssignOperator } assignment) + { + directiveNode.AddError("Property attributes must be in the form Attribute.Property = value."); + //No idea what is that, lets make it a type and move on + + var typeRef = new ActualTypeReferenceBindingParserNode(attributeReference); + typeRef.TransferTokens(attributeReference); + + return new AttributeInfo( + typeRef, + new SimpleNameBindingParserNode("") { StartPosition = attributeReference.EndPosition }, + new LiteralExpressionBindingParserNode("") { StartPosition = attributeReference.EndPosition }); + } + + var attributePropertyReference = assignment.FirstExpression as MemberAccessBindingParserNode; + var initializer = assignment.SecondExpression as LiteralExpressionBindingParserNode; + var attributeTypeReference = attributePropertyReference?.TargetExpression; + var attributePropertyNameReference = attributePropertyReference?.MemberNameExpression; + + + if (attributeTypeReference is null || attributePropertyNameReference is null) { - if (attributeReference is not BinaryOperatorBindingParserNode { Operator: BindingTokenType.AssignOperator } assignment) - { - directiveNode.AddError("Property attributes must be in the form Attribute.Property = value."); - continue; - } - - var attributePropertyReference = assignment.FirstExpression as MemberAccessBindingParserNode; - var attributeTypeReference = attributePropertyReference?.TargetExpression; - var attributePropertyNameReference = attributePropertyReference?.MemberNameExpression; - var initializer = assignment.SecondExpression as LiteralExpressionBindingParserNode; - - if (attributeTypeReference is null || attributePropertyNameReference is null) - { - directiveNode.AddError("Property attributes must be in the form Attribute.Property = value."); - continue; - } - if (initializer == null) - { - directiveNode.AddError($"Value for property {attributeTypeReference.ToDisplayString()} of attribute {attributePropertyNameReference.ToDisplayString()} is missing or not a constant."); - continue; - } - - var type = new ActualTypeReferenceBindingParserNode(attributeTypeReference); - type.TransferTokens(attributeTypeReference); - result.Add(new (type, attributePropertyNameReference, initializer)); + directiveNode.AddError("Property attributes must be in the form Attribute.Property = value."); + //Name is probably mising or type is incomplete + attributeTypeReference = attributeTypeReference ?? new SimpleNameBindingParserNode(""); + attributePropertyNameReference = attributePropertyNameReference ?? new SimpleNameBindingParserNode("") { StartPosition = attributeTypeReference.EndPosition }; } - return result; + if (initializer == null) + { + directiveNode.AddError($"Value for property {attributeTypeReference.ToDisplayString()} of attribute {attributePropertyNameReference.ToDisplayString()} is missing or not a constant."); + initializer = new LiteralExpressionBindingParserNode("") { StartPosition = attributePropertyNameReference.EndPosition }; + } + + var type = new ActualTypeReferenceBindingParserNode(attributeTypeReference); + type.TransferTokens(attributeTypeReference); + return new AttributeInfo(type, attributePropertyNameReference, initializer); } protected override ImmutableList CreateArtefact(IReadOnlyList directives) @@ -108,6 +119,8 @@ protected override ImmutableList CreateArtefact(IReadOnlyList(); + var type = root.PropertyType.CastTo(); + var name = root.Name.CastTo(); + + AssertNode(type, "string", 0, 7); + AssertNode(name, "MyProperty", 7, 10); + + Assert.AreEqual(4, root.Attributes.Count); + } + [TestMethod] public void BindingParser_NullablePropertyDeclaration() { diff --git a/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/PropertyDirectiveTests.cs b/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/PropertyDirectiveTests.cs new file mode 100644 index 0000000000..f65debc361 --- /dev/null +++ b/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/PropertyDirectiveTests.cs @@ -0,0 +1,27 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using DotVVM.Framework.Tests.Runtime.ControlTree.DefaultControlTreeResolver; +using System.Linq; +using DotVVM.Framework.Compilation.ControlTree; +using DotVVM.Framework.Controls; + +namespace DotVVM.Framework.Tests.Runtime.ControlTree +{ + [TestClass] + public class PropertyDirectiveTests : DefaultControlTreeResolverTestsBase + { + [TestMethod] + public void ResolvedTree_PropertyDirectiveHalfWritten_AttributeResolved() + { + var root = ParseSource(@$" +@viewModel object +@property string MyProperty, , DotVVM., DotVVM.Fra = , DotVVM.Framework.Controls.MarkupOptionsAttribute.Required = t"); + + var property = root.Directives["property"].SingleOrDefault() as IAbstractPropertyDeclarationDirective; + + Assert.AreEqual("System.String", property.PropertyType.FullName); + Assert.AreEqual("MyProperty", property.NameSyntax.Name); + + Assert.AreEqual(4, property.Attributes.Count); + } + } +} From cac37e4e746e3340f2b19d90e24d51cb5e50b1ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milan=20Miku=C5=A1?= Date: Fri, 10 Nov 2023 22:40:25 +0100 Subject: [PATCH 2/6] Parsing comma separated arguments rewritten to be able to parse half written and empty arguments. --- .../Parser/Binding/Parser/BindingParser.cs | 33 ++++++++++------- .../Parser/Binding/BindingParserTests.cs | 35 +++++++++++++++++-- 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/src/Framework/Framework/Compilation/Parser/Binding/Parser/BindingParser.cs b/src/Framework/Framework/Compilation/Parser/Binding/Parser/BindingParser.cs index 78e1444665..dced95e687 100644 --- a/src/Framework/Framework/Compilation/Parser/Binding/Parser/BindingParser.cs +++ b/src/Framework/Framework/Compilation/Parser/Binding/Parser/BindingParser.cs @@ -69,7 +69,7 @@ public BindingParserNode ReadArrayInitializerValue() SkipWhiteSpace(); } - if(PeekType() == BindingTokenType.CloseArrayBrace) + if (PeekType() == BindingTokenType.CloseArrayBrace) { Read(); SkipWhiteSpace(); @@ -107,7 +107,7 @@ public BindingParserNode ReadPropertyDirectiveValue() Read(); SkipWhiteSpace(); - var attributes = ReadArguments(); + var attributes = ReadArguments(OnEnd); foreach (var attribute in attributes) { @@ -131,7 +131,7 @@ public BindingParserNode ReadDirectiveTypeName() Read(); var assemblyName = ReadAssemblyName(); - return new AssemblyQualifiedNameBindingParserNode(typeName, assemblyName); + return new AssemblyQualifiedNameBindingParserNode(typeName, assemblyName); //TODO: create node } else if (Peek() is BindingToken token) { @@ -742,7 +742,8 @@ private BindingParserNode ReadFunctionCall(int startIndex, BindingParserNode exp { // function call Read(); - var arguments = ReadArguments(); + SkipWhiteSpace(); + var arguments = ReadArguments(() => PeekType() == BindingTokenType.CloseParenthesis); var error = IsCurrentTokenIncorrect(BindingTokenType.CloseParenthesis); Read(); SkipWhiteSpace(); @@ -750,21 +751,27 @@ private BindingParserNode ReadFunctionCall(int startIndex, BindingParserNode exp return expression; } - private List ReadArguments() + private List ReadArguments(Func isEndToken) { var arguments = new List(); - int previousInnerIndex = -1; - while (Peek() is BindingToken operatorToken && operatorToken.Type != BindingTokenType.CloseParenthesis && previousInnerIndex != CurrentIndex) + var previousInnerIndex = -1; + while (previousInnerIndex != CurrentIndex && !OnEnd() && !isEndToken()) { previousInnerIndex = CurrentIndex; - if (arguments.Count > 0) + + arguments.Add(ReadExpression()); + + SkipWhiteSpace(); + + if (PeekType() == BindingTokenType.Comma) { + Read(); SkipWhiteSpace(); - if (IsCurrentTokenIncorrect(BindingTokenType.Comma)) - arguments.Add(CreateNode(new LiteralExpressionBindingParserNode(null), CurrentIndex, "The ',' was expected")); - else Read(); } - arguments.Add(ReadExpression()); + else if (!isEndToken()) + { + arguments.Add(CreateNode(new SimpleNameBindingParserNode(""), CurrentIndex, "The ',' was expected")); + } } return arguments; @@ -1116,7 +1123,7 @@ private static bool ParseNumberLiteralSuffix(ref string text, ref string? error, { error = null; if (method(text, styles, CultureInfo.InvariantCulture, out var result)) return result; - error = $"could not parse { text } using { method.GetMethodInfo()?.DeclaringType?.FullName + "." + method.GetMethodInfo()?.Name }"; + error = $"could not parse {text} using {method.GetMethodInfo()?.DeclaringType?.FullName + "." + method.GetMethodInfo()?.Name}"; return null; } diff --git a/src/Tests/Parser/Binding/BindingParserTests.cs b/src/Tests/Parser/Binding/BindingParserTests.cs index c3747a59c9..61dd5643d2 100644 --- a/src/Tests/Parser/Binding/BindingParserTests.cs +++ b/src/Tests/Parser/Binding/BindingParserTests.cs @@ -1217,6 +1217,35 @@ public void BindingParser_PropertyHalfWrittenAttributes() AssertNode(name, "MyProperty", 7, 10); Assert.AreEqual(4, root.Attributes.Count); + + var emptyAttribute = root.Attributes[0].CastTo(); + AssertNode(emptyAttribute, "", 19, 0); + + var dotvvmNode = root.Attributes[1].CastTo(); + AssertNode(dotvvmNode, "DotVVM.", 21, 7); + + var dotvvmFraNode = root.Attributes[2].CastTo(); + AssertNode(dotvvmFraNode, "DotVVM.Fra = ", 30, 13); + + var longNode = root.Attributes[3].CastTo(); + AssertNode(longNode, "DotVVM.Framework.Controls.MarkupOptionsAttribute.Required = t", 45, 61); + } + + [TestMethod] + public void BindingParser_PropertyTypeHalfWritten() + { + var parser = bindingParserNodeFactory.SetupParser("System."); + var declaration = parser.ReadPropertyDirectiveValue(); + + var root = declaration.CastTo(); + var type = root.PropertyType.CastTo(); + var memberAccess = type.Type.CastTo(); + var target = memberAccess.TargetExpression; + var name = memberAccess.MemberNameExpression; + + AssertNode(type, "System.", 0, 7); + AssertNode(target, "System", 0, 6); + AssertNode(name, "", 7, 0); } [TestMethod] @@ -1314,7 +1343,7 @@ public void BindingParser_InitializedAttributedPropertyDeclaration() AssertNode(name, "MyProperty", 14, 11); AssertNode(init, "\"Test\"", 27, 6); AssertNode(att1, "MarkupOptions.AllowHardCodedValue = False", 35, 41); - AssertNode(att2, "MarkupOptions.Required = True", 77, 30); + AssertNode(att2, "MarkupOptions.Required = True", 78, 29); } [TestMethod] @@ -1338,7 +1367,7 @@ public void BindingParser_AttributedPropertyDeclaration() AssertNode(type, "System.String", 0, 14); AssertNode(name, "MyProperty", 14, 10); AssertNode(att1, "MarkupOptions.AllowHardCodedValue = False", 26, 41); - AssertNode(att2, "MarkupOptions.Required = True", 68, 30); + AssertNode(att2, "MarkupOptions.Required = True", 69, 29); } [TestMethod] @@ -1373,7 +1402,7 @@ public void BindingParser_AttributedArrayInitializedPropertyDeclaration() AssertNode(element3Initializer, "Namespace.Enum.Value3", 78, 22); AssertNode(att1, "MarkupOptions.AllowHardCodedValue = False", 103, 41); - AssertNode(att2, "MarkupOptions.Required = True", 145, 30); + AssertNode(att2, "MarkupOptions.Required = True", 146, 29); } [TestMethod] From 62ed2d5ade976df9f6e69f8bb858af38e47a5997 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milan=20Miku=C5=A1?= Date: Fri, 10 Nov 2023 23:19:15 +0100 Subject: [PATCH 3/6] Fixed missing ranges for Qualified Assembly node --- .../Parser/Binding/Parser/BindingParser.cs | 2 +- .../Parser/Binding/BindingParserTests.cs | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/Framework/Framework/Compilation/Parser/Binding/Parser/BindingParser.cs b/src/Framework/Framework/Compilation/Parser/Binding/Parser/BindingParser.cs index dced95e687..14b9c58423 100644 --- a/src/Framework/Framework/Compilation/Parser/Binding/Parser/BindingParser.cs +++ b/src/Framework/Framework/Compilation/Parser/Binding/Parser/BindingParser.cs @@ -131,7 +131,7 @@ public BindingParserNode ReadDirectiveTypeName() Read(); var assemblyName = ReadAssemblyName(); - return new AssemblyQualifiedNameBindingParserNode(typeName, assemblyName); //TODO: create node + return CreateNode(new AssemblyQualifiedNameBindingParserNode(typeName, assemblyName), startIndex); } else if (Peek() is BindingToken token) { diff --git a/src/Tests/Parser/Binding/BindingParserTests.cs b/src/Tests/Parser/Binding/BindingParserTests.cs index 61dd5643d2..33a5bbff5f 100644 --- a/src/Tests/Parser/Binding/BindingParserTests.cs +++ b/src/Tests/Parser/Binding/BindingParserTests.cs @@ -830,20 +830,23 @@ public void BindingParser_GenericExpression_MemberAccessInsteadOfType_Invalid() } [TestMethod] - [DataRow("Domain.Company.Product.DotVVM.Feature.Type[], Domain.Company.Product")] - [DataRow("Domain.Company.Product.DotVVM.Feature.Type[], Product")] - [DataRow("Domain.Company.Product.DotVVM.Feature.Type[], Domain.Company.Product")] - [DataRow("Domain.Company.Product.DotVVM.Feature.Type[], Product")] - public void BindingParser_ArrayType_AssemblyQualifiedName_ValidAssemblyName(string binding) + [DataRow("Domain.Company.Product.DotVVM.Feature.Type[], Domain.Company.Product", "Domain.Company.Product.DotVVM.Feature.Type[]")] + [DataRow("Domain.Company.Product.DotVVM.Feature.Type[], Product", "Domain.Company.Product.DotVVM.Feature.Type[]")] + [DataRow("Domain.Company.Product.DotVVM.Feature.Type[], Domain.Company.Product", "Domain.Company.Product.DotVVM.Feature.Type[]")] + [DataRow("Domain.Company.Product.DotVVM.Feature.Type[], Product", "Domain.Company.Product.DotVVM.Feature.Type[]")] + public void BindingParser_ArrayType_AssemblyQualifiedName_ValidAssemblyName(string binding, string type) { var parser = bindingParserNodeFactory.SetupParser(binding); var node = parser.ReadDirectiveTypeName() as AssemblyQualifiedNameBindingParserNode; Assert.IsNotNull(node, "expected qualified name node."); + AssertNode(node, binding, 0, binding.Length); var array = node.TypeName as ArrayTypeReferenceBindingParserNode; Assert.IsNotNull(array, "Expected array type reference"); Assert.IsFalse(node.AssemblyName.HasNodeErrors); + + AssertNode(array, type, 0, type.Length); } [TestMethod] @@ -973,7 +976,7 @@ public void BindingParser_Lambda_WithTypeInfo_SingleParameter(string expr, strin Assert.AreEqual(1, parameters.Count); - AssertNode(parameters[0].Type, type, 1, type.Length+1); + AssertNode(parameters[0].Type, type, 1, type.Length + 1); AssertNode(parameters[0].Name, "arg", type.Length + 2, 3); AssertNode(body, "Method(arg)", type.Length + 10, 11); } @@ -1438,7 +1441,7 @@ public void BindingParser_GenericMethodCall_SimpleName() var typeArgument = generic.TypeArguments[0].As(); AssertNode(root, source, 0, source.Length); - AssertNode(generic, "GetType", 0, source.Length-12); + AssertNode(generic, "GetType", 0, source.Length - 12); AssertNode(typeArgument, "string", 8, 6); } @@ -1455,7 +1458,7 @@ public void BindingParser_GenericMethodCall_MemberAccessName() var typeArgument = generic.TypeArguments[0].As(); AssertNode(root, source, 0, source.Length); - AssertNode(memberAccess, "service.GetType", 0, source.Length-12); + AssertNode(memberAccess, "service.GetType", 0, source.Length - 12); AssertNode(generic, "GetType", 8, source.Length - 20); AssertNode(typeArgument, "string?", 16, 7); From 0d20edb99c4d0f16a789e885d1fb7668037c5c38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milan=20Miku=C5=A1?= Date: Thu, 7 Dec 2023 18:02:12 +0100 Subject: [PATCH 4/6] Correct ranges for assembly name node assigned in the parser. --- .../Parser/Binding/Parser/BindingParser.cs | 7 ++- .../Parser/Binding/BindingParserTests.cs | 52 ++++++++++--------- 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/src/Framework/Framework/Compilation/Parser/Binding/Parser/BindingParser.cs b/src/Framework/Framework/Compilation/Parser/Binding/Parser/BindingParser.cs index 14b9c58423..c9347fcbb5 100644 --- a/src/Framework/Framework/Compilation/Parser/Binding/Parser/BindingParser.cs +++ b/src/Framework/Framework/Compilation/Parser/Binding/Parser/BindingParser.cs @@ -144,13 +144,16 @@ public AssemblyNameBindingParserNode ReadAssemblyName() { // almost anything can be an assembly name, so we just read everything until the end SkipWhiteSpace(); - var tokens = this.Tokens.Skip(CurrentIndex).ToList(); + + var startIndex = CurrentIndex; + + var tokens = Tokens.Skip(CurrentIndex).ToList(); CurrentIndex = Tokens.Count; while (tokens.Count > 0 && tokens[tokens.Count - 1].Type == BindingTokenType.WhiteSpace) tokens.RemoveAt(tokens.Count - 1); - var node = new AssemblyNameBindingParserNode(tokens); + var node = CreateNode(new AssemblyNameBindingParserNode(tokens), startIndex); if (node.Name.Length == 0) { node.NodeErrors.Add("Assembly name cannot be empty."); diff --git a/src/Tests/Parser/Binding/BindingParserTests.cs b/src/Tests/Parser/Binding/BindingParserTests.cs index 33a5bbff5f..6d96e5ff00 100644 --- a/src/Tests/Parser/Binding/BindingParserTests.cs +++ b/src/Tests/Parser/Binding/BindingParserTests.cs @@ -1,16 +1,8 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; +using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; -using DotVVM.Framework.Compilation.Parser; using DotVVM.Framework.Compilation.Parser.Binding.Parser; using DotVVM.Framework.Compilation.Parser.Binding.Tokenizer; -using DotVVM.Framework.Controls; using Microsoft.VisualStudio.TestTools.UnitTesting; -using BindingParser = DotVVM.Framework.Compilation.Parser.Binding.Parser.BindingParser; -using a = System.Collections.Generic.Dictionary.ValueCollection; using DotVVM.Framework.Utils; namespace DotVVM.Framework.Tests.Parser.Binding @@ -830,23 +822,26 @@ public void BindingParser_GenericExpression_MemberAccessInsteadOfType_Invalid() } [TestMethod] - [DataRow("Domain.Company.Product.DotVVM.Feature.Type[], Domain.Company.Product", "Domain.Company.Product.DotVVM.Feature.Type[]")] - [DataRow("Domain.Company.Product.DotVVM.Feature.Type[], Product", "Domain.Company.Product.DotVVM.Feature.Type[]")] - [DataRow("Domain.Company.Product.DotVVM.Feature.Type[], Domain.Company.Product", "Domain.Company.Product.DotVVM.Feature.Type[]")] - [DataRow("Domain.Company.Product.DotVVM.Feature.Type[], Product", "Domain.Company.Product.DotVVM.Feature.Type[]")] - public void BindingParser_ArrayType_AssemblyQualifiedName_ValidAssemblyName(string binding, string type) + [DataRow("Domain.Company.Product.DotVVM.Feature.Type[], Domain.Company.Product", "Domain.Company.Product.DotVVM.Feature.Type[]", "Domain.Company.Product")] + [DataRow("Domain.Company.Product.DotVVM.Feature.Type[], Product", "Domain.Company.Product.DotVVM.Feature.Type[]", "Product")] + [DataRow("Domain.Company.Product.DotVVM.Feature.Type[], Domain.Company.Product", "Domain.Company.Product.DotVVM.Feature.Type[]", "Domain.Company.Product")] + [DataRow("Domain.Company.Product.DotVVM.Feature.Type[], Product", "Domain.Company.Product.DotVVM.Feature.Type[]", "Product")] + public void BindingParser_ArrayType_AssemblyQualifiedName_ValidAssemblyName(string binding, string type, string assembly) { var parser = bindingParserNodeFactory.SetupParser(binding); var node = parser.ReadDirectiveTypeName() as AssemblyQualifiedNameBindingParserNode; + Assert.IsNotNull(node, "expected qualified name node."); AssertNode(node, binding, 0, binding.Length); - var array = node.TypeName as ArrayTypeReferenceBindingParserNode; + var arrayNode = node.TypeName as ArrayTypeReferenceBindingParserNode; + var assemblyNode = node.AssemblyName; - Assert.IsNotNull(array, "Expected array type reference"); - Assert.IsFalse(node.AssemblyName.HasNodeErrors); + Assert.IsNotNull(assemblyNode, "expected assembly name node."); + AssertNode(assemblyNode, assembly, type.Length + 2, assembly.Length); - AssertNode(array, type, 0, type.Length); + Assert.IsNotNull(arrayNode, "Expected array type reference"); + AssertNode(arrayNode, type, 0, type.Length); } [TestMethod] @@ -1222,7 +1217,7 @@ public void BindingParser_PropertyHalfWrittenAttributes() Assert.AreEqual(4, root.Attributes.Count); var emptyAttribute = root.Attributes[0].CastTo(); - AssertNode(emptyAttribute, "", 19, 0); + AssertNode(emptyAttribute, "", 19, 0, hasErrors:true); var dotvvmNode = root.Attributes[1].CastTo(); AssertNode(dotvvmNode, "DotVVM.", 21, 7); @@ -1248,7 +1243,7 @@ public void BindingParser_PropertyTypeHalfWritten() AssertNode(type, "System.", 0, 7); AssertNode(target, "System", 0, 6); - AssertNode(name, "", 7, 0); + AssertNode(name, "", 7, 0, hasErrors: true); } [TestMethod] @@ -1488,11 +1483,20 @@ public void BindingParser_GenericMethodCall_MultipleGenericArguments() AssertNode(typeArgument2, "System.String", 29, 13); } - private static void AssertNode(BindingParserNode elementType, string expectedDisplayString, int start, int length) + private static void AssertNode(BindingParserNode node, string expectedDisplayString, int start, int length, bool hasErrors = false) { - Assert.AreEqual(expectedDisplayString, elementType.ToDisplayString(), $"Node {elementType.GetType().Name}: display string incorrect."); - Assert.AreEqual(start, elementType.StartPosition, $"Node {elementType.GetType().Name}: Start position incorrect."); - Assert.AreEqual(length, elementType.Length, $"Node {elementType.GetType().Name}: Length incorrect."); + Assert.AreEqual(expectedDisplayString, node.ToDisplayString(), $"Node {node.GetType().Name}: display string incorrect."); + Assert.AreEqual(start, node.StartPosition, $"Node {node.GetType().Name}: Start position incorrect."); + Assert.AreEqual(length, node.Length, $"Node {node.GetType().Name}: Length incorrect."); + + if (hasErrors) + { + Assert.IsTrue(node.HasNodeErrors); + } + else + { + Assert.IsFalse (node.HasNodeErrors); + } } private static string SkipWhitespaces(string str) => string.Join("", str.Where(c => !char.IsWhiteSpace(c))); From e2f2f671dbe25e768dc3b96924d30c4840f1f7bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milan=20Miku=C5=A1?= Date: Thu, 7 Dec 2023 18:03:32 +0100 Subject: [PATCH 5/6] Resolving attributes for markup properties does not fail anymore on assigment into attributes if the assigned value is of incorrect type. --- .../ResolvedPropertyDeclarationDirective.cs | 13 +++++++++--- .../PropertyDirectiveTests.cs | 20 +++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/Framework/Framework/Compilation/ControlTree/Resolved/ResolvedPropertyDeclarationDirective.cs b/src/Framework/Framework/Compilation/ControlTree/Resolved/ResolvedPropertyDeclarationDirective.cs index 978a5f1cf9..33212d3832 100644 --- a/src/Framework/Framework/Compilation/ControlTree/Resolved/ResolvedPropertyDeclarationDirective.cs +++ b/src/Framework/Framework/Compilation/ControlTree/Resolved/ResolvedPropertyDeclarationDirective.cs @@ -75,7 +75,7 @@ private IEnumerable InstantiateAttributes(DothtmlDirectiveNode dothtmlDi if (attributeInstance is null) { - dothtmlDirective.AddError($"Could not create insstance of the attribute {attribute.attributeType}."); + dothtmlDirective.AddError($"Could not create instance of the attribute {attribute.attributeType}."); continue; } @@ -85,11 +85,18 @@ private IEnumerable InstantiateAttributes(DothtmlDirectiveNode dothtmlDi if (reflectedProperty is null) { - dothtmlDirective.AddError($"Could not find property {property.name} insstance of the attribute {attribute.attributeType}."); + dothtmlDirective.AddError($"Could not find property {property.name} instance of the attribute {attribute.attributeType}."); continue; } - reflectedProperty.SetValue(attributeInstance, property.value); + try + { + reflectedProperty.SetValue(attributeInstance, property.value); + } + catch (Exception ex) + { + dothtmlDirective.AddError($"Cannot assign {reflectedProperty.Name} of attribute {attribute.attributeType.FullName}: {ex.Message}"); + } } yield return attributeInstance; } diff --git a/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/PropertyDirectiveTests.cs b/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/PropertyDirectiveTests.cs index f65debc361..11df3e0b39 100644 --- a/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/PropertyDirectiveTests.cs +++ b/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/PropertyDirectiveTests.cs @@ -23,5 +23,25 @@ @viewModel object Assert.AreEqual(4, property.Attributes.Count); } + + [TestMethod] + public void ResolvedTree_PropertyDirectiveIncompatibleType_ErrorReported() + { + var root = ParseSource(@$" +@viewModel object +@property string MyProperty, DotVVM.Framework.Controls.MarkupOptionsAttribute.Required = 1"); + + var property = root.Directives["property"].SingleOrDefault() as IAbstractPropertyDeclarationDirective; + + Assert.AreEqual("System.String", property.PropertyType.FullName); + Assert.AreEqual("MyProperty", property.NameSyntax.Name); + + Assert.AreEqual(1, property.Attributes.Count); + Assert.AreEqual(1, property.DothtmlNode.NodeErrors.Count()); + + var error = property.DothtmlNode.NodeErrors.First(); + + Assert.IsTrue(error.Contains("Cannot assign"), "Expected error about failed assignment into the attribute property."); + } } } From 85d2ff4d1c0e4fedfa90334d3fd31340422b3978 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milan=20Miku=C5=A1?= Date: Thu, 7 Dec 2023 22:35:11 +0100 Subject: [PATCH 6/6] Trailing whitespaces discarded from assembly name. Improved AssemblyQualifiedName tests to test ranges. --- .../Binding/Parser/AssemblyNameBindingParserNode.cs | 10 +++------- .../Parser/Binding/Parser/BindingParser.cs | 8 ++++++-- src/Tests/Parser/Binding/BindingParserTests.cs | 12 +++++++++--- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/Framework/Framework/Compilation/Parser/Binding/Parser/AssemblyNameBindingParserNode.cs b/src/Framework/Framework/Compilation/Parser/Binding/Parser/AssemblyNameBindingParserNode.cs index 781cb28972..5eeb249911 100644 --- a/src/Framework/Framework/Compilation/Parser/Binding/Parser/AssemblyNameBindingParserNode.cs +++ b/src/Framework/Framework/Compilation/Parser/Binding/Parser/AssemblyNameBindingParserNode.cs @@ -12,15 +12,11 @@ namespace DotVVM.Framework.Compilation.Parser.Binding.Parser public class AssemblyNameBindingParserNode : BindingParserNode { public string Name { get; } - public AssemblyNameBindingParserNode(List tokens) - { - Tokens = tokens; - Name = string.Concat(tokens.Select(t => t.Text)); - } public AssemblyNameBindingParserNode(string name) - : this(new List() { new BindingToken(name, BindingTokenType.Identifier, 0, 0, 0, 0)}) - { } + { + Name = name; + } public override IEnumerable EnumerateChildNodes() => Enumerable.Empty(); diff --git a/src/Framework/Framework/Compilation/Parser/Binding/Parser/BindingParser.cs b/src/Framework/Framework/Compilation/Parser/Binding/Parser/BindingParser.cs index c9347fcbb5..d975a4f5c2 100644 --- a/src/Framework/Framework/Compilation/Parser/Binding/Parser/BindingParser.cs +++ b/src/Framework/Framework/Compilation/Parser/Binding/Parser/BindingParser.cs @@ -148,12 +148,16 @@ public AssemblyNameBindingParserNode ReadAssemblyName() var startIndex = CurrentIndex; var tokens = Tokens.Skip(CurrentIndex).ToList(); - CurrentIndex = Tokens.Count; while (tokens.Count > 0 && tokens[tokens.Count - 1].Type == BindingTokenType.WhiteSpace) tokens.RemoveAt(tokens.Count - 1); - var node = CreateNode(new AssemblyNameBindingParserNode(tokens), startIndex); + CurrentIndex += tokens.Count; + var name = string.Concat(tokens.Select(t => t.Text)); + + var node = CreateNode(new AssemblyNameBindingParserNode(name), startIndex); + CurrentIndex = Tokens.Count; + if (node.Name.Length == 0) { node.NodeErrors.Add("Assembly name cannot be empty."); diff --git a/src/Tests/Parser/Binding/BindingParserTests.cs b/src/Tests/Parser/Binding/BindingParserTests.cs index 6d96e5ff00..65413f5612 100644 --- a/src/Tests/Parser/Binding/BindingParserTests.cs +++ b/src/Tests/Parser/Binding/BindingParserTests.cs @@ -864,13 +864,19 @@ public void BindingParser_ArrayType_ValidAssemblyName(string binding) [DataRow("Domain.Company.Product.DotVVM.Feature.Type, Domain.Company.Product", "Domain.Company.Product")] [DataRow("Domain.Company.Product.DotVVM.Feature.Type, Domain.Company.Product", "Domain.Company.Product")] [DataRow("Domain.Company.Product.DotVVM.Feature.Type, Product", "Product")] + [DataRow("Domain.Company.Product.DotVVM.Feature.Type, Product ", "Product")] + [DataRow("Domain.Company.Product.DotVVM.Feature.Type, my assembly name ", "my assembly name")] public void BindingParser_AssemblyQualifiedName_ValidAssemblyName(string binding, string assemblyName) { var parser = bindingParserNodeFactory.SetupParser(binding); var node = parser.ReadDirectiveTypeName() as AssemblyQualifiedNameBindingParserNode; + + var controlSting = binding.TrimEnd(); + Assert.IsFalse(node.AssemblyName.HasNodeErrors); - Assert.AreEqual(assemblyName, node.AssemblyName.ToDisplayString()); + AssertNode(node, controlSting, 0, binding.Length); + AssertNode(node.AssemblyName, assemblyName, 44, assemblyName.Length); } [TestMethod] @@ -1217,7 +1223,7 @@ public void BindingParser_PropertyHalfWrittenAttributes() Assert.AreEqual(4, root.Attributes.Count); var emptyAttribute = root.Attributes[0].CastTo(); - AssertNode(emptyAttribute, "", 19, 0, hasErrors:true); + AssertNode(emptyAttribute, "", 19, 0, hasErrors: true); var dotvvmNode = root.Attributes[1].CastTo(); AssertNode(dotvvmNode, "DotVVM.", 21, 7); @@ -1495,7 +1501,7 @@ private static void AssertNode(BindingParserNode node, string expectedDisplayStr } else { - Assert.IsFalse (node.HasNodeErrors); + Assert.IsFalse(node.HasNodeErrors); } }