Skip to content

Commit d35b797

Browse files
authored
Merge branch 'UserDefinedCompoundAssignment' into 'extensions' (#78302)
The merge is done in preparation for the "extension operators" work
2 parents c6dfd02 + aec4186 commit d35b797

File tree

82 files changed

+26356
-465
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

82 files changed

+26356
-465
lines changed

src/Analyzers/CSharp/Tests/ImplementInterface/ImplementInterfaceTests.cs

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,19 @@ public sealed class ImplementInterfaceTests
2626
{
2727
private readonly NamingStylesTestOptionSets _options = new(LanguageNames.CSharp);
2828

29+
private const string CompilerFeatureRequiredAttribute = """
30+
31+
namespace System.Runtime.CompilerServices
32+
{
33+
public sealed class CompilerFeatureRequiredAttribute : Attribute
34+
{
35+
public CompilerFeatureRequiredAttribute(string featureName)
36+
{
37+
}
38+
}
39+
}
40+
""";
41+
2942
private static OptionsCollection AllOptionsOff
3043
=> new(LanguageNames.CSharp)
3144
{
@@ -11243,6 +11256,78 @@ class C : ITest
1124311256
}.RunAsync();
1124411257
}
1124511258

11259+
[Theory(Skip = "Yes")] // PROTOTYPE: Something doesn't work
11260+
[CombinatorialData]
11261+
public async Task TestInstanceIncrementOperator_ImplementExplicitly([CombinatorialValues("++", "--")] string op)
11262+
{
11263+
await new VerifyCS.Test
11264+
{
11265+
ReferenceAssemblies = ReferenceAssemblies.Net.Net60,
11266+
LanguageVersion = LanguageVersion.Preview,
11267+
TestCode = $$$"""
11268+
interface ITest
11269+
{
11270+
abstract void operator {{{op}}}();
11271+
}
11272+
class C : {|CS0535:ITest|}
11273+
{
11274+
}
11275+
""" + CompilerFeatureRequiredAttribute,
11276+
FixedCode = $$$"""
11277+
interface ITest
11278+
{
11279+
abstract void operator {{{op}}}();
11280+
}
11281+
class C : ITest
11282+
{
11283+
void ITest.operator {{{op}}}()
11284+
{
11285+
throw new System.NotImplementedException();
11286+
}
11287+
}
11288+
""" + CompilerFeatureRequiredAttribute,
11289+
CodeActionVerifier = (codeAction, verifier) => verifier.Equal(CodeFixesResources.Implement_all_members_explicitly, codeAction.Title),
11290+
CodeActionEquivalenceKey = "True;False;False:global::ITest;Microsoft.CodeAnalysis.ImplementInterface.AbstractImplementInterfaceService+ImplementInterfaceCodeAction;",
11291+
CodeActionIndex = 0,
11292+
}.RunAsync();
11293+
}
11294+
11295+
[Theory(Skip = "Yes")] // PROTOTYPE: Something doesn't work
11296+
[CombinatorialData]
11297+
public async Task TestInstanceCompoundAssignmentOperator_ImplementExplicitly([CombinatorialValues("+=", "-=", "*=", "/=", "%=", "&=", "|=", "^=", "<<=", ">>=", ">>>=")] string op)
11298+
{
11299+
await new VerifyCS.Test
11300+
{
11301+
ReferenceAssemblies = ReferenceAssemblies.Net.Net60,
11302+
LanguageVersion = LanguageVersion.Preview,
11303+
TestCode = $$$"""
11304+
interface ITest
11305+
{
11306+
abstract void operator {{{op}}}(int y);
11307+
}
11308+
class C : {|CS0535:ITest|}
11309+
{
11310+
}
11311+
""" + CompilerFeatureRequiredAttribute,
11312+
FixedCode = $$$"""
11313+
interface ITest
11314+
{
11315+
abstract void operator {{{op}}}(int y);
11316+
}
11317+
class C : ITest
11318+
{
11319+
void ITest.operator {{{op}}}(int y)
11320+
{
11321+
throw new System.NotImplementedException();
11322+
}
11323+
}
11324+
""" + CompilerFeatureRequiredAttribute,
11325+
CodeActionVerifier = (codeAction, verifier) => verifier.Equal(CodeFixesResources.Implement_all_members_explicitly, codeAction.Title),
11326+
CodeActionEquivalenceKey = "True;False;False:global::ITest;Microsoft.CodeAnalysis.ImplementInterface.AbstractImplementInterfaceService+ImplementInterfaceCodeAction;",
11327+
CodeActionIndex = 0,
11328+
}.RunAsync();
11329+
}
11330+
1124611331
[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/53927")]
1124711332
public async Task TestStaticAbstractInterfaceOperator_ImplementImplicitly()
1124811333
{
@@ -11320,6 +11405,80 @@ class C : ITest<C>
1132011405
}.RunAsync();
1132111406
}
1132211407

11408+
[Theory]
11409+
[CombinatorialData]
11410+
public async Task TestInstanceIncrementOperator_ImplementImplicitly([CombinatorialValues("++", "--")] string op)
11411+
{
11412+
await new VerifyCS.Test
11413+
{
11414+
ReferenceAssemblies = ReferenceAssemblies.Net.Net60,
11415+
LanguageVersion = LanguageVersion.Preview,
11416+
TestCode = $$$"""
11417+
interface ITest<T> where T : ITest<T>
11418+
{
11419+
void operator {{{op}}}();
11420+
}
11421+
class C : {|CS0535:ITest<C>|}
11422+
{
11423+
}
11424+
""" + CompilerFeatureRequiredAttribute,
11425+
// PROTOTYPE: The 'static' modifier shouldn't be added
11426+
FixedCode = $$$"""
11427+
interface ITest<T> where T : ITest<T>
11428+
{
11429+
void operator {{{op}}}();
11430+
}
11431+
class C : {|CS0736:ITest<C>|}
11432+
{
11433+
public static void operator {|CS1535:{{{op}}}|}()
11434+
{
11435+
throw new System.NotImplementedException();
11436+
}
11437+
}
11438+
""" + CompilerFeatureRequiredAttribute,
11439+
CodeActionVerifier = (codeAction, verifier) => verifier.Equal(CodeFixesResources.Implement_interface, codeAction.Title),
11440+
CodeActionEquivalenceKey = "False;False;True:global::ITest<global::C>;Microsoft.CodeAnalysis.ImplementInterface.AbstractImplementInterfaceService+ImplementInterfaceCodeAction;",
11441+
CodeActionIndex = 0,
11442+
}.RunAsync();
11443+
}
11444+
11445+
[Theory]
11446+
[CombinatorialData]
11447+
public async Task TestInstanceCompoundAssignmentOperator_ImplementImplicitly([CombinatorialValues("+=", "-=", "*=", "/=", "%=", "&=", "|=", "^=", "<<=", ">>=", ">>>=")] string op)
11448+
{
11449+
await new VerifyCS.Test
11450+
{
11451+
ReferenceAssemblies = ReferenceAssemblies.Net.Net60,
11452+
LanguageVersion = LanguageVersion.Preview,
11453+
TestCode = $$$"""
11454+
interface ITest<T> where T : ITest<T>
11455+
{
11456+
void operator {{{op}}}(int y);
11457+
}
11458+
class C : {|CS0535:ITest<C>|}
11459+
{
11460+
}
11461+
""" + CompilerFeatureRequiredAttribute,
11462+
// PROTOTYPE: The 'static' modifier shouldn't be added
11463+
FixedCode = $$$"""
11464+
interface ITest<T> where T : ITest<T>
11465+
{
11466+
void operator {{{op}}}(int y);
11467+
}
11468+
class C : ITest<C>
11469+
{
11470+
public static void operator {|CS0106:{{{op}}}|}(int y)
11471+
{
11472+
throw new System.NotImplementedException();
11473+
}
11474+
}
11475+
""" + CompilerFeatureRequiredAttribute,
11476+
CodeActionVerifier = (codeAction, verifier) => verifier.Equal(CodeFixesResources.Implement_interface, codeAction.Title),
11477+
CodeActionEquivalenceKey = "False;False;True:global::ITest<global::C>;Microsoft.CodeAnalysis.ImplementInterface.AbstractImplementInterfaceService+ImplementInterfaceCodeAction;",
11478+
CodeActionIndex = 0,
11479+
}.RunAsync();
11480+
}
11481+
1132311482
[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/53927")]
1132411483
public async Task TestStaticAbstractInterfaceOperator_ImplementExplicitly()
1132511484
{

src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4489,18 +4489,27 @@ internal SafeContext GetValEscape(BoundExpression expr, SafeContext localScopeDe
44894489
case BoundKind.CompoundAssignmentOperator:
44904490
var compound = (BoundCompoundAssignmentOperator)expr;
44914491

4492+
// https://github.com/dotnet/roslyn/issues/78198 It looks like we don't have a single test demonstrating significance of the code below.
4493+
44924494
if (compound.Operator.Method is { } compoundMethod)
44934495
{
4494-
return GetInvocationEscapeScope(
4495-
MethodInfo.Create(compoundMethod),
4496-
receiver: null,
4497-
receiverIsSubjectToCloning: ThreeState.Unknown,
4498-
compoundMethod.Parameters,
4499-
argsOpt: [compound.Left, compound.Right],
4500-
argRefKindsOpt: default,
4501-
argsToParamsOpt: default,
4502-
localScopeDepth: localScopeDepth,
4503-
isRefEscape: false);
4496+
if (compoundMethod.IsStatic)
4497+
{
4498+
return GetInvocationEscapeScope(
4499+
MethodInfo.Create(compoundMethod),
4500+
receiver: null,
4501+
receiverIsSubjectToCloning: ThreeState.Unknown,
4502+
compoundMethod.Parameters,
4503+
argsOpt: [compound.Left, compound.Right],
4504+
argRefKindsOpt: default,
4505+
argsToParamsOpt: default,
4506+
localScopeDepth: localScopeDepth,
4507+
isRefEscape: false);
4508+
}
4509+
else
4510+
{
4511+
return GetValEscape(compound.Left, localScopeDepth);
4512+
}
45044513
}
45054514

45064515
return GetValEscape(compound.Left, localScopeDepth)
@@ -5278,20 +5287,27 @@ internal bool CheckValEscape(SyntaxNode node, BoundExpression expr, SafeContext
52785287

52795288
if (compound.Operator.Method is { } compoundMethod)
52805289
{
5281-
return CheckInvocationEscape(
5282-
compound.Syntax,
5283-
MethodInfo.Create(compoundMethod),
5284-
receiver: null,
5285-
receiverIsSubjectToCloning: ThreeState.Unknown,
5286-
compoundMethod.Parameters,
5287-
argsOpt: [compound.Left, compound.Right],
5288-
argRefKindsOpt: default,
5289-
argsToParamsOpt: default,
5290-
checkingReceiver: checkingReceiver,
5291-
escapeFrom: escapeFrom,
5292-
escapeTo: escapeTo,
5293-
diagnostics,
5294-
isRefEscape: false);
5290+
if (compoundMethod.IsStatic)
5291+
{
5292+
return CheckInvocationEscape(
5293+
compound.Syntax,
5294+
MethodInfo.Create(compoundMethod),
5295+
receiver: null,
5296+
receiverIsSubjectToCloning: ThreeState.Unknown,
5297+
compoundMethod.Parameters,
5298+
argsOpt: [compound.Left, compound.Right],
5299+
argRefKindsOpt: default,
5300+
argsToParamsOpt: default,
5301+
checkingReceiver: checkingReceiver,
5302+
escapeFrom: escapeFrom,
5303+
escapeTo: escapeTo,
5304+
diagnostics,
5305+
isRefEscape: false);
5306+
}
5307+
else
5308+
{
5309+
return CheckValEscape(compound.Left.Syntax, compound.Left, escapeFrom, escapeTo, checkingReceiver: false, diagnostics: diagnostics);
5310+
}
52955311
}
52965312

52975313
return CheckValEscape(compound.Left.Syntax, compound.Left, escapeFrom, escapeTo, checkingReceiver: false, diagnostics: diagnostics) &&

src/Compilers/CSharp/Portable/Binder/Binder_Crefs.cs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -225,15 +225,24 @@ private ImmutableArray<Symbol> BindOperatorMemberCref(OperatorMemberCrefSyntax s
225225
CrefParameterListSyntax? parameterListSyntax = syntax.Parameters;
226226
bool isChecked = syntax.CheckedKeyword.IsKind(SyntaxKind.CheckedKeyword);
227227

228-
// NOTE: Prefer binary to unary, unless there is exactly one parameter.
229-
// CONSIDER: we're following dev11 by never using a binary operator name if there's
230-
// exactly one parameter, but doing so would allow us to match single-parameter constructors.
231228
SyntaxKind operatorTokenKind = syntax.OperatorToken.Kind();
232-
string? memberName = parameterListSyntax != null && parameterListSyntax.Parameters.Count == 1
233-
? null
234-
: OperatorFacts.BinaryOperatorNameFromSyntaxKindIfAny(operatorTokenKind, isChecked);
229+
string? memberName;
235230

236-
memberName = memberName ?? OperatorFacts.UnaryOperatorNameFromSyntaxKindIfAny(operatorTokenKind, isChecked: isChecked);
231+
if (SyntaxFacts.IsOverloadableCompoundAssignmentOperator(operatorTokenKind))
232+
{
233+
memberName = OperatorFacts.CompoundAssignmentOperatorNameFromSyntaxKind(operatorTokenKind, isChecked);
234+
}
235+
else
236+
{
237+
// NOTE: Prefer binary to unary, unless there is exactly one parameter.
238+
// CONSIDER: we're following dev11 by never using a binary operator name if there's
239+
// exactly one parameter, but doing so would allow us to match single-parameter constructors.
240+
memberName = parameterListSyntax != null && parameterListSyntax.Parameters.Count == 1
241+
? null
242+
: OperatorFacts.BinaryOperatorNameFromSyntaxKindIfAny(operatorTokenKind, isChecked);
243+
244+
memberName = memberName ?? OperatorFacts.UnaryOperatorNameFromSyntaxKindIfAny(operatorTokenKind, isChecked: isChecked);
245+
}
237246

238247
if (memberName == null ||
239248
(isChecked && !syntax.OperatorToken.IsMissing && !SyntaxFacts.IsCheckedOperator(memberName))) // the operator cannot be checked

src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs

Lines changed: 37 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11364,38 +11364,7 @@ private BoundConditionalAccess BindConditionalAccessExpression(ConditionalAccess
1136411364
// For improved diagnostics we detect the cases where the value will be used and produce a
1136511365
// more specific (though not technically correct) diagnostic here:
1136611366
// "Error CS0023: Operator '?' cannot be applied to operand of type 'T'"
11367-
bool resultIsUsed = true;
11368-
CSharpSyntaxNode parent = node.Parent;
11369-
11370-
if (parent != null)
11371-
{
11372-
switch (parent.Kind())
11373-
{
11374-
case SyntaxKind.ExpressionStatement:
11375-
resultIsUsed = ((ExpressionStatementSyntax)parent).Expression != node;
11376-
break;
11377-
11378-
case SyntaxKind.SimpleLambdaExpression:
11379-
resultIsUsed = (((SimpleLambdaExpressionSyntax)parent).Body != node) || MethodOrLambdaRequiresValue(ContainingMemberOrLambda, Compilation);
11380-
break;
11381-
11382-
case SyntaxKind.ParenthesizedLambdaExpression:
11383-
resultIsUsed = (((ParenthesizedLambdaExpressionSyntax)parent).Body != node) || MethodOrLambdaRequiresValue(ContainingMemberOrLambda, Compilation);
11384-
break;
11385-
11386-
case SyntaxKind.ArrowExpressionClause:
11387-
resultIsUsed = (((ArrowExpressionClauseSyntax)parent).Expression != node) || MethodOrLambdaRequiresValue(ContainingMemberOrLambda, Compilation);
11388-
break;
11389-
11390-
case SyntaxKind.ForStatement:
11391-
// Incrementors and Initializers doesn't have to produce a value
11392-
var loop = (ForStatementSyntax)parent;
11393-
resultIsUsed = !loop.Incrementors.Contains(node) && !loop.Initializers.Contains(node);
11394-
break;
11395-
}
11396-
}
11397-
11398-
if (resultIsUsed)
11367+
if (ResultIsUsed(node))
1139911368
{
1140011369
return GenerateBadConditionalAccessNodeError(node, receiver, access, diagnostics);
1140111370
}
@@ -11414,6 +11383,42 @@ private BoundConditionalAccess BindConditionalAccessExpression(ConditionalAccess
1141411383
return new BoundConditionalAccess(node, receiver, access, accessType);
1141511384
}
1141611385

11386+
private bool ResultIsUsed(ExpressionSyntax node)
11387+
{
11388+
bool resultIsUsed = true;
11389+
CSharpSyntaxNode parent = node.Parent;
11390+
11391+
if (parent != null)
11392+
{
11393+
switch (parent.Kind())
11394+
{
11395+
case SyntaxKind.ExpressionStatement:
11396+
resultIsUsed = ((ExpressionStatementSyntax)parent).Expression != node;
11397+
break;
11398+
11399+
case SyntaxKind.SimpleLambdaExpression:
11400+
resultIsUsed = (((SimpleLambdaExpressionSyntax)parent).Body != node) || MethodOrLambdaRequiresValue(ContainingMemberOrLambda, Compilation);
11401+
break;
11402+
11403+
case SyntaxKind.ParenthesizedLambdaExpression:
11404+
resultIsUsed = (((ParenthesizedLambdaExpressionSyntax)parent).Body != node) || MethodOrLambdaRequiresValue(ContainingMemberOrLambda, Compilation);
11405+
break;
11406+
11407+
case SyntaxKind.ArrowExpressionClause:
11408+
resultIsUsed = (((ArrowExpressionClauseSyntax)parent).Expression != node) || MethodOrLambdaRequiresValue(ContainingMemberOrLambda, Compilation);
11409+
break;
11410+
11411+
case SyntaxKind.ForStatement:
11412+
// Incrementors and Initializers doesn't have to produce a value
11413+
var loop = (ForStatementSyntax)parent;
11414+
resultIsUsed = !loop.Incrementors.Contains(node) && !loop.Initializers.Contains(node);
11415+
break;
11416+
}
11417+
}
11418+
11419+
return resultIsUsed;
11420+
}
11421+
1141711422
internal static bool MethodOrLambdaRequiresValue(Symbol symbol, CSharpCompilation compilation)
1141811423
{
1141911424
return symbol is MethodSymbol method &&

src/Compilers/CSharp/Portable/Binder/Binder_Lookup.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1485,6 +1485,10 @@ internal SingleLookupResult CheckViability(Symbol symbol, int arity, LookupOptio
14851485
{
14861486
return LookupResult.Empty();
14871487
}
1488+
else if ((options & LookupOptions.MustBeOperator) != 0 && unwrappedSymbol is not MethodSymbol { MethodKind: MethodKind.UserDefinedOperator })
1489+
{
1490+
return LookupResult.Empty();
1491+
}
14881492
else if (!IsInScopeOfAssociatedSyntaxTree(unwrappedSymbol))
14891493
{
14901494
return LookupResult.Empty();
@@ -1503,7 +1507,7 @@ internal SingleLookupResult CheckViability(Symbol symbol, int arity, LookupOptio
15031507
{
15041508
return LookupResult.WrongArity(symbol, diagInfo);
15051509
}
1506-
else if (!InCref && !unwrappedSymbol.CanBeReferencedByNameIgnoringIllegalCharacters)
1510+
else if (!InCref && !unwrappedSymbol.CanBeReferencedByNameIgnoringIllegalCharacters && (options & LookupOptions.MustBeOperator) == 0)
15071511
{
15081512
// Strictly speaking, this test should actually check CanBeReferencedByName.
15091513
// However, we don't want to pay that cost in cases where the lookup is based

0 commit comments

Comments
 (0)