Click here to Skip to main content
15,893,508 members
Articles / Programming Languages / C#

CodeDOM Classes for Solution and Project Files (Part 5)

Rate me:
Please Sign up or sign in to vote.
5.00/5 (12 votes)
30 Nov 2012CDDL7 min read 30K   1.2K   18  
CodeDOM objects for VS Solution and Project files.
// 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)


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