Click here to Skip to main content
12,251,027 members (38,578 online)
Click here to Skip to main content

Tagged as

Stats

7.3K views
374 downloads
13 bookmarked
Posted

Resolving Symbolic References in a CodeDOM (Part 7)

, 2 Dec 2012 CDDL
Resolving symbolic references in a CodeDOM.
Mono.Cecil.dll
Nova.CLI.exe
Nova.CodeDOM.dll
Nova.Examples.exe
Nova.Studio.exe
Nova.Test.exe
Nova.UI.dll
Nova.CLI
Properties
Nova.CodeDOM
CodeDOM
Annotations
Base
Comments
Base
DocComments
CodeRef
Base
List
Name
Base
Other
Simple
CompilerDirectives
Base
Conditionals
Base
Messages
Base
Pragmas
Base
Symbols
Base
Base
Interfaces
Expressions
AnonymousMethods
Base
Operators
Base
Binary
Arithmetic
Base
Assignment
Base
Bitwise
Base
Conditional
Relational
Base
Shift
Base
Other
Base
Unary
Base
Other
References
Base
GotoTargets
Base
Methods
Namespaces
Other
Properties
Types
Base
Variables
Base
Projects
Assemblies
Namespaces
References
Base
Statements
Base
Conditionals
Base
Exceptions
Generics
Constraints
Base
Iterators
Base
Jumps
Loops
Methods
OperatorDecls
Miscellaneous
Namespaces
Properties
Base
Events
Types
Base
Variables
Base
Parsing
Base
Properties
Rendering
Resolving
Utilities
Mono.Cecil
Reflection
Nova.Examples
Properties
Nova.Studio
Images
About.png
Configuration.png
EditCopy.png
EditCut.png
EditDelete.png
EditPaste.png
EditRedo.png
EditUndo.png
Error.png
Exit.png
FileNew.png
FileOpen.png
FileSave.png
FileSaveAll.png
FileSaveAs.png
Find.png
Help.png
Info.png
Logo.png
Options.png
Print.png
PrintPreview.png
Properties.png
Todo.png
Warning.png
Objects.ico
Properties
Nova.Test
Properties
Nova.UI
CodeDOM
Annotations
Base
Comments
Base
DocComments
CodeRef
Base
List
Name
Base
Other
Simple
CompilerDirectives
Base
Conditionals
Base
Messages
Base
Pragmas
Base
Symbols
Base
Base
Expressions
AnonymousMethods
Base
Operators
Base
Binary
Arithmetic
Base
Assignment
Base
Bitwise
Base
Conditional
Relational
Base
Shift
Base
Other
Base
Unary
Base
Other
References
Base
GotoTargets
Base
Methods
Namespaces
Other
Properties
Types
Base
Variables
Base
Projects
Namespaces
References
Base
Statements
Base
Conditionals
Base
Exceptions
Generics
Constraints
Base
Iterators
Base
Jumps
Loops
Methods
OperatorDecls
Miscellaneous
Namespaces
Properties
Base
Events
Types
Base
Variables
Base
Properties
Resolving
Utilties
// 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;
using System.Linq;

using Nova.Parsing;
using Nova.Rendering;
using Nova.Utilities;

namespace Nova.CodeDOM
{
    /// <summary>
    /// Represents user comments, and may either be independent or associated with another <see cref="CodeObject"/>.
    /// </summary>
    /// <remarks>
    /// A comment can be flagged as EOL, Block, TODO, HACK, NOTE, or combinations of these.
    /// By default, comments use the '//' style, unless flagged as Block, in which case they
    /// use the '/*...*/' style.
    /// Regular comments appear before the object they belong to, and are usually on separate
    /// lines, although they can sometimes be on the same line, in which case they are forcibly
    /// rendered in the Block style whether flagged as such or not.
    /// Comments flagged as EOL appear after the object on the same line, and are usually the
    /// last thing on the line, although they can sometimes be followed by other objects, in
    /// which case they are forcibly rendered in the Block style whether flagged as such or not.
    /// Comments can also have a "Post" format, which means they appear after the object.  EOL
    /// comments are usually Post, but can be non-post in some cases, such as when they appear
    /// after the opening symbol ('{') of a Block.
    /// A code object can have a regular comment, an EOL comment, or both.  Technically,
    /// multiple comments of each type are supported, but having more than one of the same type,
    /// or a regular comment in addition to a documentation comment, is very rare and not
    /// recommended - it gets messy in the UI, and helper properties are provided for each
    /// comment type which only return the first comment of that type found.
    /// A regular comment can have multiple lines of text whether Block type or not, but EOL
    /// comments shouldn't - newlines will be converted to ';' on display if present.
    /// </remarks>
    public class Comment : CommentBase
    {
        #region /* STATIC FIELDS */

