Skip to content

Commit 419778f

Browse files
committed
Make expression value evaluator resilient to missing span ToArray
1 parent fb4e7af commit 419778f

File tree

3 files changed

+32
-3
lines changed

3 files changed

+32
-3
lines changed

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,4 +418,5 @@ async\method: Suffix with `Async` =\> `GetDataAsync()`
418418

419419
- Expression compilation can emit invalid IL on some runtimes; fall back to `Compile(preferInterpretation: true)` when evaluating expressions to avoid CI-only `InvalidProgramException`.
420420
- Byref-like expression trees (e.g., `Span<T>`, `ReadOnlySpan<T>`) cannot be boxed; convert them to arrays before evaluation to avoid interpreter type load errors.
421+
- Reflection helpers should tolerate missing overloads (e.g., `MemoryExtensions.ToArray`) to prevent type initializer failures on runtimes where a method shape differs.
421422
- `NUnit.Assert.ThrowsAsync<T>()` returns the exception instance (not a `Task`), so async tests should await a real async assertion (e.g., `action.Should().ThrowAsync<T>()`) to avoid CS1998 warnings.

learnings.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ This document is organized by topic to consolidate key learnings about the proje
129129
- **DynamicInvoke Cross-Platform Issues:** Using `Expression.Lambda(expression).Compile().DynamicInvoke()` can fail on CI/certain runtimes with `NotSupportedException: Specified method is not supported` - use strongly-typed `Expression.Lambda<Func<object>>(Expression.Convert(expression, typeof(object))).Compile()()` for direct invocation without reflection
130130
- **Expression.Compile InvalidProgramException:** Expression compilation can emit invalid IL on some runtimes; prefer `Compile(preferInterpretation: true)` or catch `InvalidProgramException` and recompile in interpreted mode to ensure value extraction works on CI
131131
- **ByRef-like expression evaluation:** Ref structs (e.g., `Span<T>`, `ReadOnlySpan<T>`) cannot be boxed to `object`; convert them to arrays (e.g., `MemoryExtensions.ToArray`) before value extraction to avoid interpreter `TypeLoadException`/`ArgumentException`.
132+
- **Reflection resilience:** Do not assume specific overloads exist across runtimes; when reflecting for helpers like `MemoryExtensions.ToArray`, select via tolerant predicate and allow null fallback to avoid type initializer failures.
132133
- **NUnit Assert.ThrowsAsync return type:** `Assert.ThrowsAsync<T>()` returns the exception instance, not a `Task`, so async test methods need an explicit awaitable (e.g., `action.Should().ThrowAsync<T>()`) to avoid CS1998 warnings.
133134

134135
## String Diffing Implementation (Increment 5)

src/SharpAssert.Runtime/Features/Shared/ExpressionValueEvaluator.cs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Linq.Expressions;
34
using System.Linq;
45
using System.Reflection;
@@ -8,9 +9,7 @@ namespace SharpAssert.Features.Shared;
89

910
static class ExpressionValueEvaluator
1011
{
11-
static readonly MethodInfo ToArrayMethod = typeof(MemoryExtensions)
12-
.GetMethods(BindingFlags.Public | BindingFlags.Static)
13-
.Single(m => m.Name == "ToArray" && m.IsGenericMethodDefinition && m.GetParameters().Length == 1);
12+
static readonly MethodInfo? ToArrayMethod = ResolveToArrayMethod();
1413

1514
public static object? Evaluate(Expression expression) => Compile(expression)();
1615

@@ -29,6 +28,31 @@ public static Func<object> Compile(Expression expression)
2928
}
3029
}
3130

31+
static MethodInfo? ResolveToArrayMethod()
32+
{
33+
foreach (var method in typeof(MemoryExtensions).GetMethods(BindingFlags.Public | BindingFlags.Static))
34+
{
35+
if (method.Name != "ToArray" || !method.IsGenericMethodDefinition)
36+
continue;
37+
38+
var parameters = method.GetParameters();
39+
if (parameters.Length != 1)
40+
continue;
41+
42+
if (IsReadOnlySpan(parameters[0].ParameterType))
43+
return method;
44+
}
45+
46+
return null;
47+
}
48+
49+
static bool IsReadOnlySpan(Type type)
50+
{
51+
return type.IsByRefLike &&
52+
type.IsGenericType &&
53+
type.GetGenericTypeDefinition() == typeof(ReadOnlySpan<>);
54+
}
55+
3256
static Expression Normalize(Expression expression)
3357
{
3458
if (!expression.Type.IsByRefLike)
@@ -44,6 +68,9 @@ static Expression Normalize(Expression expression)
4468
? expression
4569
: Expression.Convert(expression, readOnlySpanType);
4670

71+
if (ToArrayMethod == null)
72+
return Expression.Constant(null, typeof(object));
73+
4774
var toArray = ToArrayMethod.MakeGenericMethod(elementType);
4875
var call = Expression.Call(toArray, spanExpression);
4976

0 commit comments

Comments
 (0)