diff --git a/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs b/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs index 2e135fcd03..0ff543d36f 100644 --- a/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs +++ b/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs @@ -454,12 +454,18 @@ attribute.ValueNode is DothtmlValueBindingNode binding && { var pGroup = groupedProperty.PropertyGroup; var name = groupedProperty.GroupMemberName; - if (pGroup.Name.EndsWith("Attributes") && name.ToLowerInvariant() != name) + var prefix = attribute.AttributeName.Substring(0, attribute.AttributeName.Length - name.Length); + // If the HTML attribute is used with a prefix such as `Item`, it might be clearer if the first character is uppercased + // e.g. ItemClass reads better than Itemclass + // we supress the warning in such case + var allowFirstCharacterUppercase = prefix.Length > 0 && char.IsLetter(prefix[prefix.Length - 1]); + if (pGroup.Name.EndsWith("Attributes") && + name.Substring(allowFirstCharacterUppercase ? 1 : 0).ToLowerInvariant() != name.Substring(allowFirstCharacterUppercase ? 1 : 0)) { // properties with at most two typos var similarNameProperties = control.Metadata.AllProperties - .Where(p => StringSimilarity.DamerauLevenshteinDistance(p.Name.ToLowerInvariant(), name.ToLowerInvariant()) <= 2) + .Where(p => StringSimilarity.DamerauLevenshteinDistance(p.Name.ToLowerInvariant(), (prefix + name).ToLowerInvariant()) <= 2) .Select(p => p.Name) .ToArray(); var similarPropertyHelp = diff --git a/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTests.cs b/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTests.cs index 6bd475a3ff..95b960469f 100644 --- a/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTests.cs +++ b/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTests.cs @@ -767,6 +767,25 @@ @viewModel System.Boolean Assert.AreEqual("HTML attribute name 'Visble' should not contain uppercase letters. Did you mean Visible, or another DotVVM property?", attribute3.AttributeNameNode.NodeWarnings.Single()); } + [TestMethod] + public void DefaultViewCompiler_NonExistenPropertyWarning_PrefixedGroup() + { + var markup = $@" +@viewModel System.Boolean + +"; + var repeater = ParseSource(markup) + .Content.SelectRecursively(c => c.Content) + .Single(c => c.Metadata.Type == typeof(HierarchyRepeater)); + + var elementNode = (DothtmlElementNode)repeater.DothtmlNode; + var attribute1 = elementNode.Attributes.Single(a => a.AttributeName == "ItemClass"); + var attribute2 = elementNode.Attributes.Single(a => a.AttributeName == "ItemIncludeInPage"); + + Assert.AreEqual(0, attribute1.AttributeNameNode.NodeWarnings.Count(), attribute1.AttributeNameNode.NodeWarnings.StringJoin(", ")); + Assert.AreEqual("HTML attribute name 'IncludeInPage' should not contain uppercase letters. Did you intent to use a DotVVM property instead?", XAssert.Single(attribute2.AttributeNameNode.NodeWarnings)); + } + [TestMethod] public void DefaultViewCompiler_UnsupportedCallSite_ResourceBinding_Warning() {