// 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.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis.FlowAnalysis;
using Microsoft.CodeAnalysis.Operations;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;

namespace Microsoft.CodeAnalysis.Test.Utilities
{
    public class OperationTreeVerifier : OperationWalker
    {
        protected readonly Compilation _compilation;
        protected readonly IOperation _root;
        protected readonly StringBuilder _builder;
        private readonly Dictionary<SyntaxNode, IOperation> _explicitNodeMap;
        private readonly Dictionary<ILabelSymbol, uint> _labelIdMap;

        private const string indent = "  ";
        protected string _currentIndent;
        private bool _pendingIndent;
        private uint _currentLabelId = 0;

        public OperationTreeVerifier(Compilation compilation, IOperation root, int initialIndent)
        {
            _compilation = compilation;
            _root = root;
            _builder = new StringBuilder();

            _currentIndent = new string(' ', initialIndent);
            _pendingIndent = true;

            _explicitNodeMap = new Dictionary<SyntaxNode, IOperation>();
            _labelIdMap = new Dictionary<ILabelSymbol, uint>();
        }

        public static string GetOperationTree(Compilation compilation, IOperation operation, int initialIndent = 0)
        {
            var walker = new OperationTreeVerifier(compilation, operation, initialIndent);
            walker.Visit(operation);

            var visitor = TestOperationVisitor.Singleton;
            foreach (var op in operation.DescendantsAndSelf())
            {
                visitor.Visit(op);
            }

            return walker._builder.ToString();
        }

        public static void Verify(string expectedOperationTree, string actualOperationTree)
        {
            char[] newLineChars = Environment.NewLine.ToCharArray();
            string actual = actualOperationTree.Trim(newLineChars);
            actual = actual.Replace(" \n", "\n").Replace(" \r", "\r");
            expectedOperationTree = expectedOperationTree.Trim(newLineChars);
            expectedOperationTree = expectedOperationTree.Replace("\r\n", "\n").Replace(" \n", "\n").Replace("\n", Environment.NewLine);

            AssertEx.AssertEqualToleratingWhitespaceDifferences(expectedOperationTree, actual);
        }

        #region Logging helpers

        private void LogPatternPropertiesAndNewLine(IPatternOperation operation)
        {
            LogPatternProperties(operation);
            LogString(")");
            LogNewLine();
        }

        private void LogPatternProperties(IPatternOperation operation)
        {
            LogCommonProperties(operation);
            LogString(" (");
            LogType(operation.InputType, $"{nameof(operation.InputType)}");
            LogString(", ");
            LogType(operation.NarrowedType, $"{nameof(operation.NarrowedType)}");
        }

        private void LogCommonPropertiesAndNewLine(IOperation operation)
        {
            LogCommonProperties(operation);
            LogNewLine();
        }

        private void LogCommonProperties(IOperation operation)
        {
            LogString(" (");

            // Kind
            LogString($"{nameof(OperationKind)}.{GetKindText(operation.Kind)}");

            // Type
            LogString(", ");
            LogType(operation.Type);

            // ConstantValue
            if (operation.ConstantValue.HasValue)
            {
                LogString(", ");
                LogConstant(operation.ConstantValue);
            }

            // IsInvalid
            if (operation.HasErrors(_compilation))
            {
                LogString(", IsInvalid");
            }

            // IsImplicit
            if (operation.IsImplicit)
            {
                LogString(", IsImplicit");
            }

            LogString(")");

            // Syntax
            Assert.NotNull(operation.Syntax);
            LogString($" (Syntax: {GetSnippetFromSyntax(operation.Syntax)})");

            // Some of these kinds were inconsistent in the first release, and in standardizing them the
            // string output isn't guaranteed to be one or the other. So standardize manually.
            string GetKindText(OperationKind kind)
            {
                switch (kind)
                {
                    case OperationKind.Unary:
                        return "Unary";
                    case OperationKind.Binary:
                        return "Binary";
                    case OperationKind.TupleBinary:
                        return "TupleBinary";
                    case OperationKind.MethodBody:
                        return "MethodBody";
                    case OperationKind.ConstructorBody:
                        return "ConstructorBody";
                    default:
                        return kind.ToString();
                }
            }
        }

