Skip to content

Commit

Permalink
Allow abstract and sealed modifiers on static methods/properties/…
Browse files Browse the repository at this point in the history
…events in interfaces. (#52228)

Spec: https://github.com/dotnet/csharplang/blob/9a0dddbf0643993db0da8f48436f2aac60bc20b1/proposals/statics-in-interfaces.md

Related to #52202.

Relevant quotes from the spec:

#### Abstract virtual members
Static interface members other than fields are allowed to also have the `abstract` modifier. Abstract static members are not allowed to have a body (or in the case of properties, the accessors are not allowed to have a body). 

``` c#
interface I<T> where T : I<T>
{
    static abstract void M();
    static abstract T P { get; set; }
    static abstract event Action E;
    static abstract T operator +(T l, T r);
}
```

#### Explicitly non-virtual static members
For symmetry with non-virtual instance members, static members should be allowed an optional `sealed` modifier, even though they are non-virtual by default:

``` c#
interface I0
{
    static sealed void M() => Console.WriteLine("Default behavior");
    
    static sealed int f = 0;
    
    static sealed int P1 { get; set; }
    static sealed int P2 { get => f; set => f = value; }
    
    static sealed event Action E1;
    static sealed event Action E2 { add => E1 += value; remove => E1 -= value; }
    
    static sealed I0 operator +(I0 l, I0 r) => l;
}
```
  • Loading branch information
AlekseyTs authored Apr 2, 2021
1 parent b610de2 commit ff59bc0
Show file tree
Hide file tree
Showing 28 changed files with 4,533 additions and 212 deletions.
14 changes: 14 additions & 0 deletions docs/features/StaticAbstractMembersInInterfaces.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Static Abstract Members In Interfaces
=====================================

An interface is allowed to specify abstract static members that implementing classes and structs are then
required to provide an explicit or implicit implementation of. The members can be accessed off of type
parameters that are constrained by the interface.

Proposal:
- https://github.com/dotnet/csharplang/issues/4436
- https://github.com/dotnet/csharplang/blob/main/proposals/statics-in-interfaces.md

Feature branch: https://github.com/dotnet/roslyn/tree/features/StaticAbstractMembersInInterfaces

Test plan: https://github.com/dotnet/roslyn/issues/52221
5 changes: 4 additions & 1 deletion src/Compilers/CSharp/Portable/CSharpResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -667,7 +667,7 @@
<value>Type '{1}' already defines a member called '{0}' with the same parameter types</value>
</data>
<data name="ERR_StaticNotVirtual" xml:space="preserve">
<value>A static member '{0}' cannot be marked as override, virtual, or abstract</value>
<value>A static member cannot be marked as '{0}'</value>
</data>
<data name="ERR_OverrideNotNew" xml:space="preserve">
<value>A member '{0}' marked as override cannot be marked as new or virtual</value>
Expand Down Expand Up @@ -6600,4 +6600,7 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ
<data name="ERR_FunctionPointerTypesInAttributeNotSupported" xml:space="preserve">
<value>Using a function pointer type in a 'typeof' in an attribute is not supported.</value>
</data>
<data name="IDS_FeatureStaticAbstractMembersInInterfaces" xml:space="preserve">
<value>static abstract members in interfaces</value>
</data>
</root>
2 changes: 2 additions & 0 deletions src/Compilers/CSharp/Portable/Errors/MessageID.cs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ internal enum MessageID
IDS_FeatureVarianceSafetyForStaticInterfaceMembers = MessageBase + 12791,
IDS_FeatureConstantInterpolatedStrings = MessageBase + 12792,
IDS_FeatureMixedDeclarationsAndExpressionsInDeconstruction = MessageBase + 12793,
IDS_FeatureStaticAbstractMembersInInterfaces = MessageBase + 12794,
}

