Skip to content

Commit da7cc7f

Browse files
jhonabreulclaude
andcommitted
Hint candidate overloads in the "No method matches" TypeError
When argument binding fails, the raised TypeError only reported the argument types that were passed, giving no clue what the method actually expected. For example RangeConsolidator(5.5) produced: No method matches given arguments for .ctor: (<class 'float'>) Append the candidate overload signatures to the message so the caller can see what was expected (e.g. that an int overload exists when a float was passed). This applies to every "no match" case, not just numeric conversions. - Single candidate -> ". The expected signature is:" + one signature. - Multiple candidates -> ". The following overloads are available:" + list (distinct, capped at 10 with "... and N more"). Signatures are rendered readably: friendly type names (by-ref/nullable unwrapped, generics as Name[Arg1, Arg2]), params marked, optional parameters shown with their default. The whole hint is best-effort and wrapped in a try/catch so it can never mask the original binding failure. - MethodBinder: add AppendOverloads/FormatSignature/FormatType/FormatDefaultValue and emit the hint from Invoke's no-binding path. - Add message tests to TestFloatToIntConversion (single and multiple overloads). - Update TestCallbacks.TestNoOverloadException: the argument types are no longer at the end of the message, so assert containment instead of suffix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent fb5cce6 commit da7cc7f

3 files changed

Lines changed: 161 additions & 1 deletion

File tree

src/embed_tests/TestCallbacks.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ public void TestNoOverloadException() {
2525
var error = Assert.Throws<PythonException>(() => callWith42(pyFunc));
2626
Assert.AreEqual("TypeError", error.Type.Name);
2727
string expectedArgTypes = "(<class 'list'>)";
28-
StringAssert.EndsWith(expectedArgTypes, error.Message);
28+
// The message includes the offending argument types, followed by the
29+
// candidate overload signatures, so assert containment rather than suffix.
30+
StringAssert.Contains(expectedArgTypes, error.Message);
2931
error.Traceback.Dispose();
3032
}
3133
}

src/embed_tests/TestFloatToIntConversion.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,24 @@ public void NonIntegralFloat_IsRejected(string func)
7777
var ex = Assert.Throws<PythonException>(() => Call(func, 5.5));
7878
Assert.AreEqual("TypeError", ex.Type.Name);
7979
}
80+
81+
// When no overload matches, the error should hint the expected signature(s).
82+
[Test]
83+
public void ErrorMessage_SingleOverload_ShowsExpectedSignature()
84+
{
85+
var ex = Assert.Throws<PythonException>(() => Call("single_ctor", 5.5));
86+
StringAssert.Contains("The expected signature is:", ex.Message);
87+
StringAssert.Contains("Int32 value", ex.Message);
88+
}
89+
90+
[Test]
91+
public void ErrorMessage_MultipleOverloads_ListsCandidates()
92+
{
93+
var ex = Assert.Throws<PythonException>(() => Call("overloaded_ctor", 5.5));
94+
StringAssert.Contains("The following overloads are available:", ex.Message);
95+
// The int overload is surfaced, hinting an integer was expected.
96+
StringAssert.Contains("Int32 range", ex.Message);
97+
}
8098
}
8199

82100
public class IntTaker

src/runtime/MethodBinder.cs

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,6 +1017,15 @@ internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference a
10171017

10181018
value.Append(": ");
10191019
AppendArgumentTypes(to: value, args);
1020+
1021+
// List the candidate overloads so the caller can see what was
1022+
// expected (e.g. that an int overload exists when a float was
1023+
// passed). Applies to every "no match" case, not just numeric ones.
1024+
var candidates = methodinfo != null && methodinfo.Length > 0
1025+
? methodinfo.Cast<MethodBase>()
1026+
: list?.Select(m => m.MethodBase);
1027+
AppendOverloads(value, candidates);
1028+
10201029
Exceptions.RaiseTypeError(value.ToString());
10211030
}
10221031

