Skip to content

Commit

Permalink
Improve the DataContext type mismatch error message
Browse files Browse the repository at this point in the history
  • Loading branch information
exyi committed Dec 11, 2024
1 parent 10002de commit e6de8be
Show file tree
Hide file tree
Showing 10 changed files with 508 additions and 24 deletions.
41 changes: 37 additions & 4 deletions src/Framework/Framework/Binding/BindingHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -588,10 +588,43 @@ public override string Message
{
get
{
var actualContextsHelp =
ActualContextTypes is null ? "" :
$" Real data context types: {string.Join(", ", ActualContextTypes.Select(t => t?.ToCode(stripNamespace: true) ?? "null"))}.";
return $"Could not find DataContext space of '{ContextObject}'. The DataContextType property of the binding does not correspond to DataContextType of the {Control.GetType().Name} nor any of its ancestors. Control's context is {ControlContext}, binding's context is {BindingContext}." + actualContextsHelp;
var message = new StringBuilder()
.Append($"Could not find DataContext space of '{ContextObject}'. The DataContextType property of the binding does not correspond to DataContextType of the {Control.GetType().Name} nor any of its ancestors.");

var stackComparison = DataContextStack.CompareStacksMessage(ControlContext, BindingContext);

for (var i = 0; i < stackComparison.Length; i++)
{
var level = i switch {
0 => "_this: ",
1 => "_parent: ",
_ => $"_parent{i}: "
};

message.Append($"\nControl {level}");
foreach (var (control, binding) in stackComparison[i])
{
var length = Math.Max(control.Length, binding.Length);
if (control == binding)
message.Append(control);
else
message.Append(StringUtils.PadCenter(StringUtils.UnicodeUnderline(control), length + 2));
}

message.Append($"\nBinding {level}");
foreach (var (control, binding) in stackComparison[i])
{
var length = Math.Max(control.Length, binding.Length);
if (control == binding)
message.Append(binding);
else
message.Append(StringUtils.PadCenter(StringUtils.UnicodeUnderline(binding), length + 2));
}
}

if (ActualContextTypes is {})
message.Append($"\nReal data context types: {string.Join(", ", ActualContextTypes.Select(t => t?.ToCode(stripNamespace: true) ?? "null"))}");
return message.ToString();
}
}
}
Expand Down
162 changes: 149 additions & 13 deletions src/Framework/Framework/Compilation/ControlTree/DataContextStack.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,18 +176,20 @@ int ComputeHashCode()
}
}

public override string ToString()
{
string?[] features = new [] {
$"type={this.DataContextType.ToCode()}",
this.ServerSideOnly ? "server-side-only" : null,
this.NamespaceImports.Any() ? "imports=[" + string.Join(", ", this.NamespaceImports) + "]" : null,
this.ExtensionParameters.Any() ? "ext=[" + string.Join(", ", this.ExtensionParameters.Select(e => e.Identifier + ": " + e.ParameterType.CSharpName)) + "]" : null,
this.BindingPropertyResolvers.Any() ? "resolvers=[" + string.Join(", ", this.BindingPropertyResolvers.Select(s => s.Method)) + "]" : null,
this.Parent != null ? "par=[" + string.Join(", ", this.Parents().Select(p => p.ToCode(stripNamespace: true))) + "]" : null
};
return "(" + features.Where(a => a != null).StringJoin(", ") + ")";
}
private string?[] ToStringFeatures() => [
$"type={this.DataContextType.ToCode()}",
this.ServerSideOnly ? "server-side-only" : null,
this.NamespaceImports.Any() ? "imports=[" + string.Join(", ", this.NamespaceImports) + "]" : null,
this.ExtensionParameters.Any() ? "ext=[" + string.Join(", ", this.ExtensionParameters.Select(e => e.Identifier + ": " + e.ParameterType.CSharpName)) + "]" : null,
this.BindingPropertyResolvers.Any() ? "resolvers=[" + string.Join(", ", this.BindingPropertyResolvers.Select(s => s.Method)) + "]" : null,
this.Parent != null ? "par=[" + string.Join(", ", this.Parents().Select(p => p.ToCode(stripNamespace: true))) + "]" : null
];

public override string ToString() =>
"(" + ToStringFeatures().WhereNotNull().StringJoin(", ") + ")";

private string ToStringWithoutParent() =>
ToStringFeatures()[..^1].WhereNotNull().StringJoin(", ");


