Click here to Skip to main content
15,886,518 members
Articles / Programming Languages / C#

Resolving Symbolic References in a CodeDOM (Part 7)

Rate me:
Please Sign up or sign in to vote.
4.75/5 (6 votes)
2 Dec 2012CDDL12 min read 19.4K   509   14  
Resolving symbolic references in a CodeDOM.
// The Nova Project by Ken Beckett.
// Copyright (C) 2007-2012 Inevitable Software, all rights reserved.
// Released under the Common Development and Distribution License, CDDL-1.0: http://opensource.org/licenses/cddl1.php

using System.Collections.Generic;

using Nova.Parsing;
using Nova.Rendering;
using Nova.Resolving;

namespace Nova.CodeDOM
{
    /// <summary>
    /// Represents a conditional if/then/else (ternary) expression.
    /// </summary>
    public class Conditional : Operator
    {
        #region /* FIELDS */

        protected Expression _if;
        protected Expression _then;
        protected Expression _else;

        #endregion

        #region /* CONSTRUCTORS */

        /// <summary>
        /// Create a <see cref="Conditional"/> operator.
        /// </summary>
        public Conditional(Expression @if, Expression then, Expression @else)
        {
            If = @if;
            Then = then;
            Else = @else;
        }

        #endregion

        #region /* PROPERTIES */

        /// <summary>
        /// The 'if' expression.
        /// </summary>
        public Expression If
        {
            get { return _if; }
            set { SetField(ref _if, value, true); }
        }

        /// <summary>
        /// The 'then' expression.
        /// </summary>
        public Expression Then
        {
            get { return _then; }
            set { SetField(ref _then, value, true); }

        }

        /// <summary>
        /// The 'else' expression.
        /// </summary>
        public Expression Else
        {
            get { return _else; }
            set { SetField(ref _else, value, true); }
        }

        /// <summary>
        /// True if the expression is const.
        /// </summary>
        public override bool IsConst
        {
            // A Conditional is const if both result clauses are const
            get { return (_then != null && _then.IsConst && _else != null && _else.IsConst); }
        }

        #endregion

        #region /* METHODS */

        /// <summary>
        /// Deep-clone the code object.
        /// </summary>
        public override CodeObject Clone()
        {
            Conditional clone = (Conditional)base.Clone();
            clone.CloneField(ref clone._if, _if);
            clone.CloneField(ref clone._then, _then);
            clone.CloneField(ref clone._else, _else);
            return clone;
        }

        #endregion

        #region /* PARSING */

        /// <summary>
        /// The token used to parse the 'then' part.
        /// </summary>
        public const string ParseToken1 = "?";

        /// <summary>
        /// The token used to parse the 'else' part.
        /// </summary>
        public const string ParseToken2 = ":";

        /// <summary>
        /// The precedence of the operator.
        /// </summary>
        public const int Precedence = 400;

        /// <summary>
        /// True if the operator is left-associative, or false if it's right-associative.
        /// </summary>
        public const bool LeftAssociative = false;

        internal static new void AddParsePoints()
        {
            // Use a parse-priority of 0 (TypeRef uses 100 for nullable types)
            Parser.AddOperatorParsePoint(ParseToken1, Precedence, LeftAssociative, false, Parse);
        }

        /// <summary>
        /// Parse a <see cref="Conditional"/> operator.
        /// </summary>
        public static Conditional Parse(Parser parser, CodeObject parent, ParseFlags flags)
        {
            // Disallow conditionals at the TypeDecl or NamespaceDecl levels - this prevents the possibility of matching when
            // parsing a return type expression that is a nullable type for a generic method that has constraints.
            if (parent is TypeDecl || parent is NamespaceDecl)
                return null;

            // Verify that we have a matching ':' for the '?', otherwise abort (so TypeRef can try parsing it as a nullable type).
            // If we're nested without parens, we must find one extra ':' for each nested level.
            if (PeekConditional(parser, parent, parser.ConditionalNestingLevel + 1, flags))
                return new Conditional(parser, parent);

            return null;
        }