        private static string GetSnippetFromSyntax(SyntaxNode syntax)
        {
            if (syntax == null)
            {
                return "null";
            }

            var text = syntax.ToString().Trim(Environment.NewLine.ToCharArray());
            var lines = text.Split(new[] { Environment.NewLine, "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries).Select(l => l.Trim()).ToArray();
            if (lines.Length <= 1 && text.Length < 25)
            {
                return $"'{text}'";
            }

            const int maxTokenLength = 11;
            var firstLine = lines[0];
            var lastLine = lines[lines.Length - 1];
            var prefix = firstLine.Length <= maxTokenLength ? firstLine : firstLine.Substring(0, maxTokenLength);
            var suffix = lastLine.Length <= maxTokenLength ? lastLine : lastLine.Substring(lastLine.Length - maxTokenLength, maxTokenLength);
            return $"'{prefix} ... {suffix}'";
        }

        protected void LogString(string str)
        {
            if (_pendingIndent)
            {
                str = _currentIndent + str;
                _pendingIndent = false;
            }

            _builder.Append(str);
        }

        protected void LogNewLine()
        {
            LogString(Environment.NewLine);
            _pendingIndent = true;
        }

        private void Indent()
        {
            _currentIndent += indent;
        }

        private void Unindent()
        {
            _currentIndent = _currentIndent.Substring(indent.Length);
        }

        private void LogConstant(Optional<object> constant, string header = "Constant")
        {
            if (constant.HasValue)
            {
                LogConstant(constant.Value, header);
            }
        }

        private static string ConstantToString(object constant)
        {
            switch (constant)
            {
                case null:
                    return "null";
                case string s:
                    s = s.Replace("\"", "\"\"");
                    return @"""" + s + @"""";
                case IFormattable formattable:
                    return formattable.ToString(null, CultureInfo.InvariantCulture).Replace("\"", "\"\"");
                default:
                    return constant.ToString().Replace("\"", "\"\"");
            }
        }

        private void LogConstant(object constant, string header = "Constant")
        {
            string valueStr = ConstantToString(constant);

            LogString($"{header}: {valueStr}");
        }

        private void LogConversion(CommonConversion conversion, string header = "Conversion")
        {
            var exists = FormatBoolProperty(nameof(conversion.Exists), conversion.Exists);

            var isIdentity = FormatBoolProperty(nameof(conversion.IsIdentity), conversion.IsIdentity);
            var isNumeric = FormatBoolProperty(nameof(conversion.IsNumeric), conversion.IsNumeric);
            var isReference = FormatBoolProperty(nameof(conversion.IsReference), conversion.IsReference);
            var isUserDefined = FormatBoolProperty(nameof(conversion.IsUserDefined), conversion.IsUserDefined);

            string isUnion = "";
            if (conversion.IsUnion)
            {
                isUnion = ", " + FormatBoolProperty(nameof(conversion.IsUnion), conversion.IsUnion);
            }

            LogString($"{header}: {nameof(CommonConversion)} ({exists}, {isIdentity}, {isNumeric}, {isReference}, {isUserDefined}{isUnion}) (");
            LogSymbol(conversion.MethodSymbol, nameof(conversion.MethodSymbol));
            LogString(")");
        }

        private void LogSymbol(ISymbol symbol, string header, bool logDisplayString = true)
        {
            if (!string.IsNullOrEmpty(header))
            {
                LogString($"{header}: ");
            }

            var symbolStr = symbol != null ? (logDisplayString ? symbol.ToTestDisplayString() : symbol.Name) : "null";
            LogString($"{symbolStr}");
        }

        private void LogType(ITypeSymbol type, string header = "Type")
        {
            var typeStr = type != null ? type.ToTestDisplayString() : "null";
            LogString($"{header}: {typeStr}");
        }

        private uint GetLabelId(ILabelSymbol symbol)
        {
            if (_labelIdMap.TryGetValue(symbol, out var id))
            {
                return id;
            }

            id = _currentLabelId++;
            _labelIdMap[symbol] = id;
            return id;
        }

        private static string FormatBoolProperty(string propertyName, bool value) => $"{propertyName}: {(value ? "True" : "False")}";

        #endregion

        #region Visit methods

        public override void Visit(IOperation operation)
        {
            if (operation == null)
            {
                Indent();
                LogString("null");
                LogNewLine();
                Unindent();
                return;
            }

            if (!operation.IsImplicit)
            {
                try
                {
                    _explicitNodeMap.Add(operation.Syntax, operation);
                }
                catch (ArgumentException)
                {
                    Assert.Fail($"Duplicate explicit node for syntax ({operation.Syntax.RawKind}): {operation.Syntax.ToString()}");
                }
            }

            Assert.True(operation.Type == null || !operation.MustHaveNullType(), $"Unexpected non-null type: {operation.Type}");

            if (operation != _root)
            {
                Indent();
            }

            base.Visit(operation);

            if (operation != _root)
            {
                Unindent();
            }
            Assert.True(operation.Syntax.Language == operation.Language);
        }

        private void Visit(IOperation operation, string header)
        {
            Debug.Assert(!string.IsNullOrEmpty(header));

            Indent();
            LogString($"{header}: ");
            LogNewLine();
            Visit(operation);
            Unindent();
        }

        private void VisitArrayCommon<T>(ImmutableArray<T> list, string header, bool logElementCount, bool logNullForDefault, Action<T> arrayElementVisitor)
        {
            Debug.Assert(!string.IsNullOrEmpty(header));

            Indent();
            if (!list.IsDefaultOrEmpty)
            {
                var elementCount = logElementCount ? $"({list.Count()})" : string.Empty;
                LogString($"{header}{elementCount}:");
                LogNewLine();
                Indent();
                foreach (var element in list)
                {
                    arrayElementVisitor(element);
                }
                Unindent();
            }
            else
            {
                var suffix = logNullForDefault && list.IsDefault ? ": null" : "(0)";
                LogString($"{header}{suffix}");
                LogNewLine();
            }

            Unindent();
        }

        internal void VisitSymbolArrayElement(ISymbol element)
        {
            LogSymbol(element, header: "Symbol");
            LogNewLine();
        }

        internal void VisitStringArrayElement(string element)
        {
            var valueStr = element != null ? element.ToString() : "null";
            valueStr = @"""" + valueStr + @"""";
            LogString(valueStr);
            LogNewLine();
        }

        internal void VisitRefKindArrayElement(RefKind element)
        {
            LogString(element.ToString());
            LogNewLine();
        }

        private void VisitChildren(IOperation operation)
        {
            Debug.Assert(operation.ChildOperations.All(o => o != null));

            var children = operation.ChildOperations.ToImmutableArray();
            if (!children.IsEmpty || operation.Kind != OperationKind.None)
            {
                VisitArray(children, "Children", logElementCount: true);
            }
        }

        private void VisitArray<T>(ImmutableArray<T> list, string header, bool logElementCount, bool logNullForDefault = false)
            where T : IOperation
        {
            VisitArrayCommon(list, header, logElementCount, logNullForDefault, o => Visit(o));
        }

        private void VisitArray(ImmutableArray<string> list, string header, bool logElementCount, bool logNullForDefault = false)
        {
            VisitArrayCommon(list, header, logElementCount, logNullForDefault, VisitStringArrayElement);
        }

        private void VisitArray(ImmutableArray<RefKind> list, string header, bool logElementCount, bool logNullForDefault = false)
        {
            VisitArrayCommon(list, header, logElementCount, logNullForDefault, VisitRefKindArrayElement);
        }

        private void VisitInstance(IOperation instance)
        {
            Visit(instance, header: "Instance Receiver");
        }

        internal override void VisitNoneOperation(IOperation operation)
        {
            LogString("IOperation: ");
            LogCommonPropertiesAndNewLine(operation);

            VisitChildren(operation);
        }

        public override void VisitBlock(IBlockOperation operation)
        {
            LogString(nameof(IBlockOperation));

            var statementsStr = $"{operation.Operations.Length} statements";
            var localStr = !operation.Locals.IsEmpty ? $", {operation.Locals.Length} locals" : string.Empty;
            LogString($" ({statementsStr}{localStr})");
            LogCommonPropertiesAndNewLine(operation);

            if (operation.Operations.IsEmpty)
            {
                return;
            }

            LogLocals(operation.Locals);
            base.VisitBlock(operation);
        }

        public override void VisitVariableDeclarationGroup(IVariableDeclarationGroupOperation operation)
        {
            var variablesCountStr = $"{operation.Declarations.Length} declarations";
            LogString($"{nameof(IVariableDeclarationGroupOperation)} ({variablesCountStr})");
            LogCommonPropertiesAndNewLine(operation);

            base.VisitVariableDeclarationGroup(operation);
        }

        public override void VisitUsingDeclaration(IUsingDeclarationOperation operation)
        {
            LogString($"{nameof(IUsingDeclarationOperation)}");
            LogString($"(IsAsynchronous: {operation.IsAsynchronous}");

            var disposeMethod = ((UsingDeclarationOperation)operation).DisposeInfo.DisposeMethod;
            if (disposeMethod is object)
            {
                LogSymbol(disposeMethod, ", DisposeMethod");
            }
            LogString(")");
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.DeclarationGroup, "DeclarationGroup");
            var disposeArgs = ((UsingDeclarationOperation)operation).DisposeInfo.DisposeArguments;
            if (!disposeArgs.IsDefaultOrEmpty)
            {
                VisitArray(disposeArgs, "DisposeArguments", logElementCount: true);
            }
        }

        public override void VisitVariableDeclarator(IVariableDeclaratorOperation operation)
        {
            LogString($"{nameof(IVariableDeclaratorOperation)} (");
            LogSymbol(operation.Symbol, "Symbol");
            LogString(")");
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Initializer, "Initializer");
            if (!operation.IgnoredArguments.IsEmpty)
            {
                VisitArray(operation.IgnoredArguments, "IgnoredArguments", logElementCount: true);
            }
        }

        public override void VisitVariableDeclaration(IVariableDeclarationOperation operation)
        {
            var variableCount = operation.Declarators.Length;
            LogString($"{nameof(IVariableDeclarationOperation)} ({variableCount} declarators)");
            LogCommonPropertiesAndNewLine(operation);

            if (!operation.IgnoredDimensions.IsEmpty)
            {
                VisitArray(operation.IgnoredDimensions, "Ignored Dimensions", true);
            }
            VisitArray(operation.Declarators, "Declarators", false);
            Visit(operation.Initializer, "Initializer");
        }

        public override void VisitSwitch(ISwitchOperation operation)
        {
            var caseCountStr = $"{operation.Cases.Length} cases";
            var exitLabelStr = $", Exit Label Id: {GetLabelId(operation.ExitLabel)}";
            LogString($"{nameof(ISwitchOperation)} ({caseCountStr}{exitLabelStr})");
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Value, header: "Switch expression");
            LogLocals(operation.Locals);

            foreach (ISwitchCaseOperation section in operation.Cases)
            {
                foreach (ICaseClauseOperation c in section.Clauses)
                {
                    if (c.Label != null)
                    {
                        GetLabelId(c.Label);
                    }
                }
            }

            VisitArray(operation.Cases, "Sections", logElementCount: false);
        }

        public override void VisitSwitchCase(ISwitchCaseOperation operation)
        {
            var caseClauseCountStr = $"{operation.Clauses.Length} case clauses";
            var statementCountStr = $"{operation.Body.Length} statements";
            LogString($"{nameof(ISwitchCaseOperation)} ({caseClauseCountStr}, {statementCountStr})");
            LogCommonPropertiesAndNewLine(operation);
            LogLocals(operation.Locals);

            Indent();
            VisitArray(operation.Clauses, "Clauses", logElementCount: false);
            VisitArray(operation.Body, "Body", logElementCount: false);
            Unindent();
            _ = ((SwitchCaseOperation)operation).Condition;
        }

        public override void VisitWhileLoop(IWhileLoopOperation operation)
        {
            LogString(nameof(IWhileLoopOperation));
            LogString($" (ConditionIsTop: {operation.ConditionIsTop}, ConditionIsUntil: {operation.ConditionIsUntil})");
            LogLoopStatementHeader(operation);

            Visit(operation.Condition, "Condition");
            Visit(operation.Body, "Body");
            Visit(operation.IgnoredCondition, "IgnoredCondition");
        }

        public override void VisitForLoop(IForLoopOperation operation)
        {
            LogString(nameof(IForLoopOperation));
            LogLoopStatementHeader(operation);

            LogLocals(operation.ConditionLocals, header: nameof(operation.ConditionLocals));

            Visit(operation.Condition, "Condition");
            VisitArray(operation.Before, "Before", logElementCount: false);
            VisitArray(operation.AtLoopBottom, "AtLoopBottom", logElementCount: false);
            Visit(operation.Body, "Body");
        }

