// 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.Collections;
using System.Collections.Generic;
using System.Linq;
using Nova.Parsing;
using Nova.Rendering;
using Nova.Resolving;
using Nova.Utilities;
namespace Nova.CodeDOM
{
/// <summary>
/// Represents the body of a <see cref="BlockStatement"/> or <see cref="AnonymousMethod"/> (containing
/// a sequence of 0 or more <see cref="CodeObject"/>s).
/// </summary>
/// <remarks>
/// Blocks are special in that their children's Parent references link directly to the parent of the Block, in order to simplify various logic.
/// </remarks>
public class Block : CodeObject, ICollection<CodeObject>, ICollection
{
#region /* FIELDS */
/// <summary>
/// Child <see cref="CodeObject"/>s - the Parent of this collection will be the Block's Parent, so
/// that the Parent of all child objects will be the Block's Parent, not the Block.
/// </summary>
protected ChildList<CodeObject> _codeObjects;
/// <summary>
/// Dictionary of named members in the block (LocalDecls, Labels, SwitchItems, or
/// various other members if the block's parent is a TypeDecl).
/// </summary>
protected NamedCodeObjectDictionary _namedMembers;
#endregion
#region /* CONSTRUCTORS */
/// <summary>
/// Create a <see cref="Block"/>, optionally with the specified code objects.
/// </summary>
public Block(params CodeObject[] codeObjects)
{
// The parent will be null at this point, but when it gets set later, all child
// objects will get their parent set correctly, and we'll also call ReformatBlock().
_codeObjects = new ChildList<CodeObject>();
foreach (CodeObject codeObject in codeObjects)
AddInternal(codeObject);
}
#endregion
#region /* PROPERTIES */
/// <summary>
/// The number of code objects in the <see cref="Block"/>.
/// </summary>
public int Count
{
get { return _codeObjects.Count; }
}
/// <summary>
/// Always <c>false</c>.
/// </summary>
public bool IsReadOnly
{
get { return false; }
}
/// <summary>
/// True if access to the <see cref="ICollection"/> is synchronized.
/// </summary>
public virtual bool IsSynchronized
{
get { return false; }
}
/// <summary>
/// Gets an object that can be used to synchronize access to the <see cref="ICollection"/>.
/// </summary>
public virtual object SyncRoot
{
get { return this; }
}
/// <summary>
/// Get the child <see cref="CodeObject"/> at the specified index.
/// </summary>
public CodeObject this[int index]
{
get { return _codeObjects[index]; }
}
/// <summary>
/// Get the last <see cref="CodeObject"/> in the <see cref="Block"/>.
/// </summary>
public CodeObject Last
{
get { return _codeObjects.Last; }
}
/// <summary>
/// The parent <see cref="CodeObject"/>.
/// </summary>
public override CodeObject Parent
{
set
{
// If the parent is being set for the first time, force a reformat (we can't
// do this in the constructor, because we don't have access to the parent yet).
if (value is IBlock && (_parent == null || _codeObjects.Count <= 2))
((IBlock)value).ReformatBlock();
base.Parent = value;
_codeObjects.Parent = value;
}
}
/// <summary>
/// The "Infix" End-Of-Line comment for the Initializer (if any) - appears after the open brace.
/// </summary>
/// <remarks>
/// This property allows for the very convenient setting of Infix EOL comments in object initializers.
/// Although there is support for multiple Infix EOL comments on the same object, this property doesn't
/// support that, returning the first one that it finds, and replacing all existing ones when set.
/// </remarks>
public string InfixEOLComment
{
get
{
// Just return the first Infix EOL comment if there is more than one
if (_annotations != null)
{
Comment comment = (Comment)Enumerable.FirstOrDefault(_annotations, delegate(Annotation annotation) { return annotation is Comment && annotation.IsEOL && annotation.IsInfix; });
if (comment != null)
return comment.Text;
}
return null;
}
set
{
// Remove all existing Infix EOL comments before adding the new one
RemoveAllAnnotationsWhere<Comment>(delegate(Comment annotation) { return annotation.IsEOL && annotation.IsInfix; });
if (value != null)
AttachAnnotation(new Comment(value, CommentFlags.EOL) { IsInfix = true });
}
}
#endregion
#region /* METHODS */
/// <summary>
/// Add a code object to the <see cref="Block"/>.
/// </summary>
/// <param name="codeObject">The object to be added.</param>
public void Add(CodeObject codeObject)
{
AddInternal(codeObject);
ObjectCountChanged();
}
/// <summary>
/// Add multiple code objects to the <see cref="Block"/>.
/// </summary>
/// <param name="codeObjects">The objects to be added.</param>
public void Add(params CodeObject[] codeObjects)
{
foreach (CodeObject codeObject in codeObjects)
Add(codeObject);
}
/// <summary>
/// Add a collection of code objects to the <see cref="Block"/>.
/// </summary>
/// <param name="collection">The collection to be added.</param>
public void AddRange(IEnumerable<CodeObject> collection)
{
foreach (CodeObject codeObject in collection)
Add(codeObject);
}
protected void AddInternal(CodeObject codeObject)
{
if (codeObject is Block)
{
foreach (CodeObject obj in ((Block)codeObject)._codeObjects)
AddInternal(obj);
}
else
{
AddInsertFormattingCheck(_codeObjects.Count, codeObject);
AddInternalNoFormatting(codeObject);
}
}
protected void AddInsertFormattingCheck(int index, CodeObject codeObject)
{
// Default the # of newlines for the object if it wasn't already explicitly specified
if (!codeObject.IsNewLinesSet)
{
int newLines;
// If we already have items in the block, determine newlines based upon the previous item
CodeObject previous = null;
for (int i = index - 1; i >= 0 && i < _codeObjects.Count; --i)
{
if (!_codeObjects[i].IsGenerated)
{
previous = _codeObjects[i];
break;
}
}
if (previous != null)
newLines = codeObject.DefaultNewLines(previous);
else
{
// If we're adding or inserting the first item, then if either brace has a newline, use one, otherwise none
newLines = ((IsFirstOnLine || EndNewLines > 0) ? 1 : 0);
// If the added object has a newline, and the closing brace doesn't, make it have one now
if (newLines > 0 && EndNewLines == 0)
EndNewLines = 1;
}
codeObject.SetNewLines(newLines);
}
// Check for stand-alone expressions
if (codeObject is Expression)
{
// Turn on the terminator, and default parens to off
codeObject.HasTerminator = true;
if (!codeObject.IsGroupingSet && ((Expression)codeObject).HasParens)
codeObject.SetFormatFlag(FormatFlags.Grouping, false);
}
}
protected void AddInternalNoFormatting(CodeObject codeObject)
{
// Add the code object to the block
_codeObjects.Add(codeObject);
// Add named members to the block's dictionary
if (codeObject is INamedCodeObject)
AddNamedMember((INamedCodeObject)codeObject);
// If any annotations are added directly to the block, send notifications
// (this will send special comments up to the CodeUnit and Solution levels).
if (codeObject is Annotation && ((Annotation)codeObject).IsListed)
NotifyListedAnnotationAdded((Annotation)codeObject);
}
protected void AddNamedMember(INamedCodeObject namedCodeObject)
{
if (_namedMembers == null)
_namedMembers = new NamedCodeObjectDictionary();
namedCodeObject.AddToDictionary(_namedMembers);
// If a TypeDecl is added to a NamespaceDecl, also add it to the Namespace
if (_parent is NamespaceDecl && namedCodeObject is TypeDecl)
{
Namespace @namespace = ((NamespaceDecl)_parent).Namespace;
if (@namespace != null)
@namespace.Add((TypeDecl)namedCodeObject);
}
}
/// <summary>
/// Clear all members from the <see cref="Block"/>.
/// </summary>
public void Clear()
{
RemoveAll();
}
/// <summary>
/// Deep-clone the code object.
/// </summary>
public override CodeObject Clone()
{
Block clone = (Block)base.Clone();
clone._codeObjects = ChildListHelpers.Clone(_codeObjects, null);
// Re-build the dictionary using the cloned objects
clone.RebuildDictionary();
return clone;
}
/// <summary>
/// Check if the <see cref="Block"/> contains the specified <see cref="CodeObject"/>.
/// </summary>
/// <param name="codeObject">The object being searched for.</param>
/// <returns>True if the block contains the object, otherwise false.</returns>
public bool Contains(CodeObject codeObject)
{
return Enumerable.Any(_codeObjects, delegate(CodeObject child) { return child == codeObject; });
}
/// <summary>
/// Copy the code objects in the block to the specified array, starting at the specified offset.
/// </summary>
/// <param name="codeObjects">The array to copy into.</param>
/// <param name="index">The starting index in the array.</param>
public void CopyTo(CodeObject[] codeObjects, int index)
{
CopyTo((Array)codeObjects, index);
}
/// <summary>
/// Copy the code objects in the block to the specified array, starting at the specified offset.
/// </summary>
/// <param name="array">The array to copy into.</param>
/// <param name="index">The starting index in the array.</param>
public void CopyTo(Array array, int index)
{
if (array == null)
throw new ArgumentNullException("array", "Null array reference");
if (index < 0)
throw new ArgumentOutOfRangeException("index", "Index is out of range");
if (array.Rank > 1)
throw new ArgumentException("Array is multi-dimensional", "array");
foreach (CodeObject obj in _codeObjects)
array.SetValue(obj, index++);
}
/// <summary>
/// Insert a code object into the block at the specified index.
/// </summary>
/// <param name="index">The index at which to insert.</param>
/// <param name="codeObject">The object to be inserted.</param>
public void Insert(int index, CodeObject codeObject)
{
if (codeObject is Block)
{
ChildList<CodeObject> codeObjects = ((Block)codeObject)._codeObjects;
for (int i = 0; i < codeObjects.Count; ++i)
Insert(i, codeObjects[i]);
}
else
{
AddInsertFormattingCheck(index, codeObject);
InsertInternalNoFormatting(index, codeObject);
ObjectCountChanged();
}
}
protected void InsertInternalNoFormatting(int index, CodeObject codeObject)
{
// Insert the code object into the block
_codeObjects.Insert(index, codeObject);
// Add named members to the block's dictionary
if (codeObject is INamedCodeObject)
AddNamedMember((INamedCodeObject)codeObject);
// If any annotations are inserted directly to the block, send notifications
// (this will send special comments up to the CodeUnit and Solution levels).
if (codeObject is Annotation && ((Annotation)codeObject).IsListed)
NotifyListedAnnotationAdded((Annotation)codeObject);
}
/// <summary>
/// Find children with the specified name.
/// </summary>
/// <returns>A <see cref="CodeObject"/>, <see cref="NamedCodeObjectGroup"/>, or null if no matches were found.</returns>
public INamedCodeObject FindChildren(string name)
{
return (_namedMembers != null ? _namedMembers.Find(name) : null);
}
/// <summary>
/// Find children with the specified name having type T, adding them to the specified results collection.
/// </summary>
public void FindChildren<T>(string name, NamedCodeObjectGroup results) where T : CodeObject
{
INamedCodeObject foundObj = FindChildren(name);
if (foundObj is NamedCodeObjectGroup)
{
foreach (INamedCodeObject namedCodeObject in (NamedCodeObjectGroup)foundObj)
{
if (namedCodeObject is T)
results.Add(namedCodeObject);
}
}
else if (foundObj is T)
results.Add(foundObj);
}
/// <summary>
/// Enumerate all children with the specified name.
/// </summary>
public IEnumerable<CodeObject> Find(string name)
{
INamedCodeObject foundObj = FindChildren(name);
if (foundObj is NamedCodeObjectGroup)
{
foreach (INamedCodeObject namedCodeObject in (NamedCodeObjectGroup)foundObj)
yield return (CodeObject)namedCodeObject;
}
else
yield return (CodeObject)foundObj;
}
/// <summary>
/// Enumerate all children with the specified name and type.
/// </summary>
public IEnumerable<T> Find<T>(string name) where T : CodeObject
{
INamedCodeObject foundObj = FindChildren(name);
if (foundObj is NamedCodeObjectGroup)
{
foreach (INamedCodeObject namedCodeObject in (NamedCodeObjectGroup)foundObj)
{
if (namedCodeObject is T)
yield return (T)namedCodeObject;
}
}
else if (foundObj is T)
yield return (T)foundObj;
}
/// <summary>
/// Enumerate all children of type T.
/// </summary>
public IEnumerable<T> Find<T>() where T : CodeObject
{
return Enumerable.OfType<T>(_codeObjects);
}
/// <summary>
/// Find the first child object with the specified name and type.
/// </summary>
public T FindFirst<T>(string name) where T : CodeObject
{
return Enumerable.FirstOrDefault(Find<T>(name));
}
/// <summary>
/// Find the first child object of type T.
/// </summary>
public T FindFirst<T>() where T : CodeObject
{
return Enumerable.FirstOrDefault(Enumerable.OfType<T>(_codeObjects));
}
/// <summary>
/// Find the index of the specified <see cref="CodeObject"/> in the <see cref="Block"/>.
/// </summary>
/// <param name="codeObject">The object being searched for.</param>
/// <returns>The index of the code object, or -1 if not found.</returns>
public int FindIndexOf(CodeObject codeObject)
{
for (int i = 0; i < _codeObjects.Count; ++i)
if (_codeObjects[i] == codeObject)
return i;
return -1;
}
/// <summary>
/// Get an enumerator for the code objects in the <see cref="Block"/>.
/// </summary>
IEnumerator<CodeObject> IEnumerable<CodeObject>.GetEnumerator()
{
return ((IEnumerable<CodeObject>)_codeObjects).GetEnumerator();
}
/// <summary>
/// Get an enumerator for the code objects in the <see cref="Block"/>.
/// </summary>
IEnumerator IEnumerable.GetEnumerator()
{
return _codeObjects.GetEnumerator();
}
protected void ObjectCountChanged()
{
if (_parent is IBlock && _codeObjects.Count <= 2)
((IBlock)_parent).ReformatBlock();
}
/// <summary>
/// Re-build the internal dictionary of named code objects in the <see cref="Block"/>.
/// </summary>
public void RebuildDictionary()
{
_namedMembers = new NamedCodeObjectDictionary();
if (_codeObjects != null)
{
foreach (CodeObject codeObject in _codeObjects)
{
if (codeObject is INamedCodeObject)
((INamedCodeObject)codeObject).AddToDictionary(_namedMembers);
}
}
}
/// <summary>
/// Remove the specified <see cref="CodeObject"/> from the <see cref="Block"/>.
/// </summary>
/// <returns>True if the code object was found and removed, otherwise false.</returns>
public bool Remove(CodeObject codeObject)
{
bool removed = _codeObjects.Remove(codeObject);
RemoveInternal(codeObject);
return removed;
}
/// <summary>
/// Remove the <see cref="CodeObject"/> at the specified index from the <see cref="Block"/>.
/// </summary>
public void RemoveAt(int index)
{
CodeObject codeObject = this[index];
_codeObjects.RemoveAt(index);
RemoveInternal(codeObject);
}
/// <summary>
/// Remove all code objects from the <see cref="Block"/>.
/// </summary>
public void RemoveAll()
{
foreach (CodeObject codeObject in _codeObjects)
{
if (codeObject is Annotation && ((Annotation)codeObject).IsListed)
NotifyListedAnnotationRemoved((Annotation)codeObject);
}
_codeObjects.Clear();
_namedMembers.Clear();
ObjectCountChanged();
}
protected void RemoveInternal(CodeObject codeObject)
{
if (codeObject is INamedCodeObject)
((INamedCodeObject)codeObject).RemoveFromDictionary(_namedMembers);
if (codeObject is Annotation && ((Annotation)codeObject).IsListed)
NotifyListedAnnotationRemoved((Annotation)codeObject);
ObjectCountChanged();
}
/// <summary>
/// Replace the specified <see cref="CodeObject"/> with a new one.
/// </summary>
/// <returns>True if the code object was found and replaced, otherwise false.</returns>
public bool Replace(CodeObject oldObject, CodeObject newObject)
{
int index = FindIndexOf(oldObject);
if (index >= 0)
{
RemoveAt(index);
Insert(index, newObject);
return true;
}
return false;
}
#endregion
#region /* PARSING */
/// <summary>
/// The token used to parse the start of a <see cref="Block"/>.
/// </summary>
public const string ParseTokenStart = "{";
/// <summary>
/// The token used to parse the end of a <see cref="Block"/>.
/// </summary>
public const string ParseTokenEnd = "}";
/// <summary>
/// Parse a <see cref="Block"/>.
/// </summary>
public Block(out Block parentBody, Parser parser, CodeObject parent, bool bracesRequired, params string[] terminators)
: base(parser, parent)
{
// Clear any newline count, and set IsFirstOnLine appropriately. Blocks use only 0 or 1 newlines on the '{',
// with multiple newlines possible on the '}' using the EndNewLines property.
SetNewLines(0);
IsFirstOnLine = ((parser.Token != null && parser.Token.IsFirstOnLine) || (parent != null && parent.HasCompilerDirectives));
// Allocate our list of child objects
_codeObjects = new ChildList<CodeObject>(parent);
// Tie our parent to us immediately, so that symbol resolution will work properly
parentBody = this;
// Don't process comments or conditional directives if we have no parent, or the parent has no header (CodeUnit or BlockDecl)
bool processedPostAnnotations = false;
if (parent is IBlock && ((IBlock)parent).HasHeader)
{
// Process comments
parent.MoveEOLComment(parser.LastToken); // Associate any skipped EOL comment with the parent
// Add any skipped comment objects to the block object itself if we have an open brace
if (parser.TokenText == ParseTokenStart)
MoveComments(parser.LastToken);
// Otherwise, add them inside the block as long as it's not empty (like an empty SwitchItem block)
else if (!StringUtil.Contains(terminators, parser.TokenText))
AddTrailingComments(parser.LastToken);
// Parse any post compiler directives between the parent statement and the block
processedPostAnnotations = ParseCompilerDirectives(parser, parent, AnnotationFlags.IsPostfix);
}
else
{
// Add any skipped comments at the top of a CodeUnit or BlockDecl (from the dummy token)
AddTrailingComments(parser.LastToken);
}
// Start a new Unused list in the parser
parser.PushUnusedList();
// Check if we have braces - if so, we always parse to the end brace
bool isTopLevel = (!(_parent is IBlock) || ((IBlock)_parent).IsTopLevel);
bool singleStatement;
bool specialTermination;
bool hasEmptyStatement = false;
if (parser.TokenText == ParseTokenStart)
{
HasBraces = true;
parser.NextToken(); // Move past '{'
MoveEOLCommentAsInfix(parser.LastToken); // Move any EOL comment as an Infix-EOL
parser.MoveCommentsToUnused(); // Add skipped Comment objects as unused
singleStatement = specialTermination = false;
}
else
{
if (bracesRequired)
{
// If braces are required for this block, but there aren't any, it's a parse error.
// Just abort now with a null Block, even though we might possibly have "eaten" some
// comments and/or compiler directives (this could be improved).
parentBody = null;
goto abort; // Go pop the unused list before exiting
}
// If terminators is null, parse to EOF (used by CodeUnit), otherwise assume a single
// statement block unless specific terminators are provided (such as for SwitchItems).
singleStatement = (terminators != null && terminators.Length == 0);
specialTermination = (terminators != null && terminators.Length > 0);
// Special handling for a block that consists only of a ';'
if (parser.TokenText == Statement.ParseTokenTerminator)
{
hasEmptyStatement = true;
// Skip the token so we can check any trailing comment
parser.NextToken();
// Move any EOL comment as a normal comment in the Block (a Token can only have one, and it should be the first one)
List<CommentBase> comments = parser.LastToken.TrailingComments;
if (comments != null && comments.Count > 0)
{
CommentBase comment = comments[0];
if (comment.IsEOL)
{
comment.IsEOL = false;
comment.IsFirstOnLine = true;
Add(comment);
comments.RemoveAt(0);
}
}
goto abort; // Go pop the unused list before exiting
}
// If we don't have braces, move any trailing non-conditional directive annoations that
// we moved to the parent above into the body of the block instead.
if (processedPostAnnotations && !singleStatement)
{
int index = parent.Annotations.Count - 1;
do
{
Annotation annotation = parent.Annotations[index];
if (annotation is Comment || (annotation is CompilerDirective && !(annotation is ConditionalDirectiveBase)))
{
parent.Annotations.RemoveAt(index);
InsertInternalNoFormatting(0, annotation);
}
else
break;
}
while (--index >= 0);
}
}
bool pendingConditionalDirectives = false;
// Push our parent as an EOL comment normalization blocker
parser.PushNormalizationBlocker(parent);
// Loop while not EOF, and we're either delimited by braces or we haven't hit a terminator
while (parser.Token != null && (HasBraces || !StringUtil.Contains(terminators, parser.TokenText)))
{
// Stop if we're at a '}'
if (parser.TokenText == ParseTokenEnd && !isTopLevel)
{
// Check if we were expecting it
if (HasBraces)
{
EndNewLines = parser.Token.NewLines; // Set the newline count for the '}'
parser.NextToken(); // Eat the '}'
MoveEOLComment(parser.LastToken);
}
break;
}
// Process the current token.
// Check for obj being null, because it might contain a '#if' directive from the preprocessing above.
CodeObject obj = parser.ProcessToken(parent, ParseFlags.Block);
if (obj != null)
{
// If we have a complete statement, or an expression followed by a terminator, add it to the Block
if (obj is Statement || (obj is Expression && parser.TokenText == Statement.ParseTokenTerminator))
{
pendingConditionalDirectives = false;
// Eat the terminator after an Expression used as a Statement
if (obj is Expression)
{
obj.HasTerminator = true;
parser.NextToken();
}
// Associate any skipped EOL comment - with the Block if we have a single
// statement on the same line, otherwise with the current statement.
if (singleStatement && !obj.IsFirstOnLine)
MoveEOLComment(parser.LastToken);
else
{
// Move any EOL comment to the statement.
// If there's an inline comment between statements in the block (on the
// same line), make it an EOL comment on the first statement.
obj.MoveEOLComment(parser.LastToken);
}
bool hasConditionalDirectives = false;
if (parser.HasUnused)
{
hasConditionalDirectives = (parser.LastUnusedCodeObject is ConditionalDirective);
FlushUnused(parser); // Flush any remaining unused objects
}
// Add the Statement to the Block
AddInternalNoFormatting(obj);
// Flush any post unused objects (used by IfBase and for trailing comments in specially terminated
// blocks below, that are flushed to the parent block). Skip for single statements so that things
// like region directives get pushed up to the parent block of the statement just added.
if (parser.PostUnused != null && parser.PostUnused.Count > 0 && !singleStatement)
{
parser.MovePostUnusedToUnused();
FlushUnused(parser);
}
// Stop if we only wanted a single statement
if (singleStatement)
{
IsFirstOnLine = obj.IsFirstOnLine;
// If we had unused conditional directive(s) before the statement, then we
// should also include any that come immediately after.
if (hasConditionalDirectives)
pendingConditionalDirectives = true;
else
break;
}
// If we're at the end of a specially terminated block (i.e. case/default block), then move
// any trailing comments to the Post unused list for later use, otherwise flush them now.
if (specialTermination && (StringUtil.Contains(terminators, parser.TokenText) || parser.TokenText == ParseTokenEnd))
parser.MoveCommentsToPostUnused();
else
AddTrailingComments(parser.LastToken); // Add skipped Comment objects
}
else
{
obj.MoveEOLComment(parser.LastToken); // Associate any skipped EOL comment
// Special handling for pending conditional directives
if (pendingConditionalDirectives)
{
if (obj is ConditionalDirectiveBase)
{
AddInternalNoFormatting(obj);
AddTrailingComments(parser.LastToken); // Add skipped Comment objects
if (obj is EndIfDirective)
break;
obj = null;
}
else
pendingConditionalDirectives = false;
}
// Save the object for later use
if (obj != null)
parser.AddUnused(obj);
// Move any trailing comments to the unused list
parser.MoveCommentsToUnused();
}
}
}
// Pop the normalization blocker object
parser.PopNormalizationBlocker();
// Flush any remaining unused objects
FlushUnused(parser);
// Do various checks if auto-cleanup is on
if (AutomaticFormattingCleanup && !parser.IsGenerated)
{
if (HasBraces)
{
if (_codeObjects.Count > 0)
{
// Remove unnecessary braces
if (_parent is BlockStatement && !((BlockStatement)_parent).ShouldHaveBraces())
{
// Also do a special check for 'else' clauses on the same line as the closing '}', changing them to be first-on-line
if (EndNewLines > 0 && _parent is IfBase && parser.TokenText == Else.ParseToken && parser.Token.NewLines == 0)
parser.Token.NewLines = 1;
HasBraces = false;
}
}
else if (_codeObjects.Count == 0)
{
// Force empty braces onto a single line if empty and the block starts on a new line and it's allowed by default
if (EndNewLines > 0 && IsFirstOnLine && _parent is BlockStatement && ((BlockStatement)_parent).IsCompactIfEmptyDefault)
EndNewLines = 0;
}
// Remove all blank lines at the end of a block
if (EndNewLines > 1)
EndNewLines = 1;
}
else
{
// Add missing suggested braces
if (_codeObjects.Count > 0 && _parent is BlockStatement && ((BlockStatement)_parent).ShouldHaveBraces())
HasBraces = true;
}
}
abort:
// Restore the previous Unused list in the parser
parser.PopUnusedList();
// Post-processing: Associate comments with lone statements that follow them.
// Scan bottom-up, so we can safely remove comments if we move them.
for (int i = _codeObjects.Count - 2; i >= 0; --i)
{
// Look for comment objects that start on a new line
CodeObject obj0 = _codeObjects[i];
if (obj0 is CommentBase)
PostProcessComment((CommentBase)obj0, i);
}
if (!HasBraces)
{
// If we didn't have braces, set the block's IsFirstOnLine based on the object in it
if (_codeObjects.Count > 0)
{
IsFirstOnLine = _codeObjects[0].IsFirstOnLine;
EndNewLines = 0;
}
// If we had no children, no braces, and no empty statement, eliminate the Block object (can occur for SwitchItems)
else if (_codeObjects.Count == 0 && !hasEmptyStatement)
parentBody = null;
}
}
/// <summary>
/// Parse an Expression into the Block.
/// </summary>
public void ParseExpressionAsBlock(Parser parser, CodeObject parent)
{
HasBraces = false;
AddInternalNoFormatting(Expression.Parse(parser, parent, true));
}
/// <summary>
/// Parse any compiler directives between a statement header and body (or base type list, constructor initializer, or type constraints).
/// Also used to parse any "open" conditional directives if 'includeAll' is false.
/// </summary>
public static bool ParseCompilerDirectives(Parser parser, CodeObject parent, AnnotationFlags position, bool includeAll)
{
// If the parent statement is followed by '#', then check if it's "sandwiched" by conditional directives
bool processedPostAnnotations = false;
if (parser.TokenText == CompilerDirective.ParseToken)
{
int openIfs = 0;
bool hasUnusedIfFirst = false, hasUnusedElses = false;
if (!includeAll)
{
// If 'includeAll' is false, then we only want to parse any "open" conditional directives.
// Determine how many (if any) nested open conditional directives we have by looking both at existing
// annotations and also unused parser objects. Stop if we hit a regular comment that isn't preceeded
// by a doc comment, or a global attribute.
if (parent.HasAnnotations)
{
for (int i = parent.Annotations.Count - 1; i >= 0; --i)
{
Annotation annotation = parent.Annotations[i];
if (annotation is EndIfDirective)
--openIfs;
else if (annotation is IfDirective)
++openIfs;
else if ((annotation is Comment && (i == 0 || !(parent.Annotations[i - 1] is DocComment)))
|| (annotation is Attribute && ((Attribute)annotation).IsGlobal))
break;
}
}
if (parser.HasUnused)
{
for (int i = parser.Unused.Count - 1; i >= 0; --i)
{
CodeObject unused = parser.GetUnusedCodeObject(i);
if (unused is EndIfDirective)
--openIfs;
else if (unused is IfDirective)
{
++openIfs;
if (i == 0)
hasUnusedIfFirst = true;
}
else if (unused is ConditionalDirective)
hasUnusedElses = true;
else if ((unused is Comment && (i == 0 || !(parser.GetUnusedCodeObject(i - 1) is DocComment)))
|| (unused is Attribute && ((Attribute)unused).IsGlobal))
break;
}
}
if (openIfs <= 0)
return false;
}
// Move any compiler directives to the parent as postfix directives
int endifCount = 0;
CodeObject firstConditional = null;
do
{
// Special case - abort if not including all and statement is just wrapped in #if/#endif
if (!includeAll && hasUnusedIfFirst && !hasUnusedElses && parser.PeekNextTokenText() == EndIfDirective.ParseToken && !processedPostAnnotations)
return false;
CodeObject obj = parser.ProcessToken(parent);
if (obj is CompilerDirective)
{
// Move trailing directives to the parent (whether it has immediately preceeding directives or not),
// counting total '#endif's.
if (obj is ConditionalDirectiveBase)
{
if (firstConditional == null)
firstConditional = obj;
if (obj is EndIfDirective)
++endifCount;
}
parent.AttachAnnotation((Annotation)obj, position);
parent.MoveCommentsAsPost(parser.LastToken);
processedPostAnnotations = true;
}
}
while (parser.TokenText == CompilerDirective.ParseToken && (includeAll || endifCount < openIfs));
// If the first trailing conditional directive wasn't an '#if', check for preceeding directives
if (!(firstConditional is IfDirective))
{
// If the parent statement is preceeded by an '#if', '#elif', or '#else', move all preceeding
// conditional directives to the parent (it's "sandwiched" by them), up to the total 'endifCount'
// (stop after that to avoid attaching compiler directives that are unrelated to the statement).
if (parser.LastUnusedCodeObject is ConditionalDirective)
parent.ParseUnusedAnnotations(parser, parent, true, (endifCount < 1 ? 1 : endifCount));
}
}
return processedPostAnnotations;
}
/// <summary>
/// Parse any compiler directives between a statement header and body (or base type list, constructor initializer, or type constraints).
/// </summary>
public static bool ParseCompilerDirectives(Parser parser, CodeObject parent, AnnotationFlags position)
{
return ParseCompilerDirectives(parser, parent, position, true);
}
/// <summary>
/// Post-process the specified comment object at the specified index, associating with the following object if applicable.
/// </summary>
protected internal void PostProcessComment(CommentBase obj, int index)
{
if (obj.IsFirstOnLine && index < _codeObjects.Count - 1)
{
// Check for following statements or expressions without a preceeding blank line, or allow a couple
// of blank lines if the comment is a documentation comment.
CodeObject obj1 = _codeObjects[index + 1];
int obj1NewLines = obj1.NewLines;
if (obj1.AssociateCommentWhenParsing(obj) && (obj1NewLines <= 1 || (obj is DocComment && obj1NewLines <= 3)))
{
// Remove any blank lines between DocComments and following code
if (obj1NewLines > 1)
obj1.NewLines = 1;
// Check that the following statement is also on a new line
if (obj1.IsFirstOnLine)
{
// Check if the candidate is the last object in the block, or is followed by a blank line, or is followed by
// either a Comment or an object with first-on-line annotations, or is followed by a compiler directive, or if
// the candidate comment is a documentation comment - in these cases, associate the candidate comment with the
// object that follows it.
CodeObject obj2 = (index < _codeObjects.Count - 2 ? _codeObjects[index + 2] : null);
if (obj2 == null || obj2.NewLines > 1 || obj2 is CommentBase || obj2.HasFirstOnLineAnnotations || obj2 is CompilerDirective || obj is DocComment)
{
// Special exception: Also require that either the comment is preceeded by a blank line, or less than two
// code objects, or the preceeding object is a comment or has a blank line before it, or the 2nd preceeding
// object is a comment.
if (obj.NewLines > 1 || index < 2 || _codeObjects[index - 1] is CommentBase || _codeObjects[index - 1].NewLines > 1 || _codeObjects[index - 2] is CommentBase)
{
RemoveAt(index);
obj1.AttachAnnotation(obj, true);
}
}
}
else
{
// If the following statement is inline, associate the inline comment with it
RemoveAt(index);
obj1.AttachAnnotation(obj, true);
obj1.IsFirstOnLine = true;
obj.IsFirstOnLine = false;
}
}
}
}
/// <summary>
/// Add any trailing Comment objects on the specified token to the Block.
/// </summary>
protected void AddTrailingComments(Token token)
{
if (token != null && token.TrailingComments != null)
{
foreach (CommentBase commentBase in token.TrailingComments)
{
// Add the comment first (so it acquires a parent), then adjust it
AddInternalNoFormatting(commentBase);
AdjustCommentIndentation(commentBase);
}
token.TrailingComments = null;
// If the block has no braces, force the Line/Col info to match the first comment (if any)
if (!HasBraces && Count > 0)
SetLineCol(this[0]);
}
}
/// <summary>
/// Flush unused objects in the parser into the Block.
/// </summary>
protected internal void FlushUnused(Parser parser)
{
if (parser.HasUnused)
{
// Special case: If the code is embedded in a '<code>' doc comment tag (we got here from Parser.ParseCodeBlockUntil),
// and we have an empty block so far, and only a single unused object, just flush it directly to the block.
if (parser.InDocComment && _codeObjects.Count == 0 && parser.Unused.Count == 1)
{
ParsedObject unused = parser.RemoveLastUnused();
CodeObject obj;
if (unused is Token)
obj = new UnresolvedRef((Token)unused);
else
obj = ((UnusedCodeObject)unused).CodeObject;
_codeObjects.Add(obj);
}
else
{
// Flush unused objects
Unrecognized unrecognized = null;
foreach (ParsedObject unused in parser.Unused)
{
// Flush unused tokens and expressions as Unrecognized object expressions
if (unused is Token || (unused is UnusedCodeObject && ((UnusedCodeObject)unused).CodeObject is Expression))
{
// Get the unused object as an expression
Expression expression = (unused is Token ? new UnresolvedRef((Token)unused) : (Expression)((UnusedCodeObject)unused).CodeObject);
// Flush the Unrecognized object anytime we start a new line
if (expression.IsFirstOnLine)
FlushUnrecognized(parser, ref unrecognized);
// Add the expression to the Unrecognized object
if (unrecognized == null)
unrecognized = new Unrecognized(true, unused.InDocComment, expression);
else
unrecognized.AddRight(expression);
// If the unused object has trailing comments, flush them now
if (unused.HasTrailingComments)
{
unrecognized.MoveEOLComment(unused.AsToken()); // Associate any skipped EOL comment
FlushUnrecognized(parser, ref unrecognized); // Flush the Unrecognized object
AddTrailingComments(unused.AsToken()); // Add skipped Comment objects
}
}
else
{
// Flush any Unrecognized object
FlushUnrecognized(parser, ref unrecognized);
// Flush the non-Expression code object (such as a CompilerDirective or Comment)
UnusedCodeObject unusedCodeObject = (UnusedCodeObject)unused;
CodeObject codeObject = unusedCodeObject.CodeObject;
AddInternalNoFormatting(codeObject);
if (codeObject is CommentBase)
AdjustCommentIndentation((CommentBase)codeObject);
AddTrailingComments(unusedCodeObject.LastToken); // Add skipped Comment objects
}
}
// Final flush of the Unrecognized object
FlushUnrecognized(parser, ref unrecognized);
parser.Unused.Clear();
}
}
}
// Flush the Unrecognized object and clear it
protected void FlushUnrecognized(Parser parser, ref Unrecognized unrecognized)
{
if (unrecognized != null)
{
unrecognized.HasTerminator = false; // Force the terminator off
unrecognized.UpdateMessage();
_codeObjects.Add(unrecognized);
unrecognized = null;
}
}
/// <summary>
/// Skip parsing a brace-delimited <see cref="Block"/>.
/// </summary>
public static void SkipParsingBlock(Parser parser, CodeObject parent, bool bracesRequired, params string[] terminators)
{
// Don't process comments or conditional directives if we have no parent, or the parent has no header (CodeUnit or BlockDecl)
if (parent is IBlock && ((IBlock)parent).HasHeader)
{
// Process comments
parent.MoveEOLComment(parser.LastToken); // Associate any skipped EOL comment with the parent
// Toss any skipped comment objects
parser.LastToken.TrailingComments = null;
// Parse any post compiler directives between the parent statement and the block
ParseCompilerDirectives(parser, parent, AnnotationFlags.IsPostfix);
}
else
{
// Toss any skipped comments at the top of a CodeUnit or BlockDecl (from the dummy token)
parser.LastToken.TrailingComments = null;
}
// Parse the braces and everything between them, throwing it all away
if (parser.TokenText == ParseTokenStart)
{
parser.NextToken(); // Move past '{'
parser.LastToken.TrailingComments = null; // Toss any comments
int nestLevel = 0;
while (parser.Token != null && !(parser.TokenText == ParseTokenEnd && nestLevel == 0))
{
if (parser.TokenText == ParseTokenStart)
++nestLevel;
else if (parser.TokenText == ParseTokenEnd)
--nestLevel;
if (parser.TokenType == TokenType.CompilerDirective)
parser.ProcessToken(parent); // Skip & toss any compiler directives
else
parser.NextToken();
parser.LastToken.TrailingComments = null; // Toss any comments
}
if (parser.TokenText == ParseTokenEnd)
parser.NextToken(); // Move past '}'
}
}
#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)
{
// Resolve using the Expression category, because expressions are legal (such as Assignments)
// in blocks, in addition to statements (CodeObjects).
ChildListHelpers.Resolve(_codeObjects, ResolveCategory.Expression, flags);
return this;
}
/// <summary>
/// Resolve child code objects that match the specified name.
/// </summary>
public void ResolveRef(string name, Resolver resolver)
{
if (_namedMembers != null)
resolver.AddMatch(_namedMembers.Find(name));
}
/// <summary>
/// Resolve indexers.
/// </summary>
public void ResolveIndexerRef(Resolver resolver)
{
// Although indexers (which always display their name as 'this') usually have an internal name of 'Item',
// the IndexerName attribute can be used to override the name. For example, StringBuilder has an indexer
// named 'Chars', XmlAttributeCollection has 'ItemOf', etc. So, we have to scan for all IndexerDecls.
// Also, we don't want to match any explicit interface implementations, so filter those out.
foreach (CodeObject codeObject in _codeObjects)
{
if (codeObject is IndexerDecl && !((IndexerDecl)codeObject).IsExplicitInterfaceImplementation)
resolver.AddMatch(codeObject);
}
}
/// <summary>
/// Resolve child code objects that match the specified name are valid goto targets, moving up the tree until a complete match is found.
/// </summary>
public override void ResolveGotoTargetUp(string name, Resolver resolver)
{
if (_namedMembers != null)
resolver.AddMatch(_namedMembers.FindGotoTarget(name));
}
/// <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 (ChildListHelpers.HasUnresolvedRef(_codeObjects))
return true;
return false;
}
#endregion
#region /* FORMATTING */
/// <summary>
/// True if the code object only requires a single line for display by default.
/// </summary>
public override bool IsSingleLineDefault
{
get
{
if (_codeObjects != null)
{
int count = _codeObjects.Count;
if (count == 0)
return true;
if (count == 1)
return _codeObjects[0].IsSingleLineDefault;
}
return false;
}
}
/// <summary>
/// Determines if the code object only requires a single line for display.
/// </summary>
public override bool IsSingleLine
{
get { return (base.IsSingleLine && (_codeObjects == null || _codeObjects.Count == 0 || (!_codeObjects[0].IsFirstOnLine && _codeObjects.IsSingleLine))); }
set
{
// For Blocks, EndNewLines indicates the number of new lines before the '}'
EndNewLines = (value ? 0 : 1);
// Propagate the change to all members of the Block
if (_codeObjects != null)
{
CodeObject lastObj = null;
foreach (CodeObject obj in _codeObjects)
{
if (value)
obj.NewLines = 0;
else if (!obj.IsFirstOnLine)
obj.NewLines = (lastObj != null ? obj.DefaultNewLines(lastObj) : 1);
lastObj = obj;
}
}
}
}
/// <summary>
/// True if the code object defaults to starting on a new line.
/// </summary>
public override bool IsFirstOnLineDefault
{
// Default to a single line (unless we have first-on-line annotations) - the block will be reformatted as items are added
get { return HasFirstOnLineAnnotations; }
}
/// <summary>
/// Determines if the code object appears as the first item on a line.
/// </summary>
public override bool IsFirstOnLine
{
// Special flag for IsFirstOnLine for the '{', because the newlines storage is used for EndNewLines for the '}'
get { return _formatFlags.HasFlag(FormatFlags.InfixNewLine); }
set { SetFormatFlag(FormatFlags.InfixNewLine, value); }
}
/// <summary>
/// The number of newlines preceeding the opening '{' (0 or 1 only - setting a higher value is ignored).
/// </summary>
public override int NewLines
{
get { return (IsFirstOnLine ? 1 : 0); }
set { IsFirstOnLine = (value > 0); }
}
/// <summary>
/// The number of newlines preceeding the closing '}' (0 to N).
/// </summary>
public int EndNewLines
{
get { return (int)(_formatFlags & FormatFlags.NewLineMask); }
set
{
SetNewLines(value);
_formatFlags |= FormatFlags.NewLinesSet;
}
}
/// <summary>
/// True if the <see cref="Block"/> is delimited by braces.
/// </summary>
public bool HasBraces
{
get { return _formatFlags.HasFlag(FormatFlags.Grouping); }
set
{
// Ignore any request to turn off braces if there are multiple objects in the body
if (value || _codeObjects.Count <= 1)
{
SetFormatFlag(FormatFlags.Grouping, value);
_formatFlags |= FormatFlags.GroupingSet;
if (value)
{
// If we're turning on braces, and first-on-line, force the end brace to be first-on-line
if (IsFirstOnLine && EndNewLines == 0)
EndNewLines = 1;
}
else
{
// If we're turning off braces, also clear EndNewLines
EndNewLines = 0;
}
}
}
}
/// <summary>
/// Determines if the code object has a terminator character.
/// </summary>
public override bool HasTerminator
{
// Blocks don't have terminators, so disable their use of this flag
get { return false; }
set { } // Just ignore any set attempts
}
#endregion
#region /* RENDERING */
public override void AsText(CodeWriter writer, RenderFlags flags)
{
if (IsGenerated)
return;
if (flags.HasFlag(RenderFlags.Description))
{
TypeRefBase.AsTextType(writer, GetType(), RenderFlags.None);
return;
}
bool isTopLevel = (!(_parent is IBlock) || ((IBlock)_parent).IsTopLevel);
if (isTopLevel)
flags |= RenderFlags.NoBlockIndent;
bool updatedLineCol = false;
// Render the open brace if appropriate
bool useBraces = HasBraces && !isTopLevel;
if (useBraces || HasFirstOnLineAnnotations)
{
if (IsFirstOnLine)
{
if (!flags.HasFlag(RenderFlags.SuppressNewLine))
writer.WriteLine();
}
else
writer.Write(" ");
AsTextBefore(writer, flags);
if (useBraces)
{
UpdateLineCol(writer, flags);
updatedLineCol = true;
writer.Write(ParseTokenStart);
}
flags &= ~RenderFlags.SuppressNewLine;
}
if (!flags.HasFlag(RenderFlags.NoEOLComments))
AsTextInfixEOLComments(writer, flags);
// Increase the indent level for any newlines that occur within the block
bool increaseIndent = !flags.HasFlag(RenderFlags.NoBlockIndent);
if (increaseIndent)
writer.BeginIndentOnNewLine(this);
// Render the body of the block
bool isSingleLineBody = true;
int codeObjectCount = (_codeObjects != null ? _codeObjects.Count : 0);
// Render an empty statement ';' if we have no children or a single comment and no braces
if ((codeObjectCount == 0 || (codeObjectCount == 1 && _codeObjects[0] is Comment)) && !HasBraces && (!(_parent is BlockStatement) || ((BlockStatement)_parent).RequiresEmptyStatement))
{
if (IsFirstOnLine)
{
if (!flags.HasFlag(RenderFlags.SuppressNewLine))
writer.WriteLine();
}
else
writer.Write(" ");
writer.Write(Statement.ParseTokenTerminator);
if (codeObjectCount > 0)
writer.Write(" ");
flags |= RenderFlags.SuppressNewLine;
}
int endAlignmentAt = -1;
RenderFlags passFlags = (flags & (RenderFlags.PassMask | RenderFlags.SuppressNewLine)) | RenderFlags.PrefixSpace;
for (int i = 0; i < codeObjectCount; ++i)
{
CodeObject codeObject = _codeObjects[i];
if (codeObject.IsGenerated)
continue;
// Check for newlines
if (codeObject.NewLines > 0)
isSingleLineBody = false;
// Check for possible alignment of initializations and/or EOL comments on consecutive code objects
if (i > endAlignmentAt && !isSingleLineBody && codeObject.HasEOLComments && codeObject.IsSingleLine)
{
int j = i;
int maxCodeWidth = 0;
int maxCommentWidth = 0;
do
{
CodeObject current = _codeObjects[j];
// Ignore hidden generated objects or comments without preceeding blank lines
if (!current.IsGenerated && !(current is CommentBase && current.NewLines == 1))
{
// Abort if we hit a blank line, or an object that doesn't have an EOL comment or isn't single-line
if (j > i && (current.NewLines > 1 || !current.HasEOLComments || !current.IsSingleLine))
break;
// Determine the padding width needed for alignment
int length = current.AsTextLength();
int newMaxCodeWidth = (length > maxCodeWidth ? length : maxCodeWidth);
int commentLength = current.EOLComment.Length;
int newMaxCommentWidth = (commentLength > maxCommentWidth ? commentLength : maxCommentWidth);
// Abort if the padded line length would be too long (IF the current line has some padding)
if ((writer.IndentOffset + newMaxCodeWidth + 5 + newMaxCommentWidth > MaximumLineLength)
&& (length < newMaxCodeWidth || commentLength < newMaxCommentWidth))
break;
maxCodeWidth = newMaxCodeWidth;
maxCommentWidth = newMaxCommentWidth;
}
++j;
}
while (j < _codeObjects.Count);
if (--j > i)
{
writer.BeginAlignment(this, new[] { maxCodeWidth });
endAlignmentAt = j;
}
}
// If the block's line/col info hasn't been set (no braces), we want it to match the first child, but this requires
// processing any pending newline first, and then telling the child to suppress it. We can't just render the child
// first and use its line/col info, because an Assignment's line/col is the '=' character's position, and there might
// also be prefixed annotations.
if (i == 0 && !updatedLineCol && flags.HasFlag(RenderFlags.UpdateLineCol))
{
int newLines = NewLines;
bool isPrefix = passFlags.HasFlag(RenderFlags.IsPrefix);
if (!isPrefix && newLines > 0)
{
if (!flags.HasFlag(RenderFlags.SuppressNewLine))
{
writer.WriteLines(codeObject.NewLines);
passFlags |= RenderFlags.SuppressNewLine;
}
}
else
{
writer.Write(" ");
passFlags &= ~RenderFlags.PrefixSpace;
}
UpdateLineCol(writer, flags);
updatedLineCol = true;
}
// Render the code object
codeObject.AsText(writer, passFlags | (isSingleLineBody ? 0 : RenderFlags.IncreaseIndent));
passFlags = (passFlags & ~RenderFlags.SuppressNewLine) | RenderFlags.PrefixSpace;
flags &= ~RenderFlags.SuppressNewLine;
// End any pending alignment if it's time
if (i == endAlignmentAt)
writer.EndAlignment(this);
}
// Revert the indent level
if (increaseIndent)
writer.EndIndentation(this);
// Render the close brace if appropriate
if (useBraces)
{
int endNewLines = EndNewLines;
if (endNewLines > 0)
writer.WriteLines(endNewLines);
else
writer.Write(" ");
writer.Write(ParseTokenEnd);
}
// Render any EOL comments (rendered after the close brace when it's on a line by itself - special Infix
// EOL comments may also exist and be rendered after the open brace above).
if (!flags.HasFlag(RenderFlags.NoEOLComments))
AsTextEOLComments(writer, RenderFlags.None);
AsTextAfter(writer, flags);
}
#endregion
}
}