        protected static bool PeekConditional(Parser parser, CodeObject parent, int colonCount, ParseFlags flags)
        {
            // Unfortunately, determining if the '?' is definitely part of a '?:' pair as opposed to a nullable type declaration
            // isn't easy - in fact, it's the single hardest thing to parse in the entire language.  Nicely formatted code always
            // has a space before it in the first case, and not in the second, but code is often poorly formatted.  The only way
            // to be sure how to parse it is to peek ahead looking for the matching ':'.  We can parse in a very simplified manner
            // for efficiency, just keeping track of '()', '[]', '{}' pairs, aborting if we hit a ';' anywhere, or a ',' that's
            // not in a nested scope, or finally if we find the matching ':' (not in a nested scope).  If we're in the '?' clause
            // of a nested Conditional without parens, then we need to find an extra ':' for each nested level (colonCount).
            // One more thing - we have to handle '<>' with generic arguments in order to avoid aborting on a possible ','
            // inside them, but we also have to avoid any confusion with LessThan/GreatherThan operators.

            bool firstToken = true;
            Stack<string> stack = new Stack<string>(8);
            while (true)
            {
                Token next = parser.PeekNextToken();
                if (next == null)
                    break;
            check:
                if (next.IsSymbol)
                {
                    // Abort if any invalid symbols appear immediately after the '?'
                    if (firstToken)
                    {
                        firstToken = false;
                        if (">)]};,".Contains(next.Text))
                            break;
                    }

                    // If we have a '<', skip over any possible generic type parameters so that we don't abort on a ','
                    if (next.Text == TypeRefBase.ParseTokenArgumentStart)
                    {
                        TypeRefBase.PeekTypeArguments(parser, TypeRefBase.ParseTokenArgumentEnd, flags);
                        next = parser.LastPeekedToken;
                    }

                    // Keep track of nested parens, brackets (new arrays), braces (initializers or generics in doc comments)
                    string nextText = next.Text;
                    if (nextText == ParseTokenStartGroup || nextText == NewArray.ParseTokenStart || nextText == Initializer.ParseTokenStart)
                        stack.Push(nextText);
                    else if (nextText == ParseTokenEndGroup)
                    {
                        // If we hit an unexpected ')', abort
                        if (stack.Count == 0 || stack.Peek() != ParseTokenStartGroup)
                            break;
                        stack.Pop();
                    }
                    else if (nextText == NewArray.ParseTokenEnd)
                    {
                        // If we hit an unexpected ']', abort
                        if (stack.Count == 0 || stack.Peek() != NewArray.ParseTokenStart)
                            break;
                        stack.Pop();
                    }
                    else if (nextText == Initializer.ParseTokenEnd)
                    {
                        // If we hit an unexpected '}', abort
                        if (stack.Count == 0 || stack.Peek() != Initializer.ParseTokenStart)
                            break;
                        stack.Pop();
                    }
                    else if (nextText == ParseToken1)
                    {
                        // If we found a '?', recursively call this routine to process it (in order to
                        // differentiate between a nested nullable type or another Conditional).
                        if (!PeekConditional(parser, parent, 1, flags))
                        {
                            // If it wasn't a Conditional, get the last token and check it
                            next = parser.LastPeekedToken;
                            goto check;
                        }
                    }
                    else if (stack.Count == 0)
                    {
                        // We're not inside any nested parens/brackets/braces/angle brackets.  Check for certain symbols:

                        // Terminate on a ',' or ';' (we ignore if nested because of anonymous method bodies)
                        if (nextText == ParseTokenSeparator || nextText == Statement.ParseTokenTerminator)
                            break;

                        // Process a ':'
                        if (nextText == ParseToken2)
                        {
                            // Assume we have a valid Conditional if the expected number of colons has been found
                            if (--colonCount == 0)
                                return true;
                        }
                    }
                }
                else if (next.Text == NewOperator.ParseToken)
                {
                    // If we found a 'new', treat the following as a type (in order to avoid any trailing '?' of
                    // a nullable type from being treated as a nested conditional).
                    TypeRefBase.PeekType(parser, parser.PeekNextToken(), true, flags | ParseFlags.Type);
                    // Whether it worked or not, pick up with the last peeked token
                    next = parser.LastPeekedToken;
                    firstToken = false;
                    goto check;
                }
                firstToken = false;
            }
            return false;
        }

