File: src\984dec8966cb7923\ConvertSwitchStatementToExpressionDiagnosticAnalyzer.Analyzer.cs
Web Access
Project: src\roslyn\src\CodeStyle\CSharp\Analyzers\Microsoft.CodeAnalysis.CSharp.CodeStyle.csproj (Microsoft.CodeAnalysis.CSharp.CodeStyle)
// 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.

using System;
using System.Diagnostics;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.CSharp.ConvertSwitchStatementToExpression;

using static ConvertSwitchStatementToExpressionHelpers;

internal sealed partial class ConvertSwitchStatementToExpressionDiagnosticAnalyzer
{
    private sealed class Analyzer : CSharpSyntaxVisitor<SyntaxKind>
    {
        private readonly bool _supportsOrPatterns;

        private ExpressionSyntax? _assignmentTargetOpt;

        private Analyzer(bool supportsOrPatterns)
        {
            _supportsOrPatterns = supportsOrPatterns;
        }

        public static (SyntaxKind nodeToGenerate, VariableDeclaratorSyntax? declaratorToRemoveOpt) Analyze(
            SwitchStatementSyntax node,
            SemanticModel semanticModel,
            out bool shouldRemoveNextStatement)
        {
            var analyzer = new Analyzer(supportsOrPatterns: semanticModel.SyntaxTree.Options.LanguageVersion() >= LanguageVersion.CSharp9);
            var nodeToGenerate = analyzer.AnalyzeSwitchStatement(node, out shouldRemoveNextStatement);

            if (nodeToGenerate == SyntaxKind.SimpleAssignmentExpression &&
                analyzer.TryGetVariableDeclaratorAndSymbol(semanticModel) is var (declarator, symbol))
            {
                if (shouldRemoveNextStatement && node.GetNextStatement() is StatementSyntax nextStatement)
                {
                    var dataFlow = semanticModel.AnalyzeDataFlow(nextStatement);
                    Contract.ThrowIfNull(dataFlow);
                    if (dataFlow.DataFlowsIn.Contains(symbol))
                    {
                        // Bail out if data flows into the next statement that we want to move
                        // For example:
                        //
                        //      string name = "";
                        //      switch (index)
                        //      {
                        //          case 0: name = "0"; break;
                        //          case 1: name = "1"; break;
                        //      }
                        //      throw new Exception(name);
                        //
                        return default;
                    }
                }

                var declaration = declarator.GetAncestor<StatementSyntax>();
                Contract.ThrowIfNull(declaration);
                if (declaration.Parent == node.Parent && declarator.Initializer is null)
                {
                    var beforeSwitch = node.GetPreviousStatement() is StatementSyntax previousStatement
                        ? semanticModel.AnalyzeDataFlow(declaration, previousStatement)
                        : semanticModel.AnalyzeDataFlow(declaration);
                    Contract.ThrowIfNull(beforeSwitch);
                    if (!beforeSwitch.WrittenInside.Contains(symbol))
                    {
                        // Move declarator only if it has no initializer and it's not used before switch
                        return (nodeToGenerate, declaratorToRemoveOpt: declarator);
                    }
                }
            }

            return (nodeToGenerate, declaratorToRemoveOpt: null);
        }

        private (VariableDeclaratorSyntax, ISymbol)? TryGetVariableDeclaratorAndSymbol(SemanticModel semanticModel)
        {
            if (!_assignmentTargetOpt.IsKind(SyntaxKind.IdentifierName))
            {
                return null;
            }

            var symbol = semanticModel.GetSymbolInfo(_assignmentTargetOpt).Symbol;
            if (symbol is not
                { Kind: SymbolKind.Local, DeclaringSyntaxReferences: { Length: 1 } syntaxRefs })
            {
                return null;
            }

            if (syntaxRefs[0].GetSyntax() is not VariableDeclaratorSyntax declarator)
            {
                return null;
            }

            return (declarator, symbol);
        }

        public override SyntaxKind VisitSwitchStatement(SwitchStatementSyntax node)
            => AnalyzeSwitchStatement(node, out _);

        private SyntaxKind AnalyzeSwitchStatement(SwitchStatementSyntax switchStatement, out bool shouldRemoveNextStatement)
        {
            // Fail if the switch statement is empty.
            var sections = switchStatement.Sections;
            if (sections.Count == 0)
            {
                shouldRemoveNextStatement = false;
                return default;
            }

            if (!sections.All(s => CanConvertLabelsToArms(s.Labels)))
            {
                shouldRemoveNextStatement = false;
                return default;
            }

            // If there's no "default" case, we look at the next statement.
            // For instance, it could be a "return" statement which we'll use
            // as the default case in the switch expression.
            var nextStatement = AnalyzeNextStatement(switchStatement, out shouldRemoveNextStatement);

            // We do need to intersect the next statement analysis result to catch possible
            // arm kind mismatch, e.g. a "return" after a non-exhaustive assignment switch.
            return Aggregate(nextStatement, sections, (result, section) => Intersect(result, AnalyzeSwitchSection(section)));
        }

