Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 31 additions & 11 deletions src/Compilers/CSharp/Portable/Binder/RefSafetyAnalysis.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -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<LocalSymbol> _locals;
private readonly bool _adjustDepth;

public LocalScope(RefSafetyAnalysis analysis, ImmutableArray<LocalSymbol> locals)
/// <param name="adjustDepth">When true, narrows <see cref="_localScopeDepth"/> when the instance is created, and widens it when the instance is disposed.</param>
public LocalScope(RefSafetyAnalysis analysis, ImmutableArray<LocalSymbol> 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);
Expand All @@ -109,7 +115,9 @@ public void Dispose()
{
_analysis.RemoveLocalScopes(local);
}
_analysis._localScopeDepth = _analysis._localScopeDepth.Wider();

if (_adjustDepth)
_analysis._localScopeDepth = _analysis._localScopeDepth.Wider();
}
}

Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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;
Expand All @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26561,6 +26561,143 @@ .locals init (System.ReadOnlySpan<T> 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<int> items)
{
int x = 0;
items = new ReadOnlySpan<int>(ref x);
}

void M(scoped ReadOnlySpan<int> items)
{
int x = 0;
items = new ReadOnlySpan<int>(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<int> items)
{
int x = 0;
items = [x];
}

void M(scoped ReadOnlySpan<int> 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<int> items) // 1
{
private int x = M0(M1(out int x1), items = new ReadOnlySpan<int>(ref x1)); // 2, 3
private int y = x1; // 4

static int M0(int x0, ReadOnlySpan<int> 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<int> items) // 1
Diagnostic(ErrorCode.WRN_UnreadPrimaryConstructorParameter, "items").WithArguments("items").WithLocation(3, 34),
// (5,48): error CS8347: Cannot use a result of 'ReadOnlySpan<int>.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<int>(ref x1)); // 2, 3
Diagnostic(ErrorCode.ERR_EscapeCall, "new ReadOnlySpan<int>(ref x1)").WithArguments("System.ReadOnlySpan<int>.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<int>(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<int> items, out int x1), items = new ReadOnlySpan<int>(ref x1)); // 1, 2
private static int y = items[0]; // 3

static int M0(int x0, ReadOnlySpan<int> items) => items[0];
static int M1(out ReadOnlySpan<int> 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<int>.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<int> items, out int x1), items = new ReadOnlySpan<int>(ref x1)); // 1, 2
Diagnostic(ErrorCode.ERR_EscapeCall, "new ReadOnlySpan<int>(ref x1)").WithArguments("System.ReadOnlySpan<int>.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<int> items, out int x1), items = new ReadOnlySpan<int>(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<int> items)
{
private int x = M0(items = [M1()]);

static int M0(ReadOnlySpan<int> 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<int> items)
Diagnostic(ErrorCode.WRN_UnreadPrimaryConstructorParameter, "items").WithArguments("items").WithLocation(3, 34),
// (5,32): error CS9203: A collection expression of type 'ReadOnlySpan<int>' 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<int>").WithLocation(5, 32)
);
}

[Fact]
public void SpanAssignment_WithUsingDeclaration()
{
Expand Down
Loading
Loading