diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 56b7b42208..cc0bba6d99 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -20,7 +20,7 @@ - 11.0 + 12.0 $(NoWarn);CS1591;CS1573 true diff --git a/src/Framework/Framework/Compilation/ControlTree/ControlResolverBase.cs b/src/Framework/Framework/Compilation/ControlTree/ControlResolverBase.cs index 396ba9ccc8..92dee9430c 100644 --- a/src/Framework/Framework/Compilation/ControlTree/ControlResolverBase.cs +++ b/src/Framework/Framework/Compilation/ControlTree/ControlResolverBase.cs @@ -200,6 +200,10 @@ public IControlResolverMetadata ResolveControl(IControlType controlType) /// protected abstract IControlType FindMarkupControl(string file); + /// 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, string? tagName, IControlType type)> EnumerateControlTypes(); + /// /// Gets the control metadata. /// diff --git a/src/Framework/Framework/Compilation/ControlTree/ControlTreeHelper.cs b/src/Framework/Framework/Compilation/ControlTree/ControlTreeHelper.cs index 98b2d7b4b1..a1726d4549 100644 --- a/src/Framework/Framework/Compilation/ControlTree/ControlTreeHelper.cs +++ b/src/Framework/Framework/Compilation/ControlTree/ControlTreeHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using DotVVM.Framework.Compilation.Parser.Dothtml.Parser; @@ -7,7 +7,7 @@ namespace DotVVM.Framework.Compilation.ControlTree public static class ControlTreeHelper { public static bool HasEmptyContent(this IAbstractControl control) - => control.Content.All(c => !DothtmlNodeHelper.IsNotEmpty(c.DothtmlNode)); // allow only whitespace literals + => control.Content.All(c => DothtmlNodeHelper.IsEmpty(c.DothtmlNode)); // allow only whitespace literals public static bool HasProperty(this IAbstractControl control, IPropertyDescriptor property) { diff --git a/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs b/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs index ae1474660f..59d0507420 100644 --- a/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs +++ b/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs @@ -13,6 +13,7 @@ using DotVVM.Framework.Compilation.Directives; using DotVVM.Framework.Compilation.Binding; using DotVVM.Framework.Compilation.ViewCompiler; +using DotVVM.Framework.Configuration; namespace DotVVM.Framework.Compilation.ControlTree { @@ -24,6 +25,7 @@ public abstract class ControlTreeResolverBase : IControlTreeResolver protected readonly IControlResolver controlResolver; protected readonly IAbstractTreeBuilder treeBuilder; private readonly IMarkupDirectiveCompilerPipeline markupDirectiveCompilerPipeline; + protected readonly DotvvmConfiguration configuration; protected Lazy rawLiteralMetadata; protected Lazy literalMetadata; protected Lazy placeholderMetadata; @@ -31,11 +33,12 @@ public abstract class ControlTreeResolverBase : IControlTreeResolver /// /// Initializes a new instance of the class. /// - public ControlTreeResolverBase(IControlResolver controlResolver, IAbstractTreeBuilder treeBuilder, IMarkupDirectiveCompilerPipeline markupDirectiveCompilerPipeline) + public ControlTreeResolverBase(IControlResolver controlResolver, IAbstractTreeBuilder treeBuilder, IMarkupDirectiveCompilerPipeline markupDirectiveCompilerPipeline, DotvvmConfiguration configuration) { this.controlResolver = controlResolver; this.treeBuilder = treeBuilder; this.markupDirectiveCompilerPipeline = markupDirectiveCompilerPipeline; + this.configuration = configuration; rawLiteralMetadata = new Lazy(() => controlResolver.ResolveControl(new ResolvedTypeDescriptor(typeof(RawLiteral)))); literalMetadata = new Lazy(() => controlResolver.ResolveControl(new ResolvedTypeDescriptor(typeof(Literal)))); placeholderMetadata = new Lazy(() => controlResolver.ResolveControl(new ResolvedTypeDescriptor(typeof(PlaceHolder)))); @@ -258,7 +261,12 @@ private IAbstractControl ProcessObjectElement(DothtmlElementNode element, IDataC { controlMetadata = controlResolver.ResolveControl("", element.TagName, out constructorParameters).NotNull(); constructorParameters = new[] { element.FullTagName }; - element.AddError($"The control <{element.FullTagName}> could not be resolved! Make sure that the tagPrefix is registered in DotvvmConfiguration.Markup.Controls collection!"); + var similarControls = FindSimilarControls(element.TagPrefix, element.TagName, controlBaseType: controlMetadata.Type); + 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 "" ? "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)))) { @@ -288,7 +296,7 @@ private IAbstractControl ProcessObjectElement(DothtmlElementNode element, IDataC } if (controlMetadata.DataContextConstraint != null && dataContext != null && !controlMetadata.DataContextConstraint.IsAssignableFrom(dataContext.DataContextType)) { - ((DothtmlNode?)dataContextAttribute ?? element) + ((DothtmlNode?)dataContextAttribute ?? element.TagNameNode) .AddError($"The control '{controlMetadata.Type.CSharpName}' requires a DataContext of type '{controlMetadata.DataContextConstraint.CSharpFullName}'!"); } @@ -476,7 +484,7 @@ private static void AddHtmlAttributeWarning(IAbstractControl control, DothtmlAtt // Ignore SVG attributes (unless they also start with an uppercase letter) - if ((allowFirstCharacterUppercase || !char.IsUpper(name[0])) && uppercaseHtmlAttributeList.Contains(name)) + if ((allowFirstCharacterUppercase || !char.IsUpper(name, 0)) && uppercaseHtmlAttributeList.Contains(name)) return; if (pGroup.Name.EndsWith("Attributes") && @@ -486,7 +494,8 @@ private static void AddHtmlAttributeWarning(IAbstractControl control, DothtmlAtt var similarNameProperties = control.Metadata.AllProperties .Where(p => StringSimilarity.DamerauLevenshteinDistance(p.Name.ToLowerInvariant(), (prefix + name).ToLowerInvariant()) <= 2) - .Select(p => p.Name) + // suggest the alias if the property is obsolete + .Select(p => p.ObsoleteAttribute is null && p is PropertyAliasAttribute alias ? alias.AliasedPropertyName : p.Name) .ToArray(); var similarPropertyHelp = similarNameProperties.Any() ? $" Did you mean {string.Join(", ", similarNameProperties)}, or another DotVVM property?" : " Did you intent to use a DotVVM property instead?"; @@ -496,17 +505,67 @@ private static void AddHtmlAttributeWarning(IAbstractControl control, DothtmlAtt } } + private (string tagPrefix, string name, IControlType)[] FindSimilarControls(string? tagPrefix, string elementName, ITypeDescriptor? controlBaseType = null, int threshold = 4, int limit = 5) + { + return ( + 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.tagName ?? c.type.PrimaryName ], c.type.AlternativeNames) + let nameScore = StringSimilarity.DamerauLevenshteinDistance(elementName.ToLowerInvariant(), controlName.ToLowerInvariant()) + where prefixScore + nameScore <= threshold + orderby (prefixScore + nameScore, controlName, c.tagPrefix) descending + select (c.tagPrefix, controlName, c.type) + ).Take(limit).ToArray(); + } + + private string? GetMissingElementPropertyDiagnostic(IAbstractControl parentControl, DothtmlElementNode element, ITypeDescriptor? allowedControlTypes = null) + { + // * warning: content allowed, but element has uppercase letter + // * error: content not allowed, no property matches + // * error: content allowed, HtmlGenericControl isn't allowed, no property matches + var isUppercase = char.IsUpper(element.TagName, 0); + var similarNameControls = + !parentControl.Metadata.IsContentAllowed && parentControl.Metadata.DefaultContentProperty is null + ? [] + : FindSimilarControls(element.TagPrefix, element.TagName, allowedControlTypes); + var similarNameProperties = + parentControl.Metadata.AllProperties + .Where(p => p.MarkupOptions.MappingMode.HasFlag(MappingMode.InnerElement) && + StringSimilarity.DamerauLevenshteinDistance(p.Name.ToLowerInvariant(), element.FullTagName.ToLowerInvariant()) <= 4) + .Select(p => p.Name) + .ToArray(); + if (similarNameControls.Any() && similarNameProperties.Any()) + { + return $" Did you mean property {string.Join(", ", similarNameProperties)}, or control {string.Join(", ", similarNameControls.Select(c => c.tagPrefix + ":" + c.name))}?"; + } + else if (similarNameProperties.Any()) + { + return $" Did you mean {string.Join(", ", similarNameProperties)}, or another DotVVM property?"; + } + else if (similarNameControls.Any()) + { + return $" Did you mean {string.Join(", ", similarNameControls.Select(c => c.tagPrefix + ":" + c.name))}, or another DotVVM control?"; + } + else + { + return null; + } + } + /// /// Processes the content of the control node. /// public void ProcessControlContent(IAbstractControl control, IEnumerable nodes) { + var allowsContent = control.Metadata.IsContentAllowed || control.Metadata.DefaultContentProperty is {}; var content = new List(); bool properties = true; foreach (var node in nodes) { var element = node as DothtmlElementNode; - if (element != null && properties) + if (element is {} && properties) { var property = controlResolver.FindProperty(control.Metadata, element.TagName, MappingMode.InnerElement); if (property != null && string.IsNullOrEmpty(element.TagPrefix) && property.MarkupOptions.MappingMode.HasFlag(MappingMode.InnerElement)) @@ -515,10 +574,33 @@ public void ProcessControlContent(IAbstractControl control, IEnumerable c.IsNotEmpty())}'))."); + } + else + { + element.TagNameNode.AddWarning( + $"HTML element name '{element.TagName}' should not contain uppercase letters.{GetMissingElementPropertyDiagnostic(control, element)}" + ); + } + } + } } } if (control.Metadata.DefaultContentProperty is IPropertyDescriptor contentProperty) { // don't assign the property, when content is empty - if (content.All(c => !c.IsNotEmpty())) + if (content.All(c => c.IsEmpty())) return; if (control.HasProperty(contentProperty)) @@ -551,21 +654,10 @@ public void ProcessControlContent(IAbstractControl control, IEnumerable filterByType(ITypeDescriptor type, IEnumerable c is object && c.Metadata.Type.IsAssignableTo(type), c => { - // empty nodes are only filtered, non-empty nodes cause errors + // empty nodes are only filtered out, non-empty nodes cause errors if (c.DothtmlNode.IsNotEmpty()) - c.DothtmlNode.AddError($"Control type {c.Metadata.Type.CSharpFullName} can't be used in collection of type {type.CSharpFullName}."); + { + // when used without explicit property wrapper element, add information about available content properties + var propertyHelp = propertyWrapperElement is null && c.DothtmlNode is DothtmlElementNode element ? GetMissingElementPropertyDiagnostic(control, element, type) : null; + ((c.DothtmlNode as DothtmlElementNode)?.TagNameNode ?? c.DothtmlNode) + .AddError($"Control type {c.Metadata.Type.CSharpFullName} can't be used in a property of type {type.CSharpFullName}.{propertyHelp}"); + } }); // resolve data context diff --git a/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs b/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs index 4736b7e1ec..7405493110 100644 --- a/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs +++ b/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs @@ -300,6 +300,47 @@ public override IControlResolverMetadata BuildControlMetadata(IControlType type) return new ControlResolverMetadata((ControlType)type); } + public override IEnumerable<(string tagPrefix, string? tagName, IControlType type)> EnumerateControlTypes() + { + var markupControls = new HashSet<(string, string)>(); // don't report MarkupControl with @baseType twice + + foreach (var control in configuration.Markup.Controls) + { + if (!string.IsNullOrEmpty(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); + } + } + + foreach (var assemblyGroup in configuration.Markup.Controls.Where(c => !string.IsNullOrEmpty(c.Assembly) && string.IsNullOrEmpty(c.Src)).GroupBy(c => c.Assembly!)) + { + var assembly = compiledAssemblyCache.GetAssembly(assemblyGroup.Key); + if (assembly is null) + continue; + + var namespaces = assemblyGroup.GroupBy(c => c.Namespace ?? "").ToDictionary(g => g.Key, g => g.First()); + foreach (var type in assembly.GetLoadableTypes()) + { + if (type.IsPublic && !type.IsAbstract && + type.DeclaringType is null && + typeof(DotvvmBindableObject).IsAssignableFrom(type) && + namespaces.TryGetValue(type.Namespace ?? "", out var controlConfig)) + { + if (!markupControls.Contains((controlConfig.TagPrefix!, type.Name))) + yield return (controlConfig.TagPrefix!, null, new ControlType(type)); + } + } + } + } + protected override IPropertyDescriptor? FindGlobalPropertyOrGroup(string name, MappingMode requiredMode) { // try to find property diff --git a/src/Framework/Framework/Compilation/ControlTree/DefaultControlTreeResolver.cs b/src/Framework/Framework/Compilation/ControlTree/DefaultControlTreeResolver.cs index 9e5cd0f451..fa19c3b7ef 100644 --- a/src/Framework/Framework/Compilation/ControlTree/DefaultControlTreeResolver.cs +++ b/src/Framework/Framework/Compilation/ControlTree/DefaultControlTreeResolver.cs @@ -5,6 +5,7 @@ using DotVVM.Framework.Compilation.Directives; using DotVVM.Framework.Compilation.Parser.Dothtml.Parser; using DotVVM.Framework.Compilation.ViewCompiler; +using DotVVM.Framework.Configuration; using DotVVM.Framework.Utils; namespace DotVVM.Framework.Compilation.ControlTree @@ -23,8 +24,9 @@ public DefaultControlTreeResolver( IControlResolver controlResolver, IAbstractTreeBuilder treeBuilder, IControlBuilderFactory controlBuilderFactory, - IMarkupDirectiveCompilerPipeline direrectiveCompilerPipeline) - : base(controlResolver, treeBuilder, direrectiveCompilerPipeline) + IMarkupDirectiveCompilerPipeline direrectiveCompilerPipeline, + DotvvmConfiguration configuration) + : base(controlResolver, treeBuilder, direrectiveCompilerPipeline, configuration) { this.controlBuilderFactory = controlBuilderFactory; } diff --git a/src/Framework/Framework/Compilation/ControlTree/IControlResolver.cs b/src/Framework/Framework/Compilation/ControlTree/IControlResolver.cs index a781632f9b..fb2d92c6ff 100644 --- a/src/Framework/Framework/Compilation/ControlTree/IControlResolver.cs +++ b/src/Framework/Framework/Compilation/ControlTree/IControlResolver.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using DotVVM.Framework.Controls; using DotVVM.Framework.Runtime; @@ -26,6 +27,10 @@ public interface IControlResolver /// IControlResolverMetadata ResolveControl(ITypeDescriptor 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. + IEnumerable<(string tagPrefix, string? tagName, IControlType type)> EnumerateControlTypes(); + /// /// Resolves the binding type. /// diff --git a/src/Framework/Framework/Compilation/ControlTree/ResolvedTreeHelpers.cs b/src/Framework/Framework/Compilation/ControlTree/ResolvedTreeHelpers.cs index b6b62c21c9..582394faba 100644 --- a/src/Framework/Framework/Compilation/ControlTree/ResolvedTreeHelpers.cs +++ b/src/Framework/Framework/Compilation/ControlTree/ResolvedTreeHelpers.cs @@ -55,7 +55,7 @@ public static DataContextStack GetDataContextStack(this ResolvedPropertySetter s public static bool IsOnlyWhitespace(this IAbstractControl control) => - control.Metadata.Type.IsEqualTo(ResolvedTypeDescriptor.Create(typeof(RawLiteral))) && control.DothtmlNode?.IsNotEmpty() == false; + control.Metadata.Type.IsEqualTo(ResolvedTypeDescriptor.Create(typeof(RawLiteral))) && control.DothtmlNode?.IsEmpty() == true; public static bool HasOnlyWhiteSpaceContent(this IAbstractContentNode control) => control.Content.All(IsOnlyWhitespace); diff --git a/src/Framework/Framework/Compilation/ControlType.cs b/src/Framework/Framework/Compilation/ControlType.cs index 07bb601b8d..e8c4782309 100644 --- a/src/Framework/Framework/Compilation/ControlType.cs +++ b/src/Framework/Framework/Compilation/ControlType.cs @@ -1,7 +1,9 @@ using System; using System.Diagnostics; +using System.Reflection; using DotVVM.Framework.Compilation.ControlTree; using DotVVM.Framework.Compilation.ControlTree.Resolved; +using DotVVM.Framework.Controls; namespace DotVVM.Framework.Compilation { @@ -17,6 +19,10 @@ public sealed class ControlType : IControlType ITypeDescriptor? IControlType.DataContextRequirement => ResolvedTypeDescriptor.Create(DataContextRequirement); + public string PrimaryName => GetControlNames(Type).primary; + + public string[] AlternativeNames => GetControlNames(Type).alternative; + static void ValidateControlClass(Type control) { if (!control.IsPublic && !control.IsNestedPublic) @@ -35,6 +41,19 @@ public ControlType(Type type, string? virtualPath = null, Type? dataContextRequi DataContextRequirement = dataContextRequirement; } + public static (string primary, string[] alternative) GetControlNames(Type controlType) + { + var attr = controlType.GetCustomAttribute(); + if (attr is null) + { + return (controlType.Name, Array.Empty()); + } + else + { + return (attr.PrimaryName ?? controlType.Name, attr.AlternativeNames ?? Array.Empty()); + } + } + public override bool Equals(object? obj) { diff --git a/src/Framework/Framework/Compilation/IControlType.cs b/src/Framework/Framework/Compilation/IControlType.cs index e7700133d4..af410279b7 100644 --- a/src/Framework/Framework/Compilation/IControlType.cs +++ b/src/Framework/Framework/Compilation/IControlType.cs @@ -11,5 +11,9 @@ public interface IControlType ITypeDescriptor? DataContextRequirement { get; } + string PrimaryName { get; } + + string[] AlternativeNames { get; } + } } diff --git a/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DotHtmlCommentNode.cs b/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DotHtmlCommentNode.cs index fadb6c1efd..4613325a8d 100644 --- a/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DotHtmlCommentNode.cs +++ b/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DotHtmlCommentNode.cs @@ -39,5 +39,8 @@ public override IEnumerable EnumerateNodes() { return base.EnumerateNodes().Concat(EnumerateChildNodes().SelectMany(node => node.EnumerateNodes())); } + + public override string ToString() => + IsServerSide ? $"<%-- {Value} --%>" : $""; } } diff --git a/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlAttributeNode.cs b/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlAttributeNode.cs index 5a3ef2d6e5..8eb59fd916 100644 --- a/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlAttributeNode.cs +++ b/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlAttributeNode.cs @@ -5,14 +5,14 @@ namespace DotVVM.Framework.Compilation.Parser.Dothtml.Parser { - [DebuggerDisplay("{debuggerDisplay,nq}{ValueNode}")] public sealed class DothtmlAttributeNode : DothtmlNode { - #region debugger display - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private string debuggerDisplay => - AttributeFullName + (ValueNode == null ? "" : "="); - #endregion + public override string ToString() => + AttributeFullName + ValueNode switch { + null => "", + DothtmlValueBindingNode => $"={ValueNode}", + _ => $"=\"{ValueNode}\"" + }; public string? AttributePrefix => AttributePrefixNode?.Text; public string AttributeName => AttributeNameNode.Text; diff --git a/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlBindingNode.cs b/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlBindingNode.cs index 7b782d508e..cec3939e70 100644 --- a/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlBindingNode.cs +++ b/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlBindingNode.cs @@ -5,21 +5,12 @@ namespace DotVVM.Framework.Compilation.Parser.Dothtml.Parser { - [DebuggerDisplay("{debuggerDisplay,nq}")] public class DothtmlBindingNode : DothtmlNode { - - #region debugger display - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private string debuggerDisplay + public override string ToString() { - get - { - return "{" + Name + ": " + Value + "}"; - } + return "{" + Name + ": " + Value + "}"; } - #endregion - public DothtmlBindingNode(DothtmlToken startToken, DothtmlToken endToken, DothtmlToken separatorToken, DothtmlNameNode nameNode, DothtmlValueTextNode valueNode) { diff --git a/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlDirectiveNode.cs b/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlDirectiveNode.cs index f23f498aac..e4fe1d432b 100644 --- a/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlDirectiveNode.cs +++ b/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlDirectiveNode.cs @@ -39,5 +39,7 @@ public override IEnumerable EnumerateNodes() { return base.EnumerateNodes().Concat( EnumerateChildNodes().SelectMany(node => node.EnumerateNodes() ) ); } + + public override string ToString() => $"@{Name} {Value}"; } } diff --git a/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlElementNode.cs b/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlElementNode.cs index 1240af5394..ba0f643736 100644 --- a/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlElementNode.cs +++ b/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlElementNode.cs @@ -5,19 +5,12 @@ namespace DotVVM.Framework.Compilation.Parser.Dothtml.Parser { - [DebuggerDisplay("{debuggerDisplay,nq}")] public sealed class DothtmlElementNode : DothtmlNodeWithContent { - #region debugger display - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private string debuggerDisplay + public override string ToString() { - get - { - return "<" + (IsClosingTag ? "/" : "") + FullTagName + (Attributes.Any() ? " ..." : "") + (IsSelfClosingTag ? " /" : "") + ">"; - } + return "<" + (IsClosingTag ? "/" : "") + FullTagName + (Attributes.Any() ? " ..." : "") + (IsSelfClosingTag ? " /" : "") + ">"; } - #endregion public string TagName => TagNameNode.Text; public string? TagPrefix => TagPrefixNode?.Text; diff --git a/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlLiteralNode.cs b/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlLiteralNode.cs index 95a957c87b..f468741ae7 100644 --- a/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlLiteralNode.cs +++ b/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlLiteralNode.cs @@ -5,7 +5,6 @@ namespace DotVVM.Framework.Compilation.Parser.Dothtml.Parser { - [DebuggerDisplay("{Value}")] public sealed class DothtmlLiteralNode : DothtmlNode { public DothtmlToken? MainValueToken { get; set; } @@ -20,5 +19,7 @@ public override void Accept(IDothtmlSyntaxTreeVisitor visitor) { visitor.Visit(this); } + + public override string ToString() => Value; } } diff --git a/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlNodeHelper.cs b/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlNodeHelper.cs index c8f79fe4e6..2319ffb117 100644 --- a/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlNodeHelper.cs +++ b/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlNodeHelper.cs @@ -5,11 +5,13 @@ namespace DotVVM.Framework.Compilation.Parser.Dothtml.Parser { public static class DothtmlNodeHelper { - public static bool IsNotEmpty([NotNullWhen(true)] this DothtmlNode? node) + public static bool IsNotEmpty([NotNullWhen(true)] this DothtmlNode? node) => + !IsEmpty(node); + + public static bool IsEmpty([NotNullWhen(false)] this DothtmlNode? node) { - return node is object && - !(node is DotHtmlCommentNode) && - !(node is DothtmlLiteralNode literalNode && string.IsNullOrWhiteSpace(literalNode.Value)); + return node is null or DotHtmlCommentNode || + (node is DothtmlLiteralNode literalNode && string.IsNullOrWhiteSpace(literalNode.Value)); } public static int GetContentStartPosition(this DothtmlElementNode node) diff --git a/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlValueBindingNode.cs b/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlValueBindingNode.cs index 6084be6f5a..1256d4ec20 100644 --- a/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlValueBindingNode.cs +++ b/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlValueBindingNode.cs @@ -39,5 +39,7 @@ public override IEnumerable EnumerateNodes() { return base.EnumerateNodes().Concat(EnumerateChildNodes().SelectMany(n=> n.EnumerateNodes())); } + + public override string ToString() => BindingNode.ToString(); } } diff --git a/src/Framework/Framework/Utils/StringSimilarity.cs b/src/Framework/Framework/Utils/StringSimilarity.cs index 23cd51d5e4..58757a56f0 100644 --- a/src/Framework/Framework/Utils/StringSimilarity.cs +++ b/src/Framework/Framework/Utils/StringSimilarity.cs @@ -4,7 +4,7 @@ namespace DotVVM.Framework.Utils { internal static class StringSimilarity { - /// Edit distance with deletion (Visble), insertion (Visivble), substitution (Visine) and transposition (Visilbe) + /// Edit distance with deletion (Visble), insertion (Visivble), substitution (Visinle) and transposition (Visilbe) public static int DamerauLevenshteinDistance(string a, string b) { // https://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance diff --git a/src/Samples/Tests/Tests/ErrorsTests.cs b/src/Samples/Tests/Tests/ErrorsTests.cs index a3afdd2bc3..928c55453b 100644 --- a/src/Samples/Tests/Tests/ErrorsTests.cs +++ b/src/Samples/Tests/Tests/ErrorsTests.cs @@ -57,7 +57,7 @@ public void Error_NonExistingControl() s.Contains("could not be resolved") ); AssertUI.InnerTextEquals(browser.First("[class='errorUnderline']") - , "", false); + , "NonExistingControl", false); }); } diff --git a/src/Tests/ControlTests/CompositeControlTests.cs b/src/Tests/ControlTests/CompositeControlTests.cs index d963d7a96b..3b9df58d78 100644 --- a/src/Tests/ControlTests/CompositeControlTests.cs +++ b/src/Tests/ControlTests/CompositeControlTests.cs @@ -282,7 +282,7 @@ public async Task ControlWithCollection_WrongType() ")); - Assert.AreEqual("Control type DotVVM.Framework.Controls.HtmlGenericControl can't be used in collection of type DotVVM.Framework.Controls.Repeater.", e.Message); + Assert.AreEqual("Control type DotVVM.Framework.Controls.HtmlGenericControl can't be used in a property of type DotVVM.Framework.Controls.Repeater.", e.Message); } [TestMethod] diff --git a/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/CompilationWarningsTests.cs b/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/CompilationWarningsTests.cs index a3c7c50f07..763cd1fbc8 100644 --- a/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/CompilationWarningsTests.cs +++ b/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/CompilationWarningsTests.cs @@ -90,6 +90,64 @@ @viewModel System.Boolean 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_NonExistentPropertyWarning_InnerElement() + { + var markup = $@" +@viewModel bool + + empty + --- + + test + ---- + +"; + var repeater = ParseSource(markup) + .Content.SelectRecursively(c => c.Content) + .Single(c => c.Metadata.Type == typeof(Repeater)); + + var elementNode = (DothtmlElementNode)repeater.DothtmlNode; + var correctTemplateElement = elementNode.Content.OfType().Single(e => e.TagName == "EmptyDataTemplate"); + var mistakeTemplateElement = elementNode.Content.OfType().Single(e => e.TagName == "SepratrorTemplate"); + var mistakeTextBoxElement = elementNode.Content.OfType().Single(e => e.TagName == "TextBox"); + var lateTemplate = elementNode.Content.OfType().Single(e => e.TagName == "SeparatorTemplate"); + + XAssert.Empty(correctTemplateElement.TagNameNode.NodeWarnings); + Assert.AreEqual("HTML element name 'SepratrorTemplate' should not contain uppercase letters. Did you mean SeparatorTemplate, or another DotVVM property?", XAssert.Single(mistakeTemplateElement.TagNameNode.NodeWarnings)); + Assert.AreEqual("HTML element name 'TextBox' should not contain uppercase letters. Did you mean dot:CheckBox, dot:ListBox, dot:TextBox, or another DotVVM control?", XAssert.Single(mistakeTextBoxElement.TagNameNode.NodeWarnings)); + Assert.AreEqual("This element looks like an inner element property Repeater.SeparatorTemplate, but it isn't, because it is prefixed by other content ('')).", XAssert.Single(lateTemplate.TagNameNode.NodeWarnings)); + } + + [TestMethod] + public void DefaultViewCompiler_DisallowedContentControlType() + { + var markup = $@" +@viewModel System.Collections.Generic.IEnumerable + + empty + test + + +"; + var repeater = ParseSource(markup) + .Content.SelectRecursively(c => c.Content) + .Single(c => c.Metadata.Type == typeof(GridView)); + + var elementNode = (DothtmlElementNode)repeater.DothtmlNode; + var fine = elementNode.Content.OfType().Single(e => e.TagName == "GridViewTemplateColumn"); + var unallowedType = elementNode.Content.OfType().Single(e => e.TagName == "TextBox"); + var mistypedTemplate = elementNode.Content.OfType().Single(e => e.TagName == "EmtyDataTemplate"); + + XAssert.Empty(fine.TagNameNode.NodeWarnings); + XAssert.Empty(fine.TagNameNode.NodeErrors); + + Assert.AreEqual("Control type DotVVM.Framework.Controls.TextBox can't be used in a property of type DotVVM.Framework.Controls.GridViewColumn.", XAssert.Single(unallowedType.TagNameNode.NodeErrors)); + Assert.AreEqual("Control type DotVVM.Framework.Controls.HtmlGenericControl can't be used in a property of type DotVVM.Framework.Controls.GridViewColumn. Did you mean EmptyDataTemplate, or another DotVVM property?", XAssert.Single(mistypedTemplate.TagNameNode.NodeErrors)); + Assert.AreEqual("HTML element name 'EmtyDataTemplate' should not contain uppercase letters. Did you mean EmptyDataTemplate, or another DotVVM property?", XAssert.Single(mistypedTemplate.TagNameNode.NodeWarnings)); + } + + [TestMethod] public void DefaultViewCompiler_UnsupportedCallSite_ResourceBinding_Warning() { diff --git a/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTests.cs b/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTests.cs index b24cf51373..dabdab4307 100644 --- a/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTests.cs +++ b/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTests.cs @@ -201,12 +201,39 @@ public void ResolvedTree_UnknownElement() Assert.AreEqual(typeof(HtmlGenericControl), control.Metadata.Type); Assert.AreEqual(1, control.ConstructorParameters.Length); Assert.AreEqual("dot:xxxButton", control.ConstructorParameters[0]); - Assert.IsTrue(control.DothtmlNode.HasNodeErrors); - Assert.IsTrue(control.DothtmlNode.NodeErrors.First().Contains("could not be resolved")); + var node = (control.DothtmlNode as DothtmlElementNode).TagNameNode; + Assert.AreEqual("The control could not be resolved! Did you mean dot:LinkButton, dot:Button, or other DotVVM control?", XAssert.Single(node.NodeErrors)); Assert.AreEqual(root, control.Parent); } + [TestMethod] + public void ResolvedTree_UnknownPrefix() + { + var root = ParseSource(@"@viewModel string +"); + + var control = root.Content.First(); + var node = (control.DothtmlNode as DothtmlElementNode).TagNameNode; + 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)); + } } } diff --git a/src/Tests/Runtime/ViewCompilationServiceTests.cs b/src/Tests/Runtime/ViewCompilationServiceTests.cs index 5e7a26b5b7..d07557fef0 100644 --- a/src/Tests/Runtime/ViewCompilationServiceTests.cs +++ b/src/Tests/Runtime/ViewCompilationServiceTests.cs @@ -88,7 +88,7 @@ public void ErrorInMarkup() service.BuildView(route, out _); Assert.AreEqual(CompilationState.CompilationFailed, route.Status); Assert.IsNotNull(route.Exception); - Assert.AreEqual("The control could not be resolved! Make sure that the tagPrefix is registered in DotvvmConfiguration.Markup.Controls collection!", route.Exception); + Assert.AreEqual("The control could not be resolved!", route.Exception); } [TestMethod]