        private bool CanConvertLabelsToArms(SyntaxList<SwitchLabelSyntax> labels)
        {
            Debug.Assert(labels.Count >= 1);
            if (labels.Count == 1)
            {
                // Single label can always be converted to a single arm.
                return true;
            }

            if (labels.Any(label => IsDefaultSwitchLabel(label)))
            {
                // if any of the  labels are a default/_/var (catch-all) then we can convert this set of labels into
                // a single `_` arm.
                return true;
            }

            // We have multiple labels and none of them are a 'catch-all'.  

            if (!_supportsOrPatterns)
            {
                // We don't support 'or' patterns, so no way to convert this to arms.
                return false;
            }

            // If any of the cases have when-clauses, like so:
            //
            //  case ... when Goo():
            //  case ... when Bar():
            //
            // Then we can't convert into a single arm.
            foreach (var label in labels)
            {
                if (label is CasePatternSwitchLabelSyntax { WhenClause: not null })
                {
                    return false;
                }
            }

            // We have multiple labels that can be combined together using an 'or' pattern.
            return true;
        }

        private SyntaxKind AnalyzeNextStatement(SwitchStatementSyntax switchStatement, out bool shouldRemoveNextStatement)
        {
            // Check if we have a catch-all label anywhere.  If so we don't need to pull in the next statements.
            if (switchStatement.Sections.Any(section => section.Labels.Any(label => IsDefaultSwitchLabel(label))))
            {
                // Throw can be overridden by other section bodies, therefore it has no effect on the result.
                shouldRemoveNextStatement = false;
                return SyntaxKind.ThrowStatement;
            }

            // Didn't have a default case, see if we can pull in the statement following the switch to become our default.
            shouldRemoveNextStatement = true;
            return AnalyzeNextStatement(switchStatement.GetNextStatement());
        }

        private static SyntaxKind Intersect(SyntaxKind left, SyntaxKind right)
        {
            if (left == SyntaxKind.ThrowStatement)
            {
                return right;
            }

            if (right == SyntaxKind.ThrowStatement)
            {
                return left;
            }

            if (left == right)
            {
                return left;
            }

            return default;
        }

        private SyntaxKind AnalyzeNextStatement(StatementSyntax? nextStatement)
        {
            // Only the following "throw" and "return" can be moved into the switch expression.
            return nextStatement is (kind: SyntaxKind.ThrowStatement or SyntaxKind.ReturnStatement)
                ? Visit(nextStatement)
                : default;
        }

        private SyntaxKind AnalyzeSwitchSection(SwitchSectionSyntax section)
        {
            switch (section.Statements.Count)
            {
                case 1:
                case 2 when section.Statements[1].IsKind(SyntaxKind.BreakStatement) || section.Statements[0].IsKind(SyntaxKind.SwitchStatement):
                    return Visit(section.Statements[0]);
                default:
                    return default;
            }
        }

        private static SyntaxKind Aggregate<T>(SyntaxKind seed, SyntaxList<T> nodes, Func<SyntaxKind, T, SyntaxKind> func)
            where T : SyntaxNode
        {
            var result = seed;
            foreach (var node in nodes)
            {
                result = func(result, node);
                if (result == default)
                {
                    // No point to continue if any node was not
                    // convertible to a switch arm's expression
                    break;
                }
            }

            return result;
        }

        public override SyntaxKind VisitAssignmentExpression(AssignmentExpressionSyntax node)
        {
            if (node.Right is RefExpressionSyntax)
                return default;

            if (_assignmentTargetOpt != null)
            {
                if (!SyntaxFactory.AreEquivalent(node.Left, _assignmentTargetOpt))
                {
                    return default;
                }
            }
            else
            {
                _assignmentTargetOpt = node.Left;
            }

            return node.Kind();
        }

        public override SyntaxKind VisitExpressionStatement(ExpressionStatementSyntax node)
            => Visit(node.Expression);

        public override SyntaxKind VisitReturnStatement(ReturnStatementSyntax node)
        {
            // A "return" statement's expression will be placed in the switch arm expression. We
            // also can't convert a switch statement with ref-returns to a switch-expression
            // (currently). Until the language supports ref-switch-expressions, we just disable
            // things.
            return node.Expression is null or RefExpressionSyntax
                ? default
                : SyntaxKind.ReturnStatement;
        }

        public override SyntaxKind VisitThrowStatement(ThrowStatementSyntax node)
        {
            // A "throw" statement can be converted to a throw expression.
            // Gives Failure if Expression is null because a throw expression needs one.
            return node.Expression is null ? default : SyntaxKind.ThrowStatement;
        }

        public override SyntaxKind DefaultVisit(SyntaxNode node)
        {
            // In all other cases we return failure result.
            return default;
        }
    }
}