diff --git a/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs b/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs index a2d6516952e51..8880087fc8068 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs @@ -759,11 +759,11 @@ private BoundExpression CheckValue(BoundExpression expr, BindValueKind valueKind return expr; case BoundKind.PropertyAccess: + var propertyAccess = (BoundPropertyAccess)expr; if (!InAttributeArgument) { // If the property has a synthesized backing field, record the accessor kind of the property // access for determining whether the property access can use the backing field directly. - var propertyAccess = (BoundPropertyAccess)expr; if (HasSynthesizedBackingField(propertyAccess.PropertySymbol, out _)) { expr = propertyAccess.Update( @@ -774,8 +774,24 @@ private BoundExpression CheckValue(BoundExpression expr, BindValueKind valueKind propertyAccess.ResultKind, propertyAccess.Type); } +#if DEBUG + else + { + // Under DEBUG, create a new node to mark as checked, rather than mutating the original. + // This allows the original node to be passed to CheckValue multiple times safely, without + // asserting in WasPropertyBackingFieldAccessChecked. + expr = propertyAccess.Clone(); + } +#endif } #if DEBUG + else + { + // Under DEBUG, create a new node to mark as checked, rather than mutating the original. + // This allows the original node to be passed to CheckValue multiple times safely, without + // asserting in WasPropertyBackingFieldAccessChecked. + expr = propertyAccess.Clone(); + } expr.WasPropertyBackingFieldAccessChecked = true; #endif break; diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs index 8324d4f1a0396..2f6f240289113 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs @@ -7755,7 +7755,7 @@ private BoundExpression BindDynamicMemberAccess( /// /// /// If new checks are added to this method, they will also need to be added to - /// . + /// . /// #else /// @@ -7764,7 +7764,7 @@ private BoundExpression BindDynamicMemberAccess( /// /// /// If new checks are added to this method, they will also need to be added to - /// . + /// . /// #endif private BoundExpression BindMemberAccessWithBoundLeft( diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Query.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Query.cs index 8034339586531..a1ee18f8439cf 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Query.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Query.cs @@ -54,7 +54,7 @@ internal BoundExpression BindQuery(QueryExpressionSyntax node, BindingDiagnostic if (fromClause.Type != null) { var typeRestriction = BindTypeArgument(fromClause.Type, diagnostics); - cast = MakeQueryInvocation(fromClause, state.fromExpression, receiverIsCheckedForRValue: false, "Cast", fromClause.Type, typeRestriction, diagnostics + cast = MakeQueryInvocation(fromClause, state.fromExpression, "Cast", fromClause.Type, typeRestriction, diagnostics #if DEBUG , state.nextInvokedMethodName #endif @@ -230,7 +230,7 @@ private BoundExpression FinalTranslation(QueryTranslationState state, BindingDia var e = state.fromExpression; var v = selectClause.Expression; var lambda = MakeQueryUnboundLambda(state.RangeVariableMap(), x, v, diagnostics.AccumulatesDependencies); - var result = MakeQueryInvocation(state.selectOrGroup, e, receiverIsCheckedForRValue: false, "Select", lambda, diagnostics + var result = MakeQueryInvocation(state.selectOrGroup, e, "Select", lambda, diagnostics #if DEBUG , state.nextInvokedMethodName #endif @@ -260,7 +260,7 @@ private BoundExpression FinalTranslation(QueryTranslationState state, BindingDia // this is the unoptimized form (when v is not the identifier x) var d = BindingDiagnosticBag.GetInstance(diagnostics); BoundExpression lambdaRight = MakeQueryUnboundLambda(state.RangeVariableMap(), x, v, diagnostics.AccumulatesDependencies); - result = MakeQueryInvocation(state.selectOrGroup, e, receiverIsCheckedForRValue: false, "GroupBy", ImmutableArray.Create(lambdaLeft, lambdaRight), d + result = MakeQueryInvocation(state.selectOrGroup, e, "GroupBy", ImmutableArray.Create(lambdaLeft, lambdaRight), d #if DEBUG , state.nextInvokedMethodName #endif @@ -276,7 +276,7 @@ private BoundExpression FinalTranslation(QueryTranslationState state, BindingDia { // The optimized form. We store the unoptimized form for analysis unoptimizedForm = result; - result = MakeQueryInvocation(state.selectOrGroup, e, receiverIsCheckedForRValue: false, "GroupBy", lambdaLeft, diagnostics + result = MakeQueryInvocation(state.selectOrGroup, e, "GroupBy", lambdaLeft, diagnostics #if DEBUG , state.nextInvokedMethodName #endif @@ -364,7 +364,7 @@ private void ReduceWhere(WhereClauseSyntax where, QueryTranslationState state, B // is translated into // from x in ( e ) . Where ( x => f ) var lambda = MakeQueryUnboundLambda(state.RangeVariableMap(), state.rangeVariable, where.Condition, diagnostics.AccumulatesDependencies); - var invocation = MakeQueryInvocation(where, state.fromExpression, receiverIsCheckedForRValue: false, "Where", lambda, diagnostics + var invocation = MakeQueryInvocation(where, state.fromExpression, "Where", lambda, diagnostics #if DEBUG , state.nextInvokedMethodName #endif @@ -395,7 +395,7 @@ private void ReduceJoin(JoinClauseSyntax join, QueryTranslationState state, Bind // is translated into // join x in ( e ) . Cast < T > ( ) on k1 equals k2 var castType = BindTypeArgument(join.Type, diagnostics); - castInvocation = MakeQueryInvocation(join, inExpression, receiverIsCheckedForRValue: false, "Cast", join.Type, castType, diagnostics + castInvocation = MakeQueryInvocation(join, inExpression, "Cast", join.Type, castType, diagnostics #if DEBUG , expectedMethodName: null #endif @@ -426,7 +426,6 @@ private void ReduceJoin(JoinClauseSyntax join, QueryTranslationState state, Bind invocation = MakeQueryInvocation( join, state.fromExpression, - receiverIsCheckedForRValue: false, "Join", ImmutableArray.Create(inExpression, outerKeySelectorLambda, innerKeySelectorLambda, resultSelectorLambda), diagnostics @@ -455,7 +454,6 @@ private void ReduceJoin(JoinClauseSyntax join, QueryTranslationState state, Bind invocation = MakeQueryInvocation( join, state.fromExpression, - receiverIsCheckedForRValue: false, "GroupJoin", ImmutableArray.Create(inExpression, outerKeySelectorLambda, innerKeySelectorLambda, resultSelectorLambda), diagnostics @@ -496,7 +494,6 @@ private void ReduceJoin(JoinClauseSyntax join, QueryTranslationState state, Bind invocation = MakeQueryInvocation( join, state.fromExpression, - receiverIsCheckedForRValue: false, "Join", ImmutableArray.Create(inExpression, outerKeySelectorLambda, innerKeySelectorLambda, resultSelectorLambda), diagnostics @@ -527,7 +524,6 @@ private void ReduceJoin(JoinClauseSyntax join, QueryTranslationState state, Bind invocation = MakeQueryInvocation( join, state.fromExpression, - receiverIsCheckedForRValue: false, "GroupJoin", ImmutableArray.Create(inExpression, outerKeySelectorLambda, innerKeySelectorLambda, resultSelectorLambda), diagnostics @@ -569,7 +565,7 @@ private void ReduceOrderBy(OrderByClauseSyntax orderby, QueryTranslationState st { string methodName = (first ? "OrderBy" : "ThenBy") + (ordering.IsKind(SyntaxKind.DescendingOrdering) ? "Descending" : ""); var lambda = MakeQueryUnboundLambda(state.RangeVariableMap(), state.rangeVariable, ordering.Expression, diagnostics.AccumulatesDependencies); - var invocation = MakeQueryInvocation(ordering, state.fromExpression, receiverIsCheckedForRValue: false, methodName, lambda, diagnostics + var invocation = MakeQueryInvocation(ordering, state.fromExpression, methodName, lambda, diagnostics #if DEBUG , state.nextInvokedMethodName #endif @@ -615,7 +611,6 @@ private void ReduceFrom(FromClauseSyntax from, QueryTranslationState state, Bind var invocation = MakeQueryInvocation( from, state.fromExpression, - receiverIsCheckedForRValue: false, "SelectMany", ImmutableArray.Create(collectionSelectorLambda, resultSelectorLambda), diagnostics @@ -663,7 +658,6 @@ private void ReduceFrom(FromClauseSyntax from, QueryTranslationState state, Bind var invocation = MakeQueryInvocation( from, state.fromExpression, - receiverIsCheckedForRValue: false, "SelectMany", ImmutableArray.Create(collectionSelectorLambda, resultSelectorLambda), diagnostics @@ -765,7 +759,7 @@ private void ReduceLet(LetClauseSyntax let, QueryTranslationState state, Binding state.AddTransparentIdentifier(x.Name); var y = state.AddRangeVariable(this, let.Identifier, diagnostics); state.allRangeVariables[y].Add(let.Identifier.ValueText); - var invocation = MakeQueryInvocation(let, state.fromExpression, receiverIsCheckedForRValue: false, "Select", lambda, diagnostics + var invocation = MakeQueryInvocation(let, state.fromExpression, "Select", lambda, diagnostics #if DEBUG , state.nextInvokedMethodName #endif @@ -858,7 +852,7 @@ private UnboundLambda MakeQueryUnboundLambdaWithCast(RangeVariableMap qvm, Range BoundExpression boundExpression = lambdaBodyBinder.BindValue(expression, diagnostics, BindValueKind.RValue); // We transform the expression from "expr" to "expr.Cast()". - boundExpression = lambdaBodyBinder.MakeQueryInvocation(expression, boundExpression, receiverIsCheckedForRValue: true, "Cast", castTypeSyntax, castType, diagnostics + boundExpression = lambdaBodyBinder.MakeQueryInvocation(expression, boundExpression, "Cast", castTypeSyntax, castType, diagnostics #if DEBUG , expectedMethodName: null #endif @@ -882,46 +876,46 @@ private static UnboundLambda MakeQueryUnboundLambda(CSharpSyntaxNode node, Query return lambda; } - protected BoundCall MakeQueryInvocation(CSharpSyntaxNode node, BoundExpression receiver, bool receiverIsCheckedForRValue, string methodName, BoundExpression arg, BindingDiagnosticBag diagnostics + protected BoundCall MakeQueryInvocation(CSharpSyntaxNode node, BoundExpression receiver, string methodName, BoundExpression arg, BindingDiagnosticBag diagnostics #if DEBUG , string? expectedMethodName #endif ) { - return MakeQueryInvocation(node, receiver, receiverIsCheckedForRValue, methodName, default(SeparatedSyntaxList), default(ImmutableArray), ImmutableArray.Create(arg), diagnostics + return MakeQueryInvocation(node, receiver, methodName, default(SeparatedSyntaxList), default(ImmutableArray), ImmutableArray.Create(arg), diagnostics #if DEBUG , expectedMethodName #endif ); } - protected BoundCall MakeQueryInvocation(CSharpSyntaxNode node, BoundExpression receiver, bool receiverIsCheckedForRValue, string methodName, ImmutableArray args, BindingDiagnosticBag diagnostics + protected BoundCall MakeQueryInvocation(CSharpSyntaxNode node, BoundExpression receiver, string methodName, ImmutableArray args, BindingDiagnosticBag diagnostics #if DEBUG , string? expectedMethodName #endif ) { - return MakeQueryInvocation(node, receiver, receiverIsCheckedForRValue, methodName, default(SeparatedSyntaxList), default(ImmutableArray), args, diagnostics + return MakeQueryInvocation(node, receiver, methodName, default(SeparatedSyntaxList), default(ImmutableArray), args, diagnostics #if DEBUG , expectedMethodName #endif ); } - protected BoundCall MakeQueryInvocation(CSharpSyntaxNode node, BoundExpression receiver, bool receiverIsCheckedForRValue, string methodName, TypeSyntax typeArgSyntax, TypeWithAnnotations typeArg, BindingDiagnosticBag diagnostics + protected BoundCall MakeQueryInvocation(CSharpSyntaxNode node, BoundExpression receiver, string methodName, TypeSyntax typeArgSyntax, TypeWithAnnotations typeArg, BindingDiagnosticBag diagnostics #if DEBUG , string? expectedMethodName #endif ) { - return MakeQueryInvocation(node, receiver, receiverIsCheckedForRValue, methodName, new SeparatedSyntaxList(new SyntaxNodeOrTokenList(typeArgSyntax, 0)), ImmutableArray.Create(typeArg), ImmutableArray.Empty, diagnostics + return MakeQueryInvocation(node, receiver, methodName, new SeparatedSyntaxList(new SyntaxNodeOrTokenList(typeArgSyntax, 0)), ImmutableArray.Create(typeArg), ImmutableArray.Empty, diagnostics #if DEBUG , expectedMethodName #endif ); } - protected BoundCall MakeQueryInvocation(CSharpSyntaxNode node, BoundExpression receiver, bool receiverIsCheckedForRValue, string methodName, SeparatedSyntaxList typeArgsSyntax, ImmutableArray typeArgs, ImmutableArray args, BindingDiagnosticBag diagnostics + protected BoundCall MakeQueryInvocation(CSharpSyntaxNode node, BoundExpression receiver, string methodName, SeparatedSyntaxList typeArgsSyntax, ImmutableArray typeArgs, ImmutableArray args, BindingDiagnosticBag diagnostics #if DEBUG , string? expectedMethodName #endif @@ -993,17 +987,10 @@ protected BoundCall MakeQueryInvocation(CSharpSyntaxNode node, BoundExpression r } else { - if (!receiverIsCheckedForRValue) + var checkedUltimateReceiver = CheckValue(ultimateReceiver, BindValueKind.RValue, diagnostics); + if (checkedUltimateReceiver != ultimateReceiver) { - var checkedUltimateReceiver = CheckValue(ultimateReceiver, BindValueKind.RValue, diagnostics); - if (checkedUltimateReceiver != ultimateReceiver) - { - receiver = updateUltimateReceiver(receiver, ultimateReceiver, checkedUltimateReceiver); - } - } - else - { - Debug.Assert(ultimateReceiver is not BoundQueryClause); + receiver = updateUltimateReceiver(receiver, ultimateReceiver, checkedUltimateReceiver); } } diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundPropertyAccess.cs b/src/Compilers/CSharp/Portable/BoundTree/BoundPropertyAccess.cs new file mode 100644 index 0000000000000..ee105b0647d80 --- /dev/null +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundPropertyAccess.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CodeAnalysis.CSharp; + +internal partial class BoundPropertyAccess +{ + public BoundPropertyAccess Clone() + => (BoundPropertyAccess)this.MemberwiseClone(); +} diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/QueryTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/QueryTests.cs index 50847516eefa9..d3b5cd7872e6c 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/QueryTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/QueryTests.cs @@ -4480,6 +4480,33 @@ public void SetOnlyProperty() var test = from i in c.Prop select i; +public class C { + public IEnumerable Prop + { + set {} + } +} +", options: TestOptions.ReleaseExe); + + comp.VerifyDiagnostics( + // (6,22): error CS0154: The property or indexer 'C.Prop' cannot be used in this context because it lacks the get accessor + // var test = from i in c.Prop + Diagnostic(ErrorCode.ERR_PropertyLacksGet, "c.Prop").WithArguments("C.Prop").WithLocation(6, 22) + ); + } + + [Fact, WorkItem(50316, "https://github.com/dotnet/roslyn/issues/50316")] + public void SetOnlyProperty_GroupByContinuation() + { + var comp = CreateCompilation(@" +using System.Collections.Generic; +using System.Linq; + +var c = new C(); +var test = from i in c.Prop + group i by i into g + select g; + public class C { public IEnumerable Prop { @@ -4786,5 +4813,22 @@ public static class F var comp = CreateCompilation(code); CompileAndVerify(code, expectedOutput: "ran").VerifyDiagnostics(); } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/81709")] + public void PropertyAccess_AnonymousType_GroupBy() + { + var code = """ + using System.Linq; + + var x = new { A = new[] { new { B = 1 } } }; + + var y = from a in x.A + group a by a.B into g + select 1; + """; + + var comp = CreateCompilation(code); + comp.VerifyDiagnostics(); + } } }