        /// <summary>
        /// Determines if special comments are listed.
        /// </summary>
        public static bool ListSpecialComments = true;

        #endregion

        #region /* FIELDS */

        protected string _text;
        protected CommentFlags _commentFlags;

        #endregion

        #region /* CONSTRUCTORS */

        /// <summary>
        /// Create a <see cref="Comment"/> with the specified text content.
        /// </summary>
        public Comment(string text, CommentFlags flags)
        {
            Text = text;
            _commentFlags = flags;

            // Default to same line for EOL comments, and separate line for regular comments
            SetNewLines(flags.HasFlag(CommentFlags.EOL) ? 0 : 1);
        }

        /// <summary>
        /// Create a <see cref="Comment"/> with the specified text content.
        /// </summary>
        public Comment(string text)
            : this(text, CommentFlags.None)
        { }

        #endregion

        #region /* STATIC CONSTRUCTOR */

        static Comment()
        {
            // Force a reference to CodeObject to trigger the loading of any config file if it hasn't been done yet
            ForceReference();
        }

        #endregion

        #region /* PROPERTIES */

        /// <summary>
        /// The text content of the comment.
        /// </summary>
        public override string Text
        {
            get { return _text; }
            set
            {
                _text = value.Replace("\r\n", "\n");  // Normalize newlines
                SetSpecialFlags();
            }
        }

        /// <summary>
        /// True if the comment appears at the end-of-line (EOL).
        /// </summary>
        public override bool IsEOL
        {
            get { return _commentFlags.HasFlag(CommentFlags.EOL); }
            set { SetCommentFlag(CommentFlags.EOL, value); }
        }

        /// <summary>
        /// Determines if the comment has a block style.
        /// </summary>
        public bool IsBlock
        {
            get { return _commentFlags.HasFlag(CommentFlags.Block); }
            set { SetCommentFlag(CommentFlags.Block, value); }
        }

        /// <summary>
        /// Determines if the comment has a raw format.
        /// </summary>
        public bool IsRawFormat
        {
            get { return _commentFlags.HasFlag(CommentFlags.Raw); }
            set { SetCommentFlag(CommentFlags.Raw, value); }
        }

        /// <summary>
        /// Determines if the comment is a 'TODO' comment.
        /// </summary>
        public bool IsTODO
        {
            get { return _commentFlags.HasFlag(CommentFlags.TODO); }
            set { SetCommentFlag(CommentFlags.TODO, value); }
        }

        /// <summary>
        /// Determines if the comment is a 'HACK' comment.
        /// </summary>
        public bool IsHack
        {
            get { return _commentFlags.HasFlag(CommentFlags.HACK); }
            set { SetCommentFlag(CommentFlags.HACK, value); }
        }

        /// <summary>
        /// Determines if the comment is a 'NOTE' comment.
        /// </summary>
        public bool IsNote
        {
            get { return _commentFlags.HasFlag(CommentFlags.NOTE); }
            set { SetCommentFlag(CommentFlags.NOTE, value); }
        }

        /// <summary>
        /// True if the annotation should be listed at the <see cref="CodeUnit"/> and <see cref="Solution"/> levels (for display in an output window).
        /// </summary>
        public override bool IsListed
        {
            get { return (ListSpecialComments && (_commentFlags.HasFlag(CommentFlags.TODO) || _commentFlags.HasFlag(CommentFlags.HACK))); }
        }

        /// <summary>
        /// The comment flags.
        /// </summary>
        public CommentFlags CommentFlags
        {
            get { return _commentFlags; }
        }

        #endregion

        #region /* METHODS */

        protected internal void SetSpecialFlags()
        {
            _commentFlags |= CheckForSpecialComment(_text);
        }

        /// <summary>
        /// Check if text represents a "special" comment.
        /// </summary>
        /// <returns>CommentFlag for special comment type, or None.</returns>
        protected internal CommentFlags CheckForSpecialComment(string comment)
        {
            if (ContainsSpecial(comment, "TODO"))
                return CommentFlags.TODO;
            if (ContainsSpecial(comment, "HACK"))
                return CommentFlags.HACK;
            if (ContainsSpecial(comment, "NOTE"))
                return CommentFlags.NOTE;
            return CommentFlags.None;
        }

