Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions docs/compilers/CSharp/Compiler Breaking Changes - DotNet 10.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,26 +334,33 @@ class partial { }

## `extension` treated as a contextual keyword

PROTOTYPE record which version this break is introduced in
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PROTOTYPE

Shouldn't one be able to use this word? #Closed

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not in the main branch


***Introduced in Visual Studio 2022 version 17.14.***
Starting with C# 14, the `extension` keyword serves a special purpose in denoting extension containers.
This changes how the compiler interprets certain code constructs.

If you need to use "extension" as an identifier rather than a keyword, escape it with the `@` prefix: `@extension`. This tells the compiler to treat it as a regular identifier instead of a keyword.

The compiler will parse this as an extension container rather than a constructor.
```csharp
class extension
class @extension
{
extension(object o) { } // parsed as an extension container
}
```

The compiler will fail to parse this as a method with return type `extension`.
```csharp
class extension
class @extension
{
extension M() { } // will not compile
}
```

***Introduced in Visual Studio 2022 version 17.15.***
The "extension" identifier may not be used as a type name, so the following will not compile:
```csharp
using extension = ...; // alias may not be named "extension"
class extension { } // type may not be named "extension"
class C<extension> { } // type parameter may not be named "extension"
```

1 change: 1 addition & 0 deletions docs/contributing/Compiler Test Plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ This document provides guidance for thinking about language interactions and tes
- Readonly members on structs (methods, property/indexer accessors, custom event accessors)
- SkipLocalsInit
- Method override or explicit implementation with `where T : { class, struct, default }`
- `extension` blocks