        protected Conditional(Parser parser, CodeObject parent)
            : base(parser, parent)
        {
            IsFirstOnLine = false;
            Expression conditional = parser.RemoveLastUnusedExpression();
            if (conditional != null)
            {
                MoveFormatting(conditional);
                SetField(ref _if, conditional, false);
                if (conditional.IsFirstOnLine)
                    IsFirstOnLine = true;
                // Move any comments before the '?' to the conditional expression
                conditional.MoveCommentsAsPost(parser.LastToken);
            }

            // If the '?' clause is indented less than the parent object, set the NoIndentation flag to prevent
            // it from being formatted relative to the parent object.
            if (parser.CurrentTokenIndentedLessThan(parser.ParentStartingToken))
                SetFormatFlag(FormatFlags.NoIndentation, true);

            Token ifToken = parser.Token;
            parser.NextToken();  // Move past '?'
            ++parser.ConditionalNestingLevel;
            SetField(ref _then, Parse(parser, this, false, ParseToken2), false);
            --parser.ConditionalNestingLevel;
            if (_then != null)
            {
                if (ifToken.IsFirstOnLine)
                    _then.IsFirstOnLine = true;
                _then.MoveCommentsToLeftMost(ifToken, false);
                // Move any comments before the ':' to the then expression
                _then.MoveCommentsAsPost(parser.LastToken);
            }

            Token elseToken = parser.Token;
            ParseExpectedToken(parser, ParseToken2);  // Move past ':'
            SetField(ref _else, Parse(parser, this), false);
            if (_else != null)
            {
                if (elseToken.IsFirstOnLine)
                    _else.IsFirstOnLine = true;
                _else.MoveCommentsToLeftMost(elseToken, false);
                // Move any comments at the end to the else expression
                _else.MoveCommentsAsPost(parser.LastToken);
            }

            // If the else clause isn't on a new line, set the NoIndentation flag
            if ((!elseToken.IsFirstOnLine && (_else == null || !_else.IsFirstOnLine)))
                SetFormatFlag(FormatFlags.NoIndentation, true);
        }

        /// <summary>
        /// Get the precedence of the operator.
        /// </summary>
        public override int GetPrecedence()
        {
            return Precedence;
        }

        /// <summary>
        /// Move any comments from the specified <see cref="Token"/> to the left-most sub-expression.
        /// </summary>
        public override void MoveCommentsToLeftMost(Token token, bool skipParens)
        {
            if ((HasParens && !skipParens) || _if == null)
                MoveAllComments(token);
            else
                _if.MoveCommentsToLeftMost(token, false);
        }

        #endregion

        #region /* RESOLVING */

        /// <summary>
        /// Resolve all child symbolic references, using the specified <see cref="ResolveCategory"/> and <see cref="ResolveFlags"/>.
        /// </summary>
        public override CodeObject Resolve(ResolveCategory resolveCategory, ResolveFlags flags)
        {
            if (_if != null)
                _if = (Expression)_if.Resolve(ResolveCategory.Expression, flags);
            if (_then != null)
                _then = (Expression)_then.Resolve(ResolveCategory.Expression, flags);
            if (_else != null)
                _else = (Expression)_else.Resolve(ResolveCategory.Expression, flags);
            return this;
        }

        /// <summary>
        /// Returns true if the code object is an <see cref="UnresolvedRef"/> or has any <see cref="UnresolvedRef"/> children.
        /// </summary>
        public override bool HasUnresolvedRef()
        {
            if (_if != null && _if.HasUnresolvedRef())
                return true;
            if (_then != null && _then.HasUnresolvedRef())
                return true;
            if (_else != null && _else.HasUnresolvedRef())
                return true;
            return base.HasUnresolvedRef();
        }