// Message IDs may refer to strings that need to be localized.
Expand Down Expand Up @@ -324,6 +325,7 @@ internal static LanguageVersion RequiredVersion(this MessageID feature)
{
// C# preview features.
case MessageID.IDS_FeatureMixedDeclarationsAndExpressionsInDeconstruction:
case MessageID.IDS_FeatureStaticAbstractMembersInInterfaces: // semantic check
return LanguageVersion.Preview;
// C# 9.0 features.
case MessageID.IDS_FeatureLambdaDiscardParameters: // semantic check
Expand Down
90 changes: 74 additions & 16 deletions src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ internal static DeclarationModifiers CheckModifiers(
out bool modifierErrors)
{
modifierErrors = false;
DeclarationModifiers reportStaticNotVirtualForModifiers = DeclarationModifiers.None;

if ((modifiers & allowedModifiers & DeclarationModifiers.Static) != 0)
{
reportStaticNotVirtualForModifiers = allowedModifiers & (DeclarationModifiers.Override | DeclarationModifiers.Virtual);
allowedModifiers &= ~reportStaticNotVirtualForModifiers;
}

DeclarationModifiers errorModifiers = modifiers & ~allowedModifiers;
DeclarationModifiers result = modifiers & allowedModifiers;

Expand All @@ -55,6 +63,16 @@ internal static DeclarationModifiers CheckModifiers(
ReportPartialError(errorLocation, diagnostics, modifierTokens);
break;

case DeclarationModifiers.Override:
case DeclarationModifiers.Virtual:
if ((reportStaticNotVirtualForModifiers & oneError) == 0)
{
goto default;
}

diagnostics.Add(ErrorCode.ERR_StaticNotVirtual, errorLocation, ModifierUtils.ConvertSingleModifierToSyntaxText(oneError));
break;

default:
diagnostics.Add(ErrorCode.ERR_BadMemberFlag, errorLocation, ConvertSingleModifierToSyntaxText(oneError));
break;
Expand Down Expand Up @@ -94,27 +112,63 @@ internal static void ReportDefaultInterfaceImplementationModifiers(
Location errorLocation,
BindingDiagnosticBag diagnostics)
{
if (!hasBody && (modifiers & defaultInterfaceImplementationModifiers) != 0)
if ((modifiers & defaultInterfaceImplementationModifiers) != 0)
{
LanguageVersion availableVersion = ((CSharpParseOptions)errorLocation.SourceTree.Options).LanguageVersion;
LanguageVersion requiredVersion = MessageID.IDS_DefaultInterfaceImplementation.RequiredVersion();
if (availableVersion < requiredVersion)
LanguageVersion requiredVersion;

if ((modifiers & defaultInterfaceImplementationModifiers & DeclarationModifiers.Static) != 0 &&
(modifiers & defaultInterfaceImplementationModifiers & (DeclarationModifiers.Sealed | DeclarationModifiers.Abstract)) != 0)
{
DeclarationModifiers errorModifiers = modifiers & defaultInterfaceImplementationModifiers;
var requiredVersionArgument = new CSharpRequiredLanguageVersion(requiredVersion);
var availableVersionArgument = availableVersion.ToDisplayString();
while (errorModifiers != DeclarationModifiers.None)
var reportModifiers = DeclarationModifiers.Sealed | DeclarationModifiers.Abstract;
if ((modifiers & defaultInterfaceImplementationModifiers & (DeclarationModifiers.Sealed | DeclarationModifiers.Abstract)) == (DeclarationModifiers.Sealed | DeclarationModifiers.Abstract))
{
diagnostics.Add(ErrorCode.ERR_BadMemberFlag, errorLocation, ConvertSingleModifierToSyntaxText(DeclarationModifiers.Sealed));
reportModifiers &= ~DeclarationModifiers.Sealed;
}

requiredVersion = MessageID.IDS_FeatureStaticAbstractMembersInInterfaces.RequiredVersion();
if (availableVersion < requiredVersion)
{
DeclarationModifiers oneError = errorModifiers & ~(errorModifiers - 1);
Debug.Assert(oneError != DeclarationModifiers.None);
errorModifiers = errorModifiers & ~oneError;
diagnostics.Add(ErrorCode.ERR_InvalidModifierForLanguageVersion, errorLocation,
ConvertSingleModifierToSyntaxText(oneError),
availableVersionArgument,
requiredVersionArgument);
report(modifiers, reportModifiers, errorLocation, diagnostics, availableVersion, requiredVersion);
}

return; // below we will either ask for an earlier version of the language, or will not report anything
}

if (hasBody)
{
if ((modifiers & defaultInterfaceImplementationModifiers & DeclarationModifiers.Static) != 0)
{
Binder.CheckFeatureAvailability(errorLocation.SourceTree, MessageID.IDS_DefaultInterfaceImplementation, diagnostics, errorLocation);
}
}
else
{
requiredVersion = MessageID.IDS_DefaultInterfaceImplementation.RequiredVersion();
if (availableVersion < requiredVersion)
{
report(modifiers, defaultInterfaceImplementationModifiers, errorLocation, diagnostics, availableVersion, requiredVersion);
}
}
}

static void report(DeclarationModifiers modifiers, DeclarationModifiers unsupportedModifiers, Location errorLocation, BindingDiagnosticBag diagnostics, LanguageVersion availableVersion, LanguageVersion requiredVersion)
{
DeclarationModifiers errorModifiers = modifiers & unsupportedModifiers;
var requiredVersionArgument = new CSharpRequiredLanguageVersion(requiredVersion);
var availableVersionArgument = availableVersion.ToDisplayString();
while (errorModifiers != DeclarationModifiers.None)
{
DeclarationModifiers oneError = errorModifiers & ~(errorModifiers - 1);
Debug.Assert(oneError != DeclarationModifiers.None);
errorModifiers = errorModifiers & ~oneError;
diagnostics.Add(ErrorCode.ERR_InvalidModifierForLanguageVersion, errorLocation,
ConvertSingleModifierToSyntaxText(oneError),
availableVersionArgument,
requiredVersionArgument);
}
}
}

internal static DeclarationModifiers AdjustModifiersForAnInterfaceMember(DeclarationModifiers mods, bool hasBody, bool isExplicitInterfaceImplementation)
Expand All @@ -126,7 +180,11 @@ internal static DeclarationModifiers AdjustModifiersForAnInterfaceMember(Declara
mods |= DeclarationModifiers.Sealed;
}
}
else if ((mods & (DeclarationModifiers.Static | DeclarationModifiers.Private | DeclarationModifiers.Partial | DeclarationModifiers.Virtual | DeclarationModifiers.Abstract)) == 0)
else if ((mods & DeclarationModifiers.Static) != 0)
{
mods &= ~DeclarationModifiers.Sealed;
}
else if ((mods & (DeclarationModifiers.Private | DeclarationModifiers.Partial | DeclarationModifiers.Virtual | DeclarationModifiers.Abstract)) == 0)
{
Debug.Assert(!isExplicitInterfaceImplementation);

Expand Down Expand Up @@ -162,7 +220,7 @@ internal static DeclarationModifiers AdjustModifiersForAnInterfaceMember(Declara
return mods;
}

private static string ConvertSingleModifierToSyntaxText(DeclarationModifiers modifier)
internal static string ConvertSingleModifierToSyntaxText(DeclarationModifiers modifier)
{
switch (modifier)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,8 @@ private DeclarationModifiers MakeModifiers(SyntaxTokenList modifiers, bool expli

protected void CheckModifiersAndType(BindingDiagnosticBag diagnostics)
{
Debug.Assert(!IsStatic || (!IsVirtual && !IsOverride)); // Otherwise 'virtual' and 'override' should have been reported and cleared earlier.

Location location = this.Locations[0];
var useSiteInfo = new CompoundUseSiteInfo<AssemblySymbol>(diagnostics, ContainingAssembly);
bool isExplicitInterfaceImplementationInInterface = ContainingType.IsInterface && IsExplicitInterfaceImplementation;
Expand All @@ -512,10 +514,10 @@ protected void CheckModifiersAndType(BindingDiagnosticBag diagnostics)
{
diagnostics.Add(ErrorCode.ERR_VirtualPrivate, location, this);
}
else if (IsStatic && (IsOverride || IsVirtual || IsAbstract))
else if (IsStatic && IsAbstract && !ContainingType.IsInterface)
{
// A static member '{0}' cannot be marked as override, virtual, or abstract
diagnostics.Add(ErrorCode.ERR_StaticNotVirtual, location, this);
// A static member '{0}' cannot be marked as 'abstract'
diagnostics.Add(ErrorCode.ERR_StaticNotVirtual, location, ModifierUtils.ConvertSingleModifierToSyntaxText(DeclarationModifiers.Abstract));
}
else if (IsReadOnly && IsStatic)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -969,7 +969,8 @@ protected void CheckFeatureAvailabilityAndRuntimeSupport(SyntaxNode declarationS
{
if (_containingType.IsInterface)
{
if (hasBody || IsExplicitInterfaceImplementation)
if ((!IsStatic || MethodKind is MethodKind.StaticConstructor) &&
(hasBody || IsExplicitInterfaceImplementation))
{
Binder.CheckFeatureAvailability(declarationSyntax, MessageID.IDS_DefaultInterfaceImplementation, diagnostics, location);
}
Expand All @@ -978,6 +979,8 @@ protected void CheckFeatureAvailabilityAndRuntimeSupport(SyntaxNode declarationS
{
diagnostics.Add(ErrorCode.ERR_RuntimeDoesNotSupportDefaultInterfaceImplementation, location);
}

// PROTOTYPE(StaticAbstractMembersInInterfaces): Check runtime capability.
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,8 @@ private static DeclarationModifiers AddImpliedModifiers(DeclarationModifiers mod

private void CheckModifiers(bool isExplicitInterfaceImplementation, bool isVararg, bool hasBody, Location location, BindingDiagnosticBag diagnostics)
{
Debug.Assert(!IsStatic || (!IsVirtual && !IsOverride)); // Otherwise 'virtual' and 'override' should have been reported and cleared earlier.

bool isExplicitInterfaceImplementationInInterface = isExplicitInterfaceImplementation && ContainingType.IsInterface;

if (IsPartial && HasExplicitAccessModifier)
Expand Down Expand Up @@ -525,10 +527,10 @@ private void CheckModifiers(bool isExplicitInterfaceImplementation, bool isVarar
{
diagnostics.Add(ErrorCode.ERR_VirtualPrivate, location, this);
}
else if (IsStatic && (IsOverride || IsVirtual || IsAbstract))
else if (IsStatic && IsAbstract && !ContainingType.IsInterface)
{
// A static member '{0}' cannot be marked as override, virtual, or abstract
diagnostics.Add(ErrorCode.ERR_StaticNotVirtual, location, this);
// A static member '{0}' cannot be marked as 'abstract'
diagnostics.Add(ErrorCode.ERR_StaticNotVirtual, location, ModifierUtils.ConvertSingleModifierToSyntaxText(DeclarationModifiers.Abstract));
}
else if (IsOverride && (IsNew || IsVirtual))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -846,16 +846,18 @@ private void CheckAccessibility(Location location, BindingDiagnosticBag diagnost

private void CheckModifiers(bool isExplicitInterfaceImplementation, Location location, bool isIndexer, BindingDiagnosticBag diagnostics)
{
Debug.Assert(!IsStatic || (!IsVirtual && !IsOverride)); // Otherwise 'virtual' and 'override' should have been reported and cleared earlier.

bool isExplicitInterfaceImplementationInInterface = isExplicitInterfaceImplementation && ContainingType.IsInterface;

if (this.DeclaredAccessibility == Accessibility.Private && (IsVirtual || (IsAbstract && !isExplicitInterfaceImplementationInInterface) || IsOverride))
{
diagnostics.Add(ErrorCode.ERR_VirtualPrivate, location, this);
}
else if (IsStatic && (IsOverride || IsVirtual || IsAbstract))
else if (IsStatic && IsAbstract && !ContainingType.IsInterface)
{
// A static member '{0}' cannot be marked as override, virtual, or abstract
diagnostics.Add(ErrorCode.ERR_StaticNotVirtual, location, this);
// A static member '{0}' cannot be marked as 'abstract'
diagnostics.Add(ErrorCode.ERR_StaticNotVirtual, location, ModifierUtils.ConvertSingleModifierToSyntaxText(DeclarationModifiers.Abstract));
}
else if (IsStatic && HasReadOnlyModifier)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ private SourceUserDefinedConversionSymbol(
containingType,
location,
syntax,
MakeDeclarationModifiers(syntax, location, diagnostics),
MakeDeclarationModifiers(containingType.IsInterface, syntax, location, diagnostics),
hasBody: syntax.HasAnyBody(),
isExpressionBodied: syntax.Body == null && syntax.ExpressionBody != null,
isIterator: SyntaxFacts.HasYieldOperations(syntax.Body),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ private SourceUserDefinedOperatorSymbol(
containingType,
location,
syntax,
MakeDeclarationModifiers(syntax, location, diagnostics),
MakeDeclarationModifiers(containingType.IsInterface, syntax, location, diagnostics),
hasBody: syntax.HasAnyBody(),
isExpressionBodied: syntax.Body == null && syntax.ExpressionBody != null,
isIterator: SyntaxFacts.HasYieldOperations(syntax.Body),
Expand Down
Loading

0 comments on commit ff59bc0

Please sign in to comment.