|
// 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.
#if DEBUG
// We use a struct rather than a class to represent the state for efficiency
// for data flow analysis, with 32 bits of data inline. Merely copying the state
// variable causes the first 32 bits to be cloned, as they are inline. This can
// hide a plethora of errors that would only be exhibited in programs with more
// than 32 variables to be tracked. However, few of our tests have that many
// variables.
//
// To help diagnose these problems, we use the preprocessor symbol REFERENCE_STATE
// to cause the data flow state be a class rather than a struct. When it is a class,
// this category of problems would be exhibited in programs with a small number of
// tracked variables. But it is slower, so we only do it in DEBUG mode.
#define REFERENCE_STATE
#endif
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis.CSharp.Symbols;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.PooledObjects;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.CSharp
{
/// <summary>
/// Implement C# definite assignment.
/// </summary>
internal partial class DefiniteAssignmentPass : LocalDataFlowPass<
DefiniteAssignmentPass.LocalState,
DefiniteAssignmentPass.LocalFunctionState>
{
/// <summary>
/// A mapping from local variables to the index of their slot in a flow analysis local state.
/// </summary>
private readonly PooledDictionary<VariableIdentifier, int> _variableSlot = PooledDictionary<VariableIdentifier, int>.GetInstance();
/// <summary>
/// A mapping from the local variable slot to the symbol for the local variable itself. This
/// is used in the implementation of region analysis (support for extract method) to compute
/// the set of variables "always assigned" in a region of code.
///
/// The first slot, slot 0, is reserved for indicating reachability, so the first tracked variable will
/// be given slot 1. When referring to VariableIdentifier.ContainingSlot, slot 0 indicates
/// that the variable in VariableIdentifier.Symbol is a root, i.e. not nested within another
/// tracked variable. Slots less than 0 are illegal.
/// </summary>
protected readonly ArrayBuilder<VariableIdentifier> variableBySlot = ArrayBuilder<VariableIdentifier>.GetInstance(1, default);
/// <summary>
/// Some variables that should be considered initially assigned. Used for region analysis.
/// </summary>
private readonly HashSet<Symbol>? initiallyAssignedVariables;
/// <summary>
/// Variables that were used anywhere, in the sense required to suppress warnings about
/// unused variables.
/// </summary>
private readonly PooledHashSet<LocalSymbol> _usedVariables = PooledHashSet<LocalSymbol>.GetInstance();
/// <summary>
/// Parameters of record primary constructors that were read anywhere.
/// </summary>
private PooledHashSet<ParameterSymbol>? _readParameters;
/// <summary>
/// Variables that were used anywhere, in the sense required to suppress warnings about
/// unused variables.
/// </summary>
private readonly PooledHashSet<LocalFunctionSymbol> _usedLocalFunctions = PooledHashSet<LocalFunctionSymbol>.GetInstance();
/// <summary>
/// Variables that were initialized or written anywhere.
/// </summary>
private readonly PooledHashSet<Symbol> _writtenVariables = PooledHashSet<Symbol>.GetInstance();
/// <summary>
/// Struct fields that are implicitly initialized, due to being used before being written, or not being written at an exit point.
/// </summary>
private PooledHashSet<FieldSymbol>? _implicitlyInitializedFieldsOpt;
private void AddImplicitlyInitializedField(FieldSymbol field)
{
if (TrackImplicitlyInitializedFields)
{
(_implicitlyInitializedFieldsOpt ??= PooledHashSet<FieldSymbol>.GetInstance()).Add(field);
}
}
private bool TrackImplicitlyInitializedFields
{
get
{
return _requireOutParamsAssigned
&& !this._emptyStructTypeCache._dev12CompilerCompatibility
&& CurrentSymbol is MethodSymbol { MethodKind: MethodKind.Constructor, ContainingType.TypeKind: TypeKind.Struct };
}
}
/// <summary>
/// Map from variables that had their addresses taken, to the location of the first corresponding
/// address-of expression.
/// </summary>
/// <remarks>
/// Doesn't include fixed statement address-of operands.
/// </remarks>
private readonly PooledDictionary<Symbol, Location> _unsafeAddressTakenVariables = PooledDictionary<Symbol, Location>.GetInstance();
/// <summary>
/// Variables that were captured by anonymous functions.
/// </summary>
private readonly PooledHashSet<Symbol> _capturedVariables = PooledHashSet<Symbol>.GetInstance();
private readonly PooledHashSet<Symbol> _capturedInside = PooledHashSet<Symbol>.GetInstance();
private readonly PooledHashSet<Symbol> _capturedOutside = PooledHashSet<Symbol>.GetInstance();
/// <summary>
/// The current source assembly.
/// </summary>
private readonly SourceAssemblySymbol? _sourceAssembly;
/// <summary>
/// A set of address-of expressions for which the operand is not definitely assigned.
/// </summary>
private readonly HashSet<PrefixUnaryExpressionSyntax>? _unassignedVariableAddressOfSyntaxes;
/// <summary>
/// Tracks variables for which we have already reported a definite assignment error. This
/// allows us to report at most one such error per variable.
/// </summary>
private BitVector _alreadyReported;
/// <summary>
/// true if we should check to ensure that out parameters are assigned on every exit point.
/// </summary>
private readonly bool _requireOutParamsAssigned;
/// <summary>
/// Track fields of classes in addition to structs.
/// </summary>
private readonly bool _trackClassFields;
/// <summary>
/// Track static fields, properties, events, in addition to instance members.
/// </summary>
private readonly bool _trackStaticMembers;
/// <summary>
/// The topmost method of this analysis.
/// </summary>
protected MethodSymbol? topLevelMethod;
protected bool _convertInsufficientExecutionStackExceptionToCancelledByStackGuardException = false; // By default, just let the original exception to bubble up.
/// <summary>
/// Check that every rvalue has been converted in the definite assignment pass only (not later passes deriving from it).
/// </summary>
private readonly bool _shouldCheckConverted;
internal DefiniteAssignmentPass(
CSharpCompilation compilation,
Symbol member,
BoundNode node,
bool strictAnalysis,
bool trackUnassignments = false,
HashSet<PrefixUnaryExpressionSyntax>? unassignedVariableAddressOfSyntaxes = null,
bool requireOutParamsAssigned = true,
bool trackClassFields = false,
bool trackStaticMembers = false)
: base(compilation, member, node,
strictAnalysis ? EmptyStructTypeCache.CreatePrecise() : EmptyStructTypeCache.CreateForDev12Compatibility(compilation),
trackUnassignments)
{
this.initiallyAssignedVariables = null;
_sourceAssembly = GetSourceAssembly(compilation, member, node);
_unassignedVariableAddressOfSyntaxes = unassignedVariableAddressOfSyntaxes;
_requireOutParamsAssigned = requireOutParamsAssigned;
_trackClassFields = trackClassFields;
_trackStaticMembers = trackStaticMembers;
this.topLevelMethod = member as MethodSymbol;
_shouldCheckConverted = this.GetType() == typeof(DefiniteAssignmentPass);
State = new LocalState(BitVector.Empty);
}
internal DefiniteAssignmentPass(
CSharpCompilation compilation,
Symbol member,
BoundNode node,
EmptyStructTypeCache emptyStructs,
bool trackUnassignments = false,
HashSet<Symbol>? initiallyAssignedVariables = null)
: base(compilation, member, node, emptyStructs, trackUnassignments)
{
this.initiallyAssignedVariables = initiallyAssignedVariables;
_sourceAssembly = GetSourceAssembly(compilation, member, node);
this.CurrentSymbol = member;
_unassignedVariableAddressOfSyntaxes = null;
_requireOutParamsAssigned = true;
this.topLevelMethod = member as MethodSymbol;
_shouldCheckConverted = this.GetType() == typeof(DefiniteAssignmentPass);
State = new LocalState(BitVector.Empty);
}
/// <summary>
/// Constructor to be used for region analysis, for which a struct type should never be considered empty.
/// </summary>
internal DefiniteAssignmentPass(
CSharpCompilation compilation,
Symbol member,
BoundNode node,
BoundNode firstInRegion,
BoundNode lastInRegion,
HashSet<Symbol> initiallyAssignedVariables,
HashSet<PrefixUnaryExpressionSyntax> unassignedVariableAddressOfSyntaxes,
bool trackUnassignments)
: base(compilation, member, node, EmptyStructTypeCache.CreateNeverEmpty(), firstInRegion, lastInRegion, trackRegions: true, trackUnassignments: trackUnassignments)
{
this.initiallyAssignedVariables = initiallyAssignedVariables;
_sourceAssembly = null;
this.CurrentSymbol = member;
_unassignedVariableAddressOfSyntaxes = unassignedVariableAddressOfSyntaxes;
_shouldCheckConverted = this.GetType() == typeof(DefiniteAssignmentPass);
State = new LocalState(BitVector.Empty);
}
private static SourceAssemblySymbol? GetSourceAssembly(
CSharpCompilation compilation,
Symbol member,
BoundNode node)
{
if (member is null)
{
return null;
}
if (node.Kind == BoundKind.Attribute)
{
// member is the attribute type, not the symbol where the attribute is applied.
Debug.Assert(member is TypeSymbol type &&
(type.IsErrorType() || compilation.IsAttributeType(type)));
return null;
}
Debug.Assert((object)member.ContainingAssembly == compilation?.SourceAssembly);
return member.ContainingAssembly as SourceAssemblySymbol;
}
protected override void Free()
{
variableBySlot.Free();
_variableSlot.Free();
_usedVariables.Free();
_readParameters?.Free();
_implicitlyInitializedFieldsOpt?.Free();
_usedLocalFunctions.Free();
_writtenVariables.Free();
_capturedVariables.Free();
_capturedInside.Free();
_capturedOutside.Free();
_unsafeAddressTakenVariables.Free();
base.Free();
}
protected override bool TryGetVariable(VariableIdentifier identifier, out int slot)
{
return _variableSlot.TryGetValue(identifier, out slot);
}
protected override int AddVariable(VariableIdentifier identifier)
{
int slot = variableBySlot.Count;
_variableSlot.Add(identifier, slot);
variableBySlot.Add(identifier);
return slot;
}
#nullable disable
protected Symbol GetNonMemberSymbol(int slot)
{
VariableIdentifier variableId = variableBySlot[slot];
while (variableId.ContainingSlot > 0)
{
Debug.Assert(variableId.Symbol.Kind == SymbolKind.Field || variableId.Symbol.Kind == SymbolKind.Property || variableId.Symbol.Kind == SymbolKind.Event,
"inconsistent property symbol owner");
variableId = variableBySlot[variableId.ContainingSlot];
}
return variableId.Symbol;
}
private int RootSlot(int slot)
{
while (true)
{
int containingSlot = variableBySlot[slot].ContainingSlot;
if (containingSlot == 0)
{
return slot;
}
else
{
slot = containingSlot;
}
}
}
#if DEBUG
protected override void VisitRvalue(BoundExpression node, bool isKnownToBeAnLvalue = false)
{
Debug.Assert(
node is null ||
!_shouldCheckConverted ||
isKnownToBeAnLvalue ||
!node.NeedsToBeConverted() ||
node.WasCompilerGenerated, "expressions should have been converted");
base.VisitRvalue(node, isKnownToBeAnLvalue);
}
#endif
protected override bool ConvertInsufficientExecutionStackExceptionToCancelledByStackGuardException()
{
return _convertInsufficientExecutionStackExceptionToCancelledByStackGuardException;
}
protected override ImmutableArray<PendingBranch> Scan(ref bool badRegion)
{
this.Diagnostics.Clear();
ImmutableArray<ParameterSymbol> methodParameters = MethodParameters;
ParameterSymbol methodThisParameter = MethodThisParameter;
_alreadyReported = BitVector.Empty; // no variables yet reported unassigned
this.regionPlace = RegionPlace.Before;
EnterParameters(methodParameters); // with parameters assigned
switch (_symbol)
{
case MethodSymbol { IsStatic: false, ContainingSymbol: SourceMemberContainerTypeSymbol { PrimaryConstructor: { } primaryConstructor } } and
(not SynthesizedPrimaryConstructor):
{
var save = CurrentSymbol;
CurrentSymbol = primaryConstructor;
// All primary constructor parameters are definitely assigned outside of the primary constructor
foreach (var parameter in primaryConstructor.Parameters)
{
NoteWrite(parameter, value: null, read: true, isRef: parameter.RefKind != RefKind.None);
}
CurrentSymbol = save;
}
break;
case (FieldSymbol or PropertySymbol) and { IsStatic: false, ContainingSymbol: SourceMemberContainerTypeSymbol { PrimaryConstructor: { } primaryConstructor } }:
EnterParameters(primaryConstructor.Parameters); // with parameters assigned
break;
}
if ((object)methodThisParameter != null)
{
EnterParameter(methodThisParameter);
if (methodThisParameter.Type.SpecialType.CanOptimizeBehavior())
{
int slot = GetOrCreateSlot(methodThisParameter);
SetSlotState(slot, true);
}
}
ImmutableArray<PendingBranch> pendingReturns = base.Scan(ref badRegion);
// check that each out parameter is definitely assigned at the end of the method. If
// there's more than one location, then the method is partial and we prefer to report an
// out parameter in partial method error.
Location location;
if (ShouldAnalyzeOutParameters(out location))
{
LeaveParameters(methodParameters, null, location);
if ((object)methodThisParameter != null) LeaveParameter(methodThisParameter, null, location);
var savedState = this.State;
foreach (PendingBranch returnBranch in pendingReturns)
{
this.State = returnBranch.State;
LeaveParameters(methodParameters, returnBranch.Branch.Syntax, null);
if ((object)methodThisParameter != null) LeaveParameter(methodThisParameter, returnBranch.Branch.Syntax, null);
Join(ref savedState, ref this.State);
}
this.State = savedState;
}
return pendingReturns;
}
protected override ImmutableArray<PendingBranch> RemoveReturns()
{
var result = base.RemoveReturns();
if (CurrentSymbol is MethodSymbol currentMethod && currentMethod.IsAsync && !currentMethod.IsImplicitlyDeclared)
{
var foundAwait = result.Any(static pending => HasAwait(pending));
if (!foundAwait)
{
// If we're on a LambdaSymbol, then use its 'DiagnosticLocation'. That will be
// much better than using its 'Location' (which is the entire span of the lambda).
var diagnosticLocation = CurrentSymbol is LambdaSymbol lambda
? lambda.DiagnosticLocation
: CurrentSymbol.GetFirstLocationOrNone();
Diagnostics.Add(ErrorCode.WRN_AsyncLacksAwaits, diagnosticLocation);
}
}
return result;
}
private static bool HasAwait(PendingBranch pending)
{
var pendingBranch = pending.Branch;
if (pendingBranch is null)
{
return false;
}
BoundKind kind = pendingBranch.Kind;
switch (kind)
{
case BoundKind.AwaitExpression:
return true;
case BoundKind.UsingStatement:
var usingStatement = (BoundUsingStatement)pendingBranch;
return usingStatement.AwaitOpt != null;
case BoundKind.ForEachStatement:
var foreachStatement = (BoundForEachStatement)pendingBranch;
return foreachStatement.AwaitOpt != null;
case BoundKind.UsingLocalDeclarations:
var localDeclaration = (BoundUsingLocalDeclarations)pendingBranch;
return localDeclaration.AwaitOpt != null;
default:
return false;
}
}
// For purpose of definite assignment analysis, awaits create pending branches, so async usings and foreachs do too
public sealed override bool AwaitUsingAndForeachAddsPendingBranch => true;
protected virtual void ReportUnassignedOutParameter(ParameterSymbol parameter, SyntaxNode node, Location location)
{
if (!_requireOutParamsAssigned && ReferenceEquals(topLevelMethod, CurrentSymbol))
{
return;
}
// If node and location are null "new SourceLocation(node);" will throw a NullReferenceException
Debug.Assert(node != null || location != null);
if (Diagnostics != null && this.State.Reachable)
{
if (location == null)
{
location = new SourceLocation(node);
}
bool reported = false;
if (parameter.IsThis)
{
// if it is a "this" parameter in a struct constructor, we use a different diagnostic reflecting which pieces are not assigned
int thisSlot = VariableSlot(parameter);
Debug.Assert(thisSlot > 0);
if (!this.State.IsAssigned(thisSlot))
{
TypeSymbol parameterType = parameter.Type;
foreach (var field in _emptyStructTypeCache.GetStructInstanceFields(parameterType))
{
if (_emptyStructTypeCache.IsEmptyStructType(field.Type)) continue;
if (HasInitializer(field)) continue;
int fieldSlot = VariableSlot(field, thisSlot);
if (fieldSlot == -1 || !this.State.IsAssigned(fieldSlot))
{
Symbol associatedPropertyOrEvent = field.AssociatedSymbol;
bool hasAssociatedProperty = associatedPropertyOrEvent?.Kind == SymbolKind.Property;
if (compilation.IsFeatureEnabled(MessageID.IDS_FeatureAutoDefaultStructs))
{
Diagnostics.Add(
hasAssociatedProperty ? ErrorCode.WRN_UnassignedThisAutoPropertySupportedVersion : ErrorCode.WRN_UnassignedThisSupportedVersion,
location,
hasAssociatedProperty ? associatedPropertyOrEvent : field);
}
else
{
Diagnostics.Add(
hasAssociatedProperty ? ErrorCode.ERR_UnassignedThisAutoPropertyUnsupportedVersion : ErrorCode.ERR_UnassignedThisUnsupportedVersion,
location,
hasAssociatedProperty ? associatedPropertyOrEvent : field,
new CSharpRequiredLanguageVersion(MessageID.IDS_FeatureAutoDefaultStructs.RequiredVersion()));
}
this.AddImplicitlyInitializedField(field);
reported = true;
}
}
if (!reported)
{
if (parameterType.HasInlineArrayAttribute(out int length) && length > 1 && parameterType.TryGetPossiblyUnsupportedByLanguageInlineArrayElementField() is FieldSymbol elementField)
{
if (!compilation.IsFeatureEnabled(MessageID.IDS_FeatureAutoDefaultStructs))
{
Diagnostics.Add(ErrorCode.ERR_ParamUnassigned, location, parameter.Name);
}
// Add the element field to the set of fields requiring initialization to indicate that the whole instance needs initialization.
// This is done explicitly only for unreported cases, because, if something was reported, then we already have added the
// element field in the set. It is the only instance field in the type.
// One-length inline arrays do not need special handling because we can completely rely on the tracking around the underlying
// field itself.
this.AddImplicitlyInitializedField(elementField);
}
reported = true;
}
}
}
if (!reported)
{
Debug.Assert(!parameter.IsThis);
Diagnostics.Add(ErrorCode.ERR_ParamUnassigned, location, parameter.Name);
}
}
}
/// <summary>
/// Perform data flow analysis, reporting all necessary diagnostics.
/// </summary>
public static void Analyze(
CSharpCompilation compilation,
MethodSymbol member,
BoundNode node,
DiagnosticBag diagnostics,
out ImmutableArray<FieldSymbol> implicitlyInitializedFieldsOpt,
bool requireOutParamsAssigned)
{
Debug.Assert(diagnostics != null);
// Run the strongest version of analysis
(DiagnosticBag strictDiagnostics, implicitlyInitializedFieldsOpt) = analyze(strictAnalysis: true);
if (!strictDiagnostics.HasAnyErrors())
{
// if we have no diagnostics or only warning-level diagnostics, we know we don't need to run the compat analysis.
diagnostics.AddRangeAndFree(strictDiagnostics);
return;
}
// Also run the compat (weaker) version of analysis to see if we get the same diagnostics.
// If any are missing, the extra ones from the strong analysis will be downgraded to a warning.
(DiagnosticBag compatDiagnostics, var unused) = analyze(strictAnalysis: false);
Debug.Assert(unused.IsDefault);
// If the compat diagnostics caused a stack overflow, the two analyses might not produce comparable sets of diagnostics.
// So we just report the compat ones including that error.
if (compatDiagnostics.AsEnumerable().Any(d => (ErrorCode)d.Code == ErrorCode.ERR_InsufficientStack))
{
diagnostics.AddRangeAndFree(compatDiagnostics);
strictDiagnostics.Free();
return;
}
// If the compat diagnostics did not overflow and we have the same number of diagnostics, we just report the stricter set.
// It is OK if the strict analysis had an overflow here, causing the sets to be incomparable: the reported diagnostics will
// include the error reporting that fact.
if (strictDiagnostics.Count == compatDiagnostics.Count)
{
diagnostics.AddRangeAndFree(strictDiagnostics);
compatDiagnostics.Free();
return;
}
HashSet<Diagnostic> compatDiagnosticSet = new HashSet<Diagnostic>(compatDiagnostics.AsEnumerable(), SameDiagnosticComparer.Instance);
compatDiagnostics.Free();
foreach (var diagnostic in strictDiagnostics.AsEnumerable())
{
// If it is a warning (e.g. WRN_AsyncLacksAwaits), or an error that would be reported by the compatible analysis, just report it.
if (diagnostic.Severity != DiagnosticSeverity.Error || compatDiagnosticSet.Contains(diagnostic))
{
diagnostics.Add(diagnostic);
continue;
}
// Otherwise downgrade the error to a warning.
ErrorCode oldCode = (ErrorCode)diagnostic.Code;
ErrorCode newCode = oldCode switch
{
#pragma warning disable format
ErrorCode.ERR_UnassignedThisAutoPropertyUnsupportedVersion => ErrorCode.WRN_UnassignedThisAutoPropertyUnsupportedVersion,
ErrorCode.ERR_UnassignedThisUnsupportedVersion => ErrorCode.WRN_UnassignedThisUnsupportedVersion,
ErrorCode.ERR_ParamUnassigned => ErrorCode.WRN_ParamUnassigned,
ErrorCode.ERR_UseDefViolationProperty => ErrorCode.WRN_UseDefViolationProperty,
ErrorCode.ERR_UseDefViolationField => ErrorCode.WRN_UseDefViolationField,
ErrorCode.ERR_UseDefViolationThisUnsupportedVersion => ErrorCode.WRN_UseDefViolationThisUnsupportedVersion,
ErrorCode.ERR_UseDefViolationPropertyUnsupportedVersion => ErrorCode.WRN_UseDefViolationPropertyUnsupportedVersion,
ErrorCode.ERR_UseDefViolationFieldUnsupportedVersion => ErrorCode.WRN_UseDefViolationFieldUnsupportedVersion,
ErrorCode.ERR_UseDefViolationOut => ErrorCode.WRN_UseDefViolationOut,
ErrorCode.ERR_UseDefViolation => ErrorCode.WRN_UseDefViolation,
_ => oldCode, // rare but possible, e.g. ErrorCode.ERR_InsufficientStack occurring in strict mode only due to needing extra frames
#pragma warning restore format
};
// We don't know any other way this can happen, but if it does we recover gracefully in production.
Debug.Assert(newCode != oldCode || oldCode == ErrorCode.ERR_InsufficientStack, oldCode.ToString());
var args = diagnostic is DiagnosticWithInfo { Info: { Arguments: var arguments } } ? arguments : diagnostic.Arguments.ToArray();
diagnostics.Add(newCode, diagnostic.Location, args);
}
strictDiagnostics.Free();
return;
(DiagnosticBag, ImmutableArray<FieldSymbol> implicitlyInitializedFieldsOpt) analyze(bool strictAnalysis)
{
DiagnosticBag result = DiagnosticBag.GetInstance();
ImmutableArray<FieldSymbol> implicitlyInitializedFieldsOpt = default;
var walker = new DefiniteAssignmentPass(
compilation,
member,
node,
strictAnalysis: strictAnalysis,
requireOutParamsAssigned: requireOutParamsAssigned);
walker._convertInsufficientExecutionStackExceptionToCancelledByStackGuardException = true;
try
{
bool badRegion = false;
walker.Analyze(ref badRegion, result);
if (walker._implicitlyInitializedFieldsOpt is { } implicitlyInitializedFields)
{
Debug.Assert(walker.TrackImplicitlyInitializedFields);
var builder = ArrayBuilder<FieldSymbol>.GetInstance(implicitlyInitializedFields.Count);
foreach (var field in implicitlyInitializedFields)
{
builder.Add(field);
}
builder.Sort(LexicalOrderSymbolComparer.Instance);
implicitlyInitializedFieldsOpt = builder.ToImmutableAndFree();
}
Debug.Assert(!badRegion);
}
catch (BoundTreeVisitor.CancelledByStackGuardException ex) when (diagnostics != null)
{
ex.AddAnError(result);
}
finally
{
walker.Free();
}
Debug.Assert(strictAnalysis || implicitlyInitializedFieldsOpt.IsDefault);
return (result, implicitlyInitializedFieldsOpt);
}
}
#nullable disable
private sealed class SameDiagnosticComparer : EqualityComparer<Diagnostic>
{
public static readonly SameDiagnosticComparer Instance = new SameDiagnosticComparer();
public override bool Equals(Diagnostic x, Diagnostic y) => x.Equals(y);
public override int GetHashCode(Diagnostic obj) =>
Hash.Combine(Hash.CombineValues(obj.Arguments), Hash.Combine(obj.Location.GetHashCode(), obj.Code));
}
/// <summary>
/// Analyze the body, reporting all necessary diagnostics.
/// </summary>
protected void Analyze(ref bool badRegion, DiagnosticBag diagnostics)
{
ImmutableArray<PendingBranch> returns = Analyze(ref badRegion);
if (diagnostics != null)
{
foreach (Symbol captured in _capturedVariables)
{
Location location;
if (_unsafeAddressTakenVariables.TryGetValue(captured, out location) &&
!(captured is ParameterSymbol { ContainingSymbol: SynthesizedPrimaryConstructor primaryConstructor } parameter &&
primaryConstructor.GetCapturedParameters().ContainsKey(parameter))) // Primary constructor parameter captured by the type itself is not hoisted into a closure
{
Debug.Assert(captured.Kind == SymbolKind.Parameter || captured.Kind == SymbolKind.Local || captured.Kind == SymbolKind.RangeVariable);
diagnostics.Add(ErrorCode.ERR_LocalCantBeFixedAndHoisted, location, captured.Name);
}
}
diagnostics.AddRange(this.Diagnostics);
}
}
#nullable enable
/// <summary>
/// Check if the variable is captured and, if so, add it to this._capturedVariables.
/// </summary>
/// <param name="variable">The variable to be checked</param>
/// <param name="rangeVariableUnderlyingParameter">If variable.Kind is RangeVariable, its underlying lambda parameter. Else null.</param>
private void CheckCaptured(Symbol variable, ParameterSymbol? rangeVariableUnderlyingParameter = null)
{
if (CurrentSymbol is SourceMethodSymbol sourceMethod &&
Symbol.IsCaptured(rangeVariableUnderlyingParameter ?? variable, sourceMethod))
{
NoteCaptured(variable);
}
}
#nullable disable
/// <summary>
/// Add the variable to the captured set. For range variables we only add it if inside the region.
/// </summary>
/// <param name="variable"></param>
private void NoteCaptured(Symbol variable)
{
if (this.regionPlace == RegionPlace.Inside)
{
_capturedInside.Add(variable);
_capturedVariables.Add(variable);
}
else if (variable.Kind != SymbolKind.RangeVariable)
{
_capturedOutside.Add(variable);
_capturedVariables.Add(variable);
}
}
// do not expose PooledHashSet<T> outside of this class
protected IEnumerable<Symbol> GetCapturedInside() => _capturedInside.ToArray();
protected IEnumerable<Symbol> GetCapturedOutside() => _capturedOutside.ToArray();
protected IEnumerable<Symbol> GetCaptured() => _capturedVariables.ToArray();
protected IEnumerable<Symbol> GetUnsafeAddressTaken() => _unsafeAddressTakenVariables.Keys.ToArray();
protected IEnumerable<MethodSymbol> GetUsedLocalFunctions() => _usedLocalFunctions.ToArray();
#region Tracking reads/writes of variables for warnings
private void NotePrimaryConstructorParameterReadIfNeeded(Symbol symbol)
{
if (symbol is ParameterSymbol { ContainingSymbol: SynthesizedPrimaryConstructor } parameter)
{
_readParameters ??= PooledHashSet<ParameterSymbol>.GetInstance();
_readParameters.Add(parameter);
}
}
protected virtual void NoteRead(
Symbol variable,
ParameterSymbol rangeVariableUnderlyingParameter = null)
{
var local = variable as LocalSymbol;
if ((object)local != null)
{
_usedVariables.Add(local);
}
NotePrimaryConstructorParameterReadIfNeeded(variable);
var localFunction = variable as LocalFunctionSymbol;
if ((object)localFunction != null)
{
_usedLocalFunctions.Add(localFunction);
}
if ((object)variable != null)
{
if ((object)_sourceAssembly != null && variable.Kind == SymbolKind.Field)
{
_sourceAssembly.NoteFieldAccess((FieldSymbol)variable.OriginalDefinition,
read: true,
write: false);
}
CheckCaptured(variable, rangeVariableUnderlyingParameter);
}
}
private void NoteRead(BoundNode fieldOrEventAccess)
{
Debug.Assert(fieldOrEventAccess.Kind == BoundKind.FieldAccess || fieldOrEventAccess.Kind == BoundKind.EventAccess);
BoundNode n = fieldOrEventAccess;
while (n != null)
{
switch (n.Kind)
{
case BoundKind.FieldAccess:
{
var fieldAccess = (BoundFieldAccess)n;
NoteRead(fieldAccess.FieldSymbol);
if (MayRequireTracking(fieldAccess.ReceiverOpt, fieldAccess.FieldSymbol))
{
n = fieldAccess.ReceiverOpt;
continue;
}
else
{
return;
}
}
case BoundKind.EventAccess:
{
var eventAccess = (BoundEventAccess)n;
FieldSymbol associatedField = eventAccess.EventSymbol.AssociatedField;
if ((object)associatedField != null)
{
NoteRead(associatedField);
if (MayRequireTracking(eventAccess.ReceiverOpt, associatedField))
{
n = eventAccess.ReceiverOpt;
continue;
}
}
return;
}
case BoundKind.ThisReference:
NoteRead(MethodThisParameter);
return;
case BoundKind.Local:
NoteRead(((BoundLocal)n).LocalSymbol);
return;
case BoundKind.Parameter:
NoteRead(((BoundParameter)n).ParameterSymbol);
return;
case BoundKind.InlineArrayAccess:
{
var elementAccess = (BoundInlineArrayAccess)n;
n = elementAccess.Expression;
continue;
}
default:
return;
}
}
}
protected virtual void NoteWrite(Symbol variable, BoundExpression value, bool read, bool isRef)
{
if ((object)variable != null)
{
_writtenVariables.Add(variable);
if ((object)_sourceAssembly != null && variable.Kind == SymbolKind.Field)
{
var field = (FieldSymbol)variable.OriginalDefinition;
_sourceAssembly.NoteFieldAccess(field,
read: read && WriteConsideredUse(field.Type, value),
write: field.RefKind == RefKind.None || isRef);
}
var local = variable as LocalSymbol;
if ((object)local != null && read && WriteConsideredUse(local.Type, value))
{
// A local variable that is written to is considered to also be read,
// unless the written value is always a constant. The reasons for this
// unusual behavior are:
//
// * The debugger does not make it easy to see the returned value of
// a method. Often a call whose returned value would normally be
// discarded is written into a local variable so that it can be
// easily inspected in the debugger.
//
// * An otherwise unread local variable that contains a reference to an
// object can keep the object alive longer, particularly if the jitter
// is not optimizing the lifetimes of locals. (Because, for example,
// the debugger is running.) Again, this can be useful when debugging
// because an otherwise unused object might be finalized later, allowing
// the developer to more easily examine its state.
//
// * A developer who wishes to deliberately discard a value returned by
// a method can do so in a self-documenting manner via
// "var unread = M();"
//
// We suppress the "written but not read" message on locals unless what is
// written is a constant, a null, a default(T) expression, a default constructor
// of a value type, or a built-in conversion operating on a constant, etc.
_usedVariables.Add(local);
}
CheckCaptured(variable);
}
}
/// <summary>
/// This reflects the Dev10 compiler's rules for when a variable initialization is considered a "use"
/// for the purpose of suppressing the warning about unused variables.
/// </summary>
internal static bool WriteConsideredUse(TypeSymbol type, BoundExpression value)
{
if (value == null || value.HasAnyErrors) return true;
if ((object)type != null && type.IsReferenceType &&
type.SpecialType != SpecialType.System_String &&
type is not ArrayTypeSymbol { IsSZArray: true, ElementType.SpecialType: SpecialType.System_Byte })
{
return value.ConstantValueOpt != ConstantValue.Null;
}
if ((object)type != null && type.IsPointerOrFunctionPointer())
{
// We always suppress the warning for pointer types.
return true;
}
// In C# 9 and before, interpolated string values were never constant, so field initializers
// that used them were always considered used. We now consider interpolated strings that are
// made up of only constant expressions to be constant values, but for backcompat we consider
// the writes to be uses anyway.
if (value is { ConstantValueOpt: not null, Kind: not BoundKind.InterpolatedString }) return false;
switch (value.Kind)
{
case BoundKind.Conversion:
{
BoundConversion boundConversion = (BoundConversion)value;
// The native compiler suppresses the warning for all user defined
// conversions. A cast from int to IntPtr is also treated as an explicit
// user-defined conversion. Therefore the IntPtr ConversionKind is included
// here.
if (boundConversion.ConversionKind.IsUserDefinedConversion() ||
boundConversion.ConversionKind == ConversionKind.IntPtr)
{
return true;
}
return WriteConsideredUse(null, boundConversion.Operand);
}
case BoundKind.DefaultLiteral:
case BoundKind.DefaultExpression:
return false;
case BoundKind.ObjectCreationExpression:
var init = (BoundObjectCreationExpression)value;
return !init.Constructor.IsImplicitlyDeclared || init.InitializerExpressionOpt != null;
case BoundKind.TupleLiteral:
case BoundKind.ConvertedTupleLiteral:
case BoundKind.Utf8String:
return false;
default:
return true;
}
}
/// <param name="isRef">
/// Whether this write represents a ref-assignment.
/// </param>
private void NoteWrite(BoundExpression n, BoundExpression value, bool read, bool isRef)
{
while (n != null)
{
switch (n.Kind)
{
case BoundKind.FieldAccess:
{
var fieldAccess = (BoundFieldAccess)n;
if ((object)_sourceAssembly != null)
{
var field = fieldAccess.FieldSymbol.OriginalDefinition;
_sourceAssembly.NoteFieldAccess(field,
read: value == null || WriteConsideredUse(fieldAccess.FieldSymbol.Type, value),
write: field.RefKind == RefKind.None || isRef);
}
if (MayRequireTracking(fieldAccess.ReceiverOpt, fieldAccess.FieldSymbol))
{
n = fieldAccess.ReceiverOpt;
isRef = false;
if (n.Kind == BoundKind.Local)
{
_usedVariables.Add(((BoundLocal)n).LocalSymbol);
}
continue;
}
else
{
return;
}
}
case BoundKind.EventAccess:
{
var eventAccess = (BoundEventAccess)n;
FieldSymbol associatedField = eventAccess.EventSymbol.AssociatedField;
if ((object)associatedField != null)
{
if ((object)_sourceAssembly != null)
{
var field = associatedField.OriginalDefinition;
_sourceAssembly.NoteFieldAccess(field, read: value == null || WriteConsideredUse(associatedField.Type, value), write: true);
}
if (MayRequireTracking(eventAccess.ReceiverOpt, associatedField))
{
n = eventAccess.ReceiverOpt;
continue;
}
}
return;
}
case BoundKind.ThisReference:
NoteWrite(MethodThisParameter, value, read: read, isRef: isRef);
return;
case BoundKind.Local:
NoteWrite(((BoundLocal)n).LocalSymbol, value, read: read, isRef: isRef);
return;
case BoundKind.Parameter:
NoteWrite(((BoundParameter)n).ParameterSymbol, value, read: read, isRef: isRef);
return;
case BoundKind.RangeVariable:
NoteWrite(((BoundRangeVariable)n).Value, value, read: read, isRef: isRef);
return;
case BoundKind.InlineArrayAccess:
{
var elementAccess = (BoundInlineArrayAccess)n;
n = elementAccess.Expression;
value = null;
continue;
}
default:
return;
}
}
}
protected override void Normalize(ref LocalState state)
{
int oldNext = state.Assigned.Capacity;
int n = variableBySlot.Count;
state.Assigned.EnsureCapacity(n);
for (int i = oldNext; i < n; i++)
{
var id = variableBySlot[i];
int slot = id.ContainingSlot;
bool assign = (slot > 0) &&
state.Assigned[slot] &&
variableBySlot[slot].Symbol.GetTypeOrReturnType().TypeKind == TypeKind.Struct;
if (state.NormalizeToBottom && slot == 0)
{
// NormalizeToBottom means new variables are assumed to be assigned (bottom state)
assign = true;
}
state.Assigned[i] = assign;
}
}
protected override bool TryGetReceiverAndMember(BoundExpression expr, out BoundExpression receiver, out Symbol member)
{
receiver = null;
member = null;
switch (expr.Kind)
{
case BoundKind.FieldAccess:
{
var fieldAccess = (BoundFieldAccess)expr;
var fieldSymbol = fieldAccess.FieldSymbol;
member = fieldSymbol;
if (fieldSymbol.IsFixedSizeBuffer)
{
return false;
}
if (fieldSymbol.IsStatic)
{
return _trackStaticMembers;
}
receiver = fieldAccess.ReceiverOpt;
break;
}
case BoundKind.EventAccess:
{
var eventAccess = (BoundEventAccess)expr;
var eventSymbol = eventAccess.EventSymbol;
member = eventSymbol.AssociatedField;
if (eventSymbol.IsStatic)
{
return _trackStaticMembers;
}
receiver = eventAccess.ReceiverOpt;
break;
}
case BoundKind.PropertyAccess:
{
var propAccess = (BoundPropertyAccess)expr;
if (Binder.AccessingAutoPropertyFromConstructor(propAccess, this.CurrentSymbol))
{
var propSymbol = propAccess.PropertySymbol;
member = (propSymbol as SourcePropertySymbolBase)?.BackingField;
if (member is null)
{
return false;
}
if (propSymbol.IsStatic)
{
return _trackStaticMembers;
}
receiver = propAccess.ReceiverOpt;
}
break;
}
}
return (object)member != null &&
(object)receiver != null &&
receiver.Kind != BoundKind.TypeExpression &&
MayRequireTrackingReceiverType(receiver.Type);
}
private bool MayRequireTrackingReceiverType(TypeSymbol type)
{
return (object)type != null &&
(_trackClassFields || type.TypeKind == TypeKind.Struct);
}
protected bool MayRequireTracking(BoundExpression receiverOpt, FieldSymbol fieldSymbol)
{
return
(object)fieldSymbol != null && //simplifies calling pattern for events
receiverOpt != null &&
!fieldSymbol.IsStatic &&
!fieldSymbol.IsFixedSizeBuffer &&
receiverOpt.Kind != BoundKind.TypeExpression &&
MayRequireTrackingReceiverType(receiverOpt.Type) &&
!receiverOpt.Type.IsPrimitiveRecursiveStruct();
}
#endregion Tracking reads/writes of variables for warnings
/// <summary>
/// Check that the given variable is definitely assigned. If not, produce an error.
/// </summary>
protected void CheckAssigned(Symbol symbol, SyntaxNode node)
{
Debug.Assert(!IsConditionalState);
if ((object)symbol != null)
{
NoteRead(symbol);
if (this.State.Reachable)
{
int slot = VariableSlot(symbol);
if (slot >= this.State.Assigned.Capacity) Normalize(ref this.State);
if (slot > 0 && !this.State.IsAssigned(slot))
{
ReportUnassignedIfNotCapturedInLocalFunction(symbol, node, slot);
}
}
}
}
private void ReportUnassignedIfNotCapturedInLocalFunction(Symbol symbol, SyntaxNode node, int slot, bool skipIfUseBeforeDeclaration = true)
{
// If the symbol is captured by the nearest
// local function, record the read and skip the diagnostic
if (IsCapturedInLocalFunction(slot))
{
RecordReadInLocalFunction(slot);
return;
}
ReportUnassigned(symbol, node, slot, skipIfUseBeforeDeclaration);
}
/// <summary>
/// Report a given variable as not definitely assigned. Once a variable has been so
/// reported, we suppress further reports of that variable.
/// </summary>
protected virtual void ReportUnassigned(Symbol symbol, SyntaxNode node, int slot, bool skipIfUseBeforeDeclaration)
{
if (slot <= 0)
{
return;
}
// If this is a constant, constants are always definitely assigned
// so we should skip reporting. This can happen in a local function
// where we use a constant before we actually visit its definition
// (since local function declarations are visited before other statements)
// e.g.
// void M()
// {
// L();
// const int x = 0;
// int L() => x;
// }
if (symbol is LocalSymbol local && local.IsConst)
{
return;
}
if (slot >= _alreadyReported.Capacity)
{
_alreadyReported.EnsureCapacity(variableBySlot.Count);
}
if (skipIfUseBeforeDeclaration &&
symbol.Kind == SymbolKind.Local &&
(symbol.TryGetFirstLocation() is var location && (location is null || node.Span.End < location.SourceSpan.Start)))
{
// We've already reported the use of a local before its declaration. No need to emit
// another diagnostic for the same issue.
}
else if (!_alreadyReported[slot] && !symbol.GetTypeOrReturnType().Type.IsErrorType())
{
// CONSIDER: could suppress this diagnostic in cases where the local was declared in a using
// or fixed statement because there's a special error code for not initializing those.
string symbolName = symbol.Name;
if (symbol.Kind == SymbolKind.Field)
{
addDiagnosticForStructField(slot, (FieldSymbol)symbol);
}
else if (symbol.Kind == SymbolKind.Parameter &&
((ParameterSymbol)symbol).RefKind == RefKind.Out)
{
if (((ParameterSymbol)symbol).IsThis)
{
addDiagnosticForStructThis(symbol, slot);
}
else
{
Diagnostics.Add(ErrorCode.ERR_UseDefViolationOut, node.Location, symbolName);
}
}
else
{
Diagnostics.Add(ErrorCode.ERR_UseDefViolation, node.Location, symbolName);
}
}
// mark the variable's slot so that we don't complain about the variable again
_alreadyReported[slot] = true;
return;
void addDiagnosticForStructThis(Symbol thisParameter, int thisSlot)
{
Debug.Assert(CurrentSymbol is MethodSymbol { MethodKind: MethodKind.Constructor, ContainingType.TypeKind: TypeKind.Struct });
if (TrackImplicitlyInitializedFields)
{
bool foundUnassignedField = false;
NamedTypeSymbol containingType = thisParameter.ContainingType;
foreach (var field in _emptyStructTypeCache.GetStructInstanceFields(containingType))
{
if (_emptyStructTypeCache.IsEmptyStructType(field.Type))
continue;
if (field is TupleErrorFieldSymbol)
continue;
int slot = VariableSlot(field, thisSlot);
if (slot == -1 || !State.IsAssigned(slot))
{
AddImplicitlyInitializedField(field);
foundUnassignedField = true;
}
}
if (!foundUnassignedField && containingType.HasInlineArrayAttribute(out int length) && length > 1 && containingType.TryGetPossiblyUnsupportedByLanguageInlineArrayElementField() is FieldSymbol elementField)
{
// Add the element field to the set of fields requiring initialization to indicate that the whole instance needs initialization.
// This is done explicitly only for unreported cases, because, if something was reported, then we already have added the
// element field in the set. It is the only instance field in the type.
// One-length inline arrays do not need special handling because we can completely rely on the tracking around the underlying
// field itself.
this.AddImplicitlyInitializedField(elementField);
foundUnassignedField = true;
}
Debug.Assert(foundUnassignedField);
}
if (compilation.IsFeatureEnabled(MessageID.IDS_FeatureAutoDefaultStructs))
{
Diagnostics.Add(ErrorCode.WRN_UseDefViolationThisSupportedVersion, node.Location);
}
else
{
Diagnostics.Add(
ErrorCode.ERR_UseDefViolationThisUnsupportedVersion,
node.Location,
new CSharpRequiredLanguageVersion(MessageID.IDS_FeatureAutoDefaultStructs.RequiredVersion()));
}
}
void addDiagnosticForStructField(int fieldSlot, FieldSymbol fieldSymbol)
{
var associatedSymbol = fieldSymbol.AssociatedSymbol;
var hasAssociatedProperty = associatedSymbol?.Kind == SymbolKind.Property;
var symbolName = hasAssociatedProperty ? associatedSymbol.Name : fieldSymbol.Name;
if (CurrentSymbol is not MethodSymbol { MethodKind: MethodKind.Constructor, ContainingType.TypeKind: TypeKind.Struct })
{
Diagnostics.Add(hasAssociatedProperty ? ErrorCode.ERR_UseDefViolationProperty : ErrorCode.ERR_UseDefViolationField, node.Location, symbolName);
return;
}
var thisSlot = GetOrCreateSlot(CurrentSymbol.EnclosingThisSymbol());
while (true)
{
if (fieldSlot == 0)
{
// the offending field access is not contained in 'this'.
Diagnostics.Add(hasAssociatedProperty ? ErrorCode.ERR_UseDefViolationProperty : ErrorCode.ERR_UseDefViolationField, node.Location, symbolName);
return;
}
var fieldIdentifier = variableBySlot[fieldSlot];
var containingSlot = fieldIdentifier.ContainingSlot;
if (containingSlot == thisSlot)
{
// should we handle nested fields here? https://github.com/dotnet/roslyn/issues/59890
AddImplicitlyInitializedField((FieldSymbol)fieldIdentifier.Symbol);
if (fieldSymbol.RefKind != RefKind.None)
{
// 'hasAssociatedProperty' is only true here in error scenarios where we don't need to report this as a cascading diagnostic
if (!hasAssociatedProperty)
{
Diagnostics.Add(
ErrorCode.WRN_UseDefViolationRefField,
node.Location,
symbolName);
}
}
else if (compilation.IsFeatureEnabled(MessageID.IDS_FeatureAutoDefaultStructs))
{
Diagnostics.Add(
hasAssociatedProperty ? ErrorCode.WRN_UseDefViolationPropertySupportedVersion : ErrorCode.WRN_UseDefViolationFieldSupportedVersion,
node.Location,
symbolName);
}
else
{
Diagnostics.Add(
hasAssociatedProperty ? ErrorCode.ERR_UseDefViolationPropertyUnsupportedVersion : ErrorCode.ERR_UseDefViolationFieldUnsupportedVersion,
node.Location,
symbolName,
new CSharpRequiredLanguageVersion(MessageID.IDS_FeatureAutoDefaultStructs.RequiredVersion()));
}
return;
}
fieldSlot = containingSlot;
}
}
}
protected virtual void CheckAssigned(BoundExpression expr, FieldSymbol fieldSymbol, SyntaxNode node)
{
if (this.State.Reachable && !IsAssigned(expr, out int unassignedSlot))
{
ReportUnassignedIfNotCapturedInLocalFunction(fieldSymbol, node, unassignedSlot);
}
NoteRead(expr);
}
private bool IsAssigned(BoundExpression node, out int unassignedSlot)
{
unassignedSlot = -1;
if (_emptyStructTypeCache.IsEmptyStructType(node.Type)) return true;
switch (node.Kind)
{
case BoundKind.ThisReference:
{
var self = MethodThisParameter;
if ((object)self == null)
{
unassignedSlot = -1;
return true;
}
unassignedSlot = GetOrCreateSlot(MethodThisParameter);
break;
}
case BoundKind.Local:
{
unassignedSlot = GetOrCreateSlot(((BoundLocal)node).LocalSymbol);
break;
}
case BoundKind.FieldAccess:
{
var fieldAccess = (BoundFieldAccess)node;
if (!MayRequireTracking(fieldAccess.ReceiverOpt, fieldAccess.FieldSymbol) || IsAssigned(fieldAccess.ReceiverOpt, out unassignedSlot))
{
return true;
}
unassignedSlot = GetOrCreateSlot(fieldAccess.FieldSymbol, unassignedSlot);
break;
}
case BoundKind.EventAccess:
{
var eventAccess = (BoundEventAccess)node;
if (!MayRequireTracking(eventAccess.ReceiverOpt, eventAccess.EventSymbol.AssociatedField) || IsAssigned(eventAccess.ReceiverOpt, out unassignedSlot))
{
return true;
}
unassignedSlot = GetOrCreateSlot(eventAccess.EventSymbol.AssociatedField, unassignedSlot);
break;
}
case BoundKind.InlineArrayAccess:
{
var elementAccess = (BoundInlineArrayAccess)node;
return IsAssigned(elementAccess.Expression, out unassignedSlot);
}
case BoundKind.PropertyAccess:
{
var propertyAccess = (BoundPropertyAccess)node;
if (Binder.AccessingAutoPropertyFromConstructor(propertyAccess, this.CurrentSymbol))
{
var property = propertyAccess.PropertySymbol;
var backingField = (property as SourcePropertySymbolBase)?.BackingField;
if (backingField != null)
{
if (!MayRequireTracking(propertyAccess.ReceiverOpt, backingField) || IsAssigned(propertyAccess.ReceiverOpt, out unassignedSlot))
{
return true;
}
unassignedSlot = GetOrCreateSlot(backingField, unassignedSlot);
break;
}
}
goto default;
}
case BoundKind.Parameter:
{
var parameter = ((BoundParameter)node);
unassignedSlot = GetOrCreateSlot(parameter.ParameterSymbol);
break;
}
case BoundKind.RangeVariable:
// range variables are always assigned
default:
{
// The value is a method call return value or something else we can assume is assigned.
unassignedSlot = -1;
return true;
}
}
Debug.Assert(unassignedSlot > 0);
if (unassignedSlot > 0)
{
return this.State.IsAssigned(unassignedSlot);
}
return true;
}
private Symbol UseNonFieldSymbolUnsafely(BoundExpression expression)
{
while (expression != null)
{
switch (expression.Kind)
{
case BoundKind.FieldAccess:
{
var fieldAccess = (BoundFieldAccess)expression;
var fieldSymbol = fieldAccess.FieldSymbol;
if ((object)_sourceAssembly != null) _sourceAssembly.NoteFieldAccess(fieldSymbol, true, true);
if (fieldSymbol.ContainingType.IsReferenceType || fieldSymbol.IsStatic) return null;
expression = fieldAccess.ReceiverOpt;
continue;
}
case BoundKind.Local:
var result = ((BoundLocal)expression).LocalSymbol;
_usedVariables.Add(result);
return result;
case BoundKind.RangeVariable:
return ((BoundRangeVariable)expression).RangeVariableSymbol;
case BoundKind.Parameter:
return ((BoundParameter)expression).ParameterSymbol;
case BoundKind.ThisReference:
return this.MethodThisParameter;
case BoundKind.BaseReference:
return this.MethodThisParameter;
default:
return null;
}
}
return null;
}
protected void Assign(BoundNode node, BoundExpression value, bool isRef = false, bool read = true)
{
if (!isRef && node is BoundFieldAccess { FieldSymbol.RefKind: not RefKind.None } fieldAccess)
{
CheckAssigned(fieldAccess, node.Syntax);
}
AssignImpl(node, value, written: true, isRef: isRef, read: read);
}
/// <summary>
/// Mark a variable as assigned (or unassigned).
/// </summary>
/// <param name="node">Node being assigned to.</param>
/// <param name="value">The value being assigned.</param>
/// <param name="written">True if target location is considered written to.</param>
/// <param name="isRef">Ref assignment or value assignment.</param>
/// <param name="read">True if target location is considered read from.</param>
protected virtual void AssignImpl(BoundNode node, BoundExpression value, bool isRef, bool written, bool read)
{
Debug.Assert(!IsConditionalState);
switch (node.Kind)
{
case BoundKind.ListPattern:
case BoundKind.RecursivePattern:
case BoundKind.DeclarationPattern:
{
var pattern = (BoundObjectPattern)node;
var symbol = pattern.Variable as LocalSymbol;
if ((object)symbol != null)
{
// we do not track definite assignment for pattern variables when they are
// promoted to fields for top-level code in scripts and interactive
int slot = GetOrCreateSlot(symbol);
SetSlotState(slot, assigned: written || !this.State.Reachable);
}
if (written) NoteWrite(pattern.VariableAccess, value, read: read, isRef: isRef);
break;
}
case BoundKind.LocalDeclaration:
{
var local = (BoundLocalDeclaration)node;
Debug.Assert(local.InitializerOpt == value || value == null);
LocalSymbol symbol = local.LocalSymbol;
int slot = GetOrCreateSlot(symbol);
SetSlotState(slot, assigned: written || !this.State.Reachable);
if (written) NoteWrite(symbol, value, read: read, isRef: isRef);
break;
}
case BoundKind.Local:
{
var local = (BoundLocal)node;
if (local.LocalSymbol.RefKind != RefKind.None && !isRef)
{
// Writing through the (reference) value of a reference local
// requires us to read the reference itself.
if (written) VisitRvalue(local, isKnownToBeAnLvalue: true);
}
else
{
int slot = MakeSlot(local);
SetSlotState(slot, written);
if (written) NoteWrite(local, value, read: read, isRef: isRef);
}
break;
}
case BoundKind.InlineArrayAccess:
{
var elementAccess = (BoundInlineArrayAccess)node;
if (written)
{
NoteWrite(elementAccess.Expression, value: null, read: read, isRef: isRef);
}
if (elementAccess.Expression.Type.HasInlineArrayAttribute(out int length) &&
(elementAccess.Argument.ConstantValueOpt is { SpecialType: SpecialType.System_Int32, Int32Value: 0 } ||
Binder.InferConstantIndexFromSystemIndex(compilation, elementAccess.Argument, length, out _) is 0))
{
int slot = MakeMemberSlot(elementAccess.Expression, elementAccess.Expression.Type.TryGetInlineArrayElementField());
if (slot > 0)
{
SetSlotState(slot, written);
break;
}
}
if (!written)
{
AssignImpl(elementAccess.Expression, value: null, isRef, written, read);
int slot = MakeSlot(elementAccess.Expression);
SetSlotState(slot, written);
}
break;
}
case BoundKind.Parameter:
{
var paramExpr = (BoundParameter)node;
var param = paramExpr.ParameterSymbol;
// If we're ref-reassigning an out parameter we're effectively
// leaving the original
if (isRef && param.RefKind == RefKind.Out)
{
LeaveParameter(param, node.Syntax, paramExpr.Syntax.Location);
}
int slot = MakeSlot(paramExpr);
SetSlotState(slot, written);
if (written) NoteWrite(paramExpr, value, read: read, isRef: isRef);
break;
}
case BoundKind.ObjectInitializerMember:
{
var member = (BoundObjectInitializerMember)node;
if (_sourceAssembly is not null && member.MemberSymbol is FieldSymbol field)
{
_sourceAssembly.NoteFieldAccess(field.OriginalDefinition,
read: false,
write: field.RefKind == RefKind.None || isRef);
}
break;
}
case BoundKind.ThisReference:
case BoundKind.FieldAccess:
case BoundKind.EventAccess:
case BoundKind.PropertyAccess:
{
var expression = (BoundExpression)node;
int slot = MakeSlot(expression);
SetSlotState(slot, written);
if (written) NoteWrite(expression, value, read: read, isRef: isRef);
break;
}
case BoundKind.RangeVariable:
AssignImpl(((BoundRangeVariable)node).Value, value, isRef, written, read);
break;
case BoundKind.BadExpression:
{
// Sometimes a bad node is not so bad that we cannot analyze it at all.
var bad = (BoundBadExpression)node;
if (!bad.ChildBoundNodes.IsDefault && bad.ChildBoundNodes.Length == 1)
{
AssignImpl(bad.ChildBoundNodes[0], value, isRef, written, read);
}
break;
}
case BoundKind.TupleLiteral:
case BoundKind.ConvertedTupleLiteral:
((BoundTupleExpression)node).VisitAllElements(static (x, arg) => arg.self.Assign(x, value: null, isRef: arg.isRef), (self: this, isRef));
break;
default:
// Other kinds of left-hand-sides either represent things not tracked (e.g. array elements)
// or errors that have been reported earlier (e.g. assignment to a unary increment)
break;
}
}
/// <summary>
/// Does the struct variable at the given slot have all of its instance fields assigned?
/// </summary>
private bool FieldsAllSet(int containingSlot, LocalState state)
{
Debug.Assert(containingSlot != -1);
Debug.Assert(!state.IsAssigned(containingSlot));
VariableIdentifier variable = variableBySlot[containingSlot];
TypeSymbol structType = variable.Symbol.GetTypeOrReturnType().Type;
if (structType.HasInlineArrayAttribute(out int length) && length > 1 && structType.TryGetPossiblyUnsupportedByLanguageInlineArrayElementField() is object)
{
// An inline array of length > 1 cannot be considered fully initialized judging only based on fields.
return false;
}
foreach (var field in _emptyStructTypeCache.GetStructInstanceFields(structType))
{
if (_emptyStructTypeCache.IsEmptyStructType(field.Type)) continue;
if (field is TupleErrorFieldSymbol) continue;
int slot = VariableSlot(field, containingSlot);
if (slot == -1 || !state.IsAssigned(slot)) return false;
}
return true;
}
protected void SetSlotState(int slot, bool assigned)
{
if (slot <= 0) return;
if (assigned)
{
SetSlotAssigned(slot);
}
else
{
SetSlotUnassigned(slot);
}
}
protected void SetSlotAssigned(int slot, ref LocalState state)
{
if (slot < 0) return;
VariableIdentifier id = variableBySlot[slot];
TypeSymbol type = id.Symbol.GetTypeOrReturnType().Type;
Debug.Assert(!_emptyStructTypeCache.IsEmptyStructType(type));
if (slot >= state.Assigned.Capacity) Normalize(ref state);
if (state.IsAssigned(slot)) return; // was already fully assigned.
state.Assign(slot);
bool fieldsTracked = EmptyStructTypeCache.IsTrackableStructType(type);
// if a struct, child fields are assigned
if (fieldsTracked)
{
foreach (var field in _emptyStructTypeCache.GetStructInstanceFields(type))
{
int s2 = VariableSlot(field, slot);
if (s2 > 0) SetSlotAssigned(s2, ref state);
}
}
// if a struct member, and now all fields of enclosing are assigned, then enclosing is assigned
while (id.ContainingSlot > 0)
{
slot = id.ContainingSlot;
if (state.IsAssigned(slot) || !FieldsAllSet(slot, state)) break;
state.Assign(slot);
id = variableBySlot[slot];
}
}
private void SetSlotAssigned(int slot)
{
SetSlotAssigned(slot, ref this.State);
}
private void SetSlotUnassigned(int slot, ref LocalState state)
{
if (slot < 0) return;
VariableIdentifier id = variableBySlot[slot];
TypeSymbol type = id.Symbol.GetTypeOrReturnType().Type;
Debug.Assert(!_emptyStructTypeCache.IsEmptyStructType(type));
if (!state.IsAssigned(slot)) return; // was already unassigned
state.Unassign(slot);
bool fieldsTracked = EmptyStructTypeCache.IsTrackableStructType(type);
// if a struct, child fields are unassigned
if (fieldsTracked)
{
foreach (var field in _emptyStructTypeCache.GetStructInstanceFields(type))
{
int s2 = VariableSlot(field, slot);
if (s2 > 0) SetSlotUnassigned(s2, ref state);
}
}
// if a struct member, then the parent is unassigned
while (id.ContainingSlot > 0)
{
slot = id.ContainingSlot;
state.Unassign(slot);
id = variableBySlot[slot];
}
}
private void SetSlotUnassigned(int slot)
{
if (NonMonotonicState.HasValue)
{
var state = NonMonotonicState.Value;
SetSlotUnassigned(slot, ref state);
NonMonotonicState = state;
}
SetSlotUnassigned(slot, ref this.State);
}
protected override LocalState TopState()
{
var topState = new LocalState(BitVector.Empty);
Symbol current = CurrentSymbol;
while (current?.Kind is SymbolKind.Method or SymbolKind.Field or SymbolKind.Property)
{
if ((object)current != CurrentSymbol && current is MethodSymbol method)
{
// Enclosing method input parameters are definitely assigned
foreach (var parameter in method.Parameters)
{
if (parameter.RefKind != RefKind.Out)
{
int slot = GetOrCreateSlot(parameter);
if (slot > 0)
{
SetSlotAssigned(slot, ref topState);
}
}
}
if (method.TryGetThisParameter(out ParameterSymbol thisParameter) && thisParameter is not null)
{
if (thisParameter.RefKind != RefKind.Out)
{
int slot = GetOrCreateSlot(thisParameter);
if (slot > 0)
{
SetSlotAssigned(slot, ref topState);
}
}
}
}
Symbol containing = current.ContainingSymbol;
if (!current.IsStatic &&
containing is SourceMemberContainerTypeSymbol { PrimaryConstructor: { } primaryConstructor } &&
(object)current != primaryConstructor)
{
// All primary constructor parameters are definitely assigned outside of the primary constructor
foreach (var parameter in primaryConstructor.Parameters)
{
int slot = GetOrCreateSlot(parameter);
if (slot > 0)
{
if (current is not MethodSymbol && parameter.RefKind == RefKind.Out)
{
SetSlotUnassigned(slot, ref topState);
}
else
{
SetSlotAssigned(slot, ref topState);
}
}
}
break;
}
current = containing;
}
return topState;
}
protected override LocalState ReachableBottomState()
{
var result = new LocalState(BitVector.AllSet(variableBySlot.Count));
result.Assigned[0] = false; // make the state reachable
return result;
}
protected override void EnterParameter(ParameterSymbol parameter)
{
int slot = GetOrCreateSlot(parameter);
if (parameter.RefKind == RefKind.Out && !(this.CurrentSymbol is MethodSymbol currentMethod && currentMethod.IsAsync)) // out parameters not allowed in async
{
if (slot > 0) SetSlotState(slot, initiallyAssignedVariables?.Contains(parameter) == true);
}
else
{
// this code has no effect except in region analysis APIs such as DataFlowsOut where we unassign things
if (slot > 0) SetSlotState(slot, true);
NoteWrite(parameter, value: null, read: true, isRef: parameter.RefKind != RefKind.None);
}
if (parameter is SourceComplexParameterSymbolBase { ContainingSymbol: LocalFunctionSymbol or LambdaSymbol } sourceComplexParam)
{
// Mark attribute arguments as used.
VisitAttributes(sourceComplexParam.BindParameterAttributes());
// Mark default parameter values as used.
if (sourceComplexParam.BindParameterEqualsValue() is { } boundValue)
{
VisitRvalue(boundValue.Value);
}
}
}
/// <summary>
/// Marks attribute arguments as used.
/// </summary>
private void VisitAttributes(ImmutableArray<(CSharpAttributeData, BoundAttribute)> boundAttributes)
{
if (boundAttributes.IsDefaultOrEmpty)
{
return;
}
foreach (var (attributeData, boundAttribute) in boundAttributes)
{
// Skip invalid attributes (e.g., with a non-constant argument) to avoid superfluous diagnostics.
if (attributeData.HasErrors)
{
continue;
}
foreach (var attributeArgument in boundAttribute.ConstructorArguments)
{
VisitRvalue(attributeArgument);
}
foreach (var attributeNamedArgumentAssignment in boundAttribute.NamedArguments)
{
VisitRvalue(attributeNamedArgumentAssignment.Right);
}
}
}
protected override void LeaveParameters(ImmutableArray<ParameterSymbol> parameters, SyntaxNode syntax, Location location)
{
Debug.Assert(!this.IsConditionalState);
if (!this.State.Reachable)
{
// if the code is not reachable, then it doesn't matter if out parameters are assigned.
return;
}
base.LeaveParameters(parameters, syntax, location);
}
protected override void LeaveParameter(ParameterSymbol parameter, SyntaxNode syntax, Location location)
{
if (!parameter.IsThis && parameter.RefKind != RefKind.Out && parameter.ContainingSymbol is SynthesizedPrimaryConstructor primaryCtor)
{
if (_readParameters?.Contains(parameter) != true &&
!primaryCtor.GetCapturedParameters().ContainsKey(parameter))
{
Diagnostics.Add((primaryCtor.ContainingType is { IsRecord: true } or { IsRecordStruct: true }) ?
ErrorCode.WRN_UnreadRecordParameter :
ErrorCode.WRN_UnreadPrimaryConstructorParameter,
parameter.GetFirstLocationOrNone(), parameter.Name);
}
}
if (parameter.RefKind != RefKind.None)
{
var slot = VariableSlot(parameter);
if (slot > 0 && !this.State.IsAssigned(slot))
{
ReportUnassignedOutParameter(parameter, syntax, location);
}
NoteRead(parameter);
}
}
protected override LocalState UnreachableState()
{
LocalState result = this.State.Clone();
result.Assigned.EnsureCapacity(1);
result.Assign(0);
return result;
}
#region Visitors
public override void VisitPattern(BoundPattern pattern)
{
base.VisitPattern(pattern);
var whenFail = StateWhenFalse;
SetState(StateWhenTrue);
assignPatternVariablesAndMarkReadFields(pattern);
SetConditionalState(this.State, whenFail);
// Find the pattern variables of the pattern, and make them definitely assigned if <paramref name="definitely"/>.
// That would be false under "not" and "or" patterns.
void assignPatternVariablesAndMarkReadFields(BoundPattern pattern, bool definitely = true)
{
switch (pattern.Kind)
{
case BoundKind.DeclarationPattern:
{
var pat = (BoundDeclarationPattern)pattern;
if (definitely)
Assign(pat, value: null, isRef: false, read: false);
break;
}
case BoundKind.DiscardPattern:
case BoundKind.TypePattern:
break;
case BoundKind.SlicePattern:
{
var pat = (BoundSlicePattern)pattern;
if (pat.Pattern != null)
{
assignPatternVariablesAndMarkReadFields(pat.Pattern, definitely);
}
break;
}
case BoundKind.ConstantPattern:
{
var pat = (BoundConstantPattern)pattern;
this.VisitRvalue(pat.Value);
break;
}
case BoundKind.RecursivePattern:
{
var pat = (BoundRecursivePattern)pattern;
if (!pat.Deconstruction.IsDefaultOrEmpty)
{
foreach (var subpat in pat.Deconstruction)
{
assignPatternVariablesAndMarkReadFields(subpat.Pattern, definitely);
}
}
if (!pat.Properties.IsDefaultOrEmpty)
{
foreach (BoundPropertySubpattern sub in pat.Properties)
{
if (_sourceAssembly is not null)
{
BoundPropertySubpatternMember member = sub.Member;
while (member is not null)
{
if (member.Symbol is FieldSymbol field)
{
_sourceAssembly.NoteFieldAccess(field, read: true, write: false);
}
member = member.Receiver;
}
}
assignPatternVariablesAndMarkReadFields(sub.Pattern, definitely);
}
}
if (definitely)
Assign(pat, null, false, false);
break;
}
case BoundKind.ITuplePattern:
{
var pat = (BoundITuplePattern)pattern;
foreach (var subpat in pat.Subpatterns)
{
assignPatternVariablesAndMarkReadFields(subpat.Pattern, definitely);
}
break;
}
case BoundKind.ListPattern:
{
var pat = (BoundListPattern)pattern;
foreach (BoundPattern p in pat.Subpatterns)
{
assignPatternVariablesAndMarkReadFields(p, definitely);
}
if (definitely)
Assign(pat, null, false, false);
break;
}
case BoundKind.RelationalPattern:
{
var pat = (BoundRelationalPattern)pattern;
this.VisitRvalue(pat.Value);
break;
}
case BoundKind.NegatedPattern:
{
var pat = (BoundNegatedPattern)pattern;
assignPatternVariablesAndMarkReadFields(pat.Negated, definitely: false);
break;
}
case BoundKind.BinaryPattern:
{
var pat = (BoundBinaryPattern)pattern;
if (pat.Left is not BoundBinaryPattern)
{
bool def = definitely && !pat.Disjunction;
assignPatternVariablesAndMarkReadFields(pat.Left, def);
assignPatternVariablesAndMarkReadFields(pat.Right, def);
break;
}
// Users (such as ourselves) can have many, many nested binary patterns. To avoid crashing, do left recursion manually.
var stack = ArrayBuilder<(BoundBinaryPattern pattern, bool def)>.GetInstance();
do
{
definitely = definitely && !pat.Disjunction;
stack.Push((pat, definitely));
pat = pat.Left as BoundBinaryPattern;
} while (pat is not null);
var patAndDef = stack.Pop();
assignPatternVariablesAndMarkReadFields(patAndDef.pattern.Left, patAndDef.def);
do
{
assignPatternVariablesAndMarkReadFields(patAndDef.pattern.Right, patAndDef.def);
} while (stack.TryPop(out patAndDef));
stack.Free();
break;
}
default:
throw ExceptionUtilities.UnexpectedValue(pattern.Kind);
}
}
}
#nullable enable
public override BoundNode? VisitBlock(BoundBlock node)
{
var instrumentation = node.Instrumentation;
if (instrumentation != null)
{
DeclareVariables(instrumentation.Locals);
if (instrumentation.Prologue != null)
{
Visit(instrumentation.Prologue);
}
}
DeclareVariables(node.Locals);
VisitStatementsWithLocalFunctions(node);
// any local using symbols are implicitly read at the end of the block when they get disposed
foreach (var local in node.Locals)
{
if (local.IsUsing)
{
NoteRead(local);
}
}
ReportUnusedVariables(node.Locals);
ReportUnusedVariables(node.LocalFunctions);
if (instrumentation?.Epilogue != null)
{
Visit(instrumentation.Epilogue);
}
return null;
}
#nullable disable
private void VisitStatementsWithLocalFunctions(BoundBlock block)
{
if (!TrackingRegions && !block.LocalFunctions.IsDefaultOrEmpty)
{
// Visit the statements in two phases:
// 1. Local function declarations
// 2. Everything else
//
// The idea behind visiting local functions first is
// that we may be able to gather the captured variables
// they read and write ahead of time in a single pass, so
// when they are used by other statements in the block we
// won't have to recompute the set by doing multiple passes.
//
// If the local functions contain forward calls to other local
// functions then we may have to do another pass regardless,
// but hopefully that will be an uncommon case in real-world code.
// First phase
foreach (var stmt in block.Statements)
{
if (stmt is BoundLocalFunctionStatement localFunctionStatement)
{
// Mark attribute arguments as used.
VisitAttributes(localFunctionStatement.Symbol.BindMethodAttributes());
VisitAlways(stmt);
}
}
// Second phase
foreach (var stmt in block.Statements)
{
if (stmt.Kind != BoundKind.LocalFunctionStatement)
{
VisitStatement(stmt);
}
}
}
else
{
foreach (var stmt in block.Statements)
{
VisitStatement(stmt);
}
}
}
public override BoundNode VisitSwitchStatement(BoundSwitchStatement node)
{
DeclareVariables(node.InnerLocals);
var result = base.VisitSwitchStatement(node);
ReportUnusedVariables(node.InnerLocals);
ReportUnusedVariables(node.InnerLocalFunctions);
return result;
}
protected override void VisitSwitchSection(BoundSwitchSection node, bool isLastSection)
{
DeclareVariables(node.Locals);
base.VisitSwitchSection(node, isLastSection);
}
public override BoundNode VisitForStatement(BoundForStatement node)
{
DeclareVariables(node.OuterLocals);
DeclareVariables(node.InnerLocals);
var result = base.VisitForStatement(node);
ReportUnusedVariables(node.InnerLocals);
ReportUnusedVariables(node.OuterLocals);
return result;
}
public override BoundNode VisitDoStatement(BoundDoStatement node)
{
DeclareVariables(node.Locals);
var result = base.VisitDoStatement(node);
ReportUnusedVariables(node.Locals);
return result;
}
public override BoundNode VisitWhileStatement(BoundWhileStatement node)
{
DeclareVariables(node.Locals);
var result = base.VisitWhileStatement(node);
ReportUnusedVariables(node.Locals);
return result;
}
/// <remarks>
/// Variables declared in a using statement are always considered used, so this is just an assert.
/// </remarks>
public override BoundNode VisitUsingStatement(BoundUsingStatement node)
{
var localsOpt = node.Locals;
DeclareVariables(localsOpt);
var result = base.VisitUsingStatement(node);
if (!localsOpt.IsDefaultOrEmpty)
{
foreach (LocalSymbol local in localsOpt)
{
if (local.DeclarationKind == LocalDeclarationKind.UsingVariable)
{
// At the end of the statement, there's an implied read when the local is disposed
NoteRead(local);
Debug.Assert(_usedVariables.Contains(local));
}
}
}
return result;
}
public override BoundNode VisitFixedStatement(BoundFixedStatement node)
{
DeclareVariables(node.Locals);
return base.VisitFixedStatement(node);
}
public override BoundNode VisitSequence(BoundSequence node)
{
DeclareVariables(node.Locals);
var result = base.VisitSequence(node);
ReportUnusedVariables(node.Locals);
return result;
}
private void DeclareVariables(ImmutableArray<LocalSymbol> locals)
{
foreach (var symbol in locals)
{
DeclareVariable(symbol);
}
}
private void DeclareVariables(OneOrMany<LocalSymbol> locals)
{
foreach (var symbol in locals)
{
DeclareVariable(symbol);
}
}
private void DeclareVariable(LocalSymbol symbol)
{
var initiallyAssigned =
symbol.IsConst ||
// When data flow analysis determines that the variable is sometimes used without being assigned
// first, we want to treat that variable, during region analysis, as assigned where it is introduced.
initiallyAssignedVariables?.Contains(symbol) == true;
SetSlotState(GetOrCreateSlot(symbol), initiallyAssigned);
}
private void ReportUnusedVariables(ImmutableArray<LocalSymbol> locals)
{
foreach (var symbol in locals)
{
ReportIfUnused(symbol, assigned: true);
}
}
private void ReportIfUnused(LocalSymbol symbol, bool assigned)
{
if (!_usedVariables.Contains(symbol))
{
if (symbol.DeclarationKind != LocalDeclarationKind.PatternVariable && !string.IsNullOrEmpty(symbol.Name)) // avoid diagnostics for parser-inserted names
{
Diagnostics.Add(assigned && _writtenVariables.Contains(symbol) ? ErrorCode.WRN_UnreferencedVarAssg : ErrorCode.WRN_UnreferencedVar, symbol.GetFirstLocationOrNone(), symbol.Name);
}
}
}
private void ReportUnusedVariables(ImmutableArray<LocalFunctionSymbol> locals)
{
foreach (var symbol in locals)
{
ReportIfUnused(symbol);
}
}
private void ReportIfUnused(LocalFunctionSymbol symbol)
{
if (!_usedLocalFunctions.Contains(symbol))
{
if (!string.IsNullOrEmpty(symbol.Name)) // avoid diagnostics for parser-inserted names
{
Diagnostics.Add(ErrorCode.WRN_UnreferencedLocalFunction, symbol.GetFirstLocationOrNone(), symbol.Name);
}
}
}
public override BoundNode VisitLocal(BoundLocal node)
{
LocalSymbol localSymbol = node.LocalSymbol;
if ((localSymbol as SourceLocalSymbol)?.IsVar == true && localSymbol.ForbiddenZone?.Contains(node.Syntax) == true)
{
// Since we've already reported a use of the variable where not permitted, we
// suppress the diagnostic that the variable may not be assigned where used.
int slot = GetOrCreateSlot(node.LocalSymbol);
if (slot > 0)
{
_alreadyReported[slot] = true;
}
}
// Note: the caller should avoid allowing this to be called for the left-hand-side of
// an assignment (if a simple variable or this-qualified or deconstruction variables) or an out parameter.
// That's because this code assumes the variable is being read, not written.
CheckAssigned(localSymbol, node.Syntax);
if (localSymbol.IsFixed && this.CurrentSymbol is MethodSymbol currentMethod &&
(currentMethod.MethodKind == MethodKind.AnonymousFunction ||
currentMethod.MethodKind == MethodKind.LocalFunction) &&
_capturedVariables.Contains(localSymbol))
{
Diagnostics.Add(ErrorCode.ERR_FixedLocalInLambda, new SourceLocation(node.Syntax), localSymbol);
}
SplitIfBooleanConstant(node);
return null;
}
public override BoundNode VisitLocalDeclaration(BoundLocalDeclaration node)
{
_ = GetOrCreateSlot(node.LocalSymbol); // not initially assigned
if (initiallyAssignedVariables?.Contains(node.LocalSymbol) == true)
{
// When data flow analysis determines that the variable is sometimes
// used without being assigned first, we want to treat that variable, during region analysis,
// as assigned at its point of declaration.
Assign(node, value: null);
}
var result = base.VisitLocalDeclaration(node);
if (node.InitializerOpt != null)
{
Assign(node, node.InitializerOpt);
}
return result;
}
public override BoundNode VisitLocalId(BoundLocalId node)
=> null;
public override BoundNode VisitParameterId(BoundParameterId node)
=> null;
public override BoundNode VisitStateMachineInstanceId(BoundStateMachineInstanceId node)
=> null;
public override BoundNode VisitMethodGroup(BoundMethodGroup node)
{
foreach (var method in node.Methods)
{
if (method.MethodKind == MethodKind.LocalFunction)
{
_usedLocalFunctions.Add((LocalFunctionSymbol)method);
}
}
return base.VisitMethodGroup(node);
}
public override BoundNode VisitLambda(BoundLambda node)
{
var oldSymbol = this.CurrentSymbol;
this.CurrentSymbol = node.Symbol;
// Mark attribute arguments as used.
VisitAttributes(node.Symbol.BindMethodAttributes());
var oldPending = SavePending(); // we do not support branches into a lambda
// State after the lambda declaration
LocalState stateAfterLambda = this.State;
this.State = this.State.Reachable ? this.State.Clone() : ReachableBottomState();
if (!node.WasCompilerGenerated) EnterParameters(node.Symbol.Parameters);
var oldPending2 = SavePending();
VisitAlways(node.Body);
RestorePending(oldPending2); // process any forward branches within the lambda body
ImmutableArray<PendingBranch> pendingReturns = RemoveReturns();
RestorePending(oldPending);
LeaveParameters(node.Symbol.Parameters, node.Syntax, null);
Join(ref stateAfterLambda, ref this.State); // a no-op except in region analysis
foreach (PendingBranch pending in pendingReturns)
{
this.State = pending.State;
if (pending.Branch.Kind == BoundKind.ReturnStatement)
{
// ensure out parameters are definitely assigned at each return
LeaveParameters(node.Symbol.Parameters, pending.Branch.Syntax, null);
}
else
{
// other ways of branching out of a lambda are errors, previously reported in control-flow analysis
}
Join(ref stateAfterLambda, ref this.State); // a no-op except in region analysis
}
this.State = stateAfterLambda;
this.CurrentSymbol = oldSymbol;
return null;
}
public override BoundNode VisitThisReference(BoundThisReference node)
{
// TODO: in a struct constructor, "this" is not initially assigned.
CheckAssigned(MethodThisParameter, node.Syntax);
return null;
}
public override BoundNode VisitParameter(BoundParameter node)
{
if (!node.WasCompilerGenerated)
{
CheckAssigned(node.ParameterSymbol, node.Syntax);
}
else
{
NotePrimaryConstructorParameterReadIfNeeded(node.ParameterSymbol);
}
return null;
}
public override BoundNode VisitAssignmentOperator(BoundAssignmentOperator node)
{
base.VisitAssignmentOperator(node);
Assign(node.Left, node.Right, isRef: node.IsRef);
return null;
}
public override BoundNode VisitDeconstructionAssignmentOperator(BoundDeconstructionAssignmentOperator node)
{
base.VisitDeconstructionAssignmentOperator(node);
Assign(node.Left, node.Right);
return null;
}
public override BoundNode VisitIncrementOperator(BoundIncrementOperator node)
{
base.VisitIncrementOperator(node);
Assign(node.Operand, value: node);
return null;
}
public override BoundNode VisitCompoundAssignmentOperator(BoundCompoundAssignmentOperator node)
{
VisitCompoundAssignmentTarget(node);
VisitRvalue(node.Right);
AfterRightHasBeenVisited(node);
Assign(node.Left, value: node);
return null;
}
public override BoundNode VisitFixedLocalCollectionInitializer(BoundFixedLocalCollectionInitializer node)
{
var initializer = node.Expression;
if (initializer.Kind == BoundKind.AddressOfOperator)
{
initializer = ((BoundAddressOfOperator)initializer).Operand;
}
// If the node is a fixed statement address-of operator (e.g. fixed(int *p = &...)),
// then we don't need to consider it for membership in unsafeAddressTakenVariables,
// because it is either not a local/parameter/range variable (if the variable is
// non-moveable) or it is and it has a RefKind other than None, in which case it can't
// be referred to in a lambda (i.e. can't be captured).
VisitAddressOfOperand(initializer, shouldReadOperand: false);
return null;
}
public override BoundNode VisitAddressOfOperator(BoundAddressOfOperator node)
{
BoundExpression operand = node.Operand;
bool shouldReadOperand = false;
Symbol variable = UseNonFieldSymbolUnsafely(operand);
if ((object)variable != null)
{
// The goal here is to treat address-of as a read in cases where
// we (a) care about a read happening (e.g. for DataFlowsIn) and
// (b) have information indicating that this will not result in
// a read to an unassigned variable (i.e. the operand is definitely
// assigned).
if (_unassignedVariableAddressOfSyntaxes?.Contains(node.Syntax as PrefixUnaryExpressionSyntax) == false)
{
shouldReadOperand = true;
}
if (!_unsafeAddressTakenVariables.ContainsKey(variable))
{
_unsafeAddressTakenVariables.Add(variable, node.Syntax.Location);
}
}
VisitAddressOfOperand(node.Operand, shouldReadOperand);
return null;
}
#nullable enable
protected override void WriteArgument(BoundExpression arg, RefKind refKind, MethodSymbol method)
{
if (refKind == RefKind.Ref)
{
// Though the method might write the argument, in the case of ref arguments it might not,
// thus leaving the old value in the variable. We model this as a read of the argument
// by the method after the invocation.
CheckAssigned(arg, arg.Syntax);
}
Assign(arg, value: null);
// Imitate Dev10 behavior: if the argument is passed by ref/out to an external method, then
// we assume that external method may write and/or read all of its fields (recursively).
// Strangely, the native compiler requires the "ref", even for reference types, to exhibit
// this behavior.
if (refKind != RefKind.None && ((object)method == null || method.IsExtern) && arg.Type is TypeSymbol type)
{
MarkFieldsUsed(type);
}
}
#nullable disable
protected void CheckAssigned(BoundExpression expr, SyntaxNode node)
{
if (!this.State.Reachable) return;
int slot = MakeSlot(expr);
switch (expr.Kind)
{
case BoundKind.Local:
CheckAssigned(((BoundLocal)expr).LocalSymbol, node);
break;
case BoundKind.Parameter:
CheckAssigned(((BoundParameter)expr).ParameterSymbol, node);
break;
case BoundKind.FieldAccess:
var field = (BoundFieldAccess)expr;
var symbol = field.FieldSymbol;
if (!symbol.IsFixedSizeBuffer && MayRequireTracking(field.ReceiverOpt, symbol))
{
CheckAssigned(expr, symbol, node);
}
break;
case BoundKind.EventAccess:
var @event = (BoundEventAccess)expr;
FieldSymbol associatedField = @event.EventSymbol.AssociatedField;
if ((object)associatedField != null && MayRequireTracking(@event.ReceiverOpt, associatedField))
{
CheckAssigned(@event, associatedField, node);
}
break;
case BoundKind.ThisReference:
case BoundKind.BaseReference:
CheckAssigned(MethodThisParameter, node);
break;
case BoundKind.InlineArrayAccess:
CheckAssigned(((BoundInlineArrayAccess)expr).Expression, node);
break;
}
}
#nullable enable
private void MarkFieldsUsed(TypeSymbol type)
{
switch (type.TypeKind)
{
case TypeKind.Array:
MarkFieldsUsed(((ArrayTypeSymbol)type).ElementType);
return;
case TypeKind.Class:
case TypeKind.Struct:
if (!type.IsFromCompilation(this.compilation))
{
return;
}
if (!(type.ContainingAssembly is SourceAssemblySymbol assembly))
{
return; // could be retargeting assembly
}
var seen = assembly.TypesReferencedInExternalMethods;
if (seen.Add(type))
{
var namedType = (NamedTypeSymbol)type;
foreach (var symbol in namedType.GetMembersUnordered())
{
if (symbol.Kind != SymbolKind.Field)
{
continue;
}
FieldSymbol field = (FieldSymbol)symbol;
assembly.NoteFieldAccess(field, read: true, write: true);
MarkFieldsUsed(field.Type);
}
}
return;
}
}
#nullable disable
public override BoundNode VisitBaseReference(BoundBaseReference node)
{
CheckAssigned(MethodThisParameter, node.Syntax);
return null;
}
protected override void VisitCatchBlock(BoundCatchBlock catchBlock, ref LocalState finallyState)
{
DeclareVariables(catchBlock.Locals);
var exceptionSource = catchBlock.ExceptionSourceOpt;
if (exceptionSource != null)
{
Assign(exceptionSource, value: null, read: false);
}
base.VisitCatchBlock(catchBlock, ref finallyState);
foreach (var local in catchBlock.Locals)
{
ReportIfUnused(local, assigned: local.DeclarationKind != LocalDeclarationKind.CatchVariable);
}
}
public override BoundNode VisitFieldAccess(BoundFieldAccess node)
{
var result = base.VisitFieldAccess(node);
NoteRead(node.FieldSymbol);
if (node.FieldSymbol.IsFixedSizeBuffer && node.Syntax != null && !SyntaxFacts.IsFixedStatementExpression(node.Syntax))
{
Symbol receiver = UseNonFieldSymbolUnsafely(node.ReceiverOpt);
if ((object)receiver != null)
{
CheckCaptured(receiver);
if (!_unsafeAddressTakenVariables.ContainsKey(receiver))
{
_unsafeAddressTakenVariables.Add(receiver, node.Syntax.Location);
}
}
}
else if (MayRequireTracking(node.ReceiverOpt, node.FieldSymbol))
{
// special definite assignment behavior for fields of struct local variables.
CheckAssigned(node, node.FieldSymbol, node.Syntax);
}
return result;
}
public override BoundNode VisitPropertyAccess(BoundPropertyAccess node)
{
var result = base.VisitPropertyAccess(node);
if (Binder.AccessingAutoPropertyFromConstructor(node, this.CurrentSymbol))
{
var property = node.PropertySymbol;
var backingField = (property as SourcePropertySymbolBase)?.BackingField;
if (backingField != null)
{
if (MayRequireTracking(node.ReceiverOpt, backingField))
{
// special definite assignment behavior for fields of struct local variables.
int unassignedSlot;
if (this.State.Reachable && !IsAssigned(node, out unassignedSlot))
{
ReportUnassignedIfNotCapturedInLocalFunction(backingField, node.Syntax, unassignedSlot);
}
}
}
}
return result;
}
public override BoundNode VisitEventAccess(BoundEventAccess node)
{
var result = base.VisitEventAccess(node);
// special definite assignment behavior for events of struct local variables.
FieldSymbol associatedField = node.EventSymbol.AssociatedField;
if ((object)associatedField != null)
{
NoteRead(associatedField);
if (MayRequireTracking(node.ReceiverOpt, associatedField))
{
CheckAssigned(node, associatedField, node.Syntax);
}
}
return result;
}
public override void VisitForEachIterationVariables(BoundForEachStatement node)
{
// declare and assign all iteration variables
foreach (var iterationVariable in node.IterationVariables)
{
Debug.Assert((object)iterationVariable != null);
int slot = GetOrCreateSlot(iterationVariable);
if (slot > 0) SetSlotAssigned(slot);
// NOTE: do not report unused iteration variables. They are always considered used.
NoteWrite(iterationVariable, null, read: true, isRef: iterationVariable.RefKind != RefKind.None);
}
}
public override BoundNode VisitDynamicObjectInitializerMember(BoundDynamicObjectInitializerMember node)
{
return null;
}
protected override void VisitAssignmentOfNullCoalescingAssignment(
BoundNullCoalescingAssignmentOperator node,
BoundPropertyAccess propertyAccessOpt)
{
base.VisitAssignmentOfNullCoalescingAssignment(node, propertyAccessOpt);
Assign(node.LeftOperand, node.RightOperand);
}
protected override void AdjustStateForNullCoalescingAssignmentNonNullCase(BoundNullCoalescingAssignmentOperator node)
{
// For the purposes of definite assignment in try/finally, we need to treat the left as having been assigned
// in the left-side state. If LeftOperand was not definitely assigned before this call, we will have already
// reported an error for use before assignment.
Assign(node.LeftOperand, node.LeftOperand);
}
protected override void AfterVisitInlineArrayAccess(BoundInlineArrayAccess node)
{
if (node.GetItemOrSliceHelper == WellKnownMember.System_Span_T__Slice_Int_Int)
{
// exposing ref is a potential write
NoteWrite(node.Expression, value: null, read: false, isRef: false);
}
}
protected override void AfterVisitConversion(BoundConversion node)
{
if (node.Conversion.IsInlineArray &&
node.Type.OriginalDefinition.Equals(compilation.GetWellKnownType(WellKnownType.System_Span_T), TypeCompareKind.AllIgnoreOptions))
{
// exposing ref is a potential write
NoteWrite(node.Operand, value: null, read: false, isRef: false);
}
}
#endregion Visitors
protected override string Dump(LocalState state)
{
var builder = new StringBuilder();
builder.Append("[assigned ");
AppendBitNames(state.Assigned, builder);
builder.Append(']');
return builder.ToString();
}
protected void AppendBitNames(BitVector a, StringBuilder builder)
{
bool any = false;
foreach (int bit in a.TrueBits())
{
if (any) builder.Append(", ");
any = true;
AppendBitName(bit, builder);
}
}
protected void AppendBitName(int bit, StringBuilder builder)
{
VariableIdentifier id = variableBySlot[bit];
if (id.ContainingSlot > 0)
{
AppendBitName(id.ContainingSlot, builder);
builder.Append('.');
}
builder.Append(
bit == 0 ? "<unreachable>" :
string.IsNullOrEmpty(id.Symbol.Name) ? "<anon>" + id.Symbol.GetHashCode() :
id.Symbol.Name);
}
protected override bool Meet(ref LocalState self, ref LocalState other)
{
if (self.Assigned.Capacity != other.Assigned.Capacity)
{
Normalize(ref self);
Normalize(ref other);
}
if (!other.Reachable)
{
self.Assigned[0] = true;
return true;
}
bool changed = false;
for (int slot = 1; slot < self.Assigned.Capacity; slot++)
{
if (other.Assigned[slot] && !self.Assigned[slot])
{
SetSlotAssigned(slot, ref self);
changed = true;
}
}
return changed;
}
protected override bool Join(ref LocalState self, ref LocalState other)
{
if (self.Reachable == other.Reachable)
{
if (self.Assigned.Capacity != other.Assigned.Capacity)
{
Normalize(ref self);
Normalize(ref other);
}
return self.Assigned.IntersectWith(other.Assigned);
}
else if (!self.Reachable)
{
self.Assigned = other.Assigned.Clone();
return true;
}
else
{
Debug.Assert(!other.Reachable);
return false;
}
}
#if REFERENCE_STATE
internal class LocalState : ILocalDataFlowState
#else
internal struct LocalState : ILocalDataFlowState
#endif
{
internal BitVector Assigned;
public bool NormalizeToBottom { get; }
internal LocalState(BitVector assigned, bool normalizeToBottom = false)
{
this.Assigned = assigned;
NormalizeToBottom = normalizeToBottom;
Debug.Assert(!assigned.IsNull);
}
/// <summary>
/// Produce a duplicate of this flow analysis state.
/// </summary>
/// <returns></returns>
public LocalState Clone()
{
return new LocalState(Assigned.Clone());
}
public bool IsAssigned(int slot)
{
return /*(slot == -1) || */Assigned[slot];
}
public void Assign(int slot)
{
if (slot == -1)
return;
Assigned[slot] = true;
}
public void Unassign(int slot)
{
if (slot == -1)
return;
Assigned[slot] = false;
}
public bool Reachable
{
get
{
return Assigned.Capacity <= 0 || !IsAssigned(0);
}
}
}
}
}
|