diff --git a/src/Compilers/CSharp/Portable/Binder/RefSafetyAnalysis.cs b/src/Compilers/CSharp/Portable/Binder/RefSafetyAnalysis.cs index dc0c889e7b5d0..f14dd2fd3a9b5 100644 --- a/src/Compilers/CSharp/Portable/Binder/RefSafetyAnalysis.cs +++ b/src/Compilers/CSharp/Portable/Binder/RefSafetyAnalysis.cs @@ -21,6 +21,7 @@ internal static void Analyze(CSharpCompilation compilation, MethodSymbol symbol, var visitor = new RefSafetyAnalysis( compilation, symbol, + node, inUnsafeRegion: InUnsafeMethod(symbol), useUpdatedEscapeRules: symbol.ContainingModule.UseUpdatedEscapeRules, diagnostics); @@ -57,6 +58,7 @@ private static bool InUnsafeMethod(Symbol symbol) private readonly CSharpCompilation _compilation; private readonly MethodSymbol _symbol; + private readonly BoundNode _rootNode; private readonly bool _useUpdatedEscapeRules; private readonly BindingDiagnosticBag _diagnostics; private bool _inUnsafeRegion; @@ -72,31 +74,35 @@ private static bool InUnsafeMethod(Symbol symbol) private RefSafetyAnalysis( CSharpCompilation compilation, MethodSymbol symbol, + BoundNode rootNode, bool inUnsafeRegion, bool useUpdatedEscapeRules, BindingDiagnosticBag diagnostics) { _compilation = compilation; _symbol = symbol; + _rootNode = rootNode; _useUpdatedEscapeRules = useUpdatedEscapeRules; _diagnostics = diagnostics; _inUnsafeRegion = inUnsafeRegion; - // _localScopeDepth is incremented at each block in the method, including the - // outermost. To ensure that locals in the outermost block are considered at - // the same depth as parameters, _localScopeDepth is initialized to one less. - _localScopeDepth = SafeContext.CurrentMethod.Wider(); + _localScopeDepth = SafeContext.CurrentMethod; } private ref struct LocalScope { private readonly RefSafetyAnalysis _analysis; private readonly ImmutableArray _locals; + private readonly bool _adjustDepth; - public LocalScope(RefSafetyAnalysis analysis, ImmutableArray locals) + /// When true, narrows when the instance is created, and widens it when the instance is disposed. + public LocalScope(RefSafetyAnalysis analysis, ImmutableArray locals, bool adjustDepth = true) { _analysis = analysis; _locals = locals; - _analysis._localScopeDepth = _analysis._localScopeDepth.Narrower(); + _adjustDepth = adjustDepth; + if (adjustDepth) + _analysis._localScopeDepth = _analysis._localScopeDepth.Narrower(); + foreach (var local in locals) { _analysis.AddLocalScopes(local, refEscapeScope: _analysis._localScopeDepth, valEscapeScope: SafeContext.CallingMethod); @@ -109,7 +115,9 @@ public void Dispose() { _analysis.RemoveLocalScopes(local); } - _analysis._localScopeDepth = _analysis._localScopeDepth.Wider(); + + if (_adjustDepth) + _analysis._localScopeDepth = _analysis._localScopeDepth.Wider(); } } @@ -290,7 +298,18 @@ private bool ContainsPlaceholderScope(BoundValuePlaceholderBase placeholder) public override BoundNode? VisitBlock(BoundBlock node) { using var _1 = new UnsafeRegion(this, _inUnsafeRegion || node.HasUnsafeModifier); - using var _2 = new LocalScope(this, node.Locals); + + // Do not increase the depth if this is the top-level block of a method body. + // This is needed to ensure that top-level locals have the same lifetime as by-value parameters, for example. + bool adjustDepth = _rootNode switch + { + BoundConstructorMethodBody constructorBody => constructorBody.BlockBody != node && constructorBody.ExpressionBody != node, + BoundNonConstructorMethodBody methodBody => methodBody.BlockBody != node && methodBody.ExpressionBody != node, + BoundLambda lambda => lambda.Body != node, + BoundLocalFunctionStatement localFunction => localFunction.Body != node, + _ => true, + }; + using var _2 = new LocalScope(this, node.Locals, adjustDepth); return base.VisitBlock(node); } @@ -350,7 +369,7 @@ private void AssertVisited(BoundExpression expr) public override BoundNode? VisitLocalFunctionStatement(BoundLocalFunctionStatement node) { var localFunction = (LocalFunctionSymbol)node.Symbol; - var analysis = new RefSafetyAnalysis(_compilation, localFunction, _inUnsafeRegion || localFunction.IsUnsafe, _useUpdatedEscapeRules, _diagnostics); + var analysis = new RefSafetyAnalysis(_compilation, localFunction, node, _inUnsafeRegion || localFunction.IsUnsafe, _useUpdatedEscapeRules, _diagnostics); analysis.Visit(node.BlockBody); analysis.Visit(node.ExpressionBody); return null; @@ -359,14 +378,15 @@ private void AssertVisited(BoundExpression expr) public override BoundNode? VisitLambda(BoundLambda node) { var lambda = node.Symbol; - var analysis = new RefSafetyAnalysis(_compilation, lambda, _inUnsafeRegion, _useUpdatedEscapeRules, _diagnostics); + var analysis = new RefSafetyAnalysis(_compilation, lambda, node, _inUnsafeRegion, _useUpdatedEscapeRules, _diagnostics); analysis.Visit(node.Body); return null; } public override BoundNode? VisitConstructorMethodBody(BoundConstructorMethodBody node) { - using var _ = new LocalScope(this, node.Locals); + // Variables in a constructor initializer like `public MyType(int x) : this(M(out int y))` have the same scope as the parameters. + using var _ = new LocalScope(this, node.Locals, adjustDepth: false); return base.VisitConstructorMethodBody(node); } diff --git a/src/Compilers/CSharp/Test/Emit3/Semantics/CollectionExpressionTests.cs b/src/Compilers/CSharp/Test/Emit3/Semantics/CollectionExpressionTests.cs index 7d47c2bd50a17..a79ca40918614 100644 --- a/src/Compilers/CSharp/Test/Emit3/Semantics/CollectionExpressionTests.cs +++ b/src/Compilers/CSharp/Test/Emit3/Semantics/CollectionExpressionTests.cs @@ -26561,6 +26561,143 @@ .locals init (System.ReadOnlySpan V_0, //r1 """); } + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/80745")] + public void SpanAssignment_ScopedParameter_Constructor_Span() + { + string source = """ + using System; + + class C + { + public C(scoped ReadOnlySpan items) + { + int x = 0; + items = new ReadOnlySpan(ref x); + } + + void M(scoped ReadOnlySpan items) + { + int x = 0; + items = new ReadOnlySpan(ref x); + } + } + """; + var comp = CreateCompilation(source, targetFramework: TargetFramework.Net80); + comp.VerifyEmitDiagnostics(); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/80745")] + public void SpanAssignment_ScopedParameter_Constructor_CollectionExpression() + { + string source = """ + using System; + + class C + { + public C(scoped ReadOnlySpan items) + { + int x = 0; + items = [x]; + } + + void M(scoped ReadOnlySpan items) + { + int x = 0; + items = [x]; + } + } + """; + var comp = CreateCompilation(source, targetFramework: TargetFramework.Net80); + comp.VerifyEmitDiagnostics(); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/80745")] + public void SpanAssignment_ScopedParameter_PrimaryConstructor_Span() + { + // Each field initializer can be considered its own local scope as variables declared within it are not visible to the other initializers. + string source = """ + using System; + + class C(scoped ReadOnlySpan items) // 1 + { + private int x = M0(M1(out int x1), items = new ReadOnlySpan(ref x1)); // 2, 3 + private int y = x1; // 4 + + static int M0(int x0, ReadOnlySpan items) => items[0]; + static int M1(out int x1) { return x1 = 0; } + } + """; + var comp = CreateCompilation(source, targetFramework: TargetFramework.Net80); + comp.VerifyEmitDiagnostics( + // (3,34): warning CS9113: Parameter 'items' is unread. + // class C(scoped ReadOnlySpan items) // 1 + Diagnostic(ErrorCode.WRN_UnreadPrimaryConstructorParameter, "items").WithArguments("items").WithLocation(3, 34), + // (5,48): error CS8347: Cannot use a result of 'ReadOnlySpan.ReadOnlySpan(ref readonly int)' in this context because it may expose variables referenced by parameter 'reference' outside of their declaration scope + // private int x = M0(M1(out int x1), items = new ReadOnlySpan(ref x1)); // 2, 3 + Diagnostic(ErrorCode.ERR_EscapeCall, "new ReadOnlySpan(ref x1)").WithArguments("System.ReadOnlySpan.ReadOnlySpan(ref readonly int)", "reference").WithLocation(5, 48), + // (5,74): error CS8352: Cannot use variable 'x1' in this context because it may expose referenced variables outside of their declaration scope + // private int x = M0(M1(out int x1), items = new ReadOnlySpan(ref x1)); // 2, 3 + Diagnostic(ErrorCode.ERR_EscapeVariable, "x1").WithArguments("x1").WithLocation(5, 74), + // (6,21): error CS0103: The name 'x1' does not exist in the current context + // private int y = x1; // 4 + Diagnostic(ErrorCode.ERR_NameNotInContext, "x1").WithArguments("x1").WithLocation(6, 21)); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/80745")] + public void SpanAssignment_StaticFieldInitializer() + { + // Each field initializer can be considered its own local scope as variables declared within it are not visible to the other initializers. + string source = """ + using System; + + class C + { + private static int x = M0(M1(out ReadOnlySpan items, out int x1), items = new ReadOnlySpan(ref x1)); // 1, 2 + private static int y = items[0]; // 3 + + static int M0(int x0, ReadOnlySpan items) => items[0]; + static int M1(out ReadOnlySpan span, out int x1) { span = default; return x1 = 0; } + } + """; + var comp = CreateCompilation(source, targetFramework: TargetFramework.Net80); + comp.VerifyEmitDiagnostics( + // (5,84): error CS8347: Cannot use a result of 'ReadOnlySpan.ReadOnlySpan(ref readonly int)' in this context because it may expose variables referenced by parameter 'reference' outside of their declaration scope + // private static int x = M0(M1(out ReadOnlySpan items, out int x1), items = new ReadOnlySpan(ref x1)); // 1, 2 + Diagnostic(ErrorCode.ERR_EscapeCall, "new ReadOnlySpan(ref x1)").WithArguments("System.ReadOnlySpan.ReadOnlySpan(ref readonly int)", "reference").WithLocation(5, 84), + // (5,110): error CS8168: Cannot return local 'x1' by reference because it is not a ref local + // private static int x = M0(M1(out ReadOnlySpan items, out int x1), items = new ReadOnlySpan(ref x1)); // 1, 2 + Diagnostic(ErrorCode.ERR_RefReturnLocal, "x1").WithArguments("x1").WithLocation(5, 110), + // (6,28): error CS0103: The name 'items' does not exist in the current context + // private static int y = items[0]; // 3 + Diagnostic(ErrorCode.ERR_NameNotInContext, "items").WithArguments("items").WithLocation(6, 28)); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/80745")] + public void SpanAssignment_ScopedParameter_PrimaryConstructor_CollectionExpression() + { + // '[M1()]' cannot be assigned to 'items' because its backing storage lives in a narrower scope than 'items'. + string source = """ + using System; + + class C(scoped ReadOnlySpan items) + { + private int x = M0(items = [M1()]); + + static int M0(ReadOnlySpan x) => x[0]; + static int M1() => 0; + } + """; + var comp = CreateCompilation(source, targetFramework: TargetFramework.Net80); + comp.VerifyEmitDiagnostics( + // (3,34): warning CS9113: Parameter 'items' is unread. + // class C(scoped ReadOnlySpan items) + Diagnostic(ErrorCode.WRN_UnreadPrimaryConstructorParameter, "items").WithArguments("items").WithLocation(3, 34), + // (5,32): error CS9203: A collection expression of type 'ReadOnlySpan' cannot be used in this context because it may be exposed outside of the current scope. + // private int x = M0(items = [M1()]); + Diagnostic(ErrorCode.ERR_CollectionExpressionEscape, "[M1()]").WithArguments("System.ReadOnlySpan").WithLocation(5, 32) + ); + } + [Fact] public void SpanAssignment_WithUsingDeclaration() { diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/RefFieldTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/RefFieldTests.cs index f6545643f1ca6..4c999cf04176d 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/RefFieldTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/RefFieldTests.cs @@ -30413,6 +30413,245 @@ ref struct R Diagnostic(ErrorCode.ERR_RefReturnParameter, "x").WithArguments("x").WithLocation(4, 28)); } + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/80745")] + public void ConstructorInitializer_03() + { + // Verify that a ref to local declared in a constructor initializer cannot escape into 'this' + var source = """ + ref struct R + { + R(int x, ref int y) { } + public R() : this(M(out int x), ref x) // 1, 2 + { + } + + public static int M(out int x) => x = 1; + } + """; + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (4,16): error CS8350: This combination of arguments to 'R.R(int, ref int)' is disallowed because it may expose variables referenced by parameter 'y' outside of their declaration scope + // public R() : this(M(out int x), ref x) // 1, 2 + Diagnostic(ErrorCode.ERR_CallArgMixing, ": this(M(out int x), ref x)").WithArguments("R.R(int, ref int)", "y").WithLocation(4, 16), + // (4,41): error CS8168: Cannot return local 'x' by reference because it is not a ref local + // public R() : this(M(out int x), ref x) // 1, 2 + Diagnostic(ErrorCode.ERR_RefReturnLocal, "x").WithArguments("x").WithLocation(4, 41)); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/80745")] + public void ConstructorInitializer_04() + { + // Verify that a local in a constructor initializer is assignable to a scoped ref struct parameter + var source = """ + using System; + + struct R + { + R(int x, int y) { } + public R(scoped Span span) : this(M(out int x), (span = new Span(ref x))[0]) + { + } + + public static int M(out int x) => x = 1; + } + """; + var comp = CreateCompilation(source, targetFramework: TargetFramework.Net70); + comp.VerifyDiagnostics(); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/80745")] + public void ConstructorInitializer_05() + { + // Verify that a local in a constructor initializer is not assignable to a ref struct parameter + var source = """ + using System; + + struct R + { + R(int x, int y) { } + public R(Span span) : this(M(out int x), (span = new Span(ref x))[0]) + { + } + + public static int M(out int x) => x = 1; + } + """; + var comp = CreateCompilation(source, targetFramework: TargetFramework.Net70); + comp.VerifyDiagnostics( + // (6,59): error CS8347: Cannot use a result of 'Span.Span(ref int)' in this context because it may expose variables referenced by parameter 'reference' outside of their declaration scope + // public R(Span span) : this(M(out int x), (span = new Span(ref x))[0]) + Diagnostic(ErrorCode.ERR_EscapeCall, "new Span(ref x)").WithArguments("System.Span.Span(ref int)", "reference").WithLocation(6, 59), + // (6,77): error CS8168: Cannot return local 'x' by reference because it is not a ref local + // public R(Span span) : this(M(out int x), (span = new Span(ref x))[0]) + Diagnostic(ErrorCode.ERR_RefReturnLocal, "x").WithArguments("x").WithLocation(6, 77)); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/80745")] + public void OutParamVsConstructor_01() + { + // Expect 'this' in a constructor to behave similarly to an 'out' parameter in an ordinary method with respect to ref safety. + var source = """ + ref struct R + { + private ref readonly int _i; + private R(in int i) => _i = ref i; // ok + + void M1(out R r, in int i) + => r = new R(in i); // ok + } + """; + var comp = CreateCompilation(source, targetFramework: TargetFramework.Net70); + comp.VerifyDiagnostics(); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/80745")] + public void OutParamVsConstructor_02() + { + var source = """ + ref struct R + { + private ref readonly int _i; + private R(in int i) => _i = ref i; + + public R() + { + int i = 1; + this = new R(in i); // 1, 2 + } + + void M1(out R r) + { + int i = 1; + r = new R(in i); // 3, 4 + } + } + """; + var comp = CreateCompilation(source, targetFramework: TargetFramework.Net70); + comp.VerifyDiagnostics( + // (9,16): error CS8347: Cannot use a result of 'R.R(in int)' in this context because it may expose variables referenced by parameter 'i' outside of their declaration scope + // this = new R(in i); // 1, 2 + Diagnostic(ErrorCode.ERR_EscapeCall, "new R(in i)").WithArguments("R.R(in int)", "i").WithLocation(9, 16), + // (9,25): error CS8168: Cannot return local 'i' by reference because it is not a ref local + // this = new R(in i); // 1, 2 + Diagnostic(ErrorCode.ERR_RefReturnLocal, "i").WithArguments("i").WithLocation(9, 25), + // (15,13): error CS8347: Cannot use a result of 'R.R(in int)' in this context because it may expose variables referenced by parameter 'i' outside of their declaration scope + // r = new R(in i); // 3, 4 + Diagnostic(ErrorCode.ERR_EscapeCall, "new R(in i)").WithArguments("R.R(in int)", "i").WithLocation(15, 13), + // (15,22): error CS8168: Cannot return local 'i' by reference because it is not a ref local + // r = new R(in i); // 3, 4 + Diagnostic(ErrorCode.ERR_RefReturnLocal, "i").WithArguments("i").WithLocation(15, 22)); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/80745")] + public void OutParamVsConstructor_03() + { + var source = """ + ref struct R + { + private ref readonly int _i; + private R(in int i) => _i = ref i; + + public R() + { + this = new R(1); // 1, 2 + } + + void M1(out R r) + => r = new R(1); // 3, 4 + } + """; + var comp = CreateCompilation(source, targetFramework: TargetFramework.Net70); + comp.VerifyDiagnostics( + // (8,16): error CS8347: Cannot use a result of 'R.R(in int)' in this context because it may expose variables referenced by parameter 'i' outside of their declaration scope + // this = new R(1); // 1, 2 + Diagnostic(ErrorCode.ERR_EscapeCall, "new R(1)").WithArguments("R.R(in int)", "i").WithLocation(8, 16), + // (8,22): error CS8156: An expression cannot be used in this context because it may not be passed or returned by reference + // this = new R(1); // 1, 2 + Diagnostic(ErrorCode.ERR_RefReturnLvalueExpected, "1").WithLocation(8, 22), + // (12,16): error CS8347: Cannot use a result of 'R.R(in int)' in this context because it may expose variables referenced by parameter 'i' outside of their declaration scope + // => r = new R(1); // 3, 4 + Diagnostic(ErrorCode.ERR_EscapeCall, "new R(1)").WithArguments("R.R(in int)", "i").WithLocation(12, 16), + // (12,22): error CS8156: An expression cannot be used in this context because it may not be passed or returned by reference + // => r = new R(1); // 3, 4 + Diagnostic(ErrorCode.ERR_RefReturnLvalueExpected, "1").WithLocation(12, 22)); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/80745")] + public void OutParamVsConstructor_04() + { + var source = """ + ref struct R + { + private ref readonly int _i; + private R(in int i) => _i = ref i; + + public R() + { + int i = 1; + M1(out this, in i); // 1, 2 + } + + void M1(out R r, in int i) + => r = new R(in i); // ok + + void M2(out R r) + { + int i = 1; + M1(out r, in i); // 3, 4 + } + } + """; + var comp = CreateCompilation(source, targetFramework: TargetFramework.Net70); + comp.VerifyDiagnostics( + // (9,9): error CS8350: This combination of arguments to 'R.M1(out R, in int)' is disallowed because it may expose variables referenced by parameter 'i' outside of their declaration scope + // M1(out this, in i); // 1, 2 + Diagnostic(ErrorCode.ERR_CallArgMixing, "M1(out this, in i)").WithArguments("R.M1(out R, in int)", "i").WithLocation(9, 9), + // (9,25): error CS8168: Cannot return local 'i' by reference because it is not a ref local + // M1(out this, in i); // 1, 2 + Diagnostic(ErrorCode.ERR_RefReturnLocal, "i").WithArguments("i").WithLocation(9, 25), + // (18,9): error CS8350: This combination of arguments to 'R.M1(out R, in int)' is disallowed because it may expose variables referenced by parameter 'i' outside of their declaration scope + // M1(out r, in i); // 3, 4 + Diagnostic(ErrorCode.ERR_CallArgMixing, "M1(out r, in i)").WithArguments("R.M1(out R, in int)", "i").WithLocation(18, 9), + // (18,22): error CS8168: Cannot return local 'i' by reference because it is not a ref local + // M1(out r, in i); // 3, 4 + Diagnostic(ErrorCode.ERR_RefReturnLocal, "i").WithArguments("i").WithLocation(18, 22)); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/80745")] + public void OutParamVsConstructor_05() + { + var source = """ + ref struct R + { + private ref readonly int _i; + private R(in int i) => _i = ref i; + + void M1(out R r, in int i) + => r = new R(in i); + + public R() + => M1(out this, 1); // 1, 2 + + void M2(out R r) + => M1(out r, 1); // 3, 4 + } + """; + var comp = CreateCompilation(source, targetFramework: TargetFramework.Net70); + comp.VerifyDiagnostics( + // (10,12): error CS8350: This combination of arguments to 'R.M1(out R, in int)' is disallowed because it may expose variables referenced by parameter 'i' outside of their declaration scope + // => M1(out this, 1); // 1, 2 + Diagnostic(ErrorCode.ERR_CallArgMixing, "M1(out this, 1)").WithArguments("R.M1(out R, in int)", "i").WithLocation(10, 12), + // (10,25): error CS8156: An expression cannot be used in this context because it may not be passed or returned by reference + // => M1(out this, 1); // 1, 2 + Diagnostic(ErrorCode.ERR_RefReturnLvalueExpected, "1").WithLocation(10, 25), + // (13,12): error CS8350: This combination of arguments to 'R.M1(out R, in int)' is disallowed because it may expose variables referenced by parameter 'i' outside of their declaration scope + // => M1(out r, 1); // 3, 4 + Diagnostic(ErrorCode.ERR_CallArgMixing, "M1(out r, 1)").WithArguments("R.M1(out R, in int)", "i").WithLocation(13, 12), + // (13,22): error CS8156: An expression cannot be used in this context because it may not be passed or returned by reference + // => M1(out r, 1); // 3, 4 + Diagnostic(ErrorCode.ERR_RefReturnLvalueExpected, "1").WithLocation(13, 22)); + } + [Theory] [InlineData(LanguageVersion.CSharp10)] [InlineData(LanguageVersion.CSharp11)]