@@ -1221,6 +1230,137 @@ protected static void AppendArgumentTypes(StringBuilder to, BorrowedReference ar
12211230
}
12221231
to.Append(')');
12231232
}
1233+
1234+
/// <summary>
1235+
/// Appends the signatures of the candidate overloads to the given error
1236+
/// message, so a failed bind hints the caller at what the method expects.
1237+
/// </summary>
1238+
private static void AppendOverloads(StringBuilder to, IEnumerable<MethodBase> methods)
1239+
{
1240+
if (methods == null)
1241+
{
1242+
return;
1243+
}
1244+
1245+
// Building this only runs on the error path; never let it throw and mask
1246+
// the original binding failure.
1247+
try
1248+
{
1249+
// Distinct signatures, preserving order. Snake-cased duplicates and
1250+
// repeated overloads collapse into a single entry.
1251+
var signatures = new List<string>();
1252+
var seen = new HashSet<string>();
1253+
foreach (var method in methods)
1254+
{
1255+
if (method == null)
1256+
{
1257+
continue;
1258+
}
1259+
var signature = FormatSignature(method);
1260+
if (seen.Add(signature))
1261+
{
1262+
signatures.Add(signature);
1263+
}
1264+
}
1265+
1266+
if (signatures.Count == 0)
1267+
{
1268+
return;
1269+
}
1270+
1271+
const int maxShown = 10;
1272+
to.Append(signatures.Count == 1
1273+
? ". The expected signature is:"
1274+
: ". The following overloads are available:");
1275+
for (var i = 0; i < signatures.Count && i < maxShown; i++)
1276+
{
1277+
to.Append("\n ").Append(signatures[i]);
1278+
}
1279+
if (signatures.Count > maxShown)
1280+
{
1281+
to.Append($"\n ... and {signatures.Count - maxShown} more");
1282+
}
1283+
}
1284+
catch
1285+
{
1286+
// Best-effort hint only.
1287+
}
1288+
}
1289+
1290+
/// <summary>
1291+
/// Formats a method/constructor as a readable signature, e.g.
1292+
/// <c>RangeConsolidator(Int32 range, Func[IBaseData, Decimal] selector = None)</c>.
1293+
/// </summary>
1294+
private static string FormatSignature(MethodBase method)
1295+
{
1296+
var to = new StringBuilder();
1297+
to.Append(method.Name).Append('(');
1298+
var parameters = method.GetParameters();
1299+
for (var i = 0; i < parameters.Length; i++)
1300+
{
1301+
if (i > 0)
1302+
{
1303+
to.Append(", ");
1304+
}
1305+
var parameter = parameters[i];
1306+
if (parameter.IsDefined(typeof(ParamArrayAttribute), false))
1307+
{
1308+
to.Append("params ");
1309+
}
1310+
to.Append(FormatType(parameter.ParameterType)).Append(' ').Append(parameter.Name);
1311+
if (parameter.IsOptional)
1312+
{
1313+
to.Append(" = ").Append(FormatDefaultValue(parameter.DefaultValue));
1314+
}
1315+
}
1316+
to.Append(')');
1317+
return to.ToString();
1318+
}
1319+
1320+
/// <summary>
1321+
/// Produces a concise, readable name for a CLR type, unwrapping by-ref and
1322+
/// nullable types and rendering generics as <c>Name[Arg1, Arg2]</c>.
1323+
/// </summary>
1324+
private static string FormatType(Type type)
1325+
{
1326+
if (type.IsByRef)
1327+
{
1328+
type = type.GetElementType();
1329+
}
1330+
1331+
var underlying = Nullable.GetUnderlyingType(type);
1332+
if (underlying != null)
1333+
{
1334+
return FormatType(underlying) + "?";
1335+
}
1336+
1337+
if (type.IsGenericType)
1338+
{
1339+
var name = type.Name;
1340+
var tick = name.IndexOf('`');
1341+
if (tick >= 0)
1342+
{
1343+
name = name.Substring(0, tick);
1344+
}
1345+
var args = type.GetGenericArguments().Select(FormatType);
1346+
return $"{name}[{string.Join(", ", args)}]";
1347+
}
1348+
1349+
return type.Name;
1350+
}
1351+
1352+
private static string FormatDefaultValue(object value)
1353+
{
1354+
if (value == null || value is DBNull)
1355+
{
1356+
return "None";
1357+
}
1358+
if (value is string s)
1359+
{
1360+
return $"\"{s}\"";
1361+
}
1362+
return value.ToString();
1363+
}
12241364
}
12251365

12261366

0 commit comments

Comments
 (0)