diff --git a/src/Framework/Framework/Compilation/ControlTree/ControlResolverBase.cs b/src/Framework/Framework/Compilation/ControlTree/ControlResolverBase.cs index 336ad58d11..92dee9430c 100644 --- a/src/Framework/Framework/Compilation/ControlTree/ControlResolverBase.cs +++ b/src/Framework/Framework/Compilation/ControlTree/ControlResolverBase.cs @@ -202,7 +202,7 @@ public IControlResolverMetadata ResolveControl(IControlType controlType) /// Returns a list of possible DotVVM controls. /// Used only for smart error handling, the list isn't necessarily complete, but doesn't contain false positives. - public abstract IEnumerable<(string tagPrefix, IControlType type)> EnumerateControlTypes(); + public abstract IEnumerable<(string tagPrefix, string? tagName, IControlType type)> EnumerateControlTypes(); /// /// Gets the control metadata. diff --git a/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs b/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs index 759ce41b2a..59d0507420 100644 --- a/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs +++ b/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs @@ -265,7 +265,7 @@ private IAbstractControl ProcessObjectElement(DothtmlElementNode element, IDataC var similarNameHelp = similarControls.Any() ? $" Did you mean {string.Join(", ", similarControls.Select(c => c.tagPrefix + ":" + c.name))}, or other DotVVM control?" : ""; var tagPrefixHelp = configuration.Markup.Controls.Any(c => string.Equals(c.TagPrefix, element.TagPrefix, StringComparison.OrdinalIgnoreCase)) ? "" - : $" {(similarNameHelp is null ? "Make" : "Otherwise, make")} sure that the tagPrefix '{element.TagPrefix}' is registered in DotvvmConfiguration.Markup.Controls collection!"; + : $" {(similarNameHelp is "" ? "Make" : "Otherwise, make")} sure that the tagPrefix '{element.TagPrefix}' is registered in DotvvmConfiguration.Markup.Controls collection!"; element.TagNameNode.AddError($"The control <{element.FullTagName}> could not be resolved!{similarNameHelp}{tagPrefixHelp}"); } if (controlMetadata.VirtualPath is {} && controlMetadata.Type.IsAssignableTo(ResolvedTypeDescriptor.Create(typeof(DotvvmView)))) @@ -512,10 +512,10 @@ from c in this.controlResolver.EnumerateControlTypes() where controlBaseType is null || c.type.Type.IsAssignableTo(controlBaseType) let prefixScore = tagPrefix is null ? 0 : StringSimilarity.DamerauLevenshteinDistance(c.tagPrefix, tagPrefix) where prefixScore <= threshold - from controlName in Enumerable.Concat([ c.type.Type.Name ], c.type.AlternativeNames) + from controlName in Enumerable.Concat([ c.tagName ?? c.type.PrimaryName ], c.type.AlternativeNames) let nameScore = StringSimilarity.DamerauLevenshteinDistance(elementName.ToLowerInvariant(), controlName.ToLowerInvariant()) where prefixScore + nameScore <= threshold - orderby prefixScore + nameScore descending + orderby (prefixScore + nameScore, controlName, c.tagPrefix) descending select (c.tagPrefix, controlName, c.type) ).Take(limit).ToArray(); } diff --git a/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs b/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs index 2274fb4469..7405493110 100644 --- a/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs +++ b/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs @@ -300,7 +300,7 @@ public override IControlResolverMetadata BuildControlMetadata(IControlType type) return new ControlResolverMetadata((ControlType)type); } - public override IEnumerable<(string tagPrefix, IControlType type)> EnumerateControlTypes() + public override IEnumerable<(string tagPrefix, string? tagName, IControlType type)> EnumerateControlTypes() { var markupControls = new HashSet<(string, string)>(); // don't report MarkupControl with @baseType twice @@ -308,8 +308,15 @@ public override IControlResolverMetadata BuildControlMetadata(IControlType type) { if (!string.IsNullOrEmpty(control.Src)) { - var markupControl = FindMarkupControl(control.Src); markupControls.Add((control.TagPrefix!, control.TagName!)); + IControlType? markupControl = null; + try + { + markupControl = FindMarkupControl(control.Src); + } + catch { } // ignore the error, we should not crash here + if (markupControl != null) + yield return (control.TagPrefix!, control.TagName, markupControl); } } @@ -327,7 +334,8 @@ type.DeclaringType is null && typeof(DotvvmBindableObject).IsAssignableFrom(type) && namespaces.TryGetValue(type.Namespace ?? "", out var controlConfig)) { - yield return (controlConfig.TagPrefix!, new ControlType(type)); + if (!markupControls.Contains((controlConfig.TagPrefix!, type.Name))) + yield return (controlConfig.TagPrefix!, null, new ControlType(type)); } } } diff --git a/src/Framework/Framework/Compilation/ControlTree/IControlResolver.cs b/src/Framework/Framework/Compilation/ControlTree/IControlResolver.cs index 130e27b071..fb2d92c6ff 100644 --- a/src/Framework/Framework/Compilation/ControlTree/IControlResolver.cs +++ b/src/Framework/Framework/Compilation/ControlTree/IControlResolver.cs @@ -29,7 +29,7 @@ public interface IControlResolver /// Returns a list of possible DotVVM controls. /// Used only for smart error handling, the list isn't necessarily complete, but doesn't contain false positives. - IEnumerable<(string tagPrefix, IControlType type)> EnumerateControlTypes(); + IEnumerable<(string tagPrefix, string? tagName, IControlType type)> EnumerateControlTypes(); /// /// Resolves the binding type. diff --git a/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTests.cs b/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTests.cs index 70ba70ea53..dabdab4307 100644 --- a/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTests.cs +++ b/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTests.cs @@ -218,6 +218,22 @@ public void ResolvedTree_UnknownPrefix() Assert.AreEqual("The control could not be resolved! Did you mean dot:Button, or other DotVVM control? Otherwise, make sure that the tagPrefix 'bp' is registered in DotvvmConfiguration.Markup.Controls collection!", XAssert.Single(node.NodeErrors)); } + + [TestMethod] + public void ResolvedTree_SimilarToMarkupControl() + { + var root = ParseSource(@"@viewModel string + + +"); + + var controls = root.Content.Where(c => !c.IsOnlyWhitespace()).ToArray(); + var node = (controls[0].DothtmlNode as DothtmlElementNode).TagNameNode; + Assert.AreEqual("The control could not be resolved! Did you mean cmc:ControlWithPropertyDirective, or other DotVVM control?", XAssert.Single(node.NodeErrors)); + node = (controls[1].DothtmlNode as DothtmlElementNode).TagNameNode; + Assert.AreEqual("The control could not be resolved! Did you mean cmc:ControlWithBaseType, or other DotVVM control?", XAssert.Single(node.NodeErrors)); + } + [TestMethod] public void ResolvedTree_ElementProperty() { diff --git a/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTestsBase.cs b/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTestsBase.cs index 29c08ac877..f1998d7fb6 100644 --- a/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTestsBase.cs +++ b/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTestsBase.cs @@ -1,7 +1,11 @@ -using DotVVM.Framework.Compilation.ControlTree.Resolved; +using DotVVM.Framework.Binding; +using DotVVM.Framework.Compilation.ControlTree.Resolved; using DotVVM.Framework.Configuration; +using DotVVM.Framework.Controls; +using DotVVM.Framework.Hosting; using DotVVM.Framework.Testing; using DotVVM.Framework.Tests.Runtime.ControlTree.DefaultControlTreeResolver; +using Microsoft.Extensions.DependencyInjection; namespace DotVVM.Framework.Tests.Runtime.ControlTree { @@ -11,13 +15,43 @@ public abstract class DefaultControlTreeResolverTestsBase static DefaultControlTreeResolverTestsBase() { - configuration = DotvvmTestHelper.CreateConfiguration(); + var fakeMarkupFileLoader = new FakeMarkupFileLoader() { + MarkupFiles = { + ["ControlWithBaseType.dotcontrol"] = """ + @viewModel object + @baseType DotVVM.Framework.Tests.Runtime.ControlTree.DefaultControlTreeResolverTestsBase.TestMarkupControl1 + + {{value: Text}} + """, + ["ControlWithPropertyDirective.dotcontrol"] = """ + @viewModel object + @property string Text + + {{value: Text}} + """ + } + }; + configuration = DotvvmTestHelper.CreateConfiguration(s => { + s.AddSingleton(fakeMarkupFileLoader); + }); configuration.Markup.AddCodeControls("cc", typeof(ClassWithInnerElementProperty)); + configuration.Markup.AddMarkupControl("cmc", "ControlWithBaseType", "ControlWithBaseType.dotcontrol"); + configuration.Markup.AddMarkupControl("cmc", "ControlWithPropertyDirective", "ControlWithPropertyDirective.dotcontrol"); configuration.Freeze(); } protected ResolvedTreeRoot ParseSource(string markup, string fileName = "default.dothtml", bool checkErrors = false) => DotvvmTestHelper.ParseResolvedTree(markup, fileName, configuration, checkErrors); + public class TestMarkupControl1: DotvvmMarkupControl + { + public string Text + { + get { return (string)GetValue(TextProperty); } + set { SetValue(TextProperty, value); } + } + public static readonly DotvvmProperty TextProperty = + DotvvmProperty.Register(nameof(Text)); + } } }