        protected bool ContainsSpecial(string text, string special)
        {
            // Match special comment indicators by finding a case-insensitive match
            int index = text.IndexOf(special, StringComparison.CurrentCultureIgnoreCase);
            if (index >= 0)
            {
                if (index >= 1)
                {
                    // Ignore if there is an immediately preceeding alpha char, or a '/' (nested in another comment) or '"' ignoring spaces
                    char preceeding = text[index - 1];
                    if (char.IsLetter(preceeding))
                        return false;
                    int preIndex = index - 1;
                    while (preceeding == ' ' && preIndex > 0)
                        preceeding = text[--preIndex];
                    if (preceeding == '/' || preceeding == '"')
                        return false;
                }
                // Ignore if there is a trailing alpha char
                int end = index + special.Length;
                if (end < text.Length && char.IsLetter(text, end))
                    return false;
                return true;
            }
            return false;
        }

        protected internal void SetCommentFlag(CommentFlags flag, bool value)
        {
            if (value)
                _commentFlags |= flag;
            else
                _commentFlags &= ~flag;
        }

        #endregion

        #region /* PARSING */

        /// <summary>
        /// The token used to parse the code object.
        /// </summary>
        public const string ParseToken = "//";

        /// <summary>
        /// The block-comment start token.
        /// </summary>
        public const string ParseTokenBlockStart = "/*";

        /// <summary>
        /// The block-comment end token.
        /// </summary>
        public const string ParseTokenBlockEnd = "*/";

        // NOTE: No parse-points are installed for comments - instead, the parser calls the
        //       parsing constructor directly based upon the token type.  This is because we
        //       want to treat entire comments as individual tokens to preserve whitespace.

        /// <summary>
        /// Parse a <see cref="Comment"/>.
        /// </summary>
        public Comment(Parser parser, CodeObject parent)
            : base(parser, parent)
        {
            Token token = parser.Token;
            _prefixSpaceCount = (token.LeadingWhitespace.Length < byte.MaxValue ? (byte)token.LeadingWhitespace.Length : byte.MaxValue);
            string text = token.Text;
            int start;
            bool noSpaceAfterDelimiter;

            // Parse a standard '//' comment
            if (text.StartsWith(ParseToken))
            {
                // Keep track of whether or not all '//' are followed by a space
                start = ParseToken.Length;
                noSpaceAfterDelimiter = (start < text.Length && text[start] != ' ');
                _text = text.Substring(start).TrimEnd();

                if (!IsFirstOnLine)
                {
                    // The comment occurred after another token on the same line - it's an EOL comment
                    _commentFlags |= CommentFlags.EOL;
                    parser.NextToken(true);
                }
                else
                {
                    // The comment was on a line by itself - combine with following lines if they're also comments,
                    // and they're compatible - not Doc or block comments, same # of prefix spaces, no special prefix.
                    while (true)
                    {
                        int nextLine = token.LineNumber + 1;
                        token = parser.NextToken(true);
                        if ((token != null) && token.IsComment && token.Text.StartsWith(ParseToken)
                            && (token.LineNumber == nextLine) && (token.LeadingWhitespace.Length == _prefixSpaceCount)
                            && (string.IsNullOrEmpty(_text) || CheckForSpecialComment(token.Text.Substring(ParseToken.Length)) == CommentFlags.None))
                        {
                            text = parser.TokenText;
                            start = ParseToken.Length;
                            if (start < text.Length && text[start] != ' ')
                                noSpaceAfterDelimiter = true;
                            _text += "\n" + text.Substring(start).TrimEnd();
                        }
                        else
                            break;
                    }

                    // If the comment was left-justified, set the format flag as such so that it will be displayed
                    // at the left margin regardless of the current level of code indentation.
                    if (_prefixSpaceCount == 0)
                        _formatFlags |= FormatFlags.NoIndentation;
                }
            }
            else
            {
                // Parse a '/* ... */' block comment
                IsBlock = true;
                noSpaceAfterDelimiter = false;
                bool formatted = true;

                // The parser will pass through everything between the delimiters, so we have to deal with
                // that here by breaking it up into lines, stripping leading spaces and asterisks, and then
                // putting them back together.
                string[] lines = text.Split('\n');
                for (int i = 0; i < lines.Length; ++i)
                {
                    string line = lines[i];
                    bool hasBlockEnd = (i == lines.Length - 1 && line.EndsWith(ParseTokenBlockEnd));
                    if (i == 0)
                        start = ParseTokenBlockStart.Length;
                    else
                    {
                        // Skip leading spaces on subsequent lines up to the indent of the first line
                        start = Math.Min(_prefixSpaceCount, StringUtil.CharCount(line, ' ', 0));

                        // Special format handling: Skip a leading "*"
                        if (start < line.Length && line[start] == '*' && !(hasBlockEnd && start == line.Length - 2))
                            ++start;
                        else if (start < line.Length - 1)
                        {
                            // Special format handling: Skip " *" or "\" if followed by '*' and on the last line (if not part of the ending "*/"),
                            // or "//" (and NOT "///"), or "  " (if followed by a non-space).
                            if ((line[start] == ' ' && line[start + 1] == '*') || (line[start] == '\\' && line[start + 1] == '*' && i == lines.Length - 1))
                            {
                                if (!(hasBlockEnd && start == line.Length - 3))
                                    start += 2;
                            }
                            else if ((line[start] == '/' && line[start + 1] == '/' && (start == line.Length - 2 || line[start + 2] != '/'))
                                || (line[start] == ' ' && line[start + 1] == ' ' && (start == line.Length - 2 || line[start + 2] != ' ')))
                            {
                                start += 2;
                                formatted = false;
                            }
                            else
                                formatted = false;
                        }
                    }
                    if (start < line.Length && line[start] != ' ')
                        noSpaceAfterDelimiter = true;
                    int length = line.Length - start;
                    if (hasBlockEnd)
                        length -= ParseTokenBlockEnd.Length;
                    _text += (i == 0 ? "" : "\n") + line.Substring(start, length).TrimEnd();
                }

                if (!formatted)
                    _commentFlags |= CommentFlags.Raw;

                Token nextToken = parser.NextToken(true);

                // If the comment occurred after another token on the same line AND it's a single line block
                // comment AND it's the last token on the line - it's an inline EOL comment.
                if (!IsFirstOnLine && lines.Length == 1 && nextToken.IsFirstOnLine)
                    _commentFlags |= CommentFlags.EOL;
            }

            // If any line is missing a space after the delimiter, then set the special flag indicating that no
            // spaces should be displayed.  Otherwise, remove one column of spaces and add it back when displayed.
            if (noSpaceAfterDelimiter)
                NoSpaceAfterDelimiter = true;
            else
                RemoveSpaces(1);

            SetSpecialFlags();
        }