        public override void VisitForToLoop(IForToLoopOperation operation)
        {
            LogString(nameof(IForToLoopOperation));
            LogLoopStatementHeader(operation, operation.IsChecked);

            Visit(operation.LoopControlVariable, "LoopControlVariable");
            Visit(operation.InitialValue, "InitialValue");
            Visit(operation.LimitValue, "LimitValue");
            Visit(operation.StepValue, "StepValue");
            Visit(operation.Body, "Body");
            VisitArray(operation.NextVariables, "NextVariables", logElementCount: true);

            (ILocalSymbol loopObject, ForToLoopOperationUserDefinedInfo userDefinedInfo) = ((ForToLoopOperation)operation).Info;

            if (userDefinedInfo != null)
            {
                _ = userDefinedInfo.Addition;
                _ = userDefinedInfo.Subtraction;
                _ = userDefinedInfo.LessThanOrEqual;
                _ = userDefinedInfo.GreaterThanOrEqual;
            }
        }

        private void LogLocals(IEnumerable<ILocalSymbol> locals, string header = "Locals")
        {
            if (!locals.Any())
            {
                return;
            }

            Indent();

            LogString($"{header}: ");
            Indent();

            int localIndex = 1;
            foreach (var local in locals)
            {
                LogSymbol(local, header: $"Local_{localIndex++}");
                LogNewLine();
            }

            Unindent();
            Unindent();
        }

        private void LogLoopStatementHeader(ILoopOperation operation, bool? isChecked = null)
        {
            Assert.Equal(OperationKind.Loop, operation.Kind);
            var propertyStringBuilder = new StringBuilder();
            propertyStringBuilder.Append(" (");
            propertyStringBuilder.Append($"{nameof(LoopKind)}.{operation.LoopKind}");
            if (operation is IForEachLoopOperation { IsAsynchronous: true })
            {
                propertyStringBuilder.Append($", IsAsynchronous");
            }
            propertyStringBuilder.Append($", Continue Label Id: {GetLabelId(operation.ContinueLabel)}");
            propertyStringBuilder.Append($", Exit Label Id: {GetLabelId(operation.ExitLabel)}");
            if (isChecked.GetValueOrDefault())
            {
                propertyStringBuilder.Append($", Checked");
            }
            propertyStringBuilder.Append(')');
            LogString(propertyStringBuilder.ToString());
            LogCommonPropertiesAndNewLine(operation);

            LogLocals(operation.Locals);
        }

        public override void VisitForEachLoop(IForEachLoopOperation operation)
        {
            LogString(nameof(IForEachLoopOperation));
            LogLoopStatementHeader(operation);

            Assert.NotNull(operation.LoopControlVariable);
            Visit(operation.LoopControlVariable, "LoopControlVariable");
            Visit(operation.Collection, "Collection");
            Visit(operation.Body, "Body");
            VisitArray(operation.NextVariables, "NextVariables", logElementCount: true);
            ForEachLoopOperationInfo info = ((ForEachLoopOperation)operation).Info;
        }

