Skip to content

Commit 4808352

Browse files
committed
Improve the DataContext type mismatch error message
1 parent 3bd2a01 commit 4808352

10 files changed

+508
-24
lines changed

src/Framework/Framework/Binding/BindingHelper.cs

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -570,10 +570,43 @@ public override string Message
570570
{
571571
get
572572
{
573-
var actualContextsHelp =
574-
ActualContextTypes is null ? "" :
575-
$" Real data context types: {string.Join(", ", ActualContextTypes.Select(t => t?.ToCode(stripNamespace: true) ?? "null"))}.";
576-
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;
573+
var message = new StringBuilder()
574+
.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.");
575+
576+
var stackComparison = DataContextStack.CompareStacksMessage(ControlContext, BindingContext);
577+
578+
for (var i = 0; i < stackComparison.Length; i++)
579+
{
580+
var level = i switch {
581+
0 => "_this: ",
582+
1 => "_parent: ",
583+
_ => $"_parent{i}: "
584+
};
585+
586+
message.Append($"\nControl {level}");
587+
foreach (var (control, binding) in stackComparison[i])
588+
{
589+
var length = Math.Max(control.Length, binding.Length);
590+
if (control == binding)
591+
message.Append(control);
592+
else
593+
message.Append(StringUtils.PadCenter(StringUtils.UnicodeUnderline(control), length + 2));
594+
}
595+
596+
message.Append($"\nBinding {level}");
597+
foreach (var (control, binding) in stackComparison[i])
598+
{
599+
var length = Math.Max(control.Length, binding.Length);
600+
if (control == binding)
601+
message.Append(binding);
602+
else
603+
message.Append(StringUtils.PadCenter(StringUtils.UnicodeUnderline(binding), length + 2));
604+
}
605+
}
606+
607+
if (ActualContextTypes is {})
608+
message.Append($"\nReal data context types: {string.Join(", ", ActualContextTypes.Select(t => t?.ToCode(stripNamespace: true) ?? "null"))}");
609+
return message.ToString();
577610
}
578611
}
579612
}

src/Framework/Framework/Compilation/ControlTree/DataContextStack.cs

Lines changed: 149 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -176,18 +176,20 @@ int ComputeHashCode()
176176
}
177177
}
178178

179-
public override string ToString()
180-
{
181-
string?[] features = new [] {
182-
$"type={this.DataContextType.ToCode()}",
183-
this.ServerSideOnly ? "server-side-only" : null,
184-
this.NamespaceImports.Any() ? "imports=[" + string.Join(", ", this.NamespaceImports) + "]" : null,
185-
this.ExtensionParameters.Any() ? "ext=[" + string.Join(", ", this.ExtensionParameters.Select(e => e.Identifier + ": " + e.ParameterType.CSharpName)) + "]" : null,
186-
this.BindingPropertyResolvers.Any() ? "resolvers=[" + string.Join(", ", this.BindingPropertyResolvers.Select(s => s.Method)) + "]" : null,
187-
this.Parent != null ? "par=[" + string.Join(", ", this.Parents().Select(p => p.ToCode(stripNamespace: true))) + "]" : null
188-
};
189-
return "(" + features.Where(a => a != null).StringJoin(", ") + ")";
190-
}
179+
private string?[] ToStringFeatures() => [
180+
$"type={this.DataContextType.ToCode()}",
181+
this.ServerSideOnly ? "server-side-only" : null,
182+
this.NamespaceImports.Any() ? "imports=[" + string.Join(", ", this.NamespaceImports) + "]" : null,
183+
this.ExtensionParameters.Any() ? "ext=[" + string.Join(", ", this.ExtensionParameters.Select(e => e.Identifier + ": " + e.ParameterType.CSharpName)) + "]" : null,
184+
this.BindingPropertyResolvers.Any() ? "resolvers=[" + string.Join(", ", this.BindingPropertyResolvers.Select(s => s.Method)) + "]" : null,
185+
this.Parent != null ? "par=[" + string.Join(", ", this.Parents().Select(p => p.ToCode(stripNamespace: true))) + "]" : null
186+
];
187+
188+
public override string ToString() =>
189+
"(" + ToStringFeatures().WhereNotNull().StringJoin(", ") + ")";
190+
191+
private string ToStringWithoutParent() =>
192+
ToStringFeatures()[..^1].WhereNotNull().StringJoin(", ");
191193

192194