        /// <summary>
        /// Remove the specified number of prefixed spaces from each line of the comment.
        /// Fails if any non-blank lines don't start with at least the specified number of spaces.
        /// </summary>
        public bool RemoveSpaces(int spaces)
        {
            string[] lines = _text.Split('\n');
            if (Enumerable.Any(lines, delegate(string line) { return StringUtil.CharCount(line, ' ', 0) < spaces && line.Length > 0; }))
                return false;
            _text = null;
            foreach (string line in lines)
                _text += (_text == null ? "" : "\n") + (string.IsNullOrEmpty(line) ? "" : line.Remove(0, spaces));
            return true;
        }

        #endregion

        #region /* FORMATTING */

        /// <summary>
        /// True if there is no space between the comment delimiter and the comment text.
        /// </summary>
        public bool NoSpaceAfterDelimiter
        {
            get { return _annotationFlags.HasFlag(AnnotationFlags.NoSpace); }
            set { SetAnnotationFlag(AnnotationFlags.NoSpace, value); }
        }

        /// <summary>
        /// Determines if the code object only requires a single line for display.
        /// </summary>
        public override bool IsSingleLine
        {
            get { return (base.IsSingleLine && (IsEOL || (_text == null || _text.IndexOf('\n') < 0))); }
            set
            {
                base.IsSingleLine = value;
                if (value && _text != null)
                    _text = _text.Trim().Replace("\n", "; ");
            }
        }

        #endregion

        #region /* RENDERING */