        public override void VisitLabeled(ILabeledOperation operation)
        {
            LogString(nameof(ILabeledOperation));

            if (!operation.Label.IsImplicitlyDeclared)
            {
                LogString($" (Label: {operation.Label.Name})");
            }
            else
            {
                LogString($" (Label Id: {GetLabelId(operation.Label)})");
            }

            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Operation, "Statement");
        }

        public override void VisitBranch(IBranchOperation operation)
        {
            LogString(nameof(IBranchOperation));
            var kindStr = $"{nameof(BranchKind)}.{operation.BranchKind}";
            // If the label is implicit, or if it has been assigned an id (such as VB Exit Do/While/Switch labels) then print the id, instead of the name.
            var labelStr = !(operation.Target.IsImplicitlyDeclared || _labelIdMap.ContainsKey(operation.Target)) ? $", Label: {operation.Target.Name}" : $", Label Id: {GetLabelId(operation.Target)}";
            LogString($" ({kindStr}{labelStr})");
            LogCommonPropertiesAndNewLine(operation);

            base.VisitBranch(operation);
        }

        public override void VisitEmpty(IEmptyOperation operation)
        {
            LogString(nameof(IEmptyOperation));
            LogCommonPropertiesAndNewLine(operation);
        }

        public override void VisitReturn(IReturnOperation operation)
        {
            LogString(nameof(IReturnOperation));
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.ReturnedValue, "ReturnedValue");
        }

        public override void VisitLock(ILockOperation operation)
        {
            LogString(nameof(ILockOperation));
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.LockedValue, "Expression");
            Visit(operation.Body, "Body");
        }

        public override void VisitTry(ITryOperation operation)
        {
            LogString(nameof(ITryOperation));
            if (operation.ExitLabel != null)
            {
                LogString($" (Exit Label Id: {GetLabelId(operation.ExitLabel)})");
            }
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Body, "Body");
            VisitArray(operation.Catches, "Catch clauses", logElementCount: true);
            Visit(operation.Finally, "Finally");
        }

        public override void VisitCatchClause(ICatchClauseOperation operation)
        {
            LogString(nameof(ICatchClauseOperation));
            var exceptionTypeStr = operation.ExceptionType != null ? operation.ExceptionType.ToTestDisplayString() : "null";
            LogString($" (Exception type: {exceptionTypeStr})");
            LogCommonPropertiesAndNewLine(operation);

            LogLocals(operation.Locals);
            Visit(operation.ExceptionDeclarationOrExpression, "ExceptionDeclarationOrExpression");
            Visit(operation.Filter, "Filter");
            Visit(operation.Handler, "Handler");
        }

        public override void VisitUsing(IUsingOperation operation)
        {
            LogString(nameof(IUsingOperation));
            if (operation.IsAsynchronous)
            {
                LogString($" (IsAsynchronous)");
            }
            var disposeMethod = ((UsingOperation)operation).DisposeInfo.DisposeMethod;
            if (disposeMethod is object)
            {
                LogSymbol(disposeMethod, " (DisposeMethod");
                LogString(")");
            }
            LogCommonPropertiesAndNewLine(operation);

            LogLocals(operation.Locals);
            Visit(operation.Resources, "Resources");
            Visit(operation.Body, "Body");

            Assert.NotEqual(OperationKind.VariableDeclaration, operation.Resources.Kind);
            Assert.NotEqual(OperationKind.VariableDeclarator, operation.Resources.Kind);

            var disposeArgs = ((UsingOperation)operation).DisposeInfo.DisposeArguments;
            if (!disposeArgs.IsDefaultOrEmpty)
            {
                VisitArray(disposeArgs, "DisposeArguments", logElementCount: true);
            }
        }

        // https://github.com/dotnet/roslyn/issues/21281
        internal override void VisitFixed(IFixedOperation operation)
        {
            LogString(nameof(IFixedOperation));
            LogCommonPropertiesAndNewLine(operation);

            LogLocals(operation.Locals);
            Visit(operation.Variables, "Declaration");
            Visit(operation.Body, "Body");
        }

        internal override void VisitAggregateQuery(IAggregateQueryOperation operation)
        {
            LogString(nameof(IAggregateQueryOperation));
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Group, "Group");
            Visit(operation.Aggregation, "Aggregation");
        }

        public override void VisitExpressionStatement(IExpressionStatementOperation operation)
        {
            LogString(nameof(IExpressionStatementOperation));
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Operation, "Expression");
        }

        internal override void VisitWithStatement(IWithStatementOperation operation)
        {
            LogString(nameof(IWithStatementOperation));
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Value, "Value");
            Visit(operation.Body, "Body");
        }

        public override void VisitStop(IStopOperation operation)
        {
            LogString(nameof(IStopOperation));
            LogCommonPropertiesAndNewLine(operation);
        }

        public override void VisitEnd(IEndOperation operation)
        {
            LogString(nameof(IEndOperation));
            LogCommonPropertiesAndNewLine(operation);
        }

        public override void VisitInvocation(IInvocationOperation operation)
        {
            LogString(nameof(IInvocationOperation));

            var isVirtualStr = operation.IsVirtual ? "virtual " : string.Empty;
            var spacing = !operation.IsVirtual && operation.Instance != null ? " " : string.Empty;
            LogString($" ({isVirtualStr}{spacing}");
            LogSymbol(operation.TargetMethod, header: string.Empty);

            if (operation.ConstrainedToType is not null)
            {
                LogType(operation.ConstrainedToType, header: " ConstrainedToType");
            }

            LogString(")");
            LogCommonPropertiesAndNewLine(operation);

            VisitInstance(operation.Instance);
            VisitArguments(operation.Arguments);
        }

        public override void VisitFunctionPointerInvocation(IFunctionPointerInvocationOperation operation)
        {
            LogString(nameof(IFunctionPointerInvocationOperation));
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Target, "Target");
            VisitArguments(operation.Arguments);
        }

        private void VisitArguments(ImmutableArray<IArgumentOperation> arguments)
        {
            VisitArray(arguments, "Arguments", logElementCount: true);
        }

        private void VisitDynamicArguments(HasDynamicArgumentsExpression operation)
        {
            VisitArray(operation.Arguments, "Arguments", logElementCount: true);
            VisitArray(operation.ArgumentNames, "ArgumentNames", logElementCount: true);
            VisitArray(operation.ArgumentRefKinds, "ArgumentRefKinds", logElementCount: true, logNullForDefault: true);

            VerifyGetArgumentNamePublicApi(operation, operation.ArgumentNames);
            VerifyGetArgumentRefKindPublicApi(operation, operation.ArgumentRefKinds);
        }

        private static void VerifyGetArgumentNamePublicApi(HasDynamicArgumentsExpression operation, ImmutableArray<string> argumentNames)
        {
            var length = operation.Arguments.Length;
            if (argumentNames.IsDefaultOrEmpty)
            {
                for (int i = 0; i < length; i++)
                {
                    Assert.Null(operation.GetArgumentName(i));
                }
            }
            else
            {
                Assert.Equal(length, argumentNames.Length);
                for (var i = 0; i < length; i++)
                {
                    Assert.Equal(argumentNames[i], operation.GetArgumentName(i));
                }
            }
        }

        private static void VerifyGetArgumentRefKindPublicApi(HasDynamicArgumentsExpression operation, ImmutableArray<RefKind> argumentRefKinds)
        {
            var length = operation.Arguments.Length;
            if (argumentRefKinds.IsDefault)
            {
                for (int i = 0; i < length; i++)
                {
                    Assert.Null(operation.GetArgumentRefKind(i));
                }
            }
            else if (argumentRefKinds.IsEmpty)
            {
                for (int i = 0; i < length; i++)
                {
                    Assert.Equal(RefKind.None, operation.GetArgumentRefKind(i));
                }
            }
            else
            {
                Assert.Equal(length, argumentRefKinds.Length);
                for (var i = 0; i < length; i++)
                {
                    Assert.Equal(argumentRefKinds[i], operation.GetArgumentRefKind(i));
                }
            }
        }

        public override void VisitArgument(IArgumentOperation operation)
        {
            LogString($"{nameof(IArgumentOperation)} (");
            LogString($"{nameof(ArgumentKind)}.{operation.ArgumentKind}, ");
            LogSymbol(operation.Parameter, header: "Matching Parameter", logDisplayString: false);
            LogString(")");
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Value);

            Indent();
            LogConversion(operation.InConversion, "InConversion");
            LogNewLine();
            LogConversion(operation.OutConversion, "OutConversion");
            LogNewLine();
            Unindent();
        }

        public override void VisitOmittedArgument(IOmittedArgumentOperation operation)
        {
            LogString(nameof(IOmittedArgumentOperation));
            LogCommonPropertiesAndNewLine(operation);
        }

        public override void VisitArrayElementReference(IArrayElementReferenceOperation operation)
        {
            LogString(nameof(IArrayElementReferenceOperation));
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.ArrayReference, "Array reference");
            VisitArray(operation.Indices, "Indices", logElementCount: true);
        }

        public override void VisitImplicitIndexerReference(IImplicitIndexerReferenceOperation operation)
        {
            LogString(nameof(IImplicitIndexerReferenceOperation));
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Instance, "Instance");
            Visit(operation.Argument, "Argument");

            Indent();
            LogSymbol(operation.LengthSymbol, $"{nameof(operation.LengthSymbol)}");
            LogNewLine();

            LogSymbol(operation.IndexerSymbol, $"{nameof(operation.IndexerSymbol)}");
            LogNewLine();
            Unindent();
        }

        public override void VisitInlineArrayAccess(IInlineArrayAccessOperation operation)
        {
            LogString(nameof(IInlineArrayAccessOperation));
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Instance, "Instance");
            Visit(operation.Argument, "Argument");
        }

        internal override void VisitPointerIndirectionReference(IPointerIndirectionReferenceOperation operation)
        {
            LogString(nameof(IPointerIndirectionReferenceOperation));
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Pointer, "Pointer");
        }

        public override void VisitLocalReference(ILocalReferenceOperation operation)
        {
            LogString(nameof(ILocalReferenceOperation));
            LogString($": {operation.Local.Name}");
            if (operation.IsDeclaration)
            {
                LogString($" (IsDeclaration: {operation.IsDeclaration})");
            }
            LogCommonPropertiesAndNewLine(operation);
        }

        public override void VisitFlowCapture(IFlowCaptureOperation operation)
        {
            LogString(nameof(IFlowCaptureOperation));
            LogString($": {operation.Id.Value}");
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Value, "Value");

            TestOperationVisitor.Singleton.VisitFlowCapture(operation);
        }

        public override void VisitFlowCaptureReference(IFlowCaptureReferenceOperation operation)
        {
            LogString(nameof(IFlowCaptureReferenceOperation));
            LogString($": {operation.Id.Value}");
            if (operation.IsInitialization)
            {
                LogString(" (IsInitialization)");
            }
            LogCommonPropertiesAndNewLine(operation);
        }

        public override void VisitIsNull(IIsNullOperation operation)
        {
            LogString(nameof(IIsNullOperation));
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Operand, "Operand");
        }

        public override void VisitCaughtException(ICaughtExceptionOperation operation)
        {
            LogString(nameof(ICaughtExceptionOperation));
            LogCommonPropertiesAndNewLine(operation);
        }

        public override void VisitParameterReference(IParameterReferenceOperation operation)
        {
            LogString(nameof(IParameterReferenceOperation));
            LogString($": {operation.Parameter.Name}");
            LogCommonPropertiesAndNewLine(operation);
        }

        public override void VisitInstanceReference(IInstanceReferenceOperation operation)
        {
            LogString(nameof(IInstanceReferenceOperation));
            LogString($" (ReferenceKind: {operation.ReferenceKind})");
            LogCommonPropertiesAndNewLine(operation);

            if (operation.IsImplicit)
            {
                if (operation.Parent is IMemberReferenceOperation memberReference && memberReference.Instance == operation)
                {
                    Assert.False(memberReference.Member.IsStatic && !operation.HasErrors(this._compilation));
                }
                else if (operation.Parent is IInvocationOperation invocation && invocation.Instance == operation)
                {
                    Assert.False(invocation.TargetMethod.IsStatic);
                }
            }
        }

        private void VisitMemberReferenceExpressionCommon(IMemberReferenceOperation operation)
        {
            if (operation.Member.IsStatic)
            {
                LogString(" (Static)");
            }

            LogCommonPropertiesAndNewLine(operation);
            VisitInstance(operation.Instance);
        }

        public override void VisitFieldReference(IFieldReferenceOperation operation)
        {
            LogString(nameof(IFieldReferenceOperation));
            LogString($": {operation.Field.ToTestDisplayString()}");
            if (operation.IsDeclaration)
            {
                LogString($" (IsDeclaration: {operation.IsDeclaration})");
            }

            Assert.Null(operation.ConstrainedToType);

            VisitMemberReferenceExpressionCommon(operation);
        }

        public override void VisitMethodReference(IMethodReferenceOperation operation)
        {
            LogString(nameof(IMethodReferenceOperation));
            LogString($": {operation.Method.ToTestDisplayString()}");

            if (operation.ConstrainedToType is not null)
            {
                LogType(operation.ConstrainedToType, header: " (ConstrainedToType");
                LogString(")");
            }

            if (operation.IsVirtual)
            {
                LogString(" (IsVirtual)");
            }

            Assert.Null(operation.Type);

            VisitMemberReferenceExpressionCommon(operation);
        }

        public override void VisitPropertyReference(IPropertyReferenceOperation operation)
        {
            LogString(nameof(IPropertyReferenceOperation));
            LogString($": {operation.Property.ToTestDisplayString()}");

            if (operation.ConstrainedToType is not null)
            {
                LogType(operation.ConstrainedToType, header: " (ConstrainedToType");
                LogString(")");
            }

            VisitMemberReferenceExpressionCommon(operation);

            if (operation.Arguments.Length > 0)
            {
                VisitArguments(operation.Arguments);
            }
        }

        public override void VisitEventReference(IEventReferenceOperation operation)
        {
            LogString(nameof(IEventReferenceOperation));
            LogString($": {operation.Event.ToTestDisplayString()}");

            if (operation.ConstrainedToType is not null)
            {
                LogType(operation.ConstrainedToType, header: " (ConstrainedToType");
                LogString(")");
            }

            VisitMemberReferenceExpressionCommon(operation);
        }

        public override void VisitEventAssignment(IEventAssignmentOperation operation)
        {
            var kindStr = operation.Adds ? "EventAdd" : "EventRemove";
            LogString($"{nameof(IEventAssignmentOperation)} ({kindStr})");
            LogCommonPropertiesAndNewLine(operation);

            Assert.NotNull(operation.EventReference);
            Visit(operation.EventReference, header: "Event Reference");
            Visit(operation.HandlerValue, header: "Handler");
        }

        public override void VisitConditionalAccess(IConditionalAccessOperation operation)
        {
            LogString(nameof(IConditionalAccessOperation));
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Operation, header: nameof(operation.Operation));
            Visit(operation.WhenNotNull, header: nameof(operation.WhenNotNull));
            Assert.NotNull(operation.Type);
        }

        public override void VisitConditionalAccessInstance(IConditionalAccessInstanceOperation operation)
        {
            LogString(nameof(IConditionalAccessInstanceOperation));
            LogCommonPropertiesAndNewLine(operation);
        }

        internal override void VisitPlaceholder(IPlaceholderOperation operation)
        {
            LogString(nameof(IPlaceholderOperation));
            LogCommonPropertiesAndNewLine(operation);
            Assert.Equal(PlaceholderKind.AggregationGroup, operation.PlaceholderKind);
        }

        public override void VisitCollectionExpressionElementsPlaceholder(ICollectionExpressionElementsPlaceholderOperation operation)
        {
            LogString(nameof(ICollectionExpressionElementsPlaceholderOperation));
            LogCommonPropertiesAndNewLine(operation);
        }

        public override void VisitUnaryOperator(IUnaryOperation operation)
        {
            LogString(nameof(IUnaryOperation));

            var kindStr = $"{nameof(UnaryOperatorKind)}.{operation.OperatorKind}";
            if (operation.IsLifted)
            {
                kindStr += ", IsLifted";
            }

            if (operation.IsChecked)
            {
                kindStr += ", Checked";
            }

            LogString($" ({kindStr})");
            LogHasOperatorMethodExpressionCommon(operation.OperatorMethod, operation.ConstrainedToType);
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Operand, "Operand");
        }

        public override void VisitBinaryOperator(IBinaryOperation operation)
        {
            LogString(nameof(IBinaryOperation));

            var kindStr = $"{nameof(BinaryOperatorKind)}.{operation.OperatorKind}";
            if (operation.IsLifted)
            {
                kindStr += ", IsLifted";
            }

            if (operation.IsChecked)
            {
                kindStr += ", Checked";
            }

            if (operation.IsCompareText)
            {
                kindStr += ", CompareText";
            }

            LogString($" ({kindStr})");
            LogHasOperatorMethodExpressionCommon(operation.OperatorMethod, operation.ConstrainedToType);
            var unaryOperatorMethod = ((BinaryOperation)operation).UnaryOperatorMethod;
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.LeftOperand, "Left");
            Visit(operation.RightOperand, "Right");
        }

        public override void VisitTupleBinaryOperator(ITupleBinaryOperation operation)
        {
            LogString(nameof(ITupleBinaryOperation));
            var kindStr = $"{nameof(BinaryOperatorKind)}.{operation.OperatorKind}";

            LogString($" ({kindStr})");
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.LeftOperand, "Left");
            Visit(operation.RightOperand, "Right");
        }

        private void LogHasOperatorMethodExpressionCommon(IMethodSymbol operatorMethodOpt, ITypeSymbol constrainedToTypeOpt)
        {
            if (operatorMethodOpt != null)
            {
                LogSymbol(operatorMethodOpt, header: " (OperatorMethod");

                if (constrainedToTypeOpt is not null)
                {
                    LogType(constrainedToTypeOpt, header: " ConstrainedToType");
                }

                LogString(")");
            }
            else
            {
                Assert.Null(constrainedToTypeOpt);
            }
        }

        public override void VisitConversion(IConversionOperation operation)
        {
            LogString(nameof(IConversionOperation));

            var isTryCast = $"TryCast: {(operation.IsTryCast ? "True" : "False")}";
            var isChecked = operation.IsChecked ? "Checked" : "Unchecked";
            LogString($" ({isTryCast}, {isChecked})");

            LogHasOperatorMethodExpressionCommon(operation.OperatorMethod, operation.ConstrainedToType);
            LogCommonPropertiesAndNewLine(operation);
            Indent();
            LogConversion(operation.Conversion);

            if (((Operation)operation).OwningSemanticModel == null)
            {
                LogNewLine();
                Indent();
                LogString($"({((ConversionOperation)operation).ConversionConvertible})");
                Unindent();
            }

            Unindent();
            LogNewLine();

            Visit(operation.Operand, "Operand");
        }

        public override void VisitConditional(IConditionalOperation operation)
        {
            LogString(nameof(IConditionalOperation));

            if (operation.IsRef)
            {
                LogString(" (IsRef)");
            }

            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Condition, "Condition");
            Visit(operation.WhenTrue, "WhenTrue");
            Visit(operation.WhenFalse, "WhenFalse");
        }

        public override void VisitCoalesce(ICoalesceOperation operation)
        {
            LogString(nameof(ICoalesceOperation));
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Value, "Expression");
            Indent();
            LogConversion(operation.ValueConversion, "ValueConversion");
            LogNewLine();
            Indent();
            LogString($"({((CoalesceOperation)operation).ValueConversionConvertible})");
            Unindent();
            LogNewLine();
            Unindent();

            Visit(operation.WhenNull, "WhenNull");
        }

        public override void VisitCoalesceAssignment(ICoalesceAssignmentOperation operation)
        {
            LogString(nameof(ICoalesceAssignmentOperation));
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Target, nameof(operation.Target));
            Visit(operation.Value, nameof(operation.Value));
        }

        public override void VisitIsType(IIsTypeOperation operation)
        {
            LogString(nameof(IIsTypeOperation));
            if (operation.IsNegated)
            {
                LogString(" (IsNotExpression)");
            }

            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.ValueOperand, "Operand");

            Indent();
            LogType(operation.TypeOperand, "IsType");
            LogNewLine();
            Unindent();
        }

        public override void VisitSizeOf(ISizeOfOperation operation)
        {
            LogString(nameof(ISizeOfOperation));
            LogCommonPropertiesAndNewLine(operation);

            Indent();
            LogType(operation.TypeOperand, "TypeOperand");
            LogNewLine();
            Unindent();
        }

        public override void VisitTypeOf(ITypeOfOperation operation)
        {
            LogString(nameof(ITypeOfOperation));
            LogCommonPropertiesAndNewLine(operation);

            Indent();
            LogType(operation.TypeOperand, "TypeOperand");
            LogNewLine();
            Unindent();
        }

        public override void VisitAnonymousFunction(IAnonymousFunctionOperation operation)
        {
            LogString(nameof(IAnonymousFunctionOperation));

            // For C# this prints "lambda expression", which is not very helpful if we want to tell lambdas apart.
            // That is how symbol display is implemented for C#.
            // https://github.com/dotnet/roslyn/issues/22559#issuecomment-393667316 tracks improving the output.
            LogSymbol(operation.Symbol, header: " (Symbol");
            LogString(")");
            LogCommonPropertiesAndNewLine(operation);

            base.VisitAnonymousFunction(operation);
        }

        public override void VisitFlowAnonymousFunction(IFlowAnonymousFunctionOperation operation)
        {
            LogString(nameof(IFlowAnonymousFunctionOperation));

            // For C# this prints "lambda expression", which is not very helpful if we want to tell lambdas apart.
            // That is how symbol display is implemented for C#.
            // https://github.com/dotnet/roslyn/issues/22559#issuecomment-393667316 tracks improving the output.
            LogSymbol(operation.Symbol, header: " (Symbol");
            LogString(")");
            LogCommonPropertiesAndNewLine(operation);

            base.VisitFlowAnonymousFunction(operation);
        }

        public override void VisitDelegateCreation(IDelegateCreationOperation operation)
        {
            LogString(nameof(IDelegateCreationOperation));
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Target, nameof(operation.Target));
        }

        public override void VisitLiteral(ILiteralOperation operation)
        {
            LogString(nameof(ILiteralOperation));
            LogCommonPropertiesAndNewLine(operation);
        }

        public override void VisitUtf8String(IUtf8StringOperation operation)
        {
            LogString(nameof(IUtf8StringOperation));
            LogString(" (");
            LogString(operation.Value);
            LogString(")");
            LogCommonPropertiesAndNewLine(operation);
        }

        public override void VisitAwait(IAwaitOperation operation)
        {
            LogString(nameof(IAwaitOperation));
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Operation, "Expression");
        }

        public override void VisitNameOf(INameOfOperation operation)
        {
            LogString(nameof(INameOfOperation));
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Argument);
        }

        public override void VisitThrow(IThrowOperation operation)
        {
            LogString(nameof(IThrowOperation));
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Exception);
        }

        public override void VisitAddressOf(IAddressOfOperation operation)
        {
            LogString(nameof(IAddressOfOperation));
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Reference, "Reference");
        }

        public override void VisitObjectCreation(IObjectCreationOperation operation)
        {
            LogString(nameof(IObjectCreationOperation));

            LogString($" (Constructor: {operation.Constructor?.ToTestDisplayString() ?? "<null>"})");

            LogCommonPropertiesAndNewLine(operation);

            VisitArguments(operation.Arguments);
            Visit(operation.Initializer, "Initializer");
        }

        public override void VisitAnonymousObjectCreation(IAnonymousObjectCreationOperation operation)
        {
            LogString(nameof(IAnonymousObjectCreationOperation));
            LogCommonPropertiesAndNewLine(operation);

            foreach (var initializer in operation.Initializers)
            {
                var simpleAssignment = (ISimpleAssignmentOperation)initializer;
                var propertyReference = (IPropertyReferenceOperation)simpleAssignment.Target;
                Assert.Empty(propertyReference.Arguments);
                Assert.Equal(OperationKind.InstanceReference, propertyReference.Instance.Kind);
                Assert.Equal(InstanceReferenceKind.ImplicitReceiver, ((IInstanceReferenceOperation)propertyReference.Instance).ReferenceKind);
            }

            VisitArray(operation.Initializers, "Initializers", logElementCount: true);
        }

        public override void VisitDynamicObjectCreation(IDynamicObjectCreationOperation operation)
        {
            LogString(nameof(IDynamicObjectCreationOperation));
            LogCommonPropertiesAndNewLine(operation);

            VisitDynamicArguments((HasDynamicArgumentsExpression)operation);
            Visit(operation.Initializer, "Initializer");
        }

        public override void VisitDynamicInvocation(IDynamicInvocationOperation operation)
        {
            LogString(nameof(IDynamicInvocationOperation));
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Operation, "Expression");
            VisitDynamicArguments((HasDynamicArgumentsExpression)operation);
        }

        public override void VisitDynamicIndexerAccess(IDynamicIndexerAccessOperation operation)
        {
            LogString(nameof(IDynamicIndexerAccessOperation));
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Operation, "Expression");
            VisitDynamicArguments((HasDynamicArgumentsExpression)operation);
        }

        public override void VisitObjectOrCollectionInitializer(IObjectOrCollectionInitializerOperation operation)
        {
            LogString(nameof(IObjectOrCollectionInitializerOperation));
            LogCommonPropertiesAndNewLine(operation);

            VisitArray(operation.Initializers, "Initializers", logElementCount: true);
        }

        public override void VisitMemberInitializer(IMemberInitializerOperation operation)
        {
            LogString(nameof(IMemberInitializerOperation));
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.InitializedMember, "InitializedMember");
            Visit(operation.Initializer, "Initializer");
        }

        [Obsolete("ICollectionElementInitializerOperation has been replaced with IInvocationOperation and IDynamicInvocationOperation", error: true)]
        public override void VisitCollectionElementInitializer(ICollectionElementInitializerOperation operation)

        {
            // Kept to ensure that it's never called, as we can't override DefaultVisit in this visitor
            throw ExceptionUtilities.Unreachable();
        }

        public override void VisitFieldInitializer(IFieldInitializerOperation operation)
        {
            LogString(nameof(IFieldInitializerOperation));

            if (operation.InitializedFields.Length <= 1)
            {
                if (operation.InitializedFields.Length == 1)
                {
                    LogSymbol(operation.InitializedFields[0], header: " (Field");
                    LogString(")");
                }

                LogCommonPropertiesAndNewLine(operation);
            }
            else
            {
                LogString($" ({operation.InitializedFields.Length} initialized fields)");
                LogCommonPropertiesAndNewLine(operation);

                Indent();

                int index = 1;
                foreach (var local in operation.InitializedFields)
                {
                    LogSymbol(local, header: $"Field_{index++}");
                    LogNewLine();
                }

                Unindent();
            }

            LogLocals(operation.Locals);
            base.VisitFieldInitializer(operation);
        }

        public override void VisitVariableInitializer(IVariableInitializerOperation operation)
        {
            LogString(nameof(IVariableInitializerOperation));
            LogCommonPropertiesAndNewLine(operation);
            Assert.Empty(operation.Locals);
            base.VisitVariableInitializer(operation);
        }

        public override void VisitPropertyInitializer(IPropertyInitializerOperation operation)
        {
            LogString(nameof(IPropertyInitializerOperation));

            if (operation.InitializedProperties.Length <= 1)
            {
                if (operation.InitializedProperties.Length == 1)
                {
                    LogSymbol(operation.InitializedProperties[0], header: " (Property");
                    LogString(")");
                }

                LogCommonPropertiesAndNewLine(operation);
            }
            else
            {
                LogString($" ({operation.InitializedProperties.Length} initialized properties)");
                LogCommonPropertiesAndNewLine(operation);

                Indent();

                int index = 1;
                foreach (var property in operation.InitializedProperties)
                {
                    LogSymbol(property, header: $"Property_{index++}");
                    LogNewLine();
                }

                Unindent();
            }

            LogLocals(operation.Locals);
            base.VisitPropertyInitializer(operation);
        }

        public override void VisitParameterInitializer(IParameterInitializerOperation operation)
        {
            LogString(nameof(IParameterInitializerOperation));
            LogSymbol(operation.Parameter, header: " (Parameter");
            LogString(")");
            LogCommonPropertiesAndNewLine(operation);

            LogLocals(operation.Locals);
            base.VisitParameterInitializer(operation);
        }

        public override void VisitArrayCreation(IArrayCreationOperation operation)
        {
            LogString(nameof(IArrayCreationOperation));
            LogCommonPropertiesAndNewLine(operation);

            VisitArray(operation.DimensionSizes, "Dimension Sizes", logElementCount: true);
            Visit(operation.Initializer, "Initializer");
        }

        public override void VisitArrayInitializer(IArrayInitializerOperation operation)
        {
            LogString(nameof(IArrayInitializerOperation));
            LogString($" ({operation.ElementValues.Length} elements)");
            LogCommonPropertiesAndNewLine(operation);

            Assert.Null(operation.Type);
            VisitArray(operation.ElementValues, "Element Values", logElementCount: true);
        }

        public override void VisitCollectionExpression(ICollectionExpressionOperation operation)
        {
            LogString(nameof(ICollectionExpressionOperation));
            LogString($" ({operation.Elements.Length} elements");
            LogSymbol(operation.ConstructMethod, $", {nameof(operation.ConstructMethod)}");
            LogString(")");
            LogCommonPropertiesAndNewLine(operation);

            if (operation.ConstructArguments.Length > 0)
                VisitArray(operation.ConstructArguments, nameof(operation.ConstructArguments), logElementCount: true);

            VisitArray(operation.Elements, nameof(operation.Elements), logElementCount: true);
        }

        public override void VisitSpread(ISpreadOperation operation)
        {
            LogString(nameof(ISpreadOperation));
            LogSymbol(operation.ElementType, $" ({nameof(operation.ElementType)}");
            LogString(")");
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Operand, nameof(operation.Operand));
            Indent();
            LogConversion(operation.ElementConversion, nameof(operation.ElementConversion));
            LogNewLine();
            Indent();
            LogString($"({((SpreadOperation)operation).ElementConversionConvertible})");
            Unindent();
            LogNewLine();
            Unindent();
        }

        public override void VisitSimpleAssignment(ISimpleAssignmentOperation operation)
        {
            LogString(nameof(ISimpleAssignmentOperation));

            if (operation.IsRef)
            {
                LogString(" (IsRef)");
            }

            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Target, "Left");
            Visit(operation.Value, "Right");
        }

        public override void VisitDeconstructionAssignment(IDeconstructionAssignmentOperation operation)
        {
            LogString(nameof(IDeconstructionAssignmentOperation));
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Target, "Left");
            Visit(operation.Value, "Right");
        }

        public override void VisitDeclarationExpression(IDeclarationExpressionOperation operation)
        {
            LogString(nameof(IDeclarationExpressionOperation));
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Expression);
        }

        public override void VisitCompoundAssignment(ICompoundAssignmentOperation operation)
        {
            LogString(nameof(ICompoundAssignmentOperation));

            var kindStr = $"{nameof(BinaryOperatorKind)}.{operation.OperatorKind}";
            if (operation.IsLifted)
            {
                kindStr += ", IsLifted";
            }

            if (operation.IsChecked)
            {
                kindStr += ", Checked";
            }

            LogString($" ({kindStr})");
            LogHasOperatorMethodExpressionCommon(operation.OperatorMethod, operation.ConstrainedToType);
            LogCommonPropertiesAndNewLine(operation);
            Indent();
            LogConversion(operation.InConversion, "InConversion");
            LogNewLine();
            LogConversion(operation.OutConversion, "OutConversion");
            LogNewLine();
            Unindent();

            Visit(operation.Target, "Left");
            Visit(operation.Value, "Right");
        }

        public override void VisitIncrementOrDecrement(IIncrementOrDecrementOperation operation)
        {
            LogString(nameof(IIncrementOrDecrementOperation));

            var kindStr = operation.IsPostfix ? "Postfix" : "Prefix";
            if (operation.IsLifted)
            {
                kindStr += ", IsLifted";
            }

            if (operation.IsChecked)
            {
                kindStr += ", Checked";
            }

            LogString($" ({kindStr})");
            LogHasOperatorMethodExpressionCommon(operation.OperatorMethod, operation.ConstrainedToType);
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Target, "Target");
        }

        public override void VisitParenthesized(IParenthesizedOperation operation)
        {
            LogString(nameof(IParenthesizedOperation));
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Operand, "Operand");
        }

        public override void VisitDynamicMemberReference(IDynamicMemberReferenceOperation operation)
        {
            LogString(nameof(IDynamicMemberReferenceOperation));
            // (Member Name: "quoted name", Containing Type: type)
            LogString(" (");
            LogConstant((object)operation.MemberName, "Member Name");
            LogString(", ");
            LogType(operation.ContainingType, "Containing Type");
            LogString(")");
            LogCommonPropertiesAndNewLine(operation);

            VisitArrayCommon(operation.TypeArguments, "Type Arguments", logElementCount: true, logNullForDefault: false, arrayElementVisitor: VisitSymbolArrayElement);

            VisitInstance(operation.Instance);
        }

        public override void VisitDefaultValue(IDefaultValueOperation operation)
        {
            LogString(nameof(IDefaultValueOperation));
            LogCommonPropertiesAndNewLine(operation);
        }

        public override void VisitTypeParameterObjectCreation(ITypeParameterObjectCreationOperation operation)
        {
            LogString(nameof(ITypeParameterObjectCreationOperation));
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Initializer, "Initializer");
        }

        internal override void VisitNoPiaObjectCreation(INoPiaObjectCreationOperation operation)
        {
            LogString(nameof(INoPiaObjectCreationOperation));
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Initializer, "Initializer");
        }

        public override void VisitInvalid(IInvalidOperation operation)
        {
            LogString(nameof(IInvalidOperation));
            LogCommonPropertiesAndNewLine(operation);

            VisitChildren(operation);
        }

        public override void VisitLocalFunction(ILocalFunctionOperation operation)
        {
            LogString(nameof(ILocalFunctionOperation));

            LogSymbol(operation.Symbol, header: " (Symbol");
            LogString(")");
            LogCommonPropertiesAndNewLine(operation);

            if (operation.Body != null)
            {
                if (operation.IgnoredBody != null)
                {
                    Visit(operation.Body, "Body");
                    Visit(operation.IgnoredBody, "IgnoredBody");

                }
                else
                {
                    Visit(operation.Body);
                }
            }
            else
            {
                Assert.Null(operation.IgnoredBody);
            }
        }

        private void LogCaseClauseCommon(ICaseClauseOperation operation)
        {
            Assert.Equal(OperationKind.CaseClause, operation.Kind);

            if (operation.Label != null)
            {
                LogString($" (Label Id: {GetLabelId(operation.Label)})");
            }

            var kindStr = $"{nameof(CaseKind)}.{operation.CaseKind}";
            LogString($" ({kindStr})");
            LogCommonPropertiesAndNewLine(operation);
        }

        public override void VisitSingleValueCaseClause(ISingleValueCaseClauseOperation operation)
        {
            LogString(nameof(ISingleValueCaseClauseOperation));
            LogCaseClauseCommon(operation);

            Visit(operation.Value, "Value");
        }

        public override void VisitRelationalCaseClause(IRelationalCaseClauseOperation operation)
        {
            LogString(nameof(IRelationalCaseClauseOperation));
            var kindStr = $"{nameof(BinaryOperatorKind)}.{operation.Relation}";
            LogString($" (Relational operator kind: {kindStr})");
            LogCaseClauseCommon(operation);

            Visit(operation.Value, "Value");
        }

        public override void VisitRangeCaseClause(IRangeCaseClauseOperation operation)
        {
            LogString(nameof(IRangeCaseClauseOperation));
            LogCaseClauseCommon(operation);

            Visit(operation.MinimumValue, "Min");
            Visit(operation.MaximumValue, "Max");
        }

        public override void VisitDefaultCaseClause(IDefaultCaseClauseOperation operation)
        {
            LogString(nameof(IDefaultCaseClauseOperation));
            LogCaseClauseCommon(operation);
        }

        public override void VisitTuple(ITupleOperation operation)
        {
            LogString(nameof(ITupleOperation));
            LogCommonPropertiesAndNewLine(operation);
            Indent();
            LogType(operation.NaturalType, nameof(operation.NaturalType));
            LogNewLine();
            Unindent();

            VisitArray(operation.Elements, "Elements", logElementCount: true);
        }

        public override void VisitInterpolatedString(IInterpolatedStringOperation operation)
        {
            LogString(nameof(IInterpolatedStringOperation));
            LogCommonPropertiesAndNewLine(operation);

            VisitArray(operation.Parts, "Parts", logElementCount: true);
        }

        public override void VisitInterpolatedStringHandlerCreation(IInterpolatedStringHandlerCreationOperation operation)
        {
            LogString(nameof(IInterpolatedStringHandlerCreationOperation));
            LogString($" (HandlerAppendCallsReturnBool: {operation.HandlerAppendCallsReturnBool}, HandlerCreationHasSuccessParameter: {operation.HandlerCreationHasSuccessParameter})");
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.HandlerCreation, "Creation");
            Visit(operation.Content, "Content");
        }

        public override void VisitInterpolatedStringAddition(IInterpolatedStringAdditionOperation operation)
        {
            LogString(nameof(IInterpolatedStringAdditionOperation));
            LogCommonPropertiesAndNewLine(operation);
            Visit(operation.Left, "Left");
            Visit(operation.Right, "Right");
        }

        public override void VisitInterpolatedStringText(IInterpolatedStringTextOperation operation)
        {
            LogString(nameof(IInterpolatedStringTextOperation));
            LogCommonPropertiesAndNewLine(operation);

            if (operation.Text.Kind != OperationKind.Literal)
            {
                Assert.Equal(OperationKind.Literal, ((IConversionOperation)operation.Text).Operand.Kind);
            }
            Visit(operation.Text, "Text");
        }

        public override void VisitInterpolation(IInterpolationOperation operation)
        {
            LogString(nameof(IInterpolationOperation));
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Expression, "Expression");
            Visit(operation.Alignment, "Alignment");
            Visit(operation.FormatString, "FormatString");

            if (operation.FormatString != null && operation.FormatString.Kind != OperationKind.Literal)
            {
                Assert.Equal(OperationKind.Literal, ((IConversionOperation)operation.FormatString).Operand.Kind);
            }
        }

        public override void VisitInterpolatedStringAppend(IInterpolatedStringAppendOperation operation)
        {
            LogString(nameof(IInterpolatedStringAppendOperation));
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.AppendCall, "AppendCall");
        }

        public override void VisitInterpolatedStringHandlerArgumentPlaceholder(IInterpolatedStringHandlerArgumentPlaceholderOperation operation)
        {
            LogString(nameof(IInterpolatedStringHandlerArgumentPlaceholderOperation));
            if (operation.PlaceholderKind is InterpolatedStringArgumentPlaceholderKind.CallsiteArgument)
            {
                LogString($" (ArgumentIndex: {operation.ArgumentIndex})");
            }
            else
            {
                LogString($" ({operation.PlaceholderKind})");
            }
            LogCommonPropertiesAndNewLine(operation);
        }

        public override void VisitConstantPattern(IConstantPatternOperation operation)
        {
            LogString(nameof(IConstantPatternOperation));
            LogPatternPropertiesAndNewLine(operation);

            Visit(operation.Value, "Value");
        }

        public override void VisitRelationalPattern(IRelationalPatternOperation operation)
        {
            LogString(nameof(IRelationalPatternOperation));
            LogString($" ({nameof(BinaryOperatorKind)}.{operation.OperatorKind})");
            LogPatternPropertiesAndNewLine(operation);
            Visit(operation.Value, "Value");
        }

        public override void VisitNegatedPattern(INegatedPatternOperation operation)
        {
            LogString(nameof(INegatedPatternOperation));
            LogPatternPropertiesAndNewLine(operation);
            Visit(operation.Pattern, "Pattern");
        }

        public override void VisitBinaryPattern(IBinaryPatternOperation operation)
        {
            LogString(nameof(IBinaryPatternOperation));
            LogString($" ({nameof(BinaryOperatorKind)}.{operation.OperatorKind})");
            LogPatternPropertiesAndNewLine(operation);
            Visit(operation.LeftPattern, "LeftPattern");
            Visit(operation.RightPattern, "RightPattern");
        }

        public override void VisitTypePattern(ITypePatternOperation operation)
        {
            LogString(nameof(ITypePatternOperation));
            LogPatternProperties(operation);
            LogSymbol(operation.MatchedType, $", {nameof(operation.MatchedType)}");
            LogString(")");
            LogNewLine();
        }

        public override void VisitDeclarationPattern(IDeclarationPatternOperation operation)
        {
            LogString(nameof(IDeclarationPatternOperation));
            LogPatternProperties(operation);
            LogSymbol(operation.DeclaredSymbol, $", {nameof(operation.DeclaredSymbol)}");
            LogConstant((object)operation.MatchesNull, $", {nameof(operation.MatchesNull)}");
            LogString(")");
            LogNewLine();
        }

        public override void VisitSlicePattern(ISlicePatternOperation operation)
        {
            LogString(nameof(ISlicePatternOperation));
            LogPatternProperties(operation);
            LogSymbol(operation.SliceSymbol, $", {nameof(operation.SliceSymbol)}");
            LogNewLine();

            Visit(operation.Pattern, $"{nameof(operation.Pattern)}");
        }

        public override void VisitListPattern(IListPatternOperation operation)
        {
            LogString(nameof(IListPatternOperation));
            LogPatternProperties(operation);
            LogSymbol(operation.DeclaredSymbol, $", {nameof(operation.DeclaredSymbol)}");
            LogSymbol(operation.LengthSymbol, $", {nameof(operation.LengthSymbol)}");
            LogSymbol(operation.IndexerSymbol, $", {nameof(operation.IndexerSymbol)}");
            LogString(")");
            LogNewLine();

            VisitArray(operation.Patterns, $"{nameof(operation.Patterns)} ", true, true);
        }

        public override void VisitRecursivePattern(IRecursivePatternOperation operation)
        {
            LogString(nameof(IRecursivePatternOperation));
            LogPatternProperties(operation);
            LogSymbol(operation.DeclaredSymbol, $", {nameof(operation.DeclaredSymbol)}");
            LogType(operation.MatchedType, $", {nameof(operation.MatchedType)}");
            LogSymbol(operation.DeconstructSymbol, $", {nameof(operation.DeconstructSymbol)}");
            LogString(")");
            LogNewLine();

            VisitArray(operation.DeconstructionSubpatterns, $"{nameof(operation.DeconstructionSubpatterns)} ", true, true);
            VisitArray(operation.PropertySubpatterns, $"{nameof(operation.PropertySubpatterns)} ", true, true);
        }

        public override void VisitPropertySubpattern(IPropertySubpatternOperation operation)
        {
            LogString(nameof(IPropertySubpatternOperation));
            LogCommonProperties(operation);
            LogNewLine();

            Visit(operation.Member, $"{nameof(operation.Member)}");
            Visit(operation.Pattern, $"{nameof(operation.Pattern)}");
        }

        public override void VisitIsPattern(IIsPatternOperation operation)
        {
            LogString(nameof(IIsPatternOperation));
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Value, $"{nameof(operation.Value)}");
            Visit(operation.Pattern, "Pattern");
        }

        public override void VisitPatternCaseClause(IPatternCaseClauseOperation operation)
        {
            LogString(nameof(IPatternCaseClauseOperation));
            LogCaseClauseCommon(operation);
            Assert.Same(((ICaseClauseOperation)operation).Label, operation.Label);

            Visit(operation.Pattern, "Pattern");
            if (operation.Guard != null)
                Visit(operation.Guard, nameof(operation.Guard));
        }

        public override void VisitTranslatedQuery(ITranslatedQueryOperation operation)
        {
            LogString(nameof(ITranslatedQueryOperation));
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.Operation, "Expression");
        }

        public override void VisitRaiseEvent(IRaiseEventOperation operation)
        {
            LogString(nameof(IRaiseEventOperation));
            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.EventReference, header: "Event Reference");
            VisitArguments(operation.Arguments);
        }

        public override void VisitConstructorBodyOperation(IConstructorBodyOperation operation)
        {
            LogString(nameof(IConstructorBodyOperation));
            LogCommonPropertiesAndNewLine(operation);

            LogLocals(operation.Locals);
            Visit(operation.Initializer, "Initializer");
            Visit(operation.BlockBody, "BlockBody");
            Visit(operation.ExpressionBody, "ExpressionBody");
        }

        public override void VisitMethodBodyOperation(IMethodBodyOperation operation)
        {
            LogString(nameof(IMethodBodyOperation));
            LogCommonPropertiesAndNewLine(operation);
            Visit(operation.BlockBody, "BlockBody");
            Visit(operation.ExpressionBody, "ExpressionBody");
        }

        public override void VisitDiscardOperation(IDiscardOperation operation)
        {
            LogString(nameof(IDiscardOperation));
            LogString(" (");
            LogSymbol(operation.DiscardSymbol, "Symbol");
            LogString(")");
            LogCommonPropertiesAndNewLine(operation);
        }

        public override void VisitDiscardPattern(IDiscardPatternOperation operation)
        {
            LogString(nameof(IDiscardPatternOperation));
            LogPatternPropertiesAndNewLine(operation);
        }

        public override void VisitSwitchExpression(ISwitchExpressionOperation operation)
        {
            LogString($"{nameof(ISwitchExpressionOperation)} ({operation.Arms.Length} arms, IsExhaustive: {operation.IsExhaustive})");
            LogCommonPropertiesAndNewLine(operation);
            Visit(operation.Value, nameof(operation.Value));
            VisitArray(operation.Arms, nameof(operation.Arms), logElementCount: true);
        }

        public override void VisitSwitchExpressionArm(ISwitchExpressionArmOperation operation)
        {
            LogString($"{nameof(ISwitchExpressionArmOperation)} ({operation.Locals.Length} locals)");
            LogCommonPropertiesAndNewLine(operation);
            Visit(operation.Pattern, nameof(operation.Pattern));
            if (operation.Guard != null)
                Visit(operation.Guard, nameof(operation.Guard));
            Visit(operation.Value, nameof(operation.Value));
            LogLocals(operation.Locals);
        }

        public override void VisitStaticLocalInitializationSemaphore(IStaticLocalInitializationSemaphoreOperation operation)
        {
            LogString(nameof(IStaticLocalInitializationSemaphoreOperation));
            LogSymbol(operation.Local, " (Local Symbol");
            LogString(")");
            LogCommonPropertiesAndNewLine(operation);
        }

        public override void VisitRangeOperation(IRangeOperation operation)
        {
            LogString(nameof(IRangeOperation));

            if (operation.IsLifted)
            {
                LogString(" (IsLifted)");
            }

            LogCommonPropertiesAndNewLine(operation);

            Visit(operation.LeftOperand, nameof(operation.LeftOperand));
            Visit(operation.RightOperand, nameof(operation.RightOperand));
        }

        public override void VisitReDim(IReDimOperation operation)
        {
            LogString(nameof(IReDimOperation));
            if (operation.Preserve)
            {
                LogString(" (Preserve)");
            }
            LogCommonPropertiesAndNewLine(operation);
            VisitArray(operation.Clauses, "Clauses", logElementCount: true);
        }

        public override void VisitReDimClause(IReDimClauseOperation operation)
        {
            LogString(nameof(IReDimClauseOperation));
            LogCommonPropertiesAndNewLine(operation);
            Visit(operation.Operand, "Operand");
            VisitArray(operation.DimensionSizes, "DimensionSizes", logElementCount: true);
        }

        public override void VisitWith(IWithOperation operation)
        {
            LogString(nameof(IWithOperation));
            LogCommonPropertiesAndNewLine(operation);
            Visit(operation.Operand, "Operand");
            Indent();
            LogSymbol(operation.CloneMethod, nameof(operation.CloneMethod));
            LogNewLine();
            Unindent();
            Visit(operation.Initializer, "Initializer");
        }

        public override void VisitAttribute(IAttributeOperation operation)
        {
            LogString(nameof(IAttributeOperation));
            LogCommonPropertiesAndNewLine(operation);
            Visit(operation.Operation);
        }

        #endregion
    }
}