# Code
- Operators (see Eric's list below)
Expand Down
4 changes: 2 additions & 2 deletions eng/todo-check.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Set-StrictMode -version 2.0
$ErrorActionPreference="Stop"
if ($env:SYSTEM_PULLREQUEST_TARGETBRANCH -eq "main") {
Write-Host "Checking no PROTOTYPE markers in source"
$prototypes = Get-ChildItem -Path src, eng, scripts -Exclude *.dll,*.exe,*.pdb,*.xlf,todo-check.ps1 -Recurse | Select-String -Pattern 'PROTOTYPE' -CaseSensitive -SimpleMatch
$prototypes = Get-ChildItem -Path src, eng, scripts, docs\compilers -Exclude *.dll,*.exe,*.pdb,*.xlf,todo-check.ps1 -Recurse | Select-String -Pattern 'PROTOTYPE' -CaseSensitive -SimpleMatch
if ($prototypes) {
Write-Host "Found PROTOTYPE markers in source:"
Write-Host $prototypes
Expand All @@ -16,7 +16,7 @@ if ($env:SYSTEM_PULLREQUEST_TARGETBRANCH -eq "main") {
}

# Verify no TODO2 marker left
$prototypes = Get-ChildItem -Path src, eng, scripts -Exclude *.dll,*.exe,*.pdb,*.xlf,todo-check.ps1 -Recurse | Select-String -Pattern 'TODO2' -CaseSensitive -SimpleMatch
$prototypes = Get-ChildItem -Path src, eng, scripts, docs\compilers -Exclude *.dll,*.exe,*.pdb,*.xlf,todo-check.ps1 -Recurse | Select-String -Pattern 'TODO2' -CaseSensitive -SimpleMatch
if ($prototypes) {
Write-Host "Found TODO2 markers in source:"
Write-Host $prototypes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2121,7 +2121,7 @@ private BoundExpression BindNonMethod(SimpleNameSyntax node, Symbol symbol, Bind
{
Error(diagnostics, ErrorCode.ERR_InvalidPrimaryConstructorParameterReference, node, parameter);
}
else if (parameter.ContainingSymbol is NamedTypeSymbol { IsExtension: true } &&
else if (parameter.IsExtensionParameter() &&
(InParameterDefaultValue || InAttributeArgument ||
this.ContainingMember() is not { Kind: not SymbolKind.NamedType, IsStatic: false } || // We are not in an instance member
(object)this.ContainingMember().ContainingSymbol != parameter.ContainingSymbol) &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ internal void ValidateParameterNameConflicts(
diagnostics.Add(ErrorCode.ERR_LocalSameNameAsExtensionTypeParameter, GetLocation(p), name);
}
}
else if (p.ContainingSymbol is NamedTypeSymbol { IsExtension: true })
else if (p.IsExtensionParameter())
{
diagnostics.Add(ErrorCode.ERR_TypeParameterSameNameAsExtensionParameter, tp.GetFirstLocationOrNone(), name);
}
Expand Down
13 changes: 10 additions & 3 deletions src/Compilers/CSharp/Portable/Binder/ExecutableCodeBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,21 @@ public static void ValidateIteratorMethod(CSharpCompilation compilation, MethodS
return;
}

foreach (var parameter in iterator.Parameters)
var parameters = !iterator.IsStatic
? iterator.GetParametersIncludingExtensionParameter()
Copy link
Member

@jjonescz jjonescz Apr 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the GetParametersIncludingExtensionParameter helper check that the member is not static? #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would not be convenient. That method is also used in overload resolution, and for the purpose of overload resolution, we do want to account for the extension parameter even when the method is static (the static receiver counts as an argument in that case).

: iterator.Parameters;

foreach (var parameter in parameters)
{
bool isReceiverParameter = parameter.IsExtensionParameter();
if (parameter.RefKind != RefKind.None)
{
diagnostics.Add(ErrorCode.ERR_BadIteratorArgType, parameter.GetFirstLocation());
var location = isReceiverParameter ? iterator.GetFirstLocation() : parameter.GetFirstLocation();
diagnostics.Add(ErrorCode.ERR_BadIteratorArgType, location);
}
else if (parameter.Type.IsPointerOrFunctionPointer())
else if (parameter.Type.IsPointerOrFunctionPointer() && !isReceiverParameter)
{
// We already reported an error elsewhere if the receiver parameter of an extension is a pointer type.
diagnostics.Add(ErrorCode.ERR_UnsafeIteratorArgType, parameter.GetFirstLocation());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -696,7 +696,9 @@ internal static bool HasInternalAccessTo(this AssemblySymbol fromAssembly, Assem

internal static ErrorCode GetProtectedMemberInSealedTypeError(NamedTypeSymbol containingType)
{
return containingType.TypeKind == TypeKind.Struct ? ErrorCode.ERR_ProtectedInStruct : ErrorCode.WRN_ProtectedInSealed;
return containingType.IsExtension ? ErrorCode.ERR_ProtectedInExtension
: containingType.TypeKind == TypeKind.Struct ? ErrorCode.ERR_ProtectedInStruct
: ErrorCode.WRN_ProtectedInSealed;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3878,16 +3878,16 @@ private static EffectiveParameters GetEffectiveParametersInNormalForm<TMember>(

hasAnyRefOmittedArgument = false;

bool isNewExtensionMember = member.GetIsNewExtensionMember();
ImmutableArray<ParameterSymbol> parameters = isNewExtensionMember ? GetParametersIncludingReceiver(member) : member.GetParameters();
ImmutableArray<ParameterSymbol> parameters = member.GetParametersIncludingExtensionParameter();

// We simulate an extra parameter for vararg methods
int parameterCount = parameters.Length + (member.GetIsVararg() ? 1 : 0);

if (argumentCount == parameterCount && argToParamMap.IsDefaultOrEmpty)
{
bool hasSomeRefKinds = !member.GetParameterRefKinds().IsDefaultOrEmpty;
if (member.GetIsNewExtensionMember())
bool isNewExtensionMember = member.GetIsNewExtensionMember();
if (isNewExtensionMember)
{
Debug.Assert(member.ContainingType.ExtensionParameter is not null);
hasSomeRefKinds |= member.ContainingType.ExtensionParameter.RefKind != RefKind.None;
Expand Down Expand Up @@ -4040,7 +4040,7 @@ private static EffectiveParameters GetEffectiveParametersInExpandedForm<TMember>
var types = ArrayBuilder<TypeWithAnnotations>.GetInstance();
var refs = ArrayBuilder<RefKind>.GetInstance();
bool anyRef = false;
var parameters = member.GetIsNewExtensionMember() ? GetParametersIncludingReceiver(member) : member.GetParameters();
var parameters = member.GetParametersIncludingExtensionParameter();
bool hasAnyRefArg = argumentRefKinds.Any();
hasAnyRefOmittedArgument = false;
TypeWithAnnotations paramsIterationType = default;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -895,7 +895,7 @@ private static void ReportMissingRequiredParameter(
// to required formal parameter 'y'.

TMember badMember = bad.Member;
ImmutableArray<ParameterSymbol> parameters = badMember.GetIsNewExtensionMember() ? OverloadResolution.GetParametersIncludingReceiver(badMember) : badMember.GetParameters();
ImmutableArray<ParameterSymbol> parameters = badMember.GetParametersIncludingExtensionParameter();
int badParamIndex = bad.Result.BadParameter;
string badParamName;
if (badParamIndex == parameters.Length)
Expand Down Expand Up @@ -1115,7 +1115,7 @@ private bool HadBadArguments(
// as there is no explicit call to Add method.

int argumentOffset = arguments.IncludesReceiverAsArgument ? 1 : 0;
var parameters = method.GetIsNewExtensionMember() ? OverloadResolution.GetParametersIncludingReceiver(method) : method.GetParameters();
var parameters = method.GetParametersIncludingExtensionParameter();

for (int i = argumentOffset; i < parameters.Length; i++)
{
Expand Down Expand Up @@ -1170,7 +1170,7 @@ private static void ReportBadArgumentError(

// Early out: if the bad argument is an __arglist parameter then simply report that:

var parameters = method.GetIsNewExtensionMember() ? OverloadResolution.GetParametersIncludingReceiver(method) : method.GetParameters();
var parameters = method.GetParametersIncludingExtensionParameter();
if (method.GetIsVararg() && parm == parameters.Length)
{
// NOTE: No SymbolDistinguisher required, since one of the arguments is "__arglist".
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,6 @@ public ImmutableArray<int> ToImmutableArray()
}
}

internal static ImmutableArray<ParameterSymbol> GetParametersIncludingReceiver(Symbol symbol)
{
Debug.Assert(symbol.GetIsNewExtensionMember());
// Tracked by https://github.com/dotnet/roslyn/issues/76130 : consider optimizing
return [symbol.ContainingType.ExtensionParameter, .. symbol.GetParameters()];
}

private static ImmutableArray<TypeWithAnnotations> GetParameterTypesIncludingReceiver(Symbol symbol)
{
Debug.Assert(symbol.GetIsNewExtensionMember());
Expand All @@ -78,8 +71,7 @@ private static ArgumentAnalysisResult AnalyzeArguments(
Debug.Assert((object)symbol != null);
Debug.Assert(arguments != null);

bool isNewExtensionMember = symbol.GetIsNewExtensionMember();
ImmutableArray<ParameterSymbol> parameters = isNewExtensionMember ? GetParametersIncludingReceiver(symbol) : symbol.GetParameters();
ImmutableArray<ParameterSymbol> parameters = symbol.GetParametersIncludingExtensionParameter();
bool isVararg = symbol.GetIsVararg();

// The easy out is that we have no named arguments and are in normal form.
Expand Down
23 changes: 22 additions & 1 deletion src/Compilers/CSharp/Portable/CSharpResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -2525,7 +2525,7 @@ A catch() block after a catch (System.Exception e) block can catch non-CLS excep
<value> The parameter modifier '{0}' cannot be used with '{1}'</value>
</data>
<data name="ERR_BadTypeforThis" xml:space="preserve">
<value>The first parameter of an extension method cannot be of type '{0}'</value>
<value>The receiver parameter of an extension cannot be of type '{0}'</value>
</data>
<data name="ERR_BadParamModThis" xml:space="preserve">
<value>A parameter array cannot be used with 'this' modifier on an extension method</value>
Expand Down Expand Up @@ -5914,6 +5914,9 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ
<data name="ERR_InExtensionMustBeValueType" xml:space="preserve">
<value>The first 'in' or 'ref readonly' parameter of the extension method '{0}' must be a concrete (non-generic) value type.</value>
</data>
<data name="ERR_InExtensionParameterMustBeValueType" xml:space="preserve">
<value>The 'in' or 'ref readonly' receiver parameter of extension must be a concrete (non-generic) value type.</value>
</data>
<data name="ERR_FieldsInRoStruct" xml:space="preserve">
<value>Instance fields of readonly structs must be readonly.</value>
</data>
Expand All @@ -5932,6 +5935,9 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ
<data name="ERR_RefExtensionMustBeValueTypeOrConstrainedToOne" xml:space="preserve">
<value>The first parameter of a 'ref' extension method '{0}' must be a value type or a generic type constrained to struct.</value>
</data>
<data name="ERR_RefExtensionParameterMustBeValueTypeOrConstrainedToOne" xml:space="preserve">
<value>The 'ref' receiver parameter of an extension block must be a value type or a generic type constrained to struct.</value>
</data>
<data name="ERR_OutAttrOnInParam" xml:space="preserve">
<value>An in parameter cannot have the Out attribute.</value>
</data>
Expand Down Expand Up @@ -8134,4 +8140,19 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ
<data name="ERR_PPIgnoredFollowsIf" xml:space="preserve">
<value>'#:' directives cannot be after '#if' directive</value>
</data>
<data name="ERR_ProtectedInExtension" xml:space="preserve">
<value>'{0}': new protected member declared in an extension block</value>
</data>
<data name="ERR_InstanceMemberWithUnnamedExtensionsParameter" xml:space="preserve">
<value>'{0}': cannot declare instance members in an extension block with an unnamed receiver parameter</value>
</data>
<data name="ERR_InitInExtension" xml:space="preserve">
<value>'{0}': cannot declare init-only accessors in an extension block</value>
</data>
<data name="ERR_ModifierOnUnnamedReceiverParameter" xml:space="preserve">
<value>Cannot use modifiers on the unnamed receiver parameter of extension block</value>
</data>
<data name="ERR_ExtensionTypeNameDisallowed" xml:space="preserve">
<value>Types and aliases cannot be named 'extension'.</value>
</data>
</root>
8 changes: 8 additions & 0 deletions src/Compilers/CSharp/Portable/Errors/ErrorCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2390,6 +2390,14 @@ internal enum ErrorCode
ERR_PPIgnoredNeedsFileBasedProgram = 9298,
ERR_PPIgnoredFollowsIf = 9299,

ERR_RefExtensionParameterMustBeValueTypeOrConstrainedToOne = 9300,
ERR_InExtensionParameterMustBeValueType = 9301,
ERR_ProtectedInExtension = 9302,
ERR_InstanceMemberWithUnnamedExtensionsParameter = 9303,
ERR_InitInExtension = 9304,
ERR_ModifierOnUnnamedReceiverParameter = 9305,
ERR_ExtensionTypeNameDisallowed = 9306,

// Note: you will need to do the following after adding errors:
// 1) Update ErrorFacts.IsBuildOnlyDiagnostic (src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs)
// 2) Add message to CSharpResources.resx
Expand Down
7 changes: 7 additions & 0 deletions src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2506,6 +2506,13 @@ or ErrorCode.ERR_ExpressionTreeContainsExtensionPropertyAccess
or ErrorCode.ERR_PPIgnoredFollowsToken
or ErrorCode.ERR_PPIgnoredNeedsFileBasedProgram
or ErrorCode.ERR_PPIgnoredFollowsIf
or ErrorCode.ERR_RefExtensionParameterMustBeValueTypeOrConstrainedToOne
or ErrorCode.ERR_InExtensionParameterMustBeValueType
or ErrorCode.ERR_ProtectedInExtension
or ErrorCode.ERR_InstanceMemberWithUnnamedExtensionsParameter
or ErrorCode.ERR_InitInExtension
or ErrorCode.ERR_ModifierOnUnnamedReceiverParameter
or ErrorCode.ERR_ExtensionTypeNameDisallowed
=> false,
};
#pragma warning restore CS8524 // The switch expression does not handle some values of its input type (it is not exhaustive) involving an unnamed enum value.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ protected override void GenerateControlFields()
// Add a field: bool disposeMode
_disposeModeField = F.StateMachineField(boolType, GeneratedNames.MakeDisposeModeFieldName());

if (_isEnumerable && this.method.Parameters.Any(static p => p.IsSourceParameterWithEnumeratorCancellationAttribute()))
if (_isEnumerable && this.method.Parameters.Any(static p => !p.IsExtensionParameterImplementation() && p.HasEnumeratorCancellationAttribute))
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!p.IsExtensionParameterImplementation()

It is not obvious why extension parameter is getting special treatment #Closed

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean we should leave a comment here (the attribute is ignored on extension parameter) or do you mean you think the attribute should be effective on the extension parameter too?

The reason I don't think the attribute should be effective is that this attribute is very specific to async-iterator method implementations. I don't think it belongs on an extension parameter, as the extension block may contain many kinds of members. Also, the scenario where you need an async-iterator extension method on a CancellationToken receiver seems very niche to say the least.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it belongs on an extension parameter

I am fine with this design decision, Could you please mention this explicitly in the speclet?

Also, please make sure to test scenario where extension parameter implementation is the only parameter for the method.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. Linked to corresponding spec update.
The test ReceiverParameterValidation_CancellationTokenParameter_Instance shows that the attribute doesn't have an effect (it includes another parameter, but I think that's fine)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it includes another parameter, but I think that's fine

Are you saying the CancellationToken parameter doesn't have to be the last parameter in a general case?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct. The only restriction is that only one parameter may be marked with this attribute.

{
// Add a field: CancellationTokenSource combinedTokens
_combinedTokensField = F.StateMachineField(
Expand Down Expand Up @@ -211,7 +211,8 @@ protected override BoundStatement InitializeParameterField(MethodSymbol getEnume
{
BoundStatement result;
if (_combinedTokensField is object &&
parameter.IsSourceParameterWithEnumeratorCancellationAttribute() &&
!parameter.IsExtensionParameterImplementation() &&
parameter.HasEnumeratorCancellationAttribute &&
parameter.Type.Equals(F.Compilation.GetWellKnownType(WellKnownType.System_Threading_CancellationToken), TypeCompareKind.ConsiderEverything))
{
// For a parameter of type CancellationToken with [EnumeratorCancellation]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,10 @@ public override Symbol ContainingSymbol
{
get { return _containingType; }
}

internal override bool HasEnumeratorCancellationAttribute
{
get { return _underlyingParameter.HasEnumeratorCancellationAttribute; }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ public override ImmutableArray<CustomModifier> RefCustomModifiers
return _containingMethod._typeMap.SubstituteCustomModifiers(this._underlyingParameter.RefCustomModifiers);
}
}

internal sealed override bool HasEnumeratorCancellationAttribute
{
get { return _underlyingParameter.HasEnumeratorCancellationAttribute; }
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,30 @@ protected override (TypeWithAnnotations ReturnType, ImmutableArray<ParameterSymb
if (parameter is { })
{
checkUnderspecifiedGenericExtension(parameter, ContainingType.TypeParameters, diagnostics);

TypeSymbol parameterType = parameter.TypeWithAnnotations.Type;
RefKind parameterRefKind = parameter.RefKind;
SyntaxNode? parameterTypeSyntax = parameterList.Parameters[0].Type;
Debug.Assert(parameterTypeSyntax is not null);

// Note: SourceOrdinaryMethodSymbol.ExtensionMethodChecks has similar checks, which should be kept in sync.
if (!parameterType.IsValidExtensionParameterType())
{
diagnostics.Add(ErrorCode.ERR_BadTypeforThis, parameterTypeSyntax, parameterType);
}
else if (parameterRefKind == RefKind.Ref && !parameterType.IsValueType)
{
diagnostics.Add(ErrorCode.ERR_RefExtensionParameterMustBeValueTypeOrConstrainedToOne, parameterTypeSyntax);
}
else if (parameterRefKind is RefKind.In or RefKind.RefReadOnlyParameter && parameterType.TypeKind != TypeKind.Struct)
{
diagnostics.Add(ErrorCode.ERR_InExtensionParameterMustBeValueType, parameterTypeSyntax);
}
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be desirable to try sharing logic with ExtensionMethodChecks in order to avoid an accidental diverging #Closed

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree and I tried that initially, but the logic for these three checks in ExtensionMethodChecks uses locations and also the name of the method

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider leaving a comment in both places saying that we should keep them in sync.


if (parameter.Name is "" && parameterRefKind != RefKind.None)
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parameterRefKind != RefKind.None

Should scoped be disallowed as well? #Closed

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but that's currently a parsing error. See ReceiverParameterValidation_UnnamedReceiverParameter_Scoped, which has a comment for follow-up

{
diagnostics.Add(ErrorCode.ERR_ModifierOnUnnamedReceiverParameter, parameterTypeSyntax);
}
}

if (parameter is { Name: var name } && name != "" &&
Expand Down
Loading