// 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.Diagnostics;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.CSharp.Syntax.InternalSyntax
{
    using Microsoft.CodeAnalysis.Syntax.InternalSyntax;

    // TODO: The Xml parser recognizes most commonplace XML, according to the XML spec.
    // It does not recognize the following:
    //
    //  * Document Type Definition
    //      <!DOCTYPE ... >
    //  * Element Type Declaration, Attribute-List Declarations, Entity Declarations, Notation Declarations
    //      <!ELEMENT ... >
    //      <!ATTLIST ... >
    //      <!ENTITY ... >
    //      <!NOTATION ... >
    //  * Conditional Sections
    //      <![INCLUDE[ ... ]]>
    //      <![IGNORE[ ... ]]>
    // 
    // This probably does not matter. However, if it becomes necessary to recognize any
    // of these bits of XML, the most sensible thing to do is probably to scan them without
    // trying to understand them, e.g. like comments or CDATA, so that they are available
    // to whoever processes these comments and do not produce an error. 

    internal sealed class DocumentationCommentParser : SyntaxParser
    {
        private readonly SyntaxListPool _pool = new SyntaxListPool();
        private bool _isDelimited;

        internal DocumentationCommentParser(Lexer lexer)
            : base(lexer, LexerMode.None, oldTree: null, changes: null, allowModeReset: true)
        {
        }

        internal void ReInitialize(LexerMode modeflags)
        {
            base.ReInitialize();
            this.Mode = LexerMode.XmlDocComment | LexerMode.XmlDocCommentLocationStart | modeflags;
            _isDelimited = (modeflags & LexerMode.XmlDocCommentStyleDelimited) != 0;
        }

        private LexerMode SetMode(LexerMode mode)
        {
            var tmp = this.Mode;
            this.Mode = mode | (tmp & (LexerMode.MaskXmlDocCommentLocation | LexerMode.MaskXmlDocCommentStyle));
            return tmp;
        }

        private void ResetMode(LexerMode mode)
        {
            this.Mode = mode;
        }

        public DocumentationCommentTriviaSyntax ParseDocumentationComment(out bool isTerminated)
        {
            var nodes = _pool.Allocate<XmlNodeSyntax>();
            try
            {
                this.ParseXmlNodes(nodes);

                // It's possible that we finish parsing the xml, and we are still left in the middle
                // of an Xml comment. For example,
                //
                //     /// <goo></goo></uhoh>
                //                    ^
                // In this case, we stop at the caret. We need to ensure that we consume the remainder
                // of the doc comment here, since otherwise we will return the lexer to the state
                // where it recognizes C# tokens, which means that C# parser will get the </uhoh>,
                // which is not at all what we want.

                if (this.CurrentToken.Kind != SyntaxKind.EndOfDocumentationCommentToken)
                {
                    this.ParseRemainder(nodes);
                }

                var eoc = this.EatToken(SyntaxKind.EndOfDocumentationCommentToken);

                isTerminated = !_isDelimited || (eoc.LeadingTrivia.Count > 0 && eoc.LeadingTrivia[eoc.LeadingTrivia.Count - 1].ToString() == "*/");
                SyntaxKind kind = _isDelimited ? SyntaxKind.MultiLineDocumentationCommentTrivia : SyntaxKind.SingleLineDocumentationCommentTrivia;

                return SyntaxFactory.DocumentationCommentTrivia(kind, nodes.ToList(), eoc);
            }
            finally
            {
                _pool.Free(nodes);
            }
        }

        public void ParseRemainder(SyntaxListBuilder<XmlNodeSyntax> nodes)
        {
            bool endTag = this.CurrentToken.Kind == SyntaxKind.LessThanSlashToken;

            var saveMode = this.SetMode(LexerMode.XmlCDataSectionText);

            var textTokens = _pool.Allocate();
            try
            {
                while (this.CurrentToken.Kind != SyntaxKind.EndOfDocumentationCommentToken)
                {
                    var token = this.EatToken();

                    // TODO: It is possible that a non-literal gets in here. ]]>, specifically. Is that ok?
                    textTokens.Add(token);
                }

                var allRemainderText = SyntaxFactory.XmlText(textTokens.ToList());

                XmlParseErrorCode code = endTag ? XmlParseErrorCode.XML_EndTagNotExpected : XmlParseErrorCode.XML_ExpectedEndOfXml;
                allRemainderText = WithAdditionalDiagnostics(allRemainderText, new XmlSyntaxDiagnosticInfo(0, 1, code));

                nodes.Add(allRemainderText);
            }
            finally
            {
                _pool.Free(textTokens);
            }

            this.ResetMode(saveMode);
        }

        private void ParseXmlNodes(SyntaxListBuilder<XmlNodeSyntax> nodes)
        {
            while (true)
            {
                var node = this.ParseXmlNode();
                if (node == null)
                {
                    return;
                }

                nodes.Add(node);
            }
        }

        private XmlNodeSyntax ParseXmlNode()
        {
            switch (this.CurrentToken.Kind)
            {
                case SyntaxKind.XmlTextLiteralToken:
                case SyntaxKind.XmlTextLiteralNewLineToken:
                case SyntaxKind.XmlEntityLiteralToken:
                    return this.ParseXmlText();
                case SyntaxKind.LessThanToken:
                    return this.ParseXmlElement();
                case SyntaxKind.XmlCommentStartToken:
                    return this.ParseXmlComment();
                case SyntaxKind.XmlCDataStartToken:
                    return this.ParseXmlCDataSection();
                case SyntaxKind.XmlProcessingInstructionStartToken:
                    return this.ParseXmlProcessingInstruction();
                case SyntaxKind.EndOfDocumentationCommentToken:
                    return null;
                default:
                    // This means we have some unrecognized token. We probably need to give an error.
                    return null;
            }
        }

        private bool IsXmlNodeStartOrStop()
        {
            switch (this.CurrentToken.Kind)
            {
                case SyntaxKind.LessThanToken:
                case SyntaxKind.LessThanSlashToken:
                case SyntaxKind.XmlCommentStartToken:
                case SyntaxKind.XmlCDataStartToken:
                case SyntaxKind.XmlProcessingInstructionStartToken:
                case SyntaxKind.GreaterThanToken:
                case SyntaxKind.SlashGreaterThanToken:
                case SyntaxKind.EndOfDocumentationCommentToken:
                    return true;
                default:
                    return false;
            }
        }

        private XmlNodeSyntax ParseXmlText()
        {
            var textTokens = _pool.Allocate();
            while (this.CurrentToken.Kind == SyntaxKind.XmlTextLiteralToken
                || this.CurrentToken.Kind == SyntaxKind.XmlTextLiteralNewLineToken
                || this.CurrentToken.Kind == SyntaxKind.XmlEntityLiteralToken)
            {
                textTokens.Add(this.EatToken());
            }

            var list = textTokens.ToList();
            _pool.Free(textTokens);
            return SyntaxFactory.XmlText(list);
        }

        private XmlNodeSyntax ParseXmlElement()
        {
            var lessThan = this.EatToken(SyntaxKind.LessThanToken); // guaranteed
            var saveMode = this.SetMode(LexerMode.XmlElementTag);
            var name = this.ParseXmlName();
            if (lessThan.GetTrailingTriviaWidth() > 0 || name.GetLeadingTriviaWidth() > 0)
            {
                // The Xml spec disallows whitespace here: STag ::= '<' Name (S Attribute)* S? '>' 
                name = this.WithXmlParseError(name, XmlParseErrorCode.XML_InvalidWhitespace);
            }

            var attrs = _pool.Allocate<XmlAttributeSyntax>();
            try
            {
                this.ParseXmlAttributes(ref name, attrs);

                if (this.CurrentToken.Kind == SyntaxKind.GreaterThanToken)
                {
                    var startTag = SyntaxFactory.XmlElementStartTag(lessThan, name, attrs, this.EatToken());
                    this.SetMode(LexerMode.XmlDocComment);
                    var nodes = _pool.Allocate<XmlNodeSyntax>();
                    try
                    {
                        this.ParseXmlNodes(nodes);

                        XmlNameSyntax endName;
                        SyntaxToken greaterThan;

                        // end tag
                        var lessThanSlash = this.EatToken(SyntaxKind.LessThanSlashToken, reportError: false);

                        // If we didn't see "</", then we can't really be confident that this is actually an end tag,
                        // so just insert a missing one.
                        if (lessThanSlash.IsMissing)
                        {
                            this.ResetMode(saveMode);
                            lessThanSlash = this.WithXmlParseError(lessThanSlash, XmlParseErrorCode.XML_EndTagExpected, name.ToString());
                            endName = SyntaxFactory.XmlName(prefix: null, localName: SyntaxFactory.MissingToken(SyntaxKind.IdentifierToken));
                            greaterThan = SyntaxFactory.MissingToken(SyntaxKind.GreaterThanToken);
                        }
                        else
                        {
                            this.SetMode(LexerMode.XmlElementTag);
                            endName = this.ParseXmlName();
                            if (lessThanSlash.GetTrailingTriviaWidth() > 0 || endName.GetLeadingTriviaWidth() > 0)
                            {
                                // The Xml spec disallows whitespace here: STag ::= '<' Name (S Attribute)* S? '>' 
                                endName = this.WithXmlParseError(endName, XmlParseErrorCode.XML_InvalidWhitespace);
                            }

                            if (!endName.IsMissing && !MatchingXmlNames(name, endName))
                            {
                                endName = this.WithXmlParseError(endName, XmlParseErrorCode.XML_ElementTypeMatch, endName.ToString(), name.ToString());
                            }

                            // if we don't see the greater than token then skip the badness until we do or abort
                            if (this.CurrentToken.Kind != SyntaxKind.GreaterThanToken)
                            {
                                this.SkipBadTokens(ref endName, null,
                                    p => p.CurrentToken.Kind != SyntaxKind.GreaterThanToken,
                                    p => p.IsXmlNodeStartOrStop(),
                                    XmlParseErrorCode.XML_InvalidToken
                                    );
                            }

                            greaterThan = this.EatToken(SyntaxKind.GreaterThanToken);
                        }

                        var endTag = SyntaxFactory.XmlElementEndTag(lessThanSlash, endName, greaterThan);
                        this.ResetMode(saveMode);
                        return SyntaxFactory.XmlElement(startTag, nodes.ToList(), endTag);
                    }
                    finally
                    {
                        _pool.Free(nodes);
                    }
                }
                else
                {
                    var slashGreater = this.EatToken(SyntaxKind.SlashGreaterThanToken, false);
                    if (slashGreater.IsMissing && !name.IsMissing)
                    {
                        slashGreater = this.WithXmlParseError(slashGreater, XmlParseErrorCode.XML_ExpectedEndOfTag, name.ToString());
                    }

                    this.ResetMode(saveMode);
                    return SyntaxFactory.XmlEmptyElement(lessThan, name, attrs, slashGreater);
                }
            }
            finally
            {
                _pool.Free(attrs);
            }
        }

        private static bool MatchingXmlNames(XmlNameSyntax name, XmlNameSyntax endName)
        {
            // PERF: because of deduplication we often get the same name for name and endName,
            //       so we will check for such case first before materializing text for entire nodes 
            //       and comparing that.
            if (name == endName)
            {
                return true;
            }

            // before doing ToString, check if 
            // all nodes contributing to ToString are recursively the same
            // NOTE: leading and trailing trivia do not contribute to ToString
            if (!name.HasLeadingTrivia &&
                !endName.HasTrailingTrivia &&
                name.IsEquivalentTo(endName))
            {
                return true;
            }

            return name.ToString() == endName.ToString();
        }

        // assuming this is not used concurrently
        private readonly HashSet<string> _attributesSeen = new HashSet<string>();

        private void ParseXmlAttributes(ref XmlNameSyntax elementName, SyntaxListBuilder<XmlAttributeSyntax> attrs)
        {
            _attributesSeen.Clear();
            while (true)
            {
                if (this.CurrentToken.Kind == SyntaxKind.IdentifierToken)
                {
                    var attr = this.ParseXmlAttribute(elementName);
                    string attrName = attr.Name.ToString();
                    if (!_attributesSeen.Add(attrName))
                    {
                        attr = this.WithXmlParseError(attr, XmlParseErrorCode.XML_DuplicateAttribute, attrName);
                    }

                    attrs.Add(attr);
                }
                else
                {
                    var skip = this.SkipBadTokens(ref elementName, attrs,

                    // not expected condition
                        p => p.CurrentToken.Kind != SyntaxKind.IdentifierName,

                    // abort condition (looks like something we might understand later)
                        p => p.CurrentToken.Kind == SyntaxKind.GreaterThanToken
                            || p.CurrentToken.Kind == SyntaxKind.SlashGreaterThanToken
                            || p.CurrentToken.Kind == SyntaxKind.LessThanToken
                            || p.CurrentToken.Kind == SyntaxKind.LessThanSlashToken
                            || p.CurrentToken.Kind == SyntaxKind.EndOfDocumentationCommentToken
                            || p.CurrentToken.Kind == SyntaxKind.EndOfFileToken,

                        XmlParseErrorCode.XML_InvalidToken
                        );

                    if (skip == SkipResult.Abort)
                    {
                        break;
                    }
                }
            }
        }

        private enum SkipResult
        {
            Continue,
            Abort
        }

        private SkipResult SkipBadTokens<T>(
            ref T startNode,
            SyntaxListBuilder list,
            Func<DocumentationCommentParser, bool> isNotExpectedFunction,
            Func<DocumentationCommentParser, bool> abortFunction,
            XmlParseErrorCode error
            ) where T : CSharpSyntaxNode
        {
            var badTokens = default(SyntaxListBuilder<SyntaxToken>);
            bool hasError = false;

            try
            {
                SkipResult result = SkipResult.Continue;

                while (isNotExpectedFunction(this))
                {
                    if (abortFunction(this))
                    {
                        result = SkipResult.Abort;
                        break;
                    }

                    if (badTokens.IsNull)
                    {
                        badTokens = _pool.Allocate<SyntaxToken>();
                    }

                    var token = this.EatToken();
                    if (!hasError)
                    {
                        token = this.WithXmlParseError(token, error, token.ToString());
                        hasError = true;
                    }

                    badTokens.Add(token);
                }

                if (!badTokens.IsNull && badTokens.Count > 0)
                {
                    // use skipped text since cannot nest structured trivia under structured trivia
                    if (list == null || list.Count == 0)
                    {
                        startNode = AddTrailingSkippedSyntax(startNode, badTokens.ToListNode());
                    }
                    else
                    {
                        list[list.Count - 1] = AddTrailingSkippedSyntax((CSharpSyntaxNode)list[list.Count - 1], badTokens.ToListNode());
                    }

                    return result;
                }
                else
                {
                    // somehow we did not consume anything, so tell caller to abort parse rule
                    return SkipResult.Abort;
                }
            }
            finally
            {
                if (!badTokens.IsNull)
                {
                    _pool.Free(badTokens);
                }
            }
        }

        private XmlAttributeSyntax ParseXmlAttribute(XmlNameSyntax elementName)
        {
            var attrName = this.ParseXmlName();
            if (attrName.GetLeadingTriviaWidth() == 0)
            {
                // The Xml spec requires whitespace here: STag ::= '<' Name (S Attribute)* S? '>' 
                attrName = this.WithXmlParseError(attrName, XmlParseErrorCode.XML_WhitespaceMissing);
            }

            var equals = this.EatToken(SyntaxKind.EqualsToken, false);
            if (equals.IsMissing)
            {
                equals = this.WithXmlParseError(equals, XmlParseErrorCode.XML_MissingEqualsAttribute);

                switch (this.CurrentToken.Kind)
                {
                    case SyntaxKind.SingleQuoteToken:
                    case SyntaxKind.DoubleQuoteToken:
                        // There could be a value coming up, let's keep parsing.
                        break;
                    default:
                        // This is probably not a complete attribute.
                        return SyntaxFactory.XmlTextAttribute(
                            attrName,
                            equals,
                            SyntaxFactory.MissingToken(SyntaxKind.DoubleQuoteToken),
                            default(SyntaxList<SyntaxToken>),
                            SyntaxFactory.MissingToken(SyntaxKind.DoubleQuoteToken));
                }
            }

            SyntaxToken startQuote;
            SyntaxToken endQuote;
            string attrNameText = attrName.LocalName.ValueText;
            bool hasNoPrefix = attrName.Prefix == null;
            if (hasNoPrefix && DocumentationCommentXmlNames.AttributeEquals(attrNameText, DocumentationCommentXmlNames.CrefAttributeName) &&
                !IsVerbatimCref())
            {
                CrefSyntax cref;
                this.ParseCrefAttribute(out startQuote, out cref, out endQuote);
                return SyntaxFactory.XmlCrefAttribute(attrName, equals, startQuote, cref, endQuote);
            }
            else if (hasNoPrefix && DocumentationCommentXmlNames.AttributeEquals(attrNameText, DocumentationCommentXmlNames.NameAttributeName) &&
                XmlElementSupportsNameAttribute(elementName))
            {
                IdentifierNameSyntax identifier;
                this.ParseNameAttribute(out startQuote, out identifier, out endQuote);
                return SyntaxFactory.XmlNameAttribute(attrName, equals, startQuote, identifier, endQuote);
            }
            else
            {
                var textTokens = _pool.Allocate<SyntaxToken>();
                try
                {
                    this.ParseXmlAttributeText(out startQuote, textTokens, out endQuote);
                    return SyntaxFactory.XmlTextAttribute(attrName, equals, startQuote, textTokens, endQuote);
                }
                finally
                {
                    _pool.Free(textTokens);
                }
            }
        }

        private static bool XmlElementSupportsNameAttribute(XmlNameSyntax elementName)
        {
            if (elementName.Prefix != null)
            {
                return false;
            }

            string localName = elementName.LocalName.ValueText;
            return
                DocumentationCommentXmlNames.ElementEquals(localName, DocumentationCommentXmlNames.ParameterElementName) ||
                DocumentationCommentXmlNames.ElementEquals(localName, DocumentationCommentXmlNames.ParameterReferenceElementName) ||
                DocumentationCommentXmlNames.ElementEquals(localName, DocumentationCommentXmlNames.TypeParameterElementName) ||
                DocumentationCommentXmlNames.ElementEquals(localName, DocumentationCommentXmlNames.TypeParameterReferenceElementName);
        }

        private bool IsVerbatimCref()
        {
            // As in XMLDocWriter::ReplaceReferences, if the first character of the value is not colon and the second character
            // is, then don't process the cref - just emit it as-is.
            bool isVerbatim = false;

            var resetPoint = this.GetResetPoint();

            SyntaxToken openQuote = EatToken(this.CurrentToken.Kind == SyntaxKind.SingleQuoteToken
                ? SyntaxKind.SingleQuoteToken
                : SyntaxKind.DoubleQuoteToken);

            // NOTE: Don't need to save mode, since we're already using a reset point.
            this.SetMode(LexerMode.XmlCharacter);

            SyntaxToken current = this.CurrentToken;
            if ((current.Kind == SyntaxKind.XmlTextLiteralToken || current.Kind == SyntaxKind.XmlEntityLiteralToken) &&
                current.ValueText != SyntaxFacts.GetText(openQuote.Kind) &&
                current.ValueText != ":")
            {
                EatToken();

                current = this.CurrentToken;
                if ((current.Kind == SyntaxKind.XmlTextLiteralToken || current.Kind == SyntaxKind.XmlEntityLiteralToken) &&
                    current.ValueText == ":")
                {
                    isVerbatim = true;
                }
            }

            this.Reset(ref resetPoint);
            this.Release(ref resetPoint);

            return isVerbatim;
        }

        private void ParseCrefAttribute(out SyntaxToken startQuote, out CrefSyntax cref, out SyntaxToken endQuote)
        {
            startQuote = ParseXmlAttributeStartQuote();
            SyntaxKind quoteKind = startQuote.Kind;

            {
                var saveMode = this.SetMode(quoteKind == SyntaxKind.SingleQuoteToken
                    ? LexerMode.XmlCrefQuote
                    : LexerMode.XmlCrefDoubleQuote);

                cref = this.ParseCrefAttributeValue();

                this.ResetMode(saveMode);
            }

            endQuote = ParseXmlAttributeEndQuote(quoteKind);
        }

        private void ParseNameAttribute(out SyntaxToken startQuote, out IdentifierNameSyntax identifier, out SyntaxToken endQuote)
        {
            startQuote = ParseXmlAttributeStartQuote();
            SyntaxKind quoteKind = startQuote.Kind;

            {
                var saveMode = this.SetMode(quoteKind == SyntaxKind.SingleQuoteToken
                    ? LexerMode.XmlNameQuote
                    : LexerMode.XmlNameDoubleQuote);

                identifier = this.ParseNameAttributeValue();

                this.ResetMode(saveMode);
            }

            endQuote = ParseXmlAttributeEndQuote(quoteKind);
        }

        private void ParseXmlAttributeText(out SyntaxToken startQuote, SyntaxListBuilder<SyntaxToken> textTokens, out SyntaxToken endQuote)
        {
            startQuote = ParseXmlAttributeStartQuote();
            SyntaxKind quoteKind = startQuote.Kind;

            // NOTE: Being a bit sneaky here - if the width isn't 0, we consumed something else in
            // place of the quote and we should continue parsing the attribute.
            if (startQuote.IsMissing && startQuote.FullWidth == 0)
            {
                endQuote = SyntaxFactory.MissingToken(quoteKind);
            }
            else
            {
                var saveMode = this.SetMode(quoteKind == SyntaxKind.SingleQuoteToken
                    ? LexerMode.XmlAttributeTextQuote
                    : LexerMode.XmlAttributeTextDoubleQuote);

                while (this.CurrentToken.Kind == SyntaxKind.XmlTextLiteralToken
                    || this.CurrentToken.Kind == SyntaxKind.XmlTextLiteralNewLineToken
                    || this.CurrentToken.Kind == SyntaxKind.XmlEntityLiteralToken
                    || this.CurrentToken.Kind == SyntaxKind.LessThanToken)
                {
                    var token = this.EatToken();
                    if (token.Kind == SyntaxKind.LessThanToken)
                    {
                        // TODO: It is possible that a non-literal gets in here. <, specifically. Is that ok?
                        token = this.WithXmlParseError(token, XmlParseErrorCode.XML_LessThanInAttributeValue);
                    }

                    textTokens.Add(token);
                }

                this.ResetMode(saveMode);

                // NOTE: This will never consume a non-ascii quote, since non-ascii quotes
                // are legal in the attribute value and are consumed by the preceding loop.
                endQuote = ParseXmlAttributeEndQuote(quoteKind);
            }
        }

        private SyntaxToken ParseXmlAttributeStartQuote()
        {
            if (IsNonAsciiQuotationMark(this.CurrentToken))
            {
                return SkipNonAsciiQuotationMark();
            }

            var quoteKind = this.CurrentToken.Kind == SyntaxKind.SingleQuoteToken
                ? SyntaxKind.SingleQuoteToken
                : SyntaxKind.DoubleQuoteToken;

            var startQuote = this.EatToken(quoteKind, reportError: false);
            if (startQuote.IsMissing)
            {
                startQuote = this.WithXmlParseError(startQuote, XmlParseErrorCode.XML_StringLiteralNoStartQuote);
            }
            return startQuote;
        }

        private SyntaxToken ParseXmlAttributeEndQuote(SyntaxKind quoteKind)
        {
            if (IsNonAsciiQuotationMark(this.CurrentToken))
            {
                return SkipNonAsciiQuotationMark();
            }

            var endQuote = this.EatToken(quoteKind, reportError: false);
            if (endQuote.IsMissing)
            {
                endQuote = this.WithXmlParseError(endQuote, XmlParseErrorCode.XML_StringLiteralNoEndQuote);
            }
            return endQuote;
        }

        private SyntaxToken SkipNonAsciiQuotationMark()
        {
            var quote = SyntaxFactory.MissingToken(SyntaxKind.DoubleQuoteToken);
            quote = AddTrailingSkippedSyntax(quote, EatToken());
            quote = this.WithXmlParseError(quote, XmlParseErrorCode.XML_StringLiteralNonAsciiQuote);
            return quote;
        }

        /// <summary>
        /// These aren't acceptable in place of ASCII quotation marks in XML, 
        /// but we want to consume them (and produce an appropriate error) if
        /// they occur in a place where a quotation mark is legal.
        /// </summary>
        private static bool IsNonAsciiQuotationMark(SyntaxToken token)
        {
            return token.Text.Length == 1 && SyntaxFacts.IsNonAsciiQuotationMark(token.Text[0]);
        }

        private XmlNameSyntax ParseXmlName()
        {
            var id = this.EatToken(SyntaxKind.IdentifierToken);
            XmlPrefixSyntax prefix = null;
            if (this.CurrentToken.Kind == SyntaxKind.ColonToken)
            {
                var colon = this.EatToken();

                int prefixTrailingWidth = id.GetTrailingTriviaWidth();
                int colonLeadingWidth = colon.GetLeadingTriviaWidth();

                if (prefixTrailingWidth > 0 || colonLeadingWidth > 0)
                {
                    // NOTE: offset is relative to Start (not FullStart) of colon.
                    int width = prefixTrailingWidth + colonLeadingWidth;
                    int offset = -width;
                    colon = WithAdditionalDiagnostics(colon, new XmlSyntaxDiagnosticInfo(offset, width, XmlParseErrorCode.XML_InvalidWhitespace));
                }

                prefix = SyntaxFactory.XmlPrefix(id, colon);
                id = this.EatToken(SyntaxKind.IdentifierToken);

                int colonTrailingWidth = colon.GetTrailingTriviaWidth();
                int localNameLeadingWidth = id.GetLeadingTriviaWidth();
                if (colonTrailingWidth > 0 || localNameLeadingWidth > 0)
                {
                    // NOTE: offset is relative to Start (not FullStart) of identifier.
                    int width = colonTrailingWidth + localNameLeadingWidth;
                    int offset = -width;
                    id = WithAdditionalDiagnostics(id, new XmlSyntaxDiagnosticInfo(offset, width, XmlParseErrorCode.XML_InvalidWhitespace));

                    // CONSIDER: Another interpretation would be that the local part of this name is a missing identifier and the identifier
                    // we've just consumed is actually part of something else (e.g. an attribute name).
                }
            }

            return SyntaxFactory.XmlName(prefix, id);
        }

        private XmlCommentSyntax ParseXmlComment()
        {
            var lessThanExclamationMinusMinusToken = this.EatToken(SyntaxKind.XmlCommentStartToken);
            var saveMode = this.SetMode(LexerMode.XmlCommentText);
            var textTokens = _pool.Allocate<SyntaxToken>();
            while (this.CurrentToken.Kind == SyntaxKind.XmlTextLiteralToken
                || this.CurrentToken.Kind == SyntaxKind.XmlTextLiteralNewLineToken
                || this.CurrentToken.Kind == SyntaxKind.MinusMinusToken)
            {
                var token = this.EatToken();
                if (token.Kind == SyntaxKind.MinusMinusToken)
                {
                    // TODO: It is possible that a non-literal gets in here. --, specifically. Is that ok?
                    token = this.WithXmlParseError(token, XmlParseErrorCode.XML_IncorrectComment);
                }

                textTokens.Add(token);
            }

            var list = textTokens.ToList();
            _pool.Free(textTokens);

            var minusMinusGreaterThanToken = this.EatToken(SyntaxKind.XmlCommentEndToken);
            this.ResetMode(saveMode);
            return SyntaxFactory.XmlComment(lessThanExclamationMinusMinusToken, list, minusMinusGreaterThanToken);
        }

        private XmlCDataSectionSyntax ParseXmlCDataSection()
        {
            var startCDataToken = this.EatToken(SyntaxKind.XmlCDataStartToken);
            var saveMode = this.SetMode(LexerMode.XmlCDataSectionText);
            var textTokens = new SyntaxListBuilder<SyntaxToken>(10);
            while (this.CurrentToken.Kind == SyntaxKind.XmlTextLiteralToken
               || this.CurrentToken.Kind == SyntaxKind.XmlTextLiteralNewLineToken)
            {
                textTokens.Add(this.EatToken());
            }

            var endCDataToken = this.EatToken(SyntaxKind.XmlCDataEndToken);
            this.ResetMode(saveMode);
            return SyntaxFactory.XmlCDataSection(startCDataToken, textTokens, endCDataToken);
        }

        private XmlProcessingInstructionSyntax ParseXmlProcessingInstruction()
        {
            var startProcessingInstructionToken = this.EatToken(SyntaxKind.XmlProcessingInstructionStartToken);
            var saveMode = this.SetMode(LexerMode.XmlElementTag); //this mode accepts names
            var name = this.ParseXmlName();

            // NOTE: The XML spec says that name cannot be "xml" (case-insensitive comparison), 
            // but Dev10 does not enforce this.

            this.SetMode(LexerMode.XmlProcessingInstructionText); //this mode consumes text
            var textTokens = new SyntaxListBuilder<SyntaxToken>(10);
            while (this.CurrentToken.Kind == SyntaxKind.XmlTextLiteralToken
               || this.CurrentToken.Kind == SyntaxKind.XmlTextLiteralNewLineToken)
            {
                var textToken = this.EatToken();

                // NOTE: The XML spec says that the each text token must begin with a whitespace
                // character, but Dev10 does not enforce this.

                textTokens.Add(textToken);
            }

            var endProcessingInstructionToken = this.EatToken(SyntaxKind.XmlProcessingInstructionEndToken);
            this.ResetMode(saveMode);
            return SyntaxFactory.XmlProcessingInstruction(startProcessingInstructionToken, name, textTokens, endProcessingInstructionToken);
        }

        protected override SyntaxDiagnosticInfo GetExpectedTokenError(SyntaxKind expected, SyntaxKind actual, int offset, int length)
        {
            // NOTE: There are no errors in crefs - only warnings.  We accomplish this by wrapping every diagnostic in ErrorCode.WRN_ErrorOverride.
            if (InCref)
            {
                SyntaxDiagnosticInfo rawInfo = base.GetExpectedTokenError(expected, actual, offset, length);
                SyntaxDiagnosticInfo crefInfo = new SyntaxDiagnosticInfo(rawInfo.Offset, rawInfo.Width, ErrorCode.WRN_ErrorOverride, rawInfo, rawInfo.Code);
                return crefInfo;
            }

            switch (expected)
            {
                case SyntaxKind.IdentifierToken:
                    return new XmlSyntaxDiagnosticInfo(offset, length, XmlParseErrorCode.XML_ExpectedIdentifier);

                default:
                    return new XmlSyntaxDiagnosticInfo(offset, length, XmlParseErrorCode.XML_InvalidToken, SyntaxFacts.GetText(actual));
            }
        }

        protected override SyntaxDiagnosticInfo GetExpectedMissingNodeOrTokenError(
            GreenNode missingNodeOrToken, SyntaxKind expected, SyntaxKind actual)
        {
            Debug.Assert(missingNodeOrToken.IsMissing);

            // NOTE: There are no errors in crefs - only warnings.  We accomplish this by wrapping every diagnostic in ErrorCode.WRN_ErrorOverride.
            if (InCref)
            {
                return base.GetExpectedMissingNodeOrTokenError(missingNodeOrToken, expected, actual);
            }

            switch (expected)
            {
                case SyntaxKind.IdentifierToken:
                    return new XmlSyntaxDiagnosticInfo(XmlParseErrorCode.XML_ExpectedIdentifier);

                default:
                    return new XmlSyntaxDiagnosticInfo(XmlParseErrorCode.XML_InvalidToken, SyntaxFacts.GetText(actual));
            }
        }

        private TNode WithXmlParseError<TNode>(TNode node, XmlParseErrorCode code) where TNode : CSharpSyntaxNode
        {
            return WithAdditionalDiagnostics(node, new XmlSyntaxDiagnosticInfo(0, node.Width, code));
        }

        private TNode WithXmlParseError<TNode>(TNode node, XmlParseErrorCode code, params string[] args) where TNode : CSharpSyntaxNode
        {
            return WithAdditionalDiagnostics(node, new XmlSyntaxDiagnosticInfo(0, node.Width, code, args));
        }

        private SyntaxToken WithXmlParseError(SyntaxToken node, XmlParseErrorCode code, params string[] args)
        {
            return WithAdditionalDiagnostics(node, new XmlSyntaxDiagnosticInfo(0, node.Width, code, args));
        }

        protected override TNode WithAdditionalDiagnostics<TNode>(TNode node, params DiagnosticInfo[] diagnostics)
        {
            // Don't attach any diagnostics to syntax nodes within a documentation comment if the DocumentationMode
            // is not at least Diagnose.
            return Options.DocumentationMode >= DocumentationMode.Diagnose
                ? base.WithAdditionalDiagnostics<TNode>(node, diagnostics)
                : node;
        }

        #region Cref

        /// <summary>
        /// ACASEY: This grammar is derived from the behavior and sources of the native compiler.
        /// Tokens start with underscores (I've cheated for _PredefinedTypeToken, which is not actually a
        /// SyntaxKind), "*" indicates "0 or more", "?" indicates "0 or 1", and parentheses are for grouping.
        /// 
        /// Cref	 			= CrefType _DotToken CrefMember
        /// 					| CrefType
        /// 					| CrefMember
        ///                     | CrefFirstType _OpenParenToken CrefParameterList? _CloseParenToken
        /// CrefName			= _IdentifierToken (_LessThanToken _IdentifierToken (_CommaToken _IdentifierToken)* _GreaterThanToken)?
        /// CrefFirstType 		= ((_IdentifierToken _ColonColonToken)? CrefName) 
        ///                     | _PredefinedTypeToken
        /// CrefType 			= CrefFirstType (_DotToken CrefName)*
        /// CrefMember 			= CrefName (_OpenParenToken CrefParameterList? _CloseParenToken)?
        /// 					| _ThisKeyword (_OpenBracketToken CrefParameterList _CloseBracketToken)?
        /// 					| _OperatorKeyword _OperatorToken (_OpenParenToken CrefParameterList? _CloseParenToken)?
        /// 					| (_ImplicitKeyword | _ExplicitKeyword) _OperatorKeyword CrefParameterType (_OpenParenToken CrefParameterList? _CloseParenToken)?
        /// CrefParameterList	= CrefParameter (_CommaToken CrefParameter)*
        /// CrefParameter		= (_RefKeyword | _OutKeyword)? CrefParameterType
        /// CrefParameterType	= CrefParameterType2 _QuestionToken? _AsteriskToken* (_OpenBracketToken _CommaToken* _CloseBracketToken)*
        /// CrefParameterType2 	= (((_IdentifierToken _ColonColonToken)? CrefParameterType3) | _PredefinedTypeToken) (_DotToken CrefParameterType3)*
        /// CrefParameterType3 	= _IdentifierToken (_LessThanToken CrefParameterType (_CommaToken CrefParameterType)* _GreaterThanToken)?
        ///
        /// NOTE: type parameters, not type arguments
        /// NOTE: the first production of Cref is preferred to the other two
        /// NOTE: pointer, array, and nullable types only work in parameters
        /// NOTE: CrefParameterType2 and CrefParameterType3 correspond to CrefType and CrefName, respectively.
        /// Since the only difference is that they accept non-identifier type arguments, this is accomplished
        /// using parameters on the parsing methods (rather than whole new methods).
        /// </summary>
        private CrefSyntax ParseCrefAttributeValue()
        {
            CrefSyntax result;

            TypeSyntax type = ParseCrefType(typeArgumentsMustBeIdentifiers: true, checkForMember: true);
            if (type == null)
            {
                result = ParseMemberCref();
            }
            else if (IsEndOfCrefAttribute)
            {
                result = SyntaxFactory.TypeCref(type);
            }
            else if (type.Kind != SyntaxKind.QualifiedName && this.CurrentToken.Kind == SyntaxKind.OpenParenToken)
            {
                // Special case for crefs like "string()" and "A::B()".
                CrefParameterListSyntax parameters = ParseCrefParameterList();
                result = SyntaxFactory.NameMemberCref(type, parameters);
            }
            else
            {
                SyntaxToken dot = EatToken(SyntaxKind.DotToken);
                MemberCrefSyntax member = ParseMemberCref();
                result = SyntaxFactory.QualifiedCref(type, dot, member);
            }

            bool needOverallError = !IsEndOfCrefAttribute || result.ContainsDiagnostics;

            if (!IsEndOfCrefAttribute)
            {
                var badTokens = _pool.Allocate<SyntaxToken>();
                while (!IsEndOfCrefAttribute)
                {
                    badTokens.Add(this.EatToken());
                }
                result = AddTrailingSkippedSyntax(result, badTokens.ToListNode());
                _pool.Free(badTokens);
            }

            if (needOverallError)
            {
                result = this.AddError(result, ErrorCode.WRN_BadXMLRefSyntax, result.ToFullString());
            }

            return result;
        }

        /// <summary>
        /// Parse the custom cref syntax for a named member (method, property, etc),
        /// an indexer, an overloadable operator, or a user-defined conversion.
        /// </summary>
        private MemberCrefSyntax ParseMemberCref()
        {
            switch (CurrentToken.Kind)
            {
                case SyntaxKind.ThisKeyword:
                    return ParseIndexerMemberCref();
                case SyntaxKind.OperatorKeyword:
                    return ParseOperatorMemberCref();
                case SyntaxKind.ExplicitKeyword:
                case SyntaxKind.ImplicitKeyword:
                    return ParseConversionOperatorMemberCref();
                case SyntaxKind.IdentifierToken when CurrentToken.ContextualKind == SyntaxKind.ExtensionKeyword:
                    return ParsePossibleExtensionMemberCref();
                default:
                    return ParseNameMemberCref();
            }
        }

        /// <summary>
        /// Parse a named member (method, property, etc), with optional type
        /// parameters and regular parameters.
        /// </summary>
        private NameMemberCrefSyntax ParseNameMemberCref()
        {
            SimpleNameSyntax name = ParseCrefName(typeArgumentsMustBeIdentifiers: true);
            CrefParameterListSyntax parameters = ParseCrefParameterList();

            return SyntaxFactory.NameMemberCref(name, parameters);
        }

        /// <summary>
        /// Parse an indexer member, with optional parameters.
        /// </summary>
        private IndexerMemberCrefSyntax ParseIndexerMemberCref()
        {
            Debug.Assert(CurrentToken.Kind == SyntaxKind.ThisKeyword);
            SyntaxToken thisKeyword = EatToken();
            CrefBracketedParameterListSyntax parameters = ParseBracketedCrefParameterList();

            return SyntaxFactory.IndexerMemberCref(thisKeyword, parameters);
        }

#nullable enable
        /// <summary>
        /// If we have `extension` (with optional type arguments) and parameter list and a dot, then we have an extension member cref.
        /// Otherwise, we fall back to producing the same result as <see cref="ParseNameMemberCref"/>
        /// </summary>
        private MemberCrefSyntax ParsePossibleExtensionMemberCref()
        {
            Debug.Assert(CurrentToken.ContextualKind == SyntaxKind.ExtensionKeyword);

            SyntaxToken identifierToken = EatToken();
            TypeArgumentListSyntax? typeArguments = (CurrentToken.Kind == SyntaxKind.LessThanToken) ? ParseTypeArguments(typeArgumentsMustBeIdentifiers: true) : null;
            CrefParameterListSyntax? parameters = (CurrentToken.Kind == SyntaxKind.OpenParenToken) ? ParseCrefParameterList() : null;

            if (parameters is null || CurrentToken.Kind != SyntaxKind.DotToken)
            {
                SimpleNameSyntax name = typeArguments is not null
                    ? SyntaxFactory.GenericName(identifierToken, typeArguments)
                    : SyntaxFactory.IdentifierName(identifierToken);

                return SyntaxFactory.NameMemberCref(name, parameters);
            }

            SyntaxToken dotToken = EatToken(SyntaxKind.DotToken);
            MemberCrefSyntax member = ParseMemberCref();
            if (member is ExtensionMemberCrefSyntax)
            {
                member = AddErrorAsWarning(member, ErrorCode.ERR_MisplacedExtension);
            }

            return SyntaxFactory.ExtensionMemberCref(ConvertToKeyword(identifierToken), typeArguments, parameters, dotToken, member);
        }
#nullable disable

        /// <summary>
        /// Parse an overloadable operator, with optional parameters.
        /// </summary>
        private OperatorMemberCrefSyntax ParseOperatorMemberCref()
        {
            Debug.Assert(CurrentToken.Kind == SyntaxKind.OperatorKeyword);
            SyntaxToken operatorKeyword = EatToken();
            SyntaxToken checkedKeyword = TryEatCheckedKeyword(isConversion: false, ref operatorKeyword);

            SyntaxToken operatorToken;

            if (SyntaxFacts.IsAnyOverloadableOperator(CurrentToken.Kind))
            {
                operatorToken = EatToken();
            }
            else
            {
                operatorToken = SyntaxFactory.MissingToken(SyntaxKind.PlusToken);

                // Grab the offset and width before we consume the invalid keyword and change our position.
                var (offset, width) = this.GetDiagnosticSpanForMissingNodeOrToken(missingNodeOrToken: operatorToken);

                if (SyntaxFacts.IsUnaryOperatorDeclarationToken(CurrentToken.Kind) || SyntaxFacts.IsBinaryExpressionOperatorToken(CurrentToken.Kind))
                {
                    operatorToken = AddTrailingSkippedSyntax(operatorToken, EatToken());
                }

                SyntaxDiagnosticInfo rawInfo = new SyntaxDiagnosticInfo(offset, width, ErrorCode.ERR_OvlOperatorExpected);
                SyntaxDiagnosticInfo crefInfo = new SyntaxDiagnosticInfo(offset, width, ErrorCode.WRN_ErrorOverride, rawInfo, rawInfo.Code);

                operatorToken = WithAdditionalDiagnostics(operatorToken, crefInfo);
            }

            // Have to fake >>/>>> because it looks like the closing of nested type parameter lists (e.g. A<A<T>>).
            // Have to fake >= so the lexer doesn't mishandle >>=.
            if (operatorToken.Kind == SyntaxKind.GreaterThanToken && LanguageParser.NoTriviaBetween(operatorToken, CurrentToken))
            {
                if (CurrentToken.Kind == SyntaxKind.GreaterThanToken)
                {
                    var operatorToken2 = this.EatToken();

                    if (LanguageParser.NoTriviaBetween(operatorToken2, CurrentToken) &&
                        CurrentToken.Kind is (SyntaxKind.GreaterThanToken or SyntaxKind.GreaterThanEqualsToken))
                    {
                        var operatorToken3 = this.EatToken();

                        if (operatorToken3.Kind == SyntaxKind.GreaterThanToken)
                        {
                            operatorToken = SyntaxFactory.Token(
                                operatorToken.GetLeadingTrivia(),
                                SyntaxKind.GreaterThanGreaterThanGreaterThanToken,
                                operatorToken.Text + operatorToken2.Text + operatorToken3.Text,
                                operatorToken.ValueText + operatorToken2.ValueText + operatorToken3.ValueText,
                                operatorToken3.GetTrailingTrivia());

                            operatorToken = CheckFeatureAvailability(operatorToken, MessageID.IDS_FeatureUnsignedRightShift, forceWarning: true);
                        }
                        else
                        {
                            operatorToken = SyntaxFactory.Token(
                                operatorToken.GetLeadingTrivia(),
                                SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken,
                                operatorToken.Text + operatorToken2.Text + operatorToken3.Text,
                                operatorToken.ValueText + operatorToken2.ValueText + operatorToken3.ValueText,
                                operatorToken3.GetTrailingTrivia());
                            operatorToken = CheckFeatureAvailability(operatorToken, MessageID.IDS_FeatureUserDefinedCompoundAssignmentOperators, forceWarning: true);
                        }
                    }
                    else
                    {
                        operatorToken = SyntaxFactory.Token(
                            operatorToken.GetLeadingTrivia(),
                            SyntaxKind.GreaterThanGreaterThanToken,
                            operatorToken.Text + operatorToken2.Text,
                            operatorToken.ValueText + operatorToken2.ValueText,
                            operatorToken2.GetTrailingTrivia());
                    }
                }
                else if (CurrentToken.Kind == SyntaxKind.EqualsToken)
                {
                    var operatorToken2 = this.EatToken();
                    operatorToken = SyntaxFactory.Token(
                        operatorToken.GetLeadingTrivia(),
                        SyntaxKind.GreaterThanEqualsToken,
                        operatorToken.Text + operatorToken2.Text,
                        operatorToken.ValueText + operatorToken2.ValueText,
                        operatorToken2.GetTrailingTrivia());
                }
                else if (CurrentToken.Kind == SyntaxKind.GreaterThanEqualsToken)
                {
                    var operatorToken2 = this.EatToken();
                    operatorToken = SyntaxFactory.Token(
                        operatorToken.GetLeadingTrivia(),
                        SyntaxKind.GreaterThanGreaterThanEqualsToken,
                        operatorToken.Text + operatorToken2.Text,
                        operatorToken.ValueText + operatorToken2.ValueText,
                        operatorToken2.GetTrailingTrivia());
                    operatorToken = CheckFeatureAvailability(operatorToken, MessageID.IDS_FeatureUserDefinedCompoundAssignmentOperators, forceWarning: true);
                }
            }

            switch (operatorToken.Kind)
            {
                case SyntaxKind.PlusToken:
                    operatorToken = tryParseCompoundAssignmentOperatorToken(operatorToken, SyntaxKind.PlusEqualsToken);
                    break;
                case SyntaxKind.MinusToken:
                    operatorToken = tryParseCompoundAssignmentOperatorToken(operatorToken, SyntaxKind.MinusEqualsToken);
                    break;
                case SyntaxKind.AsteriskToken:
                    operatorToken = tryParseCompoundAssignmentOperatorToken(operatorToken, SyntaxKind.AsteriskEqualsToken);
                    break;
                case SyntaxKind.SlashToken:
                    operatorToken = tryParseCompoundAssignmentOperatorToken(operatorToken, SyntaxKind.SlashEqualsToken);
                    break;
                case SyntaxKind.PercentToken:
                    operatorToken = tryParseCompoundAssignmentOperatorToken(operatorToken, SyntaxKind.PercentEqualsToken);
                    break;
                case SyntaxKind.AmpersandToken:
                    operatorToken = tryParseCompoundAssignmentOperatorToken(operatorToken, SyntaxKind.AmpersandEqualsToken);
                    break;
                case SyntaxKind.BarToken:
                    operatorToken = tryParseCompoundAssignmentOperatorToken(operatorToken, SyntaxKind.BarEqualsToken);
                    break;
                case SyntaxKind.CaretToken:
                    operatorToken = tryParseCompoundAssignmentOperatorToken(operatorToken, SyntaxKind.CaretEqualsToken);
                    break;
                case SyntaxKind.LessThanLessThanToken:
                    operatorToken = tryParseCompoundAssignmentOperatorToken(operatorToken, SyntaxKind.LessThanLessThanEqualsToken);
                    break;
                case SyntaxKind.GreaterThanGreaterThanToken:
                    operatorToken = tryParseCompoundAssignmentOperatorToken(operatorToken, SyntaxKind.GreaterThanGreaterThanEqualsToken);
                    break;
                case SyntaxKind.GreaterThanGreaterThanGreaterThanToken:
                    operatorToken = tryParseCompoundAssignmentOperatorToken(operatorToken, SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken);
                    break;
            }

            Debug.Assert(SyntaxFacts.IsAnyOverloadableOperator(operatorToken.Kind));

            CrefParameterListSyntax parameters = ParseCrefParameterList();

            return SyntaxFactory.OperatorMemberCref(operatorKeyword, checkedKeyword, operatorToken, parameters);

            SyntaxToken tryParseCompoundAssignmentOperatorToken(SyntaxToken operatorToken, SyntaxKind kind)
            {
                if (LanguageParser.NoTriviaBetween(operatorToken, CurrentToken) && CurrentToken.Kind == SyntaxKind.EqualsToken)
                {
                    Debug.Assert(kind is not (SyntaxKind.GreaterThanGreaterThanEqualsToken or SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken)); // Reaching this code path is not expected
                    var operatorToken2 = this.EatToken();
                    operatorToken = SyntaxFactory.Token(
                        operatorToken.GetLeadingTrivia(),
                        kind,
                        operatorToken.Text + operatorToken2.Text,
                        operatorToken.ValueText + operatorToken2.ValueText,
                        operatorToken2.GetTrailingTrivia());
                    operatorToken = CheckFeatureAvailability(operatorToken, MessageID.IDS_FeatureUserDefinedCompoundAssignmentOperators, forceWarning: true);
                }

                return operatorToken;
            }
        }

        private SyntaxToken TryEatCheckedKeyword(bool isConversion, ref SyntaxToken operatorKeyword)
        {
            SyntaxToken checkedKeyword = tryEatCheckedOrHandleUnchecked(ref operatorKeyword);

            if (checkedKeyword is not null &&
                (isConversion || SyntaxFacts.IsAnyOverloadableOperator(CurrentToken.Kind)))
            {
                checkedKeyword = CheckFeatureAvailability(checkedKeyword, MessageID.IDS_FeatureCheckedUserDefinedOperators, forceWarning: true);
            }

            return checkedKeyword;

            SyntaxToken tryEatCheckedOrHandleUnchecked(ref SyntaxToken operatorKeyword)
            {
                if (CurrentToken.Kind == SyntaxKind.UncheckedKeyword)
                {
                    // if we encounter `operator unchecked`, we place the `unchecked` as skipped trivia on `operator`
                    var misplacedToken = AddErrorAsWarning(EatToken(), ErrorCode.ERR_MisplacedUnchecked);
                    operatorKeyword = AddTrailingSkippedSyntax(operatorKeyword, misplacedToken);
                    return null;
                }

                return TryEatToken(SyntaxKind.CheckedKeyword);
            }
        }

        /// <summary>
        /// Parse a user-defined conversion, with optional parameters.
        /// </summary>
        private ConversionOperatorMemberCrefSyntax ParseConversionOperatorMemberCref()
        {
            Debug.Assert(CurrentToken.Kind == SyntaxKind.ExplicitKeyword ||
                CurrentToken.Kind == SyntaxKind.ImplicitKeyword);
            SyntaxToken implicitOrExplicit = EatToken();

            SyntaxToken operatorKeyword = EatToken(SyntaxKind.OperatorKeyword);
            SyntaxToken checkedKeyword = TryEatCheckedKeyword(isConversion: true, ref operatorKeyword);

            TypeSyntax type = ParseCrefType(typeArgumentsMustBeIdentifiers: false);

            CrefParameterListSyntax parameters = ParseCrefParameterList();

            return SyntaxFactory.ConversionOperatorMemberCref(implicitOrExplicit, operatorKeyword, checkedKeyword, type, parameters);
        }

        /// <summary>
        /// Parse a parenthesized parameter list.
        /// </summary>
        private CrefParameterListSyntax ParseCrefParameterList()
        {
            return (CrefParameterListSyntax)ParseBaseCrefParameterList(useSquareBrackets: false);
        }

        /// <summary>
        /// Parse a bracketed parameter list.
        /// </summary>
        private CrefBracketedParameterListSyntax ParseBracketedCrefParameterList()
        {
            return (CrefBracketedParameterListSyntax)ParseBaseCrefParameterList(useSquareBrackets: true);
        }

        /// <summary>
        /// Parse the parameter list (if any) of a cref member (name, indexer, operator, or conversion).
        /// </summary>
        private BaseCrefParameterListSyntax ParseBaseCrefParameterList(bool useSquareBrackets)
        {
            SyntaxKind openKind = useSquareBrackets ? SyntaxKind.OpenBracketToken : SyntaxKind.OpenParenToken;
            SyntaxKind closeKind = useSquareBrackets ? SyntaxKind.CloseBracketToken : SyntaxKind.CloseParenToken;

            if (CurrentToken.Kind != openKind)
            {
                return null;
            }

            SyntaxToken open = EatToken(openKind);

            var list = _pool.AllocateSeparated<CrefParameterSyntax>();
            try
            {
                while (CurrentToken.Kind == SyntaxKind.CommaToken || IsPossibleCrefParameter())
                {
                    list.Add(ParseCrefParameter());

                    if (CurrentToken.Kind != closeKind)
                    {
                        SyntaxToken comma = EatToken(SyntaxKind.CommaToken);
                        if (!comma.IsMissing || IsPossibleCrefParameter())
                        {
                            // Only do this if it won't be last in the list.
                            list.AddSeparator(comma);
                        }
                        else
                        {
                            // How could this scenario arise?  If it does, just expand the if-condition.
                            Debug.Assert(CurrentToken.Kind != SyntaxKind.CommaToken);
                        }
                    }
                }

                // NOTE: nothing follows a cref parameter list, so there's no reason to recover here.
                // Just let the cref-level recovery code handle any remaining tokens.

                SyntaxToken close = EatToken(closeKind);

                return useSquareBrackets
                    ? (BaseCrefParameterListSyntax)SyntaxFactory.CrefBracketedParameterList(open, list, close)
                    : SyntaxFactory.CrefParameterList(open, list, close);
            }
            finally
            {
                _pool.Free(list);
            }
        }

        /// <summary>
        /// True if the current token could be the beginning of a cref parameter.
        /// </summary>
        private bool IsPossibleCrefParameter()
        {
            SyntaxKind kind = this.CurrentToken.Kind;
            switch (kind)
            {
                case SyntaxKind.RefKeyword:
                case SyntaxKind.OutKeyword:
                case SyntaxKind.InKeyword:
                case SyntaxKind.IdentifierToken:
                    return true;
                default:
                    return SyntaxFacts.IsPredefinedType(kind);
            }
        }

        /// <summary>
        /// Parse an element of a cref parameter list.
        /// </summary>
        /// <remarks>
        /// "ref", "ref readonly", "in", "out" work, but "params", "this", and "__arglist" don't.
        /// </remarks>
        private CrefParameterSyntax ParseCrefParameter()
        {
            SyntaxToken refKindOpt = null;
            switch (CurrentToken.Kind)
            {
                case SyntaxKind.RefKeyword:
                case SyntaxKind.OutKeyword:
                case SyntaxKind.InKeyword:
                    refKindOpt = EatToken();
                    break;
            }

            SyntaxToken readOnlyOpt = null;
            if (CurrentToken.Kind == SyntaxKind.ReadOnlyKeyword && refKindOpt is not null)
            {
                if (refKindOpt.Kind != SyntaxKind.RefKeyword)
                {
                    // if we encounter `readonly` after `in` or `out`, we place the `readonly` as skipped trivia on the previous keyword
                    var misplacedToken = AddErrorAsWarning(EatToken(), ErrorCode.ERR_RefReadOnlyWrongOrdering);
                    refKindOpt = AddTrailingSkippedSyntax(refKindOpt, misplacedToken);
                }
                else
                {
                    readOnlyOpt = EatToken();
                }
            }

            TypeSyntax type = ParseCrefType(typeArgumentsMustBeIdentifiers: false);
            return SyntaxFactory.CrefParameter(refKindKeyword: refKindOpt, readOnlyKeyword: readOnlyOpt, type);
        }

        /// <summary>
        /// Parse an identifier, optionally followed by an angle-bracketed list of type parameters.
        /// </summary>
        /// <param name="typeArgumentsMustBeIdentifiers">True to give an error when a non-identifier
        /// type argument is seen, false to accept.  No change in the shape of the tree.</param>
        private SimpleNameSyntax ParseCrefName(bool typeArgumentsMustBeIdentifiers)
        {
            SyntaxToken identifierToken = EatToken(SyntaxKind.IdentifierToken);

            if (CurrentToken.Kind != SyntaxKind.LessThanToken)
            {
                return SyntaxFactory.IdentifierName(identifierToken);
            }

            return SyntaxFactory.GenericName(identifierToken, ParseTypeArguments(typeArgumentsMustBeIdentifiers));
        }

        private TypeArgumentListSyntax ParseTypeArguments(bool typeArgumentsMustBeIdentifiers)
        {
            Debug.Assert(CurrentToken.Kind == SyntaxKind.LessThanToken);
            var open = EatToken();

            var list = _pool.AllocateSeparated<TypeSyntax>();
            try
            {
                while (true)
                {
                    TypeSyntax typeSyntax = ParseCrefType(typeArgumentsMustBeIdentifiers);

                    if (typeArgumentsMustBeIdentifiers && typeSyntax.Kind != SyntaxKind.IdentifierName)
                    {
                        typeSyntax = this.AddError(typeSyntax, ErrorCode.WRN_ErrorOverride,
                            new SyntaxDiagnosticInfo(ErrorCode.ERR_TypeParamMustBeIdentifier), $"{(int)ErrorCode.ERR_TypeParamMustBeIdentifier:d4}");
                    }

                    list.Add(typeSyntax);

                    var currentKind = CurrentToken.Kind;
                    if (currentKind == SyntaxKind.CommaToken || currentKind == SyntaxKind.IdentifierToken ||
                        SyntaxFacts.IsPredefinedType(CurrentToken.Kind))
                    {
                        // NOTE: if the current token is an identifier or predefined type, then we're
                        // actually inserting a missing commas.
                        list.AddSeparator(EatToken(SyntaxKind.CommaToken));
                    }
                    else
                    {
                        break;
                    }
                }

                SyntaxToken close = EatToken(SyntaxKind.GreaterThanToken);

                open = CheckFeatureAvailability(open, MessageID.IDS_FeatureGenerics, forceWarning: true);

                return SyntaxFactory.TypeArgumentList(open, list, close);
            }
            finally
            {
                _pool.Free(list);
            }
        }

        /// <summary>
        /// Parse a type.  May include an alias, a predefined type, and/or a qualified name.
        /// </summary>
        /// <remarks>
        /// Pointer, nullable, or array types are only allowed if <paramref name="typeArgumentsMustBeIdentifiers"/> is false.
        /// Leaves a dot and a name unconsumed if the name is not followed by another dot
        /// and checkForMember is true.
        /// </remarks>
        /// <param name="typeArgumentsMustBeIdentifiers">True to give an error when a non-identifier
        /// type argument is seen, false to accept.  No change in the shape of the tree.</param>
        /// <param name="checkForMember">True means that the last name should not be consumed
        /// if it is followed by a parameter list.</param>
        private TypeSyntax ParseCrefType(bool typeArgumentsMustBeIdentifiers, bool checkForMember = false)
        {
            TypeSyntax typeWithoutSuffix = ParseCrefTypeHelper(typeArgumentsMustBeIdentifiers, checkForMember);
            return typeArgumentsMustBeIdentifiers
                ? typeWithoutSuffix
                : ParseCrefTypeSuffix(typeWithoutSuffix);
        }

        /// <summary>
        /// Parse a type.  May include an alias, a predefined type, and/or a qualified name.
        /// </summary>
        /// <remarks>
        /// No pointer, nullable, or array types.
        /// Leaves a dot and a name unconsumed if the name is not followed by another dot
        /// and checkForMember is true.
        /// </remarks>
        /// <param name="typeArgumentsMustBeIdentifiers">True to give an error when a non-identifier
        /// type argument is seen, false to accept.  No change in the shape of the tree.</param>
        /// <param name="checkForMember">True means that the last name should not be consumed
        /// if it is followed by a parameter list.</param>
        private TypeSyntax ParseCrefTypeHelper(bool typeArgumentsMustBeIdentifiers, bool checkForMember = false)
        {
            NameSyntax leftName;

            if (SyntaxFacts.IsPredefinedType(CurrentToken.Kind))
            {
                // e.g. "int"
                // NOTE: a predefined type will not fit into a NameSyntax, so we'll return
                // immediately.  The upshot is that you can only dot into a predefined type
                // once (e.g. not "int.A.B"), which is fine because we know that none of them
                // have nested types.
                return SyntaxFactory.PredefinedType(EatToken());
            }
            else if (CurrentToken.Kind == SyntaxKind.IdentifierToken && PeekToken(1).Kind == SyntaxKind.ColonColonToken)
            {
                // e.g. "A::B"
                SyntaxToken alias = EatToken();
                if (alias.ContextualKind == SyntaxKind.GlobalKeyword)
                {
                    alias = ConvertToKeyword(alias);
                }

                alias = CheckFeatureAvailability(alias, MessageID.IDS_FeatureGlobalNamespace, forceWarning: true);

                SyntaxToken colonColon = EatToken();
                SimpleNameSyntax name = ParseCrefName(typeArgumentsMustBeIdentifiers);
                leftName = SyntaxFactory.AliasQualifiedName(SyntaxFactory.IdentifierName(alias), colonColon, name);
            }
            else
            {
                // e.g. "A"
                ResetPoint resetPoint = GetResetPoint();
                leftName = ParseCrefName(typeArgumentsMustBeIdentifiers);
                if (checkForMember && (leftName.IsMissing || CurrentToken.Kind != SyntaxKind.DotToken))
                {
                    // If this isn't the first part of a dotted name, then we prefer to represent it
                    // as a MemberCrefSyntax.
                    this.Reset(ref resetPoint);
                    this.Release(ref resetPoint);

                    return null;
                }
                this.Release(ref resetPoint);
            }

            while (CurrentToken.Kind == SyntaxKind.DotToken)
            {
                // NOTE: we make a lot of these, but we'll reset, at most, one time.
                ResetPoint resetPoint = GetResetPoint();

                SyntaxToken dot = EatToken();

                SimpleNameSyntax rightName = ParseCrefName(typeArgumentsMustBeIdentifiers);

                if (checkForMember && (rightName.IsMissing || CurrentToken.Kind != SyntaxKind.DotToken))
                {
                    this.Reset(ref resetPoint); // Go back to before the dot - it must have been the trailing dot.
                    this.Release(ref resetPoint);

                    return leftName;
                }

                this.Release(ref resetPoint);

                leftName = SyntaxFactory.QualifiedName(leftName, dot, rightName);
            }

            return leftName;
        }

        /// <summary>
        /// Once the name part of a type (including type parameter/argument lists) is parsed,
        /// we need to consume ?, *, and rank specifiers.
        /// </summary>
        private TypeSyntax ParseCrefTypeSuffix(TypeSyntax type)
        {
            if (CurrentToken.Kind == SyntaxKind.QuestionToken)
            {
                type = SyntaxFactory.NullableType(type, EatToken());
            }

            while (CurrentToken.Kind == SyntaxKind.AsteriskToken)
            {
                type = SyntaxFactory.PointerType(type, EatToken());
            }

            if (CurrentToken.Kind == SyntaxKind.OpenBracketToken)
            {
                var omittedArraySizeExpressionInstance = SyntaxFactory.OmittedArraySizeExpression(SyntaxFactory.Token(SyntaxKind.OmittedArraySizeExpressionToken));
                var rankList = _pool.Allocate<ArrayRankSpecifierSyntax>();
                try
                {
                    while (CurrentToken.Kind == SyntaxKind.OpenBracketToken)
                    {
                        SyntaxToken open = EatToken();
                        var dimensionList = _pool.AllocateSeparated<ExpressionSyntax>();
                        try
                        {
                            while (this.CurrentToken.Kind != SyntaxKind.CloseBracketToken)
                            {
                                if (this.CurrentToken.Kind == SyntaxKind.CommaToken)
                                {
                                    // NOTE: trivia will be attached to comma, not omitted array size
                                    dimensionList.Add(omittedArraySizeExpressionInstance);
                                    dimensionList.AddSeparator(this.EatToken());
                                }
                                else
                                {
                                    // CONSIDER: if we expect people to try to put expressions in between
                                    // the commas, then it might be more reasonable to recover by skipping
                                    // tokens until we hit a CloseBracketToken (or some other terminator).
                                    break;
                                }
                            }

                            // Don't end on a comma.
                            // If the omitted size would be the only element, then skip it unless sizes were expected.
                            if ((dimensionList.Count & 1) == 0)
                            {
                                dimensionList.Add(omittedArraySizeExpressionInstance);
                            }

                            // Eat the close brace and we're done.
                            var close = this.EatToken(SyntaxKind.CloseBracketToken);

                            rankList.Add(SyntaxFactory.ArrayRankSpecifier(open, dimensionList, close));
                        }
                        finally
                        {
                            _pool.Free(dimensionList);
                        }
                    }

                    type = SyntaxFactory.ArrayType(type, rankList);
                }
                finally
                {
                    _pool.Free(rankList);
                }
            }
            return type;
        }

        /// <summary>
        /// Ends at appropriate quotation mark, EOF, or EndOfDocumentationComment.
        /// </summary>
        private bool IsEndOfCrefAttribute
        {
            get
            {
                switch (CurrentToken.Kind)
                {
                    case SyntaxKind.SingleQuoteToken:
                        return (this.Mode & LexerMode.XmlCrefQuote) == LexerMode.XmlCrefQuote;
                    case SyntaxKind.DoubleQuoteToken:
                        return (this.Mode & LexerMode.XmlCrefDoubleQuote) == LexerMode.XmlCrefDoubleQuote;
                    case SyntaxKind.EndOfFileToken:
                    case SyntaxKind.EndOfDocumentationCommentToken:
                        return true;
                    case SyntaxKind.BadToken:
                        // If it's a real '<' (not &lt;, etc), then we assume it's the beginning
                        // of the next XML element.
                        return CurrentToken.Text == SyntaxFacts.GetText(SyntaxKind.LessThanToken) ||
                            IsNonAsciiQuotationMark(CurrentToken);
                    default:
                        return false;
                }
            }
        }

        /// <summary>
        /// Convenience method for checking the mode.
        /// </summary>
        private bool InCref
        {
            get
            {
                switch (this.Mode & (LexerMode.XmlCrefDoubleQuote | LexerMode.XmlCrefQuote))
                {
                    case LexerMode.XmlCrefQuote:
                    case LexerMode.XmlCrefDoubleQuote:
                        return true;
                    default:
                        return false;
                }
            }
        }

        #endregion Cref

        #region Name attribute values

        private IdentifierNameSyntax ParseNameAttributeValue()
        {
            // Never report a parse error - just fail to bind the name later on.
            SyntaxToken identifierToken = this.EatToken(SyntaxKind.IdentifierToken, reportError: false);

            if (!IsEndOfNameAttribute)
            {
                var badTokens = _pool.Allocate<SyntaxToken>();
                while (!IsEndOfNameAttribute)
                {
                    badTokens.Add(this.EatToken());
                }
                identifierToken = AddTrailingSkippedSyntax(identifierToken, badTokens.ToListNode());
                _pool.Free(badTokens);
            }

            return SyntaxFactory.IdentifierName(identifierToken);
        }

        /// <summary>
        /// Ends at appropriate quotation mark, EOF, or EndOfDocumentationComment.
        /// </summary>
        private bool IsEndOfNameAttribute
        {
            get
            {
                switch (CurrentToken.Kind)
                {
                    case SyntaxKind.SingleQuoteToken:
                        return (this.Mode & LexerMode.XmlNameQuote) == LexerMode.XmlNameQuote;
                    case SyntaxKind.DoubleQuoteToken:
                        return (this.Mode & LexerMode.XmlNameDoubleQuote) == LexerMode.XmlNameDoubleQuote;
                    case SyntaxKind.EndOfFileToken:
                    case SyntaxKind.EndOfDocumentationCommentToken:
                        return true;
                    case SyntaxKind.BadToken:
                        // If it's a real '<' (not &lt;, etc), then we assume it's the beginning
                        // of the next XML element.
                        return CurrentToken.Text == SyntaxFacts.GetText(SyntaxKind.LessThanToken) ||
                            IsNonAsciiQuotationMark(CurrentToken);
                    default:
                        return false;
                }
            }
        }

        #endregion Name attribute values
    }
}
