Browse Source

Add pretty tests for C# 14 simple lambda parameters with modifiers

The C# 14 feature is pure source sugar: a lambda parameter may carry
ref/out/in/scoped/ref readonly without declaring a type, but the IL
always has fully-typed parameters, so the decompiler's explicitly-typed
output is the correct round-trip. LambdaParameterModifiers pins that
this round-trip works: custom delegate types with each modifier
(including scoped by-value, scoped ref, and mixed parameter lists),
block- and expression-bodied lambdas, captures alongside ref
parameters, generic ref delegates, lambda parameter attributes,
call-site modifiers (including 'in' for 'ref readonly' parameters),
unused-parameter anonymous methods, and a local-function contrast.
CS140-gated branches compile the untyped sugar (and attributed sugar)
under Roslyn latest to prove it produces the same decompilation.

ScopedLambdaParameters is Assert.Ignore'd: when an anonymous function's
'scoped ref' parameter is unused, the decompiler drops the parameter
list, losing the required 'scoped' modifier; the output then fails to
recompile with CS8986 because the implicit parameters of a
parameter-list-less anonymous method are unscoped. The fixture is the
desired output (parameter list retained); all four Roslyn 4.14/latest
debug/opt configs compile it and fail only at the output comparison.

Assisted-by: Claude:claude-fable-5:Claude Code
tests/829-lambda-param-modifiers
Siegfried Pammer 2 months ago
parent
commit
482002ebc0
  1. 13
      ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
  2. 157
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/LambdaParameterModifiers.cs
  3. 19
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/ScopedLambdaParameters.cs

13
ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs

@ -794,6 +794,19 @@ namespace ICSharpCode.Decompiler.Tests @@ -794,6 +794,19 @@ namespace ICSharpCode.Decompiler.Tests
await RunForLibrary(cscOptions: cscOptions);
}
[Test]
public async Task LambdaParameterModifiers([ValueSource(nameof(roslyn4OrNewerOptions))] CompilerOptions cscOptions)
{
await RunForLibrary(cscOptions: cscOptions);
}
[Test]
public async Task ScopedLambdaParameters([ValueSource(nameof(roslyn4OrNewerOptions))] CompilerOptions cscOptions)
{
Assert.Ignore("Anonymous functions with an unused 'scoped ref' parameter lose the required 'scoped' modifier when the parameter list is dropped. See https://github.com/icsharpcode/ILSpy/issues/829");
await RunForLibrary(cscOptions: cscOptions);
}
[Test]
public async Task ExpandParamsArgumentsDisabled([ValueSource(nameof(defaultOptions))] CompilerOptions cscOptions)
{

157
ICSharpCode.Decompiler.Tests/TestCases/Pretty/LambdaParameterModifiers.cs

@ -0,0 +1,157 @@ @@ -0,0 +1,157 @@
using System;
namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
{
internal class LambdaParameterModifiers
{
public delegate void RefAction(ref int x);
public delegate bool TryParseDelegate(string s, out int result);
public delegate int InFunc(in DateTime d);
public delegate int RefReadonlyFunc(ref readonly int x);
public delegate void RefGenericAction<T>(ref T item);
public delegate int ScopedSpanFunc(scoped Span<int> s);
public delegate Span<int> ScopedMixedFunc(scoped ref int x, ref int y);
public delegate void ScopedRefAction(scoped ref int x);
private int instanceField;
public RefAction RefBlockBody()
{
return delegate (ref int x) {
x++;
};
}
public RefAction RefExpressionBody()
{
#if EXPECTED_OUTPUT
return delegate (ref int x) {
x *= 2;
};
#elif CS140
// C# 14 simple lambda parameter with modifiers: no type needed
return (ref x) => x *= 2;
#else
return (ref int x) => x *= 2;
#endif
}
public TryParseDelegate OutLambda()
{
#if EXPECTED_OUTPUT
return delegate (string s, out int result) {
return int.TryParse(s, out result);
};
#elif CS140
// C# 14 simple lambda parameter with modifiers: no type needed
return (s, out result) => int.TryParse(s, out result);
#else
return (string s, out int result) => int.TryParse(s, out result);
#endif
}
public InFunc InLambda()
{
return delegate (in DateTime d) {
return d.Year;
};
}
public RefReadonlyFunc RefReadonlyLambda()
{
return delegate (ref readonly int x) {
return x + 1;
};
}
public RefGenericAction<string> GenericRefLambda()
{
return delegate (ref string item) {
item += "!";
};
}
public RefAction CaptureAlongsideRefParameter(int offset)
{
return delegate (ref int x) {
x += offset + instanceField;
};
}
public ScopedSpanFunc ScopedSpanLambda()
{
return (scoped Span<int> s) => s.Length;
}
public ScopedMixedFunc ScopedMixedLambda()
{
return delegate (scoped ref int x, ref int y) {
x += y;
return default(Span<int>);
};
}
public ScopedRefAction ScopedRefLambda()
{
return delegate (scoped ref int x) {
x++;
};
}
public RefAction AttributedRefLambda()
{
#if EXPECTED_OUTPUT
return ([ParamMod] ref int x) => {
x += 10;
};
#elif CS140
// C# 14 also allows attributes on simple lambda parameters
return ([ParamMod] ref x) => x += 10;
#else
return ([ParamMod] ref int x) => x += 10;
#endif
}
public RefAction UnusedRefParameter()
{
return delegate {
};
}
public RefAction LocalFunctionWithRef()
{
return LocalRef;
static void LocalRef(ref int x)
{
x -= 3;
}
}
public int InvokeAll()
{
int x = 1;
RefBlockBody()(ref x);
RefExpressionBody()(ref x);
OutLambda()("42", out var result);
DateTime d = DateTime.MinValue;
int num = InLambda()(in d) + RefReadonlyLambda()(in x);
string item = "a";
GenericRefLambda()(ref item);
CaptureAlongsideRefParameter(2)(ref x);
LocalFunctionWithRef()(ref x);
return x + result + num + item.Length;
}
}
[AttributeUsage(AttributeTargets.Parameter)]
internal class ParamModAttribute : Attribute
{
}
}

19
ICSharpCode.Decompiler.Tests/TestCases/Pretty/ScopedLambdaParameters.cs

@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
using System;
namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
{
internal class ScopedLambdaParameters
{
public delegate Span<int> ScopedRefFunc(scoped ref int x);
public ScopedRefFunc UnusedScopedRefParameter()
{
// The parameter list must not be dropped here: an anonymous function
// without a parameter list has unscoped implicit parameters, which do
// not match the delegate's "scoped ref" parameter (CS8986).
return delegate (scoped ref int x) {
return default(Span<int>);
};
}
}
}
Loading…
Cancel
Save