193195
//private static ConditionalWeakTable<DataContextStack, DataContextStack> internCache = new ConditionalWeakTable<DataContextStack, DataContextStack>();
@@ -212,7 +214,7 @@ public static DataContextStack CreateCollectionElement(Type elementType,
212214
bool serverSideOnly = false)
213215
{
214216
var indexParameters = new CollectionElementDataContextChangeAttribute(0).GetExtensionParameters(new ResolvedTypeDescriptor(elementType.MakeArrayType()));
215-
extensionParameters = extensionParameters is null ? indexParameters.ToArray() : extensionParameters.Concat(indexParameters).ToArray();
217+
extensionParameters = [..(extensionParameters ?? []), ..indexParameters ];
216218
return DataContextStack.Create(
217219
elementType, parent,
218220
imports: imports,
@@ -221,5 +223,139 @@ public static DataContextStack CreateCollectionElement(Type elementType,
221223
serverSideOnly: serverSideOnly
222224
);
223225
}
226+
227+
private static int Difference(DataContextStack a, DataContextStack b)
228+
{
229+
if (a == b) return 0;
230+
231+
var result = 0;
232+
if (a.DataContextType != b.DataContextType)
233+
result += 6;
234+
235+
if (a.DataContextType.Namespace != b.DataContextType.Namespace)
236+
result += 2;
237+
238+
if (a.DataContextType.Name != b.DataContextType.Name)
239+
result += 2;
240+
241+
result += CompareSets(a.NamespaceImports, b.NamespaceImports);
242+
243+
result += CompareSets(a.ExtensionParameters, b.ExtensionParameters);
244+
245+
result += CompareSets(a.BindingPropertyResolvers, b.BindingPropertyResolvers);
246+
247+
if (a.Parent != b.Parent)
248+
result += 1;
249+
250+
return result;
251+
252+
253+
static int CompareSets<T>(IEnumerable<T> a, IEnumerable<T> b)
254+
{
255+
return a.Union(b).Count() - a.Intersect(b).Count();
256+
}
257+
}
258+
259+
public static (string a, string b)[][] CompareStacksMessage(DataContextStack a, DataContextStack b)
260+
{
261+
var alignment = StringSimilarity.SequenceAlignment<DataContextStack>(
262+
a.EnumerableItems().ToArray().AsSpan(), b.EnumerableItems().ToArray().AsSpan(),
263+
Difference,
264+
gapCost: 10);
265+
266+
return alignment.Select(pair => {
267+
return CompareMessage(pair.a, pair.b);
268+
}).ToArray();
269+
}
270+
271+
/// <summary> Provides a formatted string for two DataContextStacks with aligned fragments used for highlighting. Does not include the parent context. </summary>
272+
public static (string a, string b)[] CompareMessage(DataContextStack? a, DataContextStack? b)
273+
{
274+
if (a == null || b == null) return new[] { (a?.ToStringWithoutParent() ?? "(missing)", b?.ToStringWithoutParent() ?? "(missing)") };
275+
276+
var result = new List<(string, string)>();
277+
278+
void same(string str) => result.Add((str, str));
279+
void different(string? a, string? b) => result.Add((a ?? "", b ?? ""));
280+
281+
same("type=");
282+
if (a.DataContextType == b.DataContextType)
283+
same(a.DataContextType.ToCode(stripNamespace: true));
284+
else
285+
{
286+
different(a.DataContextType.Namespace, b.DataContextType.Namespace);
287+
same(".");
288+
different(a.DataContextType.ToCode(stripNamespace: true), b.DataContextType.ToCode(stripNamespace: true));
289+
}
290+
291+
if (a.ServerSideOnly || b.ServerSideOnly)
292+
{
293+
same(", ");
294+
different(a.ServerSideOnly ? "server-side-only" : "", b.ServerSideOnly ? "server-side-only" : "");
295+
}
296+
297+
if (a.NamespaceImports.Any() || b.NamespaceImports.Any())
298+
{
299+
same(", imports=[");
300+
var importsAligned = StringSimilarity.SequenceAlignment(
301+
a.NamespaceImports.AsSpan(), b.NamespaceImports.AsSpan(),
302+
(a, b) => a.Equals(b) ? 0 :
303+
a.Namespace == b.Namespace || a.Alias == b.Alias ? 1 :
304+
3,
305+
gapCost: 2);
306+
foreach (var (i, (aImport, bImport)) in importsAligned.Indexed())
307+
{
308+
if (i > 0)
309+
same(", ");
310+
311+
different(aImport.ToString(), bImport.ToString());
312+
}
313+
314+
same("]");
315+
}
316+
317+
if (a.ExtensionParameters.Any() || b.ExtensionParameters.Any())
318+
{
319+
same(", ext=[");
320+
var extAligned = StringSimilarity.SequenceAlignment(
321+
a.ExtensionParameters.AsSpan(), b.ExtensionParameters.AsSpan(),
322+
(a, b) => a.Equals(b) ? 0 :
323+
a.Identifier == b.Identifier ? 1 :
324+
3,
325+
gapCost: 2);
326+
foreach (var (i, (aExt, bExt)) in extAligned.Indexed())
327+
{
328+
if (i > 0)
329+
same(", ");
330+
331+
if (Equals(aExt, bExt))
332+
same(aExt!.Identifier);
333+
else if (aExt is null)
334+
different("", bExt!.Identifier + ": " + bExt.ParameterType.CSharpName);
335+
else if (bExt is null)
336+
different(aExt.Identifier + ": " + aExt.ParameterType.CSharpName, "");
337+
else
338+
{
339+
different(aExt.Identifier, bExt.Identifier);
340+
same(": ");
341+
if (aExt.ParameterType.IsEqualTo(bExt.ParameterType))
342+
same(aExt.ParameterType.CSharpName);
343+
else
344+
different(aExt.ParameterType.CSharpFullName, bExt.ParameterType.CSharpFullName);
345+
346+
if (aExt.Identifier == bExt.Identifier && aExt.GetType() != bExt.GetType())
347+
{
348+
same(" (");
349+
different(aExt.GetType().ToCode(), bExt.GetType().ToCode());
350+
same(")");
351+
}
352+
}
353+
}
354+
355+
same("]");
356+
}
357+
358+
return result.ToArray();
359+
}
224360
}
225361
}

