Skip to content
Merged
27 changes: 27 additions & 0 deletions docs/compilers/CSharp/Compiler Breaking Changes - DotNet 10.md
Original file line number Diff line number Diff line change
Expand Up @@ -407,3 +407,30 @@ partial interface I

class C : I;
```

## Missing `ParamCollectionAttribute` is reported in more cases

***Introduced in Visual Studio 2026 version 18.0***

If you are compiling a `.netmodule` (note that this doesn't apply to normal DLL/EXE compilations),
and have a lambda or a local function with a `params` collection parameter,
and the `ParamCollectionAttribute` is not found, a compilation error is now reported
(because the attribute now must be [emitted](https://github.com/dotnet/roslyn/issues/79752) on the synthesized method
but the attribute type itself is not synthesized by the compiler into a `.netmodule`).
You can work around that by defining the attribute yourself.

```cs
using System;
using System.Collections.Generic;
class C
{
void M()
{
Func<IList<int>, int> lam = (params IList<int> xs) => xs.Count; // error if ParamCollectionAttribute does not exist
lam([1, 2, 3]);

int func(params IList<int> xs) => xs.Count; // error if ParamCollectionAttribute does not exist
func(4, 5, 6);
}
}
```
2 changes: 1 addition & 1 deletion src/Compilers/CSharp/Portable/BoundTree/UnboundLambda.cs
Original file line number Diff line number Diff line change
Expand Up @@ -843,7 +843,7 @@ private BoundLambda ReallyBind(NamedTypeSymbol delegateType, bool inExpressionTr

var lambdaParameters = lambdaSymbol.Parameters;
ParameterHelpers.EnsureRefKindAttributesExist(compilation, lambdaParameters, diagnostics, modifyCompilation: false);
// Not emitting ParamCollectionAttribute/ParamArrayAttribute for lambdas
ParameterHelpers.EnsureParamCollectionAttributeExists(compilation, lambdaParameters, diagnostics, modifyCompilation: false);

if (returnType.HasType)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ private void EnsureAttributesExist(TypeCompilationState compilationState)
}

ParameterHelpers.EnsureRefKindAttributesExist(moduleBuilder, Parameters);
// Not emitting ParamCollectionAttribute/ParamArrayAttribute for these methods because it is not a SynthesizedDelegateInvokeMethod

ParameterHelpers.EnsureParamCollectionAttributeExists(moduleBuilder, Parameters);

if (moduleBuilder.Compilation.ShouldEmitNativeIntegerAttributes())
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ private ImmutableArray<ParameterSymbol> MakeParameters()
p.ExplicitDefaultConstantValue,
// the synthesized parameter doesn't need to have the same ref custom modifiers as the base
refCustomModifiers: default,
baseParameterForAttributes: inheritAttributes ? p : null));
baseParameterForAttributes: inheritAttributes ? p : null,
isParams: this is SynthesizedClosureMethod && p.IsParams));
}

var extraSynthed = ExtraSynthesizedRefParameters;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -537,9 +537,9 @@ internal void EnsureRequiresLocationAttributeExists(BindingDiagnosticBag? diagno
EnsureEmbeddableAttributeExists(EmbeddableAttributes.RequiresLocationAttribute, diagnostics, location, modifyCompilation);
}

internal void EnsureParamCollectionAttributeExistsAndModifyCompilation(BindingDiagnosticBag? diagnostics, Location location)
internal void EnsureParamCollectionAttributeExists(BindingDiagnosticBag? diagnostics, Location location, bool modifyCompilation)
{
EnsureEmbeddableAttributeExists(EmbeddableAttributes.ParamCollectionAttribute, diagnostics, location, modifyCompilation: true);
EnsureEmbeddableAttributeExists(EmbeddableAttributes.ParamCollectionAttribute, diagnostics, location, modifyCompilation: modifyCompilation);
}

internal void EnsureIsByRefLikeAttributeExists(BindingDiagnosticBag? diagnostics, Location location, bool modifyCompilation)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ internal void GetDeclarationDiagnostics(BindingDiagnosticBag addTo)

var compilation = DeclaringCompilation;
ParameterHelpers.EnsureRefKindAttributesExist(compilation, Parameters, addTo, modifyCompilation: false);
// Not emitting ParamCollectionAttribute/ParamArrayAttribute for local functions
ParameterHelpers.EnsureParamCollectionAttributeExists(compilation, Parameters, addTo, modifyCompilation: false);
ParameterHelpers.EnsureNativeIntegerAttributeExists(compilation, Parameters, addTo, modifyCompilation: false);
ParameterHelpers.EnsureScopedRefAttributeExists(compilation, Parameters, addTo, modifyCompilation: false);
ParameterHelpers.EnsureNullableAttributeExists(compilation, this, Parameters, addTo, modifyCompilation: false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,11 +370,19 @@ private static void EnsureRefKindAttributesExist(CSharpCompilation compilation,
}
}

internal static void EnsureParamCollectionAttributeExistsAndModifyCompilation(CSharpCompilation compilation, ImmutableArray<ParameterSymbol> parameters, BindingDiagnosticBag diagnostics)
internal static void EnsureParamCollectionAttributeExists(PEModuleBuilder moduleBuilder, ImmutableArray<ParameterSymbol> parameters)
{
if (parameters.LastOrDefault(static (p) => p.IsParamsCollection) is { } parameter)
{
compilation.EnsureParamCollectionAttributeExistsAndModifyCompilation(diagnostics, GetParameterLocation(parameter));
moduleBuilder.EnsureParamCollectionAttributeExists(null, null);
}
}

internal static void EnsureParamCollectionAttributeExists(CSharpCompilation compilation, ImmutableArray<ParameterSymbol> parameters, BindingDiagnosticBag diagnostics, bool modifyCompilation)
{
if (parameters.LastOrDefault(static (p) => p.IsParamsCollection) is { } parameter)
{
compilation.EnsureParamCollectionAttributeExists(diagnostics, GetParameterLocation(parameter), modifyCompilation);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ internal sealed override void AfterAddingTypeMembersChecks(ConversionsBase conve

var compilation = DeclaringCompilation;
ParameterHelpers.EnsureRefKindAttributesExist(compilation, Parameters, diagnostics, modifyCompilation: true);
ParameterHelpers.EnsureParamCollectionAttributeExistsAndModifyCompilation(compilation, Parameters, diagnostics);
ParameterHelpers.EnsureParamCollectionAttributeExists(compilation, Parameters, diagnostics, modifyCompilation: true);
ParameterHelpers.EnsureNativeIntegerAttributeExists(compilation, Parameters, diagnostics, modifyCompilation: true);
ParameterHelpers.EnsureScopedRefAttributeExists(compilation, Parameters, diagnostics, modifyCompilation: true);
ParameterHelpers.EnsureNullableAttributeExists(compilation, this, Parameters, diagnostics, modifyCompilation: true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ internal override void AfterAddingTypeMembersChecks(ConversionsBase conversions,
}

ParameterHelpers.EnsureRefKindAttributesExist(compilation, Parameters, diagnostics, modifyCompilation: true);
ParameterHelpers.EnsureParamCollectionAttributeExistsAndModifyCompilation(compilation, Parameters, diagnostics);
ParameterHelpers.EnsureParamCollectionAttributeExists(compilation, Parameters, diagnostics, modifyCompilation: true);

if (compilation.ShouldEmitNativeIntegerAttributes(ReturnType))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ internal override void AfterAddingTypeMembersChecks(ConversionsBase conversions,
}

ParameterHelpers.EnsureRefKindAttributesExist(compilation, Parameters, diagnostics, modifyCompilation: true);
ParameterHelpers.EnsureParamCollectionAttributeExistsAndModifyCompilation(compilation, Parameters, diagnostics);
ParameterHelpers.EnsureParamCollectionAttributeExists(compilation, Parameters, diagnostics, modifyCompilation: true);

if (compilation.ShouldEmitNativeIntegerAttributes(ReturnType))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1044,7 +1044,7 @@ internal override void AfterAddingTypeMembersChecks(ConversionsBase conversions,
}

ParameterHelpers.EnsureRefKindAttributesExist(compilation, Parameters, diagnostics, modifyCompilation: true);
ParameterHelpers.EnsureParamCollectionAttributeExistsAndModifyCompilation(compilation, Parameters, diagnostics);
ParameterHelpers.EnsureParamCollectionAttributeExists(compilation, Parameters, diagnostics, modifyCompilation: true);

if (compilation.ShouldEmitNativeIntegerAttributes(Type))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,11 +194,11 @@ internal override void AddSynthesizedAttributes(PEModuleBuilder moduleBuilder, r
AddSynthesizedAttribute(ref attributes, compilation.TrySynthesizeAttribute(WellKnownMember.System_Diagnostics_CodeAnalysis_UnscopedRefAttribute__ctor));
}

if (this.IsParamsArray && this.ContainingSymbol is SynthesizedDelegateInvokeMethod)
if (this.IsParamsArray)
{
AddSynthesizedAttribute(ref attributes, compilation.TrySynthesizeAttribute(WellKnownMember.System_ParamArrayAttribute__ctor));
}
else if (this.IsParamsCollection && this.ContainingSymbol is SynthesizedDelegateInvokeMethod)
else if (this.IsParamsCollection)
{
AddSynthesizedAttribute(ref attributes, moduleBuilder.SynthesizeParamCollectionAttribute(this));
}
Expand Down Expand Up @@ -363,6 +363,8 @@ public SynthesizedComplexParameterSymbol(
Debug.Assert(isParams || !refCustomModifiers.IsEmpty || baseParameterForAttributes is object || defaultValue is not null || hasUnscopedRefAttribute);
Debug.Assert(baseParameterForAttributes is null || baseParameterForAttributes.ExplicitDefaultConstantValue == defaultValue);
Debug.Assert(baseParameterForAttributes is null || baseParameterForAttributes.RefKind == refKind);
Debug.Assert(!isParams || container is SynthesizedDelegateInvokeMethod or SynthesizedClosureMethod,
"If this fails, make sure we don't synthesize ParamsArrayAttribute for symbols we don't intend to.");

_refCustomModifiers = refCustomModifiers;
_baseParameterForAttributes = baseParameterForAttributes;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,7 @@ namespace Microsoft.CodeAnalysis.CSharp.UnitTests.Semantics
{
public class ParamsCollectionTests : CompilingTestBase
{
private const string ParamCollectionAttributeSource = @"
namespace System.Runtime.CompilerServices
{
public sealed class ParamCollectionAttribute : Attribute
{
public ParamCollectionAttribute() { }
}
}
";
private static string ParamCollectionAttributeSource => TestSources.ParamsCollectionAttribute;

private static void VerifyParamsAndAttribute(ParameterSymbol parameter, bool isParamArray = false, bool isParamCollection = false)
{
Expand Down Expand Up @@ -2741,10 +2733,8 @@ static void Main()
symbolValidator: (m) =>
{
MethodSymbol l1 = m.GlobalNamespace.GetMember<MethodSymbol>("Program.<>c.<Main>b__0_0");
AssertEx.Equal("void Program.<>c.<Main>b__0_0(System.Collections.Generic.IEnumerable<System.Int64> x)", l1.ToTestDisplayString());
VerifyParamsAndAttribute(l1.Parameters.Last());

Assert.Empty(((NamespaceSymbol)m.GlobalNamespace.GetMember("System.Runtime.CompilerServices")).GetMembers("ParamCollectionAttribute"));
AssertEx.Equal("void Program.<>c.<Main>b__0_0(params System.Collections.Generic.IEnumerable<System.Int64> x)", l1.ToTestDisplayString());
VerifyParamsAndAttribute(l1.Parameters.Last(), isParamCollection: true);
}).VerifyDiagnostics(
// (7,72): warning CS9100: Parameter 1 has params modifier in lambda but not in target delegate type.
// System.Action<IEnumerable<long>> l = (params IEnumerable<long> x) => {};
Expand Down Expand Up @@ -2797,8 +2787,8 @@ static void Main()
symbolValidator: (m) =>
{
MethodSymbol l1 = m.GlobalNamespace.GetMember<MethodSymbol>("Program.<>c.<Main>b__0_0");
AssertEx.Equal("void Program.<>c.<Main>b__0_0(System.Int64[] x)", l1.ToTestDisplayString());
VerifyParamsAndAttribute(l1.Parameters.Last());
AssertEx.Equal("void Program.<>c.<Main>b__0_0(params System.Int64[] x)", l1.ToTestDisplayString());
VerifyParamsAndAttribute(l1.Parameters.Last(), isParamArray: true);
}).VerifyDiagnostics(
// (5,50): warning CS9100: Parameter 1 has params modifier in lambda but not in target delegate type.
// System.Action<long[]> l = (params long[] x) => {};
Expand Down Expand Up @@ -2943,10 +2933,8 @@ static void Main()
symbolValidator: (m) =>
{
MethodSymbol l1 = m.GlobalNamespace.GetMember<MethodSymbol>("Program.<Main>g__local|0_0");
AssertEx.Equal("void Program.<Main>g__local|0_0(System.Collections.Generic.IEnumerable<System.Int64> x)", l1.ToTestDisplayString());
VerifyParamsAndAttribute(l1.Parameters.Last());

Assert.Empty(((NamespaceSymbol)m.GlobalNamespace.GetMember("System.Runtime.CompilerServices")).GetMembers("ParamCollectionAttribute"));
AssertEx.Equal("void Program.<Main>g__local|0_0(params System.Collections.Generic.IEnumerable<System.Int64> x)", l1.ToTestDisplayString());
VerifyParamsAndAttribute(l1.Parameters.Last(), isParamCollection: true);
}).VerifyDiagnostics();

var tree = comp.SyntaxTrees.Single();
Expand Down Expand Up @@ -2992,8 +2980,8 @@ static void Main()
symbolValidator: (m) =>
{
MethodSymbol l1 = m.GlobalNamespace.GetMember<MethodSymbol>("Program.<Main>g__local|0_0");
AssertEx.Equal("void Program.<Main>g__local|0_0(System.Int64[] x)", l1.ToTestDisplayString());
VerifyParamsAndAttribute(l1.Parameters.Last());
AssertEx.Equal("void Program.<Main>g__local|0_0(params System.Int64[] x)", l1.ToTestDisplayString());
VerifyParamsAndAttribute(l1.Parameters.Last(), isParamArray: true);
}).VerifyDiagnostics();
}

Expand Down Expand Up @@ -3078,7 +3066,7 @@ .locals init (<>y__InlineArray2<CustomHandler> V_0,
IL_006c: ldloca.s V_0
IL_006e: ldc.i4.2
IL_006f: call ""System.ReadOnlySpan<CustomHandler> <PrivateImplementationDetails>.InlineArrayAsReadOnlySpan<<>y__InlineArray2<CustomHandler>, CustomHandler>(in <>y__InlineArray2<CustomHandler>, int)""
IL_0074: call ""void Program.<<Main>$>g__M|0_0(scoped System.ReadOnlySpan<CustomHandler>)""
IL_0074: call ""void Program.<<Main>$>g__M|0_0(params System.ReadOnlySpan<CustomHandler>)""
IL_0079: ret
}
");
Expand Down Expand Up @@ -4533,15 +4521,13 @@ void verify(CSharpCompilation comp, bool attributeIsEmbedded)
AssertEx.Equal("void <>f__AnonymousDelegate1<T1>.Invoke(params T1[] arg)", delegateInvokeMethod2.ToTestDisplayString());
VerifyParamsAndAttribute(delegateInvokeMethod2.Parameters.Last(), isParamArray: true);

// Note, no attributes on lambdas

MethodSymbol l1 = m.GlobalNamespace.GetMember<MethodSymbol>("Program.<>c.<Main>b__0_0");
AssertEx.Equal("void Program.<>c.<Main>b__0_0(scoped System.ReadOnlySpan<System.Int64> a)", l1.ToTestDisplayString());
VerifyParamsAndAttribute(l1.Parameters.Last());
AssertEx.Equal("void Program.<>c.<Main>b__0_0(params System.ReadOnlySpan<System.Int64> a)", l1.ToTestDisplayString());
VerifyParamsAndAttribute(l1.Parameters.Last(), isParamCollection: true);

MethodSymbol l2 = m.GlobalNamespace.GetMember<MethodSymbol>("Program.<>c.<Main>b__0_1");
AssertEx.Equal("void Program.<>c.<Main>b__0_1(System.Int64[] a)", l2.ToTestDisplayString());
VerifyParamsAndAttribute(l2.Parameters.Last());
AssertEx.Equal("void Program.<>c.<Main>b__0_1(params System.Int64[] a)", l2.ToTestDisplayString());
VerifyParamsAndAttribute(l2.Parameters.Last(), isParamArray: true);

if (attributeIsEmbedded)
{
Expand Down Expand Up @@ -4686,15 +4672,13 @@ void verify(CSharpCompilation comp, bool attributeIsEmbedded)
AssertEx.Equal("void <>f__AnonymousDelegate1<T1>.Invoke(params T1[] arg)", delegateInvokeMethod2.ToTestDisplayString());
VerifyParamsAndAttribute(delegateInvokeMethod2.Parameters.Last(), isParamArray: true);

// Note, no attributes on local functions

MethodSymbol l1 = m.GlobalNamespace.GetMember<MethodSymbol>("Program.<Main>g__Test1|0_0");
AssertEx.Equal("void Program.<Main>g__Test1|0_0(scoped System.ReadOnlySpan<System.Int64> a)", l1.ToTestDisplayString());
VerifyParamsAndAttribute(l1.Parameters.Last());
AssertEx.Equal("void Program.<Main>g__Test1|0_0(params System.ReadOnlySpan<System.Int64> a)", l1.ToTestDisplayString());
VerifyParamsAndAttribute(l1.Parameters.Last(), isParamCollection: true);

MethodSymbol l2 = m.GlobalNamespace.GetMember<MethodSymbol>("Program.<Main>g__Test2|0_1");
AssertEx.Equal("void Program.<Main>g__Test2|0_1(System.Int64[] a)", l2.ToTestDisplayString());
VerifyParamsAndAttribute(l2.Parameters.Last());
AssertEx.Equal("void Program.<Main>g__Test2|0_1(params System.Int64[] a)", l2.ToTestDisplayString());
VerifyParamsAndAttribute(l2.Parameters.Last(), isParamArray: true);

if (attributeIsEmbedded)
{
Expand Down Expand Up @@ -12970,9 +12954,9 @@ void Test1(params IEnumerable<long> a) { }
src,
"<>f__AnonymousDelegate0.Invoke",
"void <>f__AnonymousDelegate0.Invoke(params System.Collections.Generic.IEnumerable<System.Int64> arg)",
// (7,18): error CS0518: Predefined type 'System.Runtime.CompilerServices.ParamCollectionAttribute' is not defined or imported
// var x1 = Test1;
Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, "Test1").WithArguments("System.Runtime.CompilerServices.ParamCollectionAttribute").WithLocation(7, 18)
// (11,20): error CS0518: Predefined type 'System.Runtime.CompilerServices.ParamCollectionAttribute' is not defined or imported
// void Test1(params IEnumerable<long> a) { }
Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, "params IEnumerable<long> a").WithArguments("System.Runtime.CompilerServices.ParamCollectionAttribute").WithLocation(11, 20)
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19463,6 +19463,10 @@ .method assembly hidebysig
int32[] ys
) cil managed
{
.param [2]
.custom instance void [{{s_libPrefix}}]System.ParamArrayAttribute::.ctor() = (
01 00 00 00
)
// Method begins at RVA 0x20b0
// Code size 1 (0x1)
.maxstack 8
Expand Down Expand Up @@ -19520,6 +19524,10 @@ int32[] ys
.custom instance void [{{s_libPrefix}}]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
01 00 00 00
)
.param [2]
.custom instance void [{{s_libPrefix}}]System.ParamArrayAttribute::.ctor() = (
01 00 00 00
)
// Method begins at RVA 0x2067
// Code size 1 (0x1)
.maxstack 8
Expand Down
Loading