        public override void AsText(CodeWriter writer, RenderFlags flags)
        {
            bool isEOL = IsEOL;
            bool isInline = flags.HasFlag(RenderFlags.CommentsInline);
            bool isBlock = (IsBlock || isInline);
            bool isRawFormat = IsRawFormat;

            int newLines = NewLines;
            bool isPrefix = flags.HasFlag(RenderFlags.IsPrefix);
            if (!isPrefix)
            {
                if (newLines > 0)
                {
                    if (!flags.HasFlag(RenderFlags.SuppressNewLine))
                        writer.WriteLines(newLines);
                }
                else if (!IsInfix || isEOL)
                {
                    // Handle a special case of an EOL comment on an 'else' that is actually commented-out
                    // code for an 'if' (common practice to document an implied 'if' condition).
                    bool elseIf = (isEOL && Parent is Else && _text.StartsWith("if ("));
                    writer.Write(isBlock || elseIf ? " " : "  ");
                }
            }

            if (_text == null)
                UpdateLineCol(writer, flags);
            else
            {
                string space = (NoSpaceAfterDelimiter ? "" : " ");
                writer.EscapeUnicode = false;
                if (isEOL || isInline)
                {
                    // Render a single-line EOL or in-line block comment.
                    // There shouldn't be any newlines in the comment, but handle them just in case.
                    UpdateLineCol(writer, flags);
                    string comment = (isBlock ? ParseTokenBlockStart : ParseToken) + space
                        + _text.Replace("\n", "; ") + (isBlock ? ((_text.EndsWith("*") ? "" : space) + ParseTokenBlockEnd) : "");
                    writer.Write(comment);
                }
                else
                {
                    // Render a non-EOL (one or more lines) comment or block comment
                    if (HasNoIndentation)
                        writer.BeginOutdentOnNewLine(this, 0);
                    UpdateLineCol(writer, flags);
                    string[] lines = _text.Split('\n');
                    bool lastIsEmpty = true;
                    for (int i = 0; i < lines.Length; ++i)
                    {
                        string line = lines[i];
                        bool isEmptyLine = string.IsNullOrEmpty(line);
                        string prefixSpace = (isEmptyLine ? "" : space);
                        if (i > 0)
                            writer.WriteLine();

                        // Write comment prefix as appropriate
                        if (isBlock)
                        {
                            lastIsEmpty = ((i == lines.Length - 1) && isEmptyLine);
                            writer.Write(i == 0 ? ParseTokenBlockStart + prefixSpace : (isRawFormat ? "" : (lastIsEmpty ? " " : " *" + prefixSpace)));
                            if (lastIsEmpty)
                                break;
                        }
                        else
                            writer.Write(ParseToken + prefixSpace);

                        writer.Write(line);
                    }
                    if (isBlock)
                    {
                        space = ((isRawFormat || lastIsEmpty || _text.EndsWith("*")) ? "" : space);
                        writer.Write(space + ParseTokenBlockEnd);
                    }
                    if (HasNoIndentation)
                        writer.EndIndentation(this);
                }
                writer.EscapeUnicode = true;
            }

            if (isPrefix)
            {
                // If this object is rendered as a child prefix object of another, then any whitespace is
                // rendered here *after* the object instead of before it.
                if (newLines > 0)
                    writer.WriteLines(newLines);
                else
                    writer.Write(" ");
            }
        }

        /// <summary>
        /// Get any <see cref="CommentFlags"/> and <see cref="AnnotationFlags"/> as a comma separated string.
        /// </summary>
        public string FlagsAsText()
        {
            string result = "";
            if (_commentFlags != CommentFlags.None)
                result += _commentFlags;
            if (_annotationFlags != AnnotationFlags.None || string.IsNullOrEmpty(result))
                result = StringUtil.Append(result, ", ", _annotationFlags.ToString());
            return result;
        }

        #endregion
    }

    #region /* COMMENT TYPE FLAGS */

    /// <summary>
    /// Comment type flags.
    /// </summary>
    [Flags]
    public enum CommentFlags
    {
        /// <summary>No flags.</summary>
        None  = 0x00,
        /// <summary>The comment is an "end-of-line" comment - the last item on the current line.</summary>
        EOL   = 0x01,
        /// <summary>The comment is "block" style.</summary>
        Block = 0x02,
        /// <summary>The comment isn't formatted - for block style, this means no preceeding asterisks on each line.</summary>
        Raw   = 0x04,
        /// <summary>The comment is a 'to-do' comment.</summary>
        TODO  = 0x10,
        /// <summary>The comment documents a 'hack' in the code.</summary>
        HACK  = 0x20,
        /// <summary>The comment represents a special note.</summary>
        NOTE  = 0x40
    }

    #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)

Share

About the Author

KenBeckett
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).

You may also be interested in...

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.160426.1 | Last Updated 2 Dec 2012
Article Copyright 2012 by KenBeckett
Everything else Copyright © CodeProject, 1999-2016
Layout: fixed | fluid