// 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.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Nova.Rendering;
namespace Nova.CodeDOM
{
/// <summary>
/// Declares a unit of independent code that belongs to the root-level namespace (also known as a "compilation unit").
/// Usually is the contents of a source file, but it can optionally be in-memory only.
/// A <see cref="CodeUnit"/> is essentially a <see cref="NamespaceDecl"/> of the global namespace.
/// </summary>
/// <remarks>
/// The format of a code unit is (in order):
/// - Zero or more "extern alias" directives
/// - Zero or more "using" directives (or "using aliasname = ...")
/// - Zero or more global attributes
/// - Zero or more namespace member declarations (child namespaces and/or type declarations)
/// Of course, comments and preprocessor directives may be mixed in.
///
/// The term "code unit" is used because the code might not be mapped to a file, and because it's shorter
/// and more generic than the term "compilation unit" (which is specifically associated with compilation).
/// </remarks>
public class CodeUnit : NamespaceDecl, INamedCodeObject, IComparable
{
#region /* STATIC FIELDS */
/// <summary>
/// Determines if changes are saved to a separate ".Nova.cs" file instead of the original.
/// </summary>
public static bool SaveChangesToSeparateFile;
/// <summary>
/// The global namespace.
/// </summary>
protected static RootNamespace _globalNamespace = new RootNamespace(ExternAlias.GlobalName, null); // Setup the 'global' namespace;
#endregion
#region /* FIELDS */
protected string _name; // Name of source file or in-memory source
protected bool _isNew; // True if newly created and not saved yet
protected string _fileName; // The file name (if any)
protected string _code; // Optional in-memory source string (in lieu of a file)
protected int _totalLines; // Total number of text lines in the source (when first parsed)
protected int _SLOC; // "Source Lines Of Code" in the source (when first parsed)
protected bool _isWorkflowCodeBesideFile; // True if this is a Workflow code-beside file
/// <summary>
/// Compiler directive symbols defined in the current file.
/// </summary>
protected HashSet<string> _compilerDirectiveSymbols = new HashSet<string>();
/// <summary>
/// Generated 'extern alias global' statement.
/// </summary>
protected ExternAlias _globalAlias;
/// <summary>
/// All 'listed' code annotations (<see cref="Message"/>s and special <see cref="Comment"/>s) for this <see cref="CodeUnit"/>.
/// </summary>
protected List<Annotation> _listedAnnotations = new List<Annotation>();
#endregion
#region /* CONSTRUCTORS */
/// <summary>
/// Create a new <see cref="CodeUnit"/> with the specified file name (or text source).
/// </summary>
public CodeUnit(string fileName, string code)
: base(new NamespaceRef(_globalNamespace) { IsGenerated = true })
{
Name = Path.GetFileName(fileName);
_isNew = true;
_fileName = fileName;
if (!Path.HasExtension(fileName))
_fileName += ".cs";
FileEncoding = Encoding.UTF8; // Default to UTF8 encoding with a BOM
FileHasUTF8BOM = true;
_code = code;
}
/// <summary>
/// Create a new <see cref="CodeUnit"/> with the specified file name.
/// </summary>
public CodeUnit(string fileName)
: this(fileName, null)
{ }
#endregion
#region /* STATIC CONSTRUCTOR */
static CodeUnit()
{
// 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 global namespace.
/// </summary>
public static RootNamespace GlobalNamespace
{
get { return _globalNamespace; }
}
/// <summary>
/// The name of the <see cref="CodeUnit"/>. If associated with a file, this is the file name and extension.
/// </summary>
public string Name
{
get { return _name; }
set
{
_name = value;
_isWorkflowCodeBesideFile = _name.EndsWith(".xoml.cs");
}
}
/// <summary>
/// True if the <see cref="CodeUnit"/> is newly created and hasn't been saved yet.
/// </summary>
public bool IsNew
{
get { return _isNew; }
}
/// <summary>
/// True if the <see cref="CodeUnit"/> is associated with a file (as opposed to being only in memory).
/// </summary>
public bool IsFile
{
get { return (_code == null); }
}
/// <summary>
/// The associated file name of the <see cref="CodeUnit"/>.
/// </summary>
public string FileName
{
get { return _fileName; }
set { _fileName = value; }
}
/// <summary>
/// True if the associated file exists.
/// </summary>
public bool FileExists
{
get { return File.Exists(_fileName); }
}
/// <summary>
/// The encoding of the file (normally UTF8).
/// </summary>
public Encoding FileEncoding { get; set; }
/// <summary>
/// True if the file has a UTF8 byte-order-mark.
/// </summary>
public bool FileHasUTF8BOM { get; set; }
/// <summary>
/// True if the associated file is formatted using tabs, otherwise false (using spaces).
/// </summary>
public bool FileUsingTabs { get; set; }
/// <summary>
/// True if the <see cref="CodeUnit"/> contains C# code.
/// </summary>
public bool IsCSharp
{
get
{
// Treat no extension as C# so that in-memory code can omit the extension
string extension = Path.GetExtension(Name);
return (string.IsNullOrEmpty(extension) || extension == ".cs");
}
}
/// <summary>
/// The associated text source code if no file is being used.
/// </summary>
public string Code
{
get { return _code; }
}
/// <summary>
/// The descriptive category of the code object.
/// </summary>
public string Category
{
get { return "file"; }
}
/// <summary>
/// The implied global extern alias to the global <see cref="RootNamespace"/>.
/// </summary>
public ExternAlias GlobalAlias
{
get
{
if (_globalAlias == null)
_globalAlias = new ExternAlias(_globalNamespace) { Parent = this, IsGenerated = true };
return _globalAlias;
}
}
/// <summary>
/// True for all <see cref="BlockStatement"/>s that have a header (all except <see cref="CodeUnit"/> and <see cref="BlockDecl"/>).
/// </summary>
public override bool HasHeader
{
get { return false; }
}
/// <summary>
/// True if a <see cref="BlockStatement"/> is at the top level (those that have no header and no indent).
/// For example, a <see cref="CodeUnit"/>, a <see cref="BlockDecl"/> with no parent, or a <see cref="DocComment"/> parent.
/// </summary>
public override bool IsTopLevel
{
get { return true; }
}
/// <summary>
/// All 'listed' code annotations (<see cref="Message"/>s and special <see cref="Comment"/>s) for this CodeUnit.
/// </summary>
public List<Annotation> ListedAnnotations
{
get { return _listedAnnotations; }
}
/// <summary>
/// Total number of text lines in the source (when first parsed).
/// </summary>
public int TotalLines
{
get { return _totalLines; }
}
/// <summary>
/// "Source Lines Of Code" in the source (when first parsed).
/// </summary>
public int SLOC
{
get { return _SLOC; }
}
#endregion
#region /* METHODS */
/// <summary>
/// Parse the specified name into a <see cref="NamespaceRef"/> or <see cref="TypeRef"/>, or a <see cref="Dot"/> or <see cref="Lookup"/> expression that evaluates to one.
/// </summary>
public Expression ParseName(string fullName)
{
return _globalNamespace.ParseName(fullName);
}
/// <summary>
/// Get the name of the save file.
/// </summary>
public static string GetSaveFileName(string filePath)
{
if (SaveChangesToSeparateFile)
return Path.GetDirectoryName(filePath) + @"\" + Path.GetFileNameWithoutExtension(filePath) + ".Nova" + Path.GetExtension(filePath);
return filePath;
}
/// <summary>
/// Save the <see cref="CodeUnit"/> to the specified file name.
/// </summary>
public void SaveAs(string fileName)
{
RemoveAllMessages(MessageSource.Save);
try
{
// Save as text, suppressing the implied leading newline, and adding one at the end
using (CodeWriter writer = new CodeWriter(fileName, FileEncoding, FileHasUTF8BOM, FileUsingTabs, IsGenerated))
{
AsText(writer, RenderFlags.SuppressNewLine);
writer.WriteLine();
}
_isNew = false;
}
catch (Exception ex)
{
LogAndAttachException(ex, "writing", MessageSource.Save);
}
}
/// <summary>
/// Save the <see cref="CodeUnit"/>.
/// </summary>
public void Save()
{
// Skip saving generated (".g.cs") files
if (!IsGenerated)
SaveAs(GetSaveFileName(_fileName));
}
/// <summary>
/// Update the LineNumber and ColumnNumber properties of all child <see cref="CodeObject"/>s of the <see cref="CodeUnit"/>.
/// </summary>
public void UpdateAllLineColInfo()
{
// Use the length calculating method to avoid the overhead of building a big string
AsTextLength(RenderFlags.UpdateLineCol);
}
/// <summary>
/// Get the indent level of this object.
/// </summary>
public override int GetIndentLevel()
{
// A code unit is never indented
return 0;
}
/// <summary>
/// Returns true if the specified child object is indented from the parent.
/// </summary>
protected override bool IsChildIndented(CodeObject obj)
{
// Children of a code unit are never indented
return false;
}
/// <summary>
/// Determine if the specified compiler directive symbol exists.
/// </summary>
public bool IsCompilerDirectiveSymbolDefined(string name)
{
return (_compilerDirectiveSymbols.Contains(name) || name == "USING_NOVA" || name == "USING_NOVA_2");
}
/// <summary>
/// Define the specified compiler directive symbol.
/// </summary>
public void DefineCompilerDirectiveSymbol(string name)
{
_compilerDirectiveSymbols.Add(name);
}
/// <summary>
/// Undefine the specified compiler directive symbol.
/// </summary>
public void UndefineCompilerDirectiveSymbol(string name)
{
if (_compilerDirectiveSymbols.Contains(name))
_compilerDirectiveSymbols.Remove(name);
}
protected override void NotifyListedAnnotationAdded(Annotation annotation)
{
_listedAnnotations.Add(annotation);
}
protected override void NotifyListedAnnotationRemoved(Annotation annotation)
{
_listedAnnotations.Remove(annotation);
}
/// <summary>
/// Log the specified text message with the specified severity level.
/// </summary>
public void LogMessage(string message, MessageSeverity severity, string toolTip)
{
string prefix = (severity == MessageSeverity.Error ? "ERROR: " : (severity == MessageSeverity.Warning ? "Warning: " : ""));
Log.WriteLine(prefix + "File '" + _name + "': " + message, toolTip != null ? toolTip.TrimEnd() : null);
}
/// <summary>
/// Log the specified text message with the specified severity level.
/// </summary>
public void LogMessage(string message, MessageSeverity severity)
{
LogMessage(message, severity, null);
}
/// <summary>
/// Log the specified exception and message.
/// </summary>
public string LogException(Exception ex, string message)
{
return Log.Exception(ex, message + " file '" + _name + "'");
}
/// <summary>
/// Log the specified text message and also attach it as an annotation.
/// </summary>
public void LogAndAttachMessage(string message, MessageSeverity severity, MessageSource source, string toolTip)
{
LogMessage(message, severity, toolTip);
AttachMessage(message, severity, source);
}
/// <summary>
/// Log the specified text message and also attach it as an annotation.
/// </summary>
public void LogAndAttachMessage(string message, MessageSeverity severity, MessageSource source)
{
LogMessage(message, severity, null);
AttachMessage(message, severity, source);
}
/// <summary>
/// Log the specified exception and message and also attach it as an annotation.
/// </summary>
public void LogAndAttachException(Exception ex, string message, MessageSource source)
{
message = LogException(ex, message);
AttachMessage(message, MessageSeverity.Error, source);
}
/// <summary>
/// Add the <see cref="CodeObject"/> to the specified dictionary.
/// </summary>
public virtual void AddToDictionary(NamedCodeObjectDictionary dictionary)
{
dictionary.Add(Name, this);
}
/// <summary>
/// Remove the <see cref="CodeObject"/> from the specified dictionary.
/// </summary>
public virtual void RemoveFromDictionary(NamedCodeObjectDictionary dictionary)
{
dictionary.Remove(Name, this);
}
/// <summary>
/// Get the full name of the <see cref="INamedCodeObject"/>, including any namespace name.
/// </summary>
public string GetFullName(bool descriptive)
{
return _name;
}
/// <summary>
/// Get the full name of the <see cref="INamedCodeObject"/>, including any namespace name.
/// </summary>
public string GetFullName()
{
return _name;
}
/// <summary>
/// Compare one <see cref="CodeUnit"/> to another.
/// </summary>
public int CompareTo(object obj2)
{
// Sort by directory first, with special logic so that parent directories
// come after their children, then sort by file name within the directories.
string obj2Path = ((CodeUnit)obj2).FileName;
string directory1 = Path.GetDirectoryName(_fileName);
string directory2 = Path.GetDirectoryName(obj2Path);
int diff;
if (directory1 == directory2)
diff = 0;
else if (directory1 == null || (directory2 != null && directory1.StartsWith(directory2)))
diff = -1;
else if (directory2 == null || directory2.StartsWith(directory1))
diff = 1;
else
diff = directory1.CompareTo(directory2);
if (diff == 0)
{
string fileName1 = Path.GetFileName(_fileName);
string fileName2 = Path.GetFileName(obj2Path);
if (fileName1 == null)
{
if (fileName2 != null)
diff = -1;
}
else
diff = fileName1.CompareTo(fileName2);
}
return diff;
}
#endregion
#region /* FORMATTING */
/// <summary>
/// The number of newlines preceeding the object (0 to N).
/// </summary>
public override int NewLines
{
get { return 1; }
set { throw new Exception("Can't set NewLines on a CodeUnit (it's always 1)."); }
}
/// <summary>
/// True if the <see cref="Statement"/> has an argument.
/// </summary>
public override bool HasArgument
{
get { return false; }
}
/// <summary>
/// True if the <see cref="BlockStatement"/> always requires braces.
/// </summary>
public override bool HasBracesAlways
{
get { return false; }
}
/// <summary>
/// Determines if the body of the <see cref="BlockStatement"/> should be formatted with braces.
/// </summary>
public override bool ShouldHaveBraces()
{
return false;
}
/// <summary>
/// True if the <see cref="BlockStatement"/> requires an empty statement if it has an empty block with no braces.
/// </summary>
public override bool RequiresEmptyStatement
{
get { return false; }
}
/// <summary>
/// True if the <see cref="Statement"/> has a terminator character by default.
/// </summary>
public override bool HasTerminatorDefault
{
get { return false; }
}
#endregion
#region /* RENDERING */
/// <summary>
/// True if the <see cref="CodeObject"/> is renderable.
/// </summary>
public override bool IsRenderable
{
get
{
// Don't render if not C#, or if there are any Load or Parse errors (other than lost comments)
return (IsCSharp && (_annotations == null || !Enumerable.Any(_annotations, delegate(Annotation annotation)
{
return annotation is Message && ((Message)annotation).Severity == MessageSeverity.Error
&& ((Message)annotation).Source == MessageSource.Load || (((Message)annotation).Source == MessageSource.Parse && !annotation.Text.StartsWith("Line#"));
})));
}
}
protected internal override void UpdateLineCol(CodeWriter writer, RenderFlags flags)
{ }
public override void AsText(CodeWriter writer, RenderFlags flags)
{
base.AsText(writer, flags | RenderFlags.UpdateLineCol);
}
protected override void AsTextStatement(CodeWriter writer, RenderFlags flags)
{
if (flags.HasFlag(RenderFlags.Description))
writer.Write(Name);
}
protected override void AsTextAfter(CodeWriter writer, RenderFlags flags)
{
base.AsTextAfter(writer, flags | RenderFlags.SuppressNewLine);
writer.Flush(); // Make sure everything is flushed
}
#endregion
}
}