//private static ConditionalWeakTable<DataContextStack, DataContextStack> internCache = new ConditionalWeakTable<DataContextStack, DataContextStack>();
Expand All @@ -212,7 +214,7 @@ public static DataContextStack CreateCollectionElement(Type elementType,
bool serverSideOnly = false)
{
var indexParameters = new CollectionElementDataContextChangeAttribute(0).GetExtensionParameters(new ResolvedTypeDescriptor(elementType.MakeArrayType()));
extensionParameters = extensionParameters is null ? indexParameters.ToArray() : extensionParameters.Concat(indexParameters).ToArray();
extensionParameters = [..(extensionParameters ?? []), ..indexParameters ];
return DataContextStack.Create(
elementType, parent,
imports: imports,
Expand All @@ -221,5 +223,139 @@ public static DataContextStack CreateCollectionElement(Type elementType,
serverSideOnly: serverSideOnly
);
}

private static int Difference(DataContextStack a, DataContextStack b)
{
if (a == b) return 0;

var result = 0;
if (a.DataContextType != b.DataContextType)
result += 6;

if (a.DataContextType.Namespace != b.DataContextType.Namespace)
result += 2;

if (a.DataContextType.Name != b.DataContextType.Name)
result += 2;

result += CompareSets(a.NamespaceImports, b.NamespaceImports);

result += CompareSets(a.ExtensionParameters, b.ExtensionParameters);

result += CompareSets(a.BindingPropertyResolvers, b.BindingPropertyResolvers);

if (a.Parent != b.Parent)
result += 1;

return result;


static int CompareSets<T>(IEnumerable<T> a, IEnumerable<T> b)
{
return a.Union(b).Count() - a.Intersect(b).Count();
}
}

public static (string a, string b)[][] CompareStacksMessage(DataContextStack a, DataContextStack b)
{
var alignment = StringSimilarity.SequenceAlignment<DataContextStack>(
a.EnumerableItems().ToArray().AsSpan(), b.EnumerableItems().ToArray().AsSpan(),
Difference,
gapCost: 10);

return alignment.Select(pair => {
return CompareMessage(pair.a, pair.b);
}).ToArray();
}

/// <summary> Provides a formatted string for two DataContextStacks with aligned fragments used for highlighting. Does not include the parent context. </summary>
public static (string a, string b)[] CompareMessage(DataContextStack? a, DataContextStack? b)
{
if (a == null || b == null) return new[] { (a?.ToStringWithoutParent() ?? "(missing)", b?.ToStringWithoutParent() ?? "(missing)") };

var result = new List<(string, string)>();

void same(string str) => result.Add((str, str));
void different(string? a, string? b) => result.Add((a ?? "", b ?? ""));

same("type=");
if (a.DataContextType == b.DataContextType)
same(a.DataContextType.ToCode(stripNamespace: true));
else
{
different(a.DataContextType.Namespace, b.DataContextType.Namespace);
same(".");
different(a.DataContextType.ToCode(stripNamespace: true), b.DataContextType.ToCode(stripNamespace: true));
}

if (a.ServerSideOnly || b.ServerSideOnly)
{
same(", ");
different(a.ServerSideOnly ? "server-side-only" : "", b.ServerSideOnly ? "server-side-only" : "");
}

if (a.NamespaceImports.Any() || b.NamespaceImports.Any())
{
same(", imports=[");
var importsAligned = StringSimilarity.SequenceAlignment(
a.NamespaceImports.AsSpan(), b.NamespaceImports.AsSpan(),
(a, b) => a.Equals(b) ? 0 :
a.Namespace == b.Namespace || a.Alias == b.Alias ? 1 :
3,
gapCost: 2);
foreach (var (i, (aImport, bImport)) in importsAligned.Indexed())
{
if (i > 0)
same(", ");

different(aImport.ToString(), bImport.ToString());
}

same("]");
}

if (a.ExtensionParameters.Any() || b.ExtensionParameters.Any())
{
same(", ext=[");
var extAligned = StringSimilarity.SequenceAlignment(
a.ExtensionParameters.AsSpan(), b.ExtensionParameters.AsSpan(),
(a, b) => a.Equals(b) ? 0 :
a.Identifier == b.Identifier ? 1 :
3,
gapCost: 2);
foreach (var (i, (aExt, bExt)) in extAligned.Indexed())
{
if (i > 0)
same(", ");

if (Equals(aExt, bExt))
same(aExt!.Identifier);
else if (aExt is null)
different("", bExt!.Identifier + ": " + bExt.ParameterType.CSharpName);
else if (bExt is null)
different(aExt.Identifier + ": " + aExt.ParameterType.CSharpName, "");
else
{
different(aExt.Identifier, bExt.Identifier);
same(": ");
if (aExt.ParameterType.IsEqualTo(bExt.ParameterType))
same(aExt.ParameterType.CSharpName);
else
different(aExt.ParameterType.CSharpFullName, bExt.ParameterType.CSharpFullName);

if (aExt.Identifier == bExt.Identifier && aExt.GetType() != bExt.GetType())
{
same(" (");
different(aExt.GetType().ToCode(), bExt.GetType().ToCode());
same(")");
}
}
}

same("]");
}

return result.ToArray();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ public string ErrorHtml(Exception exception, IHttpContext context)
.ToArray()!,
errorCode: context.Response.StatusCode,
errorDescription: "Unhandled exception occurred",
summary: exception.GetType().FullName + ": " + exception.Message.LimitLength(600),
summary: exception.GetType().FullName + ": " + exception.Message.LimitLength(3000),
context: DotvvmRequestContext.TryGetCurrent(context),
exception: exception);

Expand Down
107 changes: 107 additions & 0 deletions src/Framework/Framework/Utils/StringSimilarity.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace DotVVM.Framework.Utils
{
Expand Down Expand Up @@ -44,5 +46,110 @@ public static int DamerauLevenshteinDistance(string a, string b)

static int min(int a, int b) => Math.Min(a, b);
static int min(int a, int b, int c) => min(min(a, b), c);


public static (T? a, T? b)[] SequenceAlignment<T>(ReadOnlySpan<T> a, ReadOnlySpan<T> b, Func<T, T, int> substituionCost, int gapCost = 10)
{
// common case: strings are almost equal
// -> skip same prefix and suffix since the rest of the algorithm is quadratic

var prefix = new List<(T?, T?)>();
for (var i = 0; i < min(a.Length, b.Length); i++)
{
if (substituionCost(a[i], b[i]) <= 0)
prefix.Add((a[i], b[i]));
else
break;
}
a = a.Slice(prefix.Count);
b = b.Slice(prefix.Count);
// Console.WriteLine("Prefix length: " + prefix.Count);

var suffix = new List<(T?, T?)>();

for (var i = 1; i <= min(a.Length, b.Length); i++)
{
if (substituionCost(a[^i], b[^i]) <= 0)
suffix.Add((a[^i], b[^i]));
else
break;
}
a = a.Slice(0, a.Length - suffix.Count);
b = b.Slice(0, b.Length - suffix.Count);
// Console.WriteLine("Suffix length: " + suffix.Count);

var d = new int[a.Length + 1, b.Length + 1];
var arrows = new sbyte[a.Length + 1, b.Length + 1];
for (var i = 0; i <= a.Length; i++)
d[i, 0] = i * gapCost;

for (var j = 0; j <= b.Length; j++)
d[0, j] = j * gapCost;

for (var i = 0; i < a.Length; i ++)
{
for (var j = 0; j < b.Length; j++)
{
var substitutionCost = substituionCost(a[i], b[j]);
var dist = d[i, j] + substitutionCost;
sbyte arrow = 0; // record which direction is optimal
if (dist > d[i, j+1] + gapCost)
{
dist = d[i, j+1] + gapCost;
arrow = -1;
}
if (dist > d[i+1, j] + gapCost)
{
dist = d[i+1, j] + gapCost;
arrow = 1;
}

d[i+1, j+1] = dist;
arrows[i+1, j+1] = arrow;
}
}

// for (int i = 0; i <= a.Length; i++)
// {
// Console.WriteLine("D: " + string.Join("\t", Enumerable.Range(0, b.Length + 1).Select(j => d[i, j])));
// Console.WriteLine("A: " + string.Join("\t", Enumerable.Range(0, b.Length + 1).Select(j => arrows[i, j] switch { 0 => "↖", 1 => "←", -1 => "↑" })));
// }


// follow arrows from the back
for (int i = a.Length, j = b.Length; i > 0 || j > 0;)
{
// we are on border
if (i == 0)
{
j--;
suffix.Add((default, b[j]));
}
else if (j == 0)
{
i--;
suffix.Add((a[i], default));
}
else if (arrows[i, j] == 0)
{
i--;
j--;
suffix.Add((a[i], b[j]));
}
else if (arrows[i, j] == 1)
{
j--;
suffix.Add((default, b[j]));
}
else if (arrows[i, j] == -1)
{
i--;
suffix.Add((a[i], default));
}
}

suffix.Reverse();
return [..prefix, ..suffix];
}
}
}
Loading

0 comments on commit e6de8be

Please sign in to comment.