/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Compilers/CSharp/Portable/FlowAnalysis/DefiniteAssignment.cs
2 949 строк
125 KB
Jan Jones
Handle instrumented conditions in post-lowering passes (#84244)
01 июл 2026, 20:53
Не верифицирован
01 июл 2026, 20:53
4ca37e3
Код
Авторство
О чём код?
// 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.Collections; 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, fillWithValue: 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); } } ParameterSymbol extensionParameter = null; if (_symbol.TryGetInstanceExtensionParameter(out extensionParameter)) { EnterParameter(extensionParameter); } 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); if ((object)extensionParameter != null) LeaveParameter(extensionParameter, 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); if ((object)extensionParameter != null) LeaveParameter(extensionParameter, returnBranch.Branch.Syntax, null); Join(ref savedState, ref this.State); } this.State = savedState; } return pendingReturns; } // 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, 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 /// <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.IsUnionConversion() || 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); } } } if (_symbol.TryGetInstanceExtensionParameter(out ParameterSymbol extensionParameter)) { if (extensionParameter.RefKind != RefKind.Out) { int slot = GetOrCreateSlot(extensionParameter); 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(((LocalFunctionSymbol)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 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<MethodSymbol> locals) { foreach (var symbol in locals) { ReportIfUnused(symbol); } } private void ReportIfUnused(MethodSymbol 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 (node.Type == (object)this.compilation.ImplicitlyTypedVariableUsedInForbiddenZoneType) { // 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(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; } protected sealed override void VisitCondition(BoundExpression node) { // Debug instrumentation wraps conditions in a sequence that stores the result in a branch discriminator // (see DebugInfoInjector.AddConditionSequencePoint). Preserve the original condition's true/false // assignment states through that wrapper. if (DebugInfoInjector.TryGetConditionalBranchDiscriminatorCondition(node, out BoundExpression condition, out BoundLocal target)) { Debug.Assert(target.LocalSymbol.Type is not null); if (target.LocalSymbol.Type.SpecialType == SpecialType.System_Boolean) { VisitLvalue(target); base.VisitCondition(condition); Debug.Assert(IsConditionalState); var whenTrue = StateWhenTrue.Clone(); var whenFalse = StateWhenFalse.Clone(); Unsplit(); Assign(target, condition); Meet(ref whenTrue, ref State); Meet(ref whenFalse, ref State); SetConditionalState(whenTrue, whenFalse); return; } } base.VisitCondition(node); } 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(((LambdaSymbol)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) { type = type.OriginalDefinition; 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; } public override BoundNode VisitCatchBlock(BoundCatchBlock catchBlock) { DeclareVariables(catchBlock.Locals); var exceptionSource = catchBlock.ExceptionSourceOpt; if (exceptionSource != null) { Assign(exceptionSource, value: null, read: false); } base.VisitCatchBlock(catchBlock); foreach (var local in catchBlock.Locals) { ReportIfUnused(local, assigned: local.DeclarationKind != LocalDeclarationKind.CatchVariable); } return null; } 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); } } } } }