File: Binder\SwitchBinder_Patterns.cs
Web Access
Project: src\src\Compilers\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.csproj (Microsoft.CodeAnalysis.CSharp)
// 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.
 
#nullable disable
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using Microsoft.CodeAnalysis.CSharp.Symbols;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.PooledObjects;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp
{
    internal partial class SwitchBinder : LocalScopeBinder
    {
        internal static SwitchBinder Create(Binder next, SwitchStatementSyntax switchSyntax)
        {
            return new SwitchBinder(next, switchSyntax);
        }
 
        /// <summary>
        /// Bind the switch statement, reporting in the process any switch labels that are subsumed by previous cases.
        /// </summary>
        internal override BoundStatement BindSwitchStatementCore(SwitchStatementSyntax node, Binder originalBinder, BindingDiagnosticBag diagnostics)
        {
            Debug.Assert(SwitchSyntax.Equals(node));
 
            if (node.Sections.Count == 0)
            {
                diagnostics.Add(ErrorCode.WRN_EmptySwitch, node.OpenBraceToken.GetLocation());
            }
 
            // Bind switch expression and set the switch governing type.
            BoundExpression boundSwitchGoverningExpression = SwitchGoverningExpression;
            diagnostics.AddRange(SwitchGoverningDiagnostics, allowMismatchInDependencyAccumulation: true);
 
            ImmutableArray<BoundSwitchSection> switchSections = BindSwitchSections(originalBinder, diagnostics, out BoundSwitchLabel defaultLabel);
            ImmutableArray<LocalSymbol> locals = GetDeclaredLocalsForScope(node);
            ImmutableArray<LocalFunctionSymbol> functions = GetDeclaredLocalFunctionsForScope(node);
            BoundDecisionDag decisionDag = DecisionDagBuilder.CreateDecisionDagForSwitchStatement(
                compilation: this.Compilation,
                syntax: node,
                switchGoverningExpression: boundSwitchGoverningExpression,
                switchSections: switchSections,
                // If there is no explicit default label, the default action is to break out of the switch
                defaultLabel: defaultLabel?.Label ?? BreakLabel,
                diagnostics);
 
            // Report subsumption errors, but ignore the input's constant value for that.
            CheckSwitchErrors(ref switchSections, decisionDag, diagnostics);
 
            // When the input is constant, we use that to reshape the decision dag that is returned
            // so that flow analysis will see that some of the cases may be unreachable.
            decisionDag = decisionDag.SimplifyDecisionDagIfConstantInput(boundSwitchGoverningExpression);
 
            return new BoundSwitchStatement(
                syntax: node,
                expression: boundSwitchGoverningExpression,
                innerLocals: locals,
                innerLocalFunctions: functions,
                switchSections: switchSections,
                defaultLabel: defaultLabel,
                breakLabel: this.BreakLabel,
                reachabilityDecisionDag: decisionDag);
        }
 
        private void CheckSwitchErrors(
            ref ImmutableArray<BoundSwitchSection> switchSections,
            BoundDecisionDag decisionDag,
            BindingDiagnosticBag diagnostics)
        {
            var reachableLabels = decisionDag.ReachableLabels;
            static bool isSubsumed(BoundSwitchLabel switchLabel, ImmutableHashSet<LabelSymbol> reachableLabels)
            {
                return !reachableLabels.Contains(switchLabel.Label);
            }
 
            // If no switch sections are subsumed, just return
            if (!switchSections.Any(static (s, reachableLabels) => s.SwitchLabels.Any(isSubsumed, reachableLabels), reachableLabels))
            {
                return;
            }
 
            var sectionBuilder = ArrayBuilder<BoundSwitchSection>.GetInstance(switchSections.Length);
            bool anyPreviousErrors = false;
            foreach (var oldSection in switchSections)
            {
                var labelBuilder = ArrayBuilder<BoundSwitchLabel>.GetInstance(oldSection.SwitchLabels.Length);
                foreach (var label in oldSection.SwitchLabels)
                {
                    var newLabel = label;
                    if (!label.HasErrors && isSubsumed(label, reachableLabels) && label.Syntax.Kind() != SyntaxKind.DefaultSwitchLabel)
                    {
                        var syntax = label.Syntax;
                        switch (syntax)
                        {
                            case CasePatternSwitchLabelSyntax p:
                                if (!p.Pattern.HasErrors && !anyPreviousErrors)
                                {
                                    diagnostics.Add(ErrorCode.ERR_SwitchCaseSubsumed, p.Pattern.Location);
                                }
                                break;
                            case CaseSwitchLabelSyntax p:
                                if (label.Pattern is BoundConstantPattern cp && !cp.ConstantValue.IsBad && FindMatchingSwitchCaseLabel(cp.ConstantValue, p) != label.Label)
                                {
                                    // We use the traditional diagnostic when possible
                                    diagnostics.Add(ErrorCode.ERR_DuplicateCaseLabel, syntax.Location, cp.ConstantValue.GetValueToDisplay());
                                }
                                else if (!label.Pattern.HasErrors && !anyPreviousErrors)
                                {
                                    diagnostics.Add(ErrorCode.ERR_SwitchCaseSubsumed, p.Value.Location);
                                }
                                break;
                            default:
                                throw ExceptionUtilities.UnexpectedValue(syntax.Kind());
                        }
 
                        // We mark any subsumed sections as erroneous for the benefit of flow analysis
                        newLabel = new BoundSwitchLabel(label.Syntax, label.Label, label.Pattern, label.WhenClause, hasErrors: true);
                    }
 
                    anyPreviousErrors |= label.HasErrors;
                    labelBuilder.Add(newLabel);
                }
 
                sectionBuilder.Add(oldSection.Update(oldSection.Locals, labelBuilder.ToImmutableAndFree(), oldSection.Statements));
            }
 
            switchSections = sectionBuilder.ToImmutableAndFree();
        }
 
        /// <summary>
        /// Bind a pattern switch label in order to force inference of the type of pattern variables.
        /// </summary>
        internal override void BindPatternSwitchLabelForInference(CasePatternSwitchLabelSyntax node, BindingDiagnosticBag diagnostics)
        {
            // node should be a label of this switch statement.
            Debug.Assert(this.SwitchSyntax == node.Parent.Parent);
 
            // This simulates enough of the normal binding path of a switch statement to cause
            // the label's pattern variables to have their types inferred, if necessary.
            // It also binds the when clause, and therefore any pattern and out variables there.
            BoundSwitchLabel defaultLabel = null;
            BindSwitchSectionLabel(
                sectionBinder: GetBinder(node.Parent),
                node: node,
                label: LabelsByNode[node],
                defaultLabel: ref defaultLabel,
                diagnostics: diagnostics);
        }
 
        /// <summary>
        /// Bind the pattern switch labels.
        /// </summary>
        private ImmutableArray<BoundSwitchSection> BindSwitchSections(
            Binder originalBinder,
            BindingDiagnosticBag diagnostics,
            out BoundSwitchLabel defaultLabel)
        {
            // Bind match sections
            var boundSwitchSectionsBuilder = ArrayBuilder<BoundSwitchSection>.GetInstance(SwitchSyntax.Sections.Count);
            defaultLabel = null;
            foreach (SwitchSectionSyntax sectionSyntax in SwitchSyntax.Sections)
            {
                BoundSwitchSection section = BindSwitchSection(sectionSyntax, originalBinder, ref defaultLabel, diagnostics);
                boundSwitchSectionsBuilder.Add(section);
            }
 
            return boundSwitchSectionsBuilder.ToImmutableAndFree();
        }
 
        /// <summary>
        /// Bind the pattern switch section.
        /// </summary>
        private BoundSwitchSection BindSwitchSection(
            SwitchSectionSyntax node,
            Binder originalBinder,
            ref BoundSwitchLabel defaultLabel,
            BindingDiagnosticBag diagnostics)
        {
            // Bind match section labels
            var boundLabelsBuilder = ArrayBuilder<BoundSwitchLabel>.GetInstance(node.Labels.Count);
            Binder sectionBinder = originalBinder.GetBinder(node); // this binder can bind pattern variables from the section.
            Debug.Assert(sectionBinder != null);
            Dictionary<SyntaxNode, LabelSymbol> labelsByNode = LabelsByNode;
 
            foreach (SwitchLabelSyntax labelSyntax in node.Labels)
            {
                LabelSymbol label = labelsByNode[labelSyntax];
                BoundSwitchLabel boundLabel = BindSwitchSectionLabel(sectionBinder, labelSyntax, label, ref defaultLabel, diagnostics);
                boundLabelsBuilder.Add(boundLabel);
            }
 
            // Bind switch section statements
            var boundStatementsBuilder = ArrayBuilder<BoundStatement>.GetInstance(node.Statements.Count);
            foreach (StatementSyntax statement in node.Statements)
            {
                var boundStatement = sectionBinder.BindStatement(statement, diagnostics);
                if (ContainsUsingVariable(boundStatement))
                {
                    diagnostics.Add(ErrorCode.ERR_UsingVarInSwitchCase, statement.Location);
                }
                boundStatementsBuilder.Add(boundStatement);
            }
 
            return new BoundSwitchSection(node, sectionBinder.GetDeclaredLocalsForScope(node), boundLabelsBuilder.ToImmutableAndFree(), boundStatementsBuilder.ToImmutableAndFree());
        }
 
        internal static bool ContainsUsingVariable(BoundStatement boundStatement)
        {
            if (boundStatement is BoundLocalDeclaration boundLocal)
            {
                return boundLocal.LocalSymbol.IsUsing;
            }
            else if (boundStatement is BoundMultipleLocalDeclarationsBase boundMultiple && !boundMultiple.LocalDeclarations.IsDefaultOrEmpty)
            {
                return boundMultiple.LocalDeclarations[0].LocalSymbol.IsUsing;
            }
            return false;
        }
 
        private BoundSwitchLabel BindSwitchSectionLabel(
            Binder sectionBinder,
            SwitchLabelSyntax node,
            LabelSymbol label,
            ref BoundSwitchLabel defaultLabel,
            BindingDiagnosticBag diagnostics)
        {
            switch (node.Kind())
            {
                case SyntaxKind.CaseSwitchLabel:
                    {
                        var caseLabelSyntax = (CaseSwitchLabelSyntax)node;
                        bool hasErrors = node.HasErrors;
                        BoundPattern pattern = sectionBinder.BindConstantPatternWithFallbackToTypePattern(
                            caseLabelSyntax.Value, caseLabelSyntax.Value, SwitchGoverningType, hasErrors, diagnostics);
                        pattern.WasCompilerGenerated = true; // we don't have a pattern syntax here
                        reportIfConstantNamedUnderscore(pattern, caseLabelSyntax.Value);
 
                        return new BoundSwitchLabel(node, label, pattern, null, pattern.HasErrors);
                    }
 
                case SyntaxKind.DefaultSwitchLabel:
                    {
                        var pattern = new BoundDiscardPattern(node, inputType: SwitchGoverningType, narrowedType: SwitchGoverningType);
                        bool hasErrors = pattern.HasErrors;
                        if (defaultLabel != null)
                        {
                            diagnostics.Add(ErrorCode.ERR_DuplicateCaseLabel, node.Location, label.Name);
                            hasErrors = true;
                            return new BoundSwitchLabel(node, label, pattern, null, hasErrors);
                        }
                        else
                        {
                            // Note that this is semantically last! The caller will place it in the decision dag
                            // in the final position.
                            return defaultLabel = new BoundSwitchLabel(node, label, pattern, null, hasErrors);
                        }
                    }
 
                case SyntaxKind.CasePatternSwitchLabel:
                    {
                        var matchLabelSyntax = (CasePatternSwitchLabelSyntax)node;
 
                        MessageID.IDS_FeaturePatternMatching.CheckFeatureAvailability(diagnostics, node.Keyword);
 
                        BoundPattern pattern = sectionBinder.BindPattern(
                            matchLabelSyntax.Pattern, SwitchGoverningType, permitDesignations: true, node.HasErrors, diagnostics);
                        if (matchLabelSyntax.Pattern is ConstantPatternSyntax p)
                            reportIfConstantNamedUnderscore(pattern, p.Expression);
 
                        return new BoundSwitchLabel(node, label, pattern,
                            matchLabelSyntax.WhenClause != null ? sectionBinder.BindBooleanExpression(matchLabelSyntax.WhenClause.Condition, diagnostics) : null,
                            node.HasErrors);
                    }
 
                default:
                    throw ExceptionUtilities.UnexpectedValue(node);
            }
 
            void reportIfConstantNamedUnderscore(BoundPattern pattern, ExpressionSyntax expression)
            {
                if (pattern is BoundConstantPattern { HasErrors: false } && IsUnderscore(expression))
                {
                    diagnostics.Add(ErrorCode.WRN_CaseConstantNamedUnderscore, expression.Location);
                }
            }
        }
    }
}