src/Framework/Framework/Hosting/ErrorPages/ErrorFormatter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ public string ErrorHtml(Exception exception, IHttpContext context)
351351
.ToArray()!,
352352
errorCode: context.Response.StatusCode,
353353
errorDescription: "Unhandled exception occurred",
354-
summary: exception.GetType().FullName + ": " + exception.Message.LimitLength(600),
354+
summary: exception.GetType().FullName + ": " + exception.Message.LimitLength(3000),
355355
context: DotvvmRequestContext.TryGetCurrent(context),
356356
exception: exception);
357357

src/Framework/Framework/Utils/StringSimilarity.cs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
24

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

4547
static int min(int a, int b) => Math.Min(a, b);
4648
static int min(int a, int b, int c) => min(min(a, b), c);
49+
50+
51+
public static (T? a, T? b)[] SequenceAlignment<T>(ReadOnlySpan<T> a, ReadOnlySpan<T> b, Func<T, T, int> substituionCost, int gapCost = 10)
52+
{
53+
// common case: strings are almost equal
54+
// -> skip same prefix and suffix since the rest of the algorithm is quadratic
55+
56+
var prefix = new List<(T?, T?)>();
57+
for (var i = 0; i < min(a.Length, b.Length); i++)
58+
{
59+
if (substituionCost(a[i], b[i]) <= 0)
60+
prefix.Add((a[i], b[i]));
61+
else
62+
break;
63+
}
64+
a = a.Slice(prefix.Count);
65+
b = b.Slice(prefix.Count);
66+
// Console.WriteLine("Prefix length: " + prefix.Count);
67+
68+
var suffix = new List<(T?, T?)>();
69+
70+
for (var i = 1; i <= min(a.Length, b.Length); i++)
71+
{
72+
if (substituionCost(a[^i], b[^i]) <= 0)
73+
suffix.Add((a[^i], b[^i]));
74+
else
75+
break;
76+
}
77+
a = a.Slice(0, a.Length - suffix.Count);
78+
b = b.Slice(0, b.Length - suffix.Count);
79+
// Console.WriteLine("Suffix length: " + suffix.Count);
80+
81+
var d = new int[a.Length + 1, b.Length + 1];
82+
var arrows = new sbyte[a.Length + 1, b.Length + 1];
83+
for (var i = 0; i <= a.Length; i++)
84+
d[i, 0] = i * gapCost;
85+
86+
for (var j = 0; j <= b.Length; j++)
87+
d[0, j] = j * gapCost;
88+
89+
for (var i = 0; i < a.Length; i ++)
90+
{
91+
for (var j = 0; j < b.Length; j++)
92+
{
93+
var substitutionCost = substituionCost(a[i], b[j]);
94+
var dist = d[i, j] + substitutionCost;
95+
sbyte arrow = 0; // record which direction is optimal
96+
if (dist > d[i, j+1] + gapCost)
97+
{
98+
dist = d[i, j+1] + gapCost;
99+
arrow = -1;
100+
}
101+
if (dist > d[i+1, j] + gapCost)
102+
{
103+
dist = d[i+1, j] + gapCost;
104+
arrow = 1;
105+
}
106+
107+
d[i+1, j+1] = dist;
108+
arrows[i+1, j+1] = arrow;
109+
}
110+
}
111+
112+
// for (int i = 0; i <= a.Length; i++)
113+
// {
114+
// Console.WriteLine("D: " + string.Join("\t", Enumerable.Range(0, b.Length + 1).Select(j => d[i, j])));
115+
// Console.WriteLine("A: " + string.Join("\t", Enumerable.Range(0, b.Length + 1).Select(j => arrows[i, j] switch { 0 => "↖", 1 => "←", -1 => "↑" })));
116+
// }
117+
118+
119+
// follow arrows from the back
120+
for (int i = a.Length, j = b.Length; i > 0 || j > 0;)
121+
{
122+
// we are on border
123+
if (i == 0)
124+
{
125+
j--;
126+
suffix.Add((default, b[j]));
127+
}
128+
else if (j == 0)
129+
{
130+
i--;
131+
suffix.Add((a[i], default));
132+
}
133+
else if (arrows[i, j] == 0)
134+
{
135+
i--;
136+
j--;
137+
suffix.Add((a[i], b[j]));
138+
}
139+
else if (arrows[i, j] == 1)
140+
{
141+
j--;
142+
suffix.Add((default, b[j]));
143+
}
144+
else if (arrows[i, j] == -1)
145+
{
146+
i--;
147+
suffix.Add((a[i], default));
148+
}
149+
}
150+
151+
suffix.Reverse();
152+
return [..prefix, ..suffix];
153+
}
47154
}
48155
}

0 commit comments

Comments
 (0)