        /// <summary>
        /// Evaluate the type of the <see cref="Expression"/>.
        /// </summary>
        /// <returns>The resulting <see cref="TypeRef"/> or <see cref="UnresolvedRef"/>.</returns>
        public override TypeRefBase EvaluateType(bool withoutConstants)
        {
            // Determine the types of the two expressions (ignore 'withoutConstants' here - we must always use them so that
            // we can handle the 'null' constant properly).
            TypeRefBase thenRef = (_then != null ? _then.EvaluateType() : null);
            TypeRefBase elseRef = (_else != null ? _else.EvaluateType() : null);
            if (thenRef == null)
                return elseRef;
            if (elseRef == null)
                return thenRef;

            // Check if the types of both expressions are the same
            if (thenRef.IsSameRef(elseRef))
            {
                // Return a non-constant reference (a conditional can't evaluate to a single constant
                // value, since it has two possible values).
                if (!thenRef.IsConst)
                    return thenRef;
                if (!elseRef.IsConst)
                    return elseRef;
                return thenRef.GetTypeWithoutConstant();
            }

            // Check if the TypeRef of each expression is implicitly convertible to the TypeRef of
            // the other expression.
            // In this particular case, we do NOT want to treat the destination type as 'object' if
            // it's a 'null' literal - we want such a conversion to fail, although the opposite
            // direction ('null' to a reference type) is fine.
            bool thenToElse = (thenRef.IsImplicitlyConvertibleTo(elseRef) && !(_else is Literal && ((Literal)_else).IsNull));
            bool elseToThen = (elseRef.IsImplicitlyConvertibleTo(thenRef) && !(_then is Literal && ((Literal)_then).IsNull));

            // If the first is implicitly convertible to the second, but not vice-versa, use the second type
            if (thenToElse && !elseToThen)
                return (!elseRef.IsConst ? elseRef : elseRef.GetTypeWithoutConstant());

            // If the second is implicitly convertible to the first, but not vice-versa, use the first type
            if (elseToThen && !thenToElse)
                return (!thenRef.IsConst ? thenRef : thenRef.GetTypeWithoutConstant());

            // Although it seems reasonable to resolve to one type if the other is unresolved in order to reduce
            // cascading errors, this can cause a serious problem where member lookups are done somewhere later
            // in the code on the evaluated type of this expression, and are not re-resolved when the evaluated
            // type changes in a 2nd resolve pass due to the unresolved becoming resolved.  Someday, we'll need
            // to automatically re-resolve all member lookups on a type that changes due to code edits, and then
            // perhaps we could add this logic back in IF we do the same thing if its evaluated type changes.
            //// If one of the two types is an UnresolvedRef, use the other type
            //if (thenRef is UnresolvedRef && !(elseRef is UnresolvedRef))
            //    return (!elseRef.IsConst ? elseRef : elseRef.GetTypeWithoutConstant());
            //if (elseRef is UnresolvedRef && !(thenRef is UnresolvedRef))
            //    return (!thenRef.IsConst ? thenRef : thenRef.GetTypeWithoutConstant());

            // No common type could be determined - default to 'object'
            return TypeRef.ObjectRef;
        }

        #endregion

        #region /* FORMATTING */

        /// <summary>
        /// True if the expression should have parens by default.
        /// </summary>
        public override bool HasParensDefault
        {
            get { return true; }
        }

        /// <summary>
        /// Determines if the code object only requires a single line for display.
        /// </summary>
        public override bool IsSingleLine
        {
            get
            {
                return (base.IsSingleLine && (_if == null || _if.IsSingleLine)
                    && (_then == null || (!_then.IsFirstOnLine && _then.IsSingleLine))
                    && (_else == null || (!_else.IsFirstOnLine && _else.IsSingleLine)));
            }
            set
            {
                base.IsSingleLine = value;
                if (_if != null)
                {
                    _if.IsFirstOnLine = false;
                    _if.IsSingleLine = value;
                }
                if (_then != null)
                {
                    _then.IsFirstOnLine = !value;
                    _then.IsSingleLine = value;
                }
                if (_else != null)
                {
                    _else.IsFirstOnLine = !value;
                    _else.IsSingleLine = value;
                }
            }
        }

        #endregion

        #region /* RENDERING */

        public override void AsTextExpression(CodeWriter writer, RenderFlags flags)
        {
            bool relativeToParent = (!HasNoIndentation && _then != null && _then.IsFirstOnLine && _if != null && _if.IsSingleLine);
            if (relativeToParent)
            {
                writer.SetParentOffset();
                writer.BeginIndentOnNewLineRelativeToParentOffset(this, true);
            }

            if (_if != null)
                _if.AsText(writer, flags);

            RenderFlags thenFlags = flags;
            if (_then != null && _then.IsFirstOnLine)
            {
                writer.WriteLine();
                thenFlags |= RenderFlags.SuppressNewLine;
            }
            else
                writer.Write(" ");
            UpdateLineCol(writer, flags);
            writer.Write("? ");
            if (_then != null)
                _then.AsText(writer, thenFlags);

            if (_else != null && _else.IsFirstOnLine)
            {
                writer.WriteLine();
                writer.Write(": ");
                flags |= RenderFlags.SuppressNewLine;
            }
            else
                writer.Write(" : ");
            if (_else != null)
                _else.AsText(writer, flags);

            if (relativeToParent)
                writer.EndIndentation(this);
        }

        #endregion
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Common Development and Distribution License (CDDL)


Written By
Software Developer (Senior)
United States United States
I've been writing software since the late 70's, currently focusing mainly on C#.NET. I also like to travel around the world, and I own a Chocolate Factory (sadly, none of my employees are oompa loompas).

Comments and Discussions