diff --git a/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs b/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs index a69dfe3c04d33..35b861c97a320 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs @@ -4272,7 +4272,10 @@ internal SafeContext GetValEscape(BoundExpression expr, SafeContext scopeOfTheCo var initializerOpt = objectCreation.InitializerExpressionOpt; if (initializerOpt != null) { - escape = escape.Intersect(GetValEscape(initializerOpt, scopeOfTheContainingExpression)); + escape = escape.Intersect( + initializerOpt is BoundCollectionInitializerExpression colInitExpr + ? GetValEscapeOfCollectionInitializer(colInitExpr, scopeOfTheContainingExpression, receiverScope: escape) + : GetValEscape(initializerOpt, scopeOfTheContainingExpression)); } return escape; @@ -4455,11 +4458,7 @@ internal SafeContext GetValEscape(BoundExpression expr, SafeContext scopeOfTheCo case BoundKind.CollectionInitializerExpression: var colExpr = (BoundCollectionInitializerExpression)expr; - return GetValEscape(colExpr.Initializers, scopeOfTheContainingExpression); - - case BoundKind.CollectionElementInitializer: - var colElement = (BoundCollectionElementInitializer)expr; - return GetValEscape(colElement.Arguments, scopeOfTheContainingExpression); + return GetValEscapeOfCollectionInitializer(colExpr, scopeOfTheContainingExpression, receiverScope: SafeContext.CallingMethod); case BoundKind.ObjectInitializerMember: // this node generally makes no sense outside of the context of containing initializer @@ -4468,11 +4467,18 @@ internal SafeContext GetValEscape(BoundExpression expr, SafeContext scopeOfTheCo return scopeOfTheContainingExpression; case BoundKind.ImplicitReceiver: - case BoundKind.ObjectOrCollectionValuePlaceholder: // binder uses this as a placeholder when binding members inside an object initializer // just say it does not escape anywhere, so that we do not get false errors. return scopeOfTheContainingExpression; + case BoundKind.ObjectOrCollectionValuePlaceholder: + if (_placeholderScopes?.TryGetValue((BoundObjectOrCollectionValuePlaceholder)expr, out var scope) == true) + { + return scope; + } + + return scopeOfTheContainingExpression; + case BoundKind.InterpolatedStringHandlerPlaceholder: // The handler placeholder cannot escape out of the current expression, as it's a compiler-synthesized // location. @@ -4589,6 +4595,17 @@ private SafeContext GetValEscapeOfObjectInitializer(BoundObjectInitializerExpres return result; } + private SafeContext GetValEscapeOfCollectionInitializer(BoundCollectionInitializerExpression colExpr, SafeContext scopeOfTheContainingExpression, SafeContext receiverScope) + { + // We are interested in the CheckValEscape's return value, but it can be false only if not in an unsafe region. + using var _ = new UnsafeRegion(this, inUnsafeRegion: false); + + // If arg mixing fails (i.e., some arguments could escape into the receiver), make the result scoped. + return CheckValEscapeOfCollectionInitializer(colExpr, escapeFrom: scopeOfTheContainingExpression, escapeTo: receiverScope, BindingDiagnosticBag.Discarded) + ? receiverScope + : scopeOfTheContainingExpression; + } + private SafeContext GetValEscapeOfObjectMemberInitializer(BoundExpression expr, SafeContext scopeOfTheContainingExpression) { SafeContext result; @@ -4774,6 +4791,18 @@ internal bool CheckValEscape(SyntaxNode node, BoundExpression expr, SafeContext } return true; + case BoundKind.ObjectOrCollectionValuePlaceholder: + if (_placeholderScopes?.TryGetValue((BoundObjectOrCollectionValuePlaceholder)expr, out var scope) == true) + { + if (!scope.IsConvertibleTo(escapeTo)) + { + Error(diagnostics, inUnsafeRegion ? ErrorCode.WRN_EscapeVariable : ErrorCode.ERR_EscapeVariable, node, expr.Syntax); + return inUnsafeRegion; + } + return true; + } + goto default; + case BoundKind.Local: var localSymbol = ((BoundLocal)expr).LocalSymbol; if (!GetLocalScopes(localSymbol).ValEscapeScope.IsConvertibleTo(escapeTo)) @@ -5257,17 +5286,9 @@ internal bool CheckValEscape(SyntaxNode node, BoundExpression expr, SafeContext var initExpr = (BoundObjectInitializerExpression)expr; return CheckValEscapeOfObjectInitializer(initExpr, escapeFrom, escapeTo, diagnostics); - // this would be correct implementation for CollectionInitializerExpression - // however it is unclear if it is reachable since the initialized type must implement IEnumerable case BoundKind.CollectionInitializerExpression: var colExpr = (BoundCollectionInitializerExpression)expr; - return CheckValEscape(colExpr.Initializers, escapeFrom, escapeTo, diagnostics); - - // this would be correct implementation for CollectionElementInitializer - // however it is unclear if it is reachable since the initialized type must implement IEnumerable - case BoundKind.CollectionElementInitializer: - var colElement = (BoundCollectionElementInitializer)expr; - return CheckValEscape(colElement.Arguments, escapeFrom, escapeTo, diagnostics); + return CheckValEscapeOfCollectionInitializer(colExpr, escapeFrom, escapeTo, diagnostics); case BoundKind.PointerElementAccess: var accessedExpression = ((BoundPointerElementAccess)expr).Expression; @@ -5561,21 +5582,53 @@ private bool CheckValEscapeOfObjectInitializer(BoundObjectInitializerExpression return true; } -#nullable disable - - private bool CheckValEscape(ImmutableArray expressions, SafeContext escapeFrom, SafeContext escapeTo, BindingDiagnosticBag diagnostics) + private bool CheckValEscapeOfCollectionInitializer(BoundCollectionInitializerExpression colExpr, SafeContext escapeFrom, SafeContext escapeTo, BindingDiagnosticBag diagnostics) { - foreach (var expression in expressions) + // return new C() { element }; + // + // is equivalent to + // + // var c = new C(); + // c.Add(element); + // return c; + // + // So we check arg mixing of `(receiverPlaceholder).Add(element)` calls + // where the placeholder has `escapeTo` scope and the call has `escapeFrom` scope. + + bool result = true; + using var _ = new PlaceholderRegion(this, [(colExpr.Placeholder, escapeTo)]) { ForceRemoveOnDispose = true }; + foreach (var initializer in colExpr.Initializers) { - if (!CheckValEscape(expression.Syntax, expression, escapeFrom, escapeTo, checkingReceiver: false, diagnostics: diagnostics)) + switch (initializer) { - return false; + case BoundCollectionElementInitializer init: + result &= CheckInvocationArgMixing( + init.Syntax, + MethodInfo.Create(init.AddMethod), + receiverOpt: colExpr.Placeholder, + receiverIsSubjectToCloning: init.InitialBindingReceiverIsSubjectToCloning, + parameters: init.AddMethod.Parameters, + argsOpt: init.Arguments, + argRefKindsOpt: default, + argsToParamsOpt: init.ArgsToParamsOpt, + scopeOfTheContainingExpression: escapeFrom, + diagnostics); + break; + + case BoundDynamicCollectionElementInitializer: + break; + + default: + Debug.Fail($"{initializer.Kind} expression of {initializer.Type} type"); + break; } } - return true; + return result; } +#nullable disable + private bool CheckInterpolatedStringHandlerConversionEscape(BoundExpression expression, SafeContext escapeFrom, SafeContext escapeTo, BindingDiagnosticBag diagnostics) { var data = expression.GetInterpolatedStringHandlerData(); diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs index 248134bfb8d59..2ae74735159b8 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs @@ -6473,6 +6473,7 @@ BoundExpression bindCollectionInitializerElementAddMethod( boundCall.Method, boundCall.Arguments, boundCall.ReceiverOpt, + boundCall.InitialBindingReceiverIsSubjectToCloning, boundCall.Expanded, boundCall.ArgsToParamsOpt, boundCall.DefaultArguments, diff --git a/src/Compilers/CSharp/Portable/Binder/RefSafetyAnalysis.cs b/src/Compilers/CSharp/Portable/Binder/RefSafetyAnalysis.cs index 9e7ff5ea2f103..039de4928298d 100644 --- a/src/Compilers/CSharp/Portable/Binder/RefSafetyAnalysis.cs +++ b/src/Compilers/CSharp/Portable/Binder/RefSafetyAnalysis.cs @@ -153,6 +153,8 @@ private ref struct PlaceholderRegion private readonly RefSafetyAnalysis _analysis; private readonly ArrayBuilder<(BoundValuePlaceholderBase, SafeContext)> _placeholders; + public bool ForceRemoveOnDispose { get; init; } + public PlaceholderRegion(RefSafetyAnalysis analysis, ArrayBuilder<(BoundValuePlaceholderBase, SafeContext)> placeholders) { _analysis = analysis; @@ -167,7 +169,7 @@ public void Dispose() { foreach (var (placeholder, _) in _placeholders) { - _analysis.RemovePlaceholderScope(placeholder); + _analysis.RemovePlaceholderScope(placeholder, forceRemove: ForceRemoveOnDispose); } _placeholders.Free(); } @@ -200,16 +202,17 @@ private void AddPlaceholderScope(BoundValuePlaceholderBase placeholder, SafeCont _placeholderScopes[placeholder] = valEscapeScope; } -#pragma warning disable IDE0060 - private void RemovePlaceholderScope(BoundValuePlaceholderBase placeholder) + private void RemovePlaceholderScope(BoundValuePlaceholderBase placeholder, bool forceRemove) { Debug.Assert(_placeholderScopes?.ContainsKey(placeholder) == true); // https://github.com/dotnet/roslyn/issues/65961: Currently, analysis may require subsequent calls // to GetRefEscape(), etc. for the same expression so we cannot remove placeholders eagerly. - //_placeholderScopes.Remove(placeholder); + if (forceRemove) + { + _placeholderScopes?.Remove(placeholder); + } } -#pragma warning restore IDE0060 private SafeContext GetPlaceholderScope(BoundValuePlaceholderBase placeholder) { diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml b/src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml index b68f25b9d7efe..c64c1cd98ab64 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml @@ -2057,6 +2057,8 @@ + + diff --git a/src/Compilers/CSharp/Portable/Generated/BoundNodes.xml.Generated.cs b/src/Compilers/CSharp/Portable/Generated/BoundNodes.xml.Generated.cs index 595e5105ae73c..e688571a087b4 100644 --- a/src/Compilers/CSharp/Portable/Generated/BoundNodes.xml.Generated.cs +++ b/src/Compilers/CSharp/Portable/Generated/BoundNodes.xml.Generated.cs @@ -6863,7 +6863,7 @@ public BoundCollectionInitializerExpression Update(BoundObjectOrCollectionValueP internal sealed partial class BoundCollectionElementInitializer : BoundExpression { - public BoundCollectionElementInitializer(SyntaxNode syntax, MethodSymbol addMethod, ImmutableArray arguments, BoundExpression? implicitReceiverOpt, bool expanded, ImmutableArray argsToParamsOpt, BitVector defaultArguments, bool invokedAsExtensionMethod, LookupResultKind resultKind, TypeSymbol type, bool hasErrors = false) + public BoundCollectionElementInitializer(SyntaxNode syntax, MethodSymbol addMethod, ImmutableArray arguments, BoundExpression? implicitReceiverOpt, ThreeState initialBindingReceiverIsSubjectToCloning, bool expanded, ImmutableArray argsToParamsOpt, BitVector defaultArguments, bool invokedAsExtensionMethod, LookupResultKind resultKind, TypeSymbol type, bool hasErrors = false) : base(BoundKind.CollectionElementInitializer, syntax, type, hasErrors || arguments.HasErrors() || implicitReceiverOpt.HasErrors()) { @@ -6874,6 +6874,7 @@ public BoundCollectionElementInitializer(SyntaxNode syntax, MethodSymbol addMeth this.AddMethod = addMethod; this.Arguments = arguments; this.ImplicitReceiverOpt = implicitReceiverOpt; + this.InitialBindingReceiverIsSubjectToCloning = initialBindingReceiverIsSubjectToCloning; this.Expanded = expanded; this.ArgsToParamsOpt = argsToParamsOpt; this.DefaultArguments = defaultArguments; @@ -6885,6 +6886,7 @@ public BoundCollectionElementInitializer(SyntaxNode syntax, MethodSymbol addMeth public MethodSymbol AddMethod { get; } public ImmutableArray Arguments { get; } public BoundExpression? ImplicitReceiverOpt { get; } + public ThreeState InitialBindingReceiverIsSubjectToCloning { get; } public bool Expanded { get; } public ImmutableArray ArgsToParamsOpt { get; } public BitVector DefaultArguments { get; } @@ -6894,11 +6896,11 @@ public BoundCollectionElementInitializer(SyntaxNode syntax, MethodSymbol addMeth [DebuggerStepThrough] public override BoundNode? Accept(BoundTreeVisitor visitor) => visitor.VisitCollectionElementInitializer(this); - public BoundCollectionElementInitializer Update(MethodSymbol addMethod, ImmutableArray arguments, BoundExpression? implicitReceiverOpt, bool expanded, ImmutableArray argsToParamsOpt, BitVector defaultArguments, bool invokedAsExtensionMethod, LookupResultKind resultKind, TypeSymbol type) + public BoundCollectionElementInitializer Update(MethodSymbol addMethod, ImmutableArray arguments, BoundExpression? implicitReceiverOpt, ThreeState initialBindingReceiverIsSubjectToCloning, bool expanded, ImmutableArray argsToParamsOpt, BitVector defaultArguments, bool invokedAsExtensionMethod, LookupResultKind resultKind, TypeSymbol type) { - if (!Symbols.SymbolEqualityComparer.ConsiderEverything.Equals(addMethod, this.AddMethod) || arguments != this.Arguments || implicitReceiverOpt != this.ImplicitReceiverOpt || expanded != this.Expanded || argsToParamsOpt != this.ArgsToParamsOpt || defaultArguments != this.DefaultArguments || invokedAsExtensionMethod != this.InvokedAsExtensionMethod || resultKind != this.ResultKind || !TypeSymbol.Equals(type, this.Type, TypeCompareKind.ConsiderEverything)) + if (!Symbols.SymbolEqualityComparer.ConsiderEverything.Equals(addMethod, this.AddMethod) || arguments != this.Arguments || implicitReceiverOpt != this.ImplicitReceiverOpt || initialBindingReceiverIsSubjectToCloning != this.InitialBindingReceiverIsSubjectToCloning || expanded != this.Expanded || argsToParamsOpt != this.ArgsToParamsOpt || defaultArguments != this.DefaultArguments || invokedAsExtensionMethod != this.InvokedAsExtensionMethod || resultKind != this.ResultKind || !TypeSymbol.Equals(type, this.Type, TypeCompareKind.ConsiderEverything)) { - var result = new BoundCollectionElementInitializer(this.Syntax, addMethod, arguments, implicitReceiverOpt, expanded, argsToParamsOpt, defaultArguments, invokedAsExtensionMethod, resultKind, type, this.HasErrors); + var result = new BoundCollectionElementInitializer(this.Syntax, addMethod, arguments, implicitReceiverOpt, initialBindingReceiverIsSubjectToCloning, expanded, argsToParamsOpt, defaultArguments, invokedAsExtensionMethod, resultKind, type, this.HasErrors); result.CopyAttributes(this); return result; } @@ -11859,7 +11861,7 @@ internal abstract partial class BoundTreeRewriter : BoundTreeVisitor ImmutableArray arguments = this.VisitList(node.Arguments); BoundExpression? implicitReceiverOpt = (BoundExpression?)this.Visit(node.ImplicitReceiverOpt); TypeSymbol? type = this.VisitType(node.Type); - return node.Update(node.AddMethod, arguments, implicitReceiverOpt, node.Expanded, node.ArgsToParamsOpt, node.DefaultArguments, node.InvokedAsExtensionMethod, node.ResultKind, type); + return node.Update(node.AddMethod, arguments, implicitReceiverOpt, node.InitialBindingReceiverIsSubjectToCloning, node.Expanded, node.ArgsToParamsOpt, node.DefaultArguments, node.InvokedAsExtensionMethod, node.ResultKind, type); } public override BoundNode? VisitDynamicCollectionElementInitializer(BoundDynamicCollectionElementInitializer node) { @@ -14225,12 +14227,12 @@ public NullabilityRewriter(ImmutableDictionary throw null; + } + """; + CreateCompilation([source, UnscopedRefAttributeDefinition], options: TestOptions.UnsafeReleaseDll).VerifyDiagnostics( + // (9,26): error CS8168: Cannot return local 'local' by reference because it is not a ref local + // return new R() { local }; // 1 + Diagnostic(ErrorCode.ERR_RefReturnLocal, "local").WithArguments("local").WithLocation(9, 26), + // (9,26): error CS8350: This combination of arguments to 'R.Add(in int)' is disallowed because it may expose variables referenced by parameter 'x' outside of their declaration scope + // return new R() { local }; // 1 + Diagnostic(ErrorCode.ERR_CallArgMixing, "local").WithArguments("R.Add(in int)", "x").WithLocation(9, 26), + // (15,26): warning CS9091: This returns local 'local' by reference but it is not a ref local + // return new R() { local }; // 2 + Diagnostic(ErrorCode.WRN_RefReturnLocal, "local").WithArguments("local").WithLocation(15, 26), + // (20,26): error CS8156: An expression cannot be used in this context because it may not be passed or returned by reference + // return new R() { 1 }; // 3 + Diagnostic(ErrorCode.ERR_RefReturnLvalueExpected, "1").WithLocation(20, 26), + // (20,26): error CS8350: This combination of arguments to 'R.Add(in int)' is disallowed because it may expose variables referenced by parameter 'x' outside of their declaration scope + // return new R() { 1 }; // 3 + Diagnostic(ErrorCode.ERR_CallArgMixing, "1").WithArguments("R.Add(in int)", "x").WithLocation(20, 26), + // (25,26): error CS8156: An expression cannot be used in this context because it may not be passed or returned by reference + // return new R() { 1 }; // 4 + Diagnostic(ErrorCode.ERR_RefReturnLvalueExpected, "1").WithLocation(25, 26), + // (25,26): error CS8350: This combination of arguments to 'R.Add(in int)' is disallowed because it may expose variables referenced by parameter 'x' outside of their declaration scope + // return new R() { 1 }; // 4 + Diagnostic(ErrorCode.ERR_CallArgMixing, "1").WithArguments("R.Add(in int)", "x").WithLocation(25, 26)); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/75802")] + public void CollectionInitializer_UnscopedRef_Assignment() + { + var source = """ + using System.Collections; + using System.Diagnostics.CodeAnalysis; + + class C + { + R M1() + { + var local = 1; + var r = new R() { local }; + return r; // 1 + } + + void M2() + { + var local = 1; + scoped R r; + r = new R() { local }; + } + + void M3() + { + var local = 1; + R r; + r = new R() { local }; // 2 + } + + unsafe R M4() + { + var local = 1; + var r = new R() { local }; + return r; // 3 + } + + unsafe void M5() + { + var local = 1; + scoped R r; + r = new R() { local }; + } + + unsafe void M6() + { + var local = 1; + R r; + r = new R() { local }; // 4 + } + } + + ref struct R : IEnumerable + { + public void Add([UnscopedRef] in int x) { } + IEnumerator IEnumerable.GetEnumerator() => throw null; + } + """; + CreateCompilation([source, UnscopedRefAttributeDefinition], options: TestOptions.UnsafeReleaseDll).VerifyDiagnostics( + // (10,16): error CS8352: Cannot use variable 'r' in this context because it may expose referenced variables outside of their declaration scope + // return r; // 1 + Diagnostic(ErrorCode.ERR_EscapeVariable, "r").WithArguments("r").WithLocation(10, 16), + // (24,23): error CS8168: Cannot return local 'local' by reference because it is not a ref local + // r = new R() { local }; // 2 + Diagnostic(ErrorCode.ERR_RefReturnLocal, "local").WithArguments("local").WithLocation(24, 23), + // (24,23): error CS8350: This combination of arguments to 'R.Add(in int)' is disallowed because it may expose variables referenced by parameter 'x' outside of their declaration scope + // r = new R() { local }; // 2 + Diagnostic(ErrorCode.ERR_CallArgMixing, "local").WithArguments("R.Add(in int)", "x").WithLocation(24, 23), + // (31,16): warning CS9080: Use of variable 'r' in this context may expose referenced variables outside of their declaration scope + // return r; // 3 + Diagnostic(ErrorCode.WRN_EscapeVariable, "r").WithArguments("r").WithLocation(31, 16), + // (45,23): warning CS9091: This returns local 'local' by reference but it is not a ref local + // r = new R() { local }; // 4 + Diagnostic(ErrorCode.WRN_RefReturnLocal, "local").WithArguments("local").WithLocation(45, 23)); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/75802")] + public void CollectionInitializer_UnscopedRef_OtherScopes() + { + var source = """ + using System.Collections; + using System.Diagnostics.CodeAnalysis; + + class C + { + R M1(ref int param) + { + var r = new R() { param }; + return r; // 1 + } + + R M2(ref int param) + { + var r = new R(param) { param }; + return r; + } + + R M3([UnscopedRef] ref int param) + { + var r = new R() { param }; + return r; + } + + R M4([UnscopedRef] ref int x, ref int y) + { + var r = new R(x) { y }; + return r; // 2 + } + + R M5([UnscopedRef] ref int x, ref int y) + { + var r = new R() { x, y }; + return r; // 3 + } + } + + ref struct R : IEnumerable + { + public R() { } + public R(in int p) : this() { } + + public void Add([UnscopedRef] in int x) { } + IEnumerator IEnumerable.GetEnumerator() => throw null; + } + """; + + // The `new R() { param }` expression could be inferred to have ReturnOnly scope in M1 (then there would be no errors), + // however the ref safety analysis is currently more conservative and only infers either CallingMethod or local scope for the expression. + // It is equivalent to what would happen for `var r = new R(); r.Add(param);` where the scope of `r` is also either CallingMethod or local (if `scoped` keyword is used). + // Similarly `new R(param) { param }` in M2 is equivalent to `var r = new R(param); r.Add(param);` so the scope of `r` is ReturnOnly. + + CreateCompilation([source, UnscopedRefAttributeDefinition], options: TestOptions.UnsafeReleaseDll).VerifyDiagnostics( + // (9,16): error CS8352: Cannot use variable 'r' in this context because it may expose referenced variables outside of their declaration scope + // return r; // 1 + Diagnostic(ErrorCode.ERR_EscapeVariable, "r").WithArguments("r").WithLocation(9, 16), + // (27,16): error CS8352: Cannot use variable 'r' in this context because it may expose referenced variables outside of their declaration scope + // return r; // 2 + Diagnostic(ErrorCode.ERR_EscapeVariable, "r").WithArguments("r").WithLocation(27, 16), + // (33,16): error CS8352: Cannot use variable 'r' in this context because it may expose referenced variables outside of their declaration scope + // return r; // 3 + Diagnostic(ErrorCode.ERR_EscapeVariable, "r").WithArguments("r").WithLocation(33, 16)); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/75802")] + public void CollectionInitializer_ScopedRef_Return() + { + var source = """ + using System.Collections; + + class C + { + R M() + { + var local = 1; + return new R() { local }; + } + } + + ref struct R : IEnumerable + { + public void Add(in int x) { } + IEnumerator IEnumerable.GetEnumerator() => throw null; + } + """; + CreateCompilation(source).VerifyDiagnostics(); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/75802")] + public void CollectionInitializer_ScopedRef_Assignment() + { + var source = """ + using System.Collections; + + class C + { + R M1() + { + var local = 1; + var r = new R() { local }; + return r; + } + + void M2() + { + var local = 1; + scoped R r; + r = new R() { local }; + } + + void M3() + { + var local = 1; + R r; + r = new R() { local }; + } + } + + ref struct R : IEnumerable + { + public void Add(in int x) { } + IEnumerator IEnumerable.GetEnumerator() => throw null; + } + """; + CreateCompilation(source).VerifyDiagnostics(); + } + + [Theory] + [WorkItem("https://github.com/dotnet/roslyn/issues/75802")] + [WorkItem("https://github.com/dotnet/roslyn/issues/76374")] + [InlineData("[System.Diagnostics.CodeAnalysis.UnscopedRef]")] + [InlineData("")] + public void CollectionExpression_AddMethod(string attr) + { + var source = $$""" + using System.Collections; + + class C + { + R M() + { + var local = 1; + return [local]; + } + } + + ref struct R : IEnumerable + { + public void Add({{attr}} in int x) { } + IEnumerator IEnumerable.GetEnumerator() => throw null; + } + """; + CreateCompilation([source, UnscopedRefAttributeDefinition]).VerifyDiagnostics( + // (8,16): error CS9203: A collection expression of type 'R' cannot be used in this context because it may be exposed outside of the current scope. + // return [local]; + Diagnostic(ErrorCode.ERR_CollectionExpressionEscape, "[local]").WithArguments("R").WithLocation(8, 16)); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/75802")] + public void CollectionExpression_Builder() + { + var source = """ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Runtime.CompilerServices; + + class C + { + R M() + { + var local = 1; + return [local]; + } + } + + [CollectionBuilder(typeof(Builder), nameof(Builder.Create))] + ref struct R : IEnumerable + { + public IEnumerator GetEnumerator() => throw null; + IEnumerator IEnumerable.GetEnumerator() => throw null; + } + + static class Builder + { + public static R Create(ReadOnlySpan x) => throw null; + } + """; + CreateCompilationWithSpan([source, CollectionBuilderAttributeDefinition]).VerifyDiagnostics( + // (11,16): error CS9203: A collection expression of type 'R' cannot be used in this context because it may be exposed outside of the current scope. + // return [local]; + Diagnostic(ErrorCode.ERR_CollectionExpressionEscape, "[local]").WithArguments("R").WithLocation(11, 16)); + } + [Fact] public void RefLikeEscapeMixingDelegate() {