/*       Copyright (c) Eric Ledoux.  All rights reserved.       */
/* See http://www.dwell.net/terms for code sharing information. */

// DocumentationSet.cs
//
// Implements the DocumentationSet class and related functionality.
//

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using DwellNet.CodeDoc;
using CodeDocApi.Properties;

namespace DwellNet.CodeDoc
{

DocumentationSet Class

Represents a CodeDoc documentation set, which is a collection of source files that generates a single linked set of documentation.

public class DocumentationSet
{
    //////////////////////////////////////////////////////////////////////////
    // Private Static Fields
    //

    
DocumentationSet.s_supportedExtensions Field

Holds the value of the SupportedExtensions property.

    readonly static string[] s_supportedExtensions = new string[] { ".cs" };

    
DocumentationSet.s_aliasPrefixRegex Field

Matches a namespace alias qualifier prefix, e.g. "RE." in "RE.Regex". 25.3 of the C# Language Specification 2.0. spaces.

    readonly static Regex s_aliasPrefixRegex =
        new Regex(@"^([a-zA-Z_][a-zA-Z0-9_]*)\.(.*)");

    
DocumentationSet.s_namespaceAliasQualifierRegex Field

Matches a namespace alias qualifier, e.g. "SIO::" in "SIO::Stream". See section 25.3 of the C# Language Specification 2.0.

    readonly static Regex s_namespaceAliasQualifierRegex =
        new Regex(@"^([a-zA-Z_][a-zA-Z0-9_]*)::(.*)");

    //////////////////////////////////////////////////////////////////////////
    // Private Fields That Persist After ClearSourceFiles()
    //

    
DocumentationSet.m_lastGeneratedTopicId Field

The last TopicId that was automatically generated by DocumentationSet.

See Also
    int m_lastGeneratedTopicId = 1000000;

    
DocumentationSet.m_definedSymbols Field

Holds the value of the DefinedSymbols property.

    List<string> m_definedSymbols = new List<string>(20);

    
DocumentationSet.m_allowedXmlElements Field

Contains (within the dictionary keys) XML element names (e.g. "b" for "<b>") that are allowed within XML comment sections; elements not listed are still allowed but a warning is generated, since such an element may be a mistake (e.g. "<r>" mistyped). The values in this dictionary are all true.

Remarks

The <AllowedXmlElements> section of the CodeDoc XML project file contains XML element names that are used to populate this field.

    Dictionary<string, bool> m_allowedXmlElements =
        new Dictionary<string,bool>(100);

    //////////////////////////////////////////////////////////////////////////
    // Private Fields That Are Cleared By ClearSourceFiles()
    //

    
DocumentationSet.m_externalOnly Field

Holds the value of the ExternalOnly property.

    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    bool m_externalOnly;

    
DocumentationSet.m_begunUsingSources Field

Set to true once BeginUsingSources is called.

    bool m_begunUsingSources;

    
DocumentationSet.m_sourceFiles Field

Holds the value of the SourceFiles property.

    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    List<SourceFile> m_sourceFiles;

    
DocumentationSet.m_topics Field

Contains all topics in the documentation set. The key is a Topic.CanonicalName; the value is the Topic that that canonical name refers to.

    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    Dictionary<string, Topic> m_topics;

    
DocumentationSet.m_namespaceTopics Field

Holds the value of the NamespaceTopics property.

    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    Dictionary<string, NamespaceTopic> m_namespaceTopics;

    
DocumentationSet.m_overloadListTopics Field

Holds the value of the OverloadListTopics property.

    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    Dictionary<string, OverloadListTopic> m_overloadListTopics;

    //////////////////////////////////////////////////////////////////////////
    // Public Properties
    //

    
DocumentationSet.SupportedExtensions Property

Gets the file name extensions (for example, ".cs") of each type of source file supported by this class.

    public static string[] SupportedExtensions
    {
        [DebuggerStepThrough]
        get
        {
            return s_supportedExtensions;
        }
    }

    
DocumentationSet.SourceFiles Property

The collection of source files in this documentation set.

    public ICollection<SourceFile> SourceFiles
    {
        [DebuggerStepThrough]
        get
        {
            return new ReadOnlyCollection<SourceFile>(m_sourceFiles);
        }
    }

    
DocumentationSet.NamespaceTopics Property

The collection of TopicKind.NamespaceTopic topics.

    public ICollection<NamespaceTopic> NamespaceTopics
    {
        [DebuggerStepThrough]
        get
        {
            return m_namespaceTopics.Values;
        }
    }

    
DocumentationSet.OverloadListTopics Property

The collection of OverloadListTopic topics. These are topics that represent a collection of other topics that all have the same Topic.BaseName property value.

    public ICollection<OverloadListTopic> OverloadListTopics
    {
        [DebuggerStepThrough]
        get
        {
            return m_overloadListTopics.Values;
        }
    }

    
DocumentationSet.ExternalOnly Property

Gets or sets a value indicating if this is an external-only documentation build, meaning that only types and members accessible outside this program or library are being documented.

Remarks

In addition to setting ExternalOnly, the application must set IsDocumented on individual topics.

    public bool ExternalOnly
    {
        [DebuggerStepThrough]
        get
        {
            return m_externalOnly;
        }
        [DebuggerStepThrough]
        set
        {
            m_externalOnly = value;
        }
    }

    
DocumentationSet.DefinedSymbols Property

Gets the initial set of defined conditional compilation symbols that are defined at the beginning of subsequently-loaded source file; for example, "DEBUG". Additional symbols may be defined using #define directives within the source file.

Remarks

The <DefinedSymbols> section of the CodeDoc XML project file contains the symbols used to populate this field. The application is required to initialize DefinedSymbols by calling DefineSymbol once per symbol. ClearSymbols can be used to empty the list of defined symbols so that the list can be changed for subsequently-loaded source files.

    public ICollection<string> DefinedSymbols
    {
        [DebuggerStepThrough]
        get
        {
            return new ReadOnlyCollection<string>(m_definedSymbols);
        }
    }

    
DocumentationSet.AllowedXmlElements Property

Enumerates through the list of XML element names (e.g. "b" for "<b>") that are allowed within XML comment sections; elements not listed are still allowed but a warning is generated, since such an element may be a mistake (e.g. "<r>" mistyped).

Remarks

The <AllowedXmlElements> section of the CodeDoc XML project file contains XML element names that are used to populate this property.

See Also
    public IEnumerable<string> AllowedXmlElements
    {
        get
        {
            return m_allowedXmlElements.Keys;
        }
    }

    
DocumentationSet.GetSortedNamespaceTopics Method

Returns the collection of TopicKind.NamespaceTopic topics, sorted by namespace name. The order of NamespaceTopics is not affected.

    public NamespaceTopic[] GetSortedNamespaceTopics()
    {
        Dictionary<string, NamespaceTopic>.ValueCollection values =
            m_namespaceTopics.Values;
        NamespaceTopic[] array = new NamespaceTopic[values.Count];
        values.CopyTo(array, 0);
        Array.Sort<Topic>(array);
        return array;
    }

    
DocumentationSet.Topics Property

Enumerates all topics in the documentation set.

    public IEnumerable<Topic> Topics
    {
        get
        {
            // loop once per namespace, including the global namespace
            foreach (NamespaceTopic namespaceTopic in NamespaceTopics)
            {
#if true
                // yield this namespace topic
                yield return namespaceTopic;
#else
                // yield this namespace topic, unless it's the global
                // namespace
                if (namespaceTopic.MemberName.Length > 0)
                    yield return namespaceTopic;
#endif

                // loop once per type in this namespace, including nested types
                foreach (Topic typeTopic in namespaceTopic.TypeTopics)
                {
                    // yield this type topic
                    yield return typeTopic;

                    // yield members of this type topic, but skip type topics
                    // since we're already enumerating through all type topics
                    foreach (Topic childTopic in
                            Topic.EnumerateDescendentNonTypes(typeTopic))
                        yield return childTopic;
                }
            }
        }
    }

    //////////////////////////////////////////////////////////////////////////
    // Public Events
    //

    
DocumentationSet.AddingSourceFile Event

Fired before a source file is added to the documentation set.

Remarks

The application may want to use this event to provide feedback to the user indicating that a source file is being loaded.

    public event AddingSourceFileEventDelegate AddingSourceFile;

    
DocumentationSet.AddedSourceFile Event

Fired after a source file is successfully added to the documentation set (with or without warnings).

Remarks

The application may want to use this event to check for warnings in the SourceFile object and display them to the user.

    public event AddedSourceFileEventDelegate AddedSourceFile;

    
DocumentationSet.ParsingError Event

Fired to indicate that an error occurred while trying to parse a source file.

    public event ParsingErrorEventDelegate ParsingError;

    
DocumentationSet.DuplicateTopicTitle Event

Fired to indicate that two topics have the same Topic.LongTitle. This is typically a warning, displayed to the user -- it may indicate that the same source file was added twice to the documentation set.

    public event DuplicateTopicTitleEventDelegate DuplicateTopicTitle;

    
DocumentationSet.DuplicateCanonicalName Event

Fired to indicate that two topics have the same Topic.CanonicalName. This is purely informational -- it does not indicate an error. (Typially it indicates that one or more overload list topics are being generated.)

    public event DuplicateCanonicalNameEventDelegate DuplicateCanonicalName;

    
DocumentationSet.ReferenceNotFound Event

Fired to indicate that a topic made a potential reference to another topic which wasn't found. This is typically an informational message, displayed to the user after sorting all such messages and removing duplicates. This event does not necessarily indicate an error.

    public event ReferenceNotFoundEventDelegate ReferenceNotFound;

    //////////////////////////////////////////////////////////////////////////
    // Public Methods
    //

    
DocumentationSet Constructor

Initializes an instance of this class.

    public DocumentationSet()
    {
        // initialize
        ClearSourceFiles();
    }

    
DocumentationSet.CopyCoreState Method

Copies the "core" state of this DocumentationSet to another DocumentationSet. "Core" state includes the list of source files, defined symbols, allowed XML elements, and the state of GenerateTopicId, but does not include which topics are documented.

Parameters

other

The DocumentationSet to copy to.

    public void CopyCoreState(DocumentationSet other)
    {
        // copy state used by GenerateTopicId()
        other.m_lastGeneratedTopicId = m_lastGeneratedTopicId;

        // copy source files
        foreach (SourceFile sourceFile in SourceFiles)
        {
            other.AddSourceFile(sourceFile.SourceFileAbsolutePath,
                sourceFile.SourceFileRelativePath, sourceFile.AssemblyName);
        }

        // copy the collection of defined symbols
        foreach (string symbol in DefinedSymbols)
            other.DefineSymbol(symbol);

        // copy the collection of allowed XML elements
        foreach (string elementName in AllowedXmlElements)
            other.AllowXmlElement(elementName);
    }

    
DocumentationSet.AddSourceFile Method

Adds a source code file to this documentation set. Creates an instance of the correct subclass of SourceFile based on the extension of a given source file name.

Parameters

sourceFileAbsolutePath

The value to initialize the SourceFile.SourceFileAbsolutePath property to.

sourceFileRelativePath

The value to initialize the SourceFile.SourceFileRelativePath property to.

assemblyName

Specifies the name of the assembly that contains the code compiled from these source files, or null to specify none. If specified, the assembly name is included in topic HTML.

Return Value

The subclass of SourceFile that was created to represent the specified source file. The returned object is added to this documentation set.

Exceptions
Exception type Condition
ArgumentException

The file name extension of sourceFileAbsolutePath and sourceFileRelativePath isn't supported, or these two arguments have different exensions. In the former case, the exception message provides further information for the user.

Remarks

The programming language of the source file is determined based on the file name extension of sourceFileAbsolutePath and sourceFileRelativePath (which must be the same for both files). Currently, only C# source files, with an extension of ".cs", are supported.

    public SourceFile AddSourceFile(string sourceFileAbsolutePath,
        string sourceFileRelativePath, string assemblyName)
    {
        // don't allow source files to be added if operations have begun that
        // make use of those source files
        if (m_begunUsingSources)
            throw new BegunUsingSourcesException(Resources.BegunUsingSources);

        // notify the application that a source file is about to be added to
        // the documentation set
        if (AddingSourceFile != null)
            AddingSourceFile(sourceFileAbsolutePath, sourceFileRelativePath);

        // set <extension> to the file name extension of the source file,
        // converted to lowercase
        string extension = Path.GetExtension(sourceFileAbsolutePath)
            .ToLowerInvariant();
        if ((sourceFileRelativePath != null) &&
            (extension != Path.GetExtension(sourceFileRelativePath)
             .ToLowerInvariant()))
        {
            throw new ArgumentException(Resources.InconsistentExtension,
                "sourceFileAbsolutePath");
        }

        // create an instance, <sourceFile>, of the appropriate subclass based
        // on <extension>
        SourceFile sourceFile;
        if (extension == ".cs")
        {
            sourceFile = new CSharpSourceFile(this, sourceFileAbsolutePath,
                sourceFileRelativePath);
        }
        else
        {
            throw new ArgumentException(
                String.Format(Resources.SourceFileExtensionNotSupported,
                    extension, sourceFileAbsolutePath),
                "sourceFileAbsolutePath");
        }

        // associate <assemblyName> (if any) with <sourceFile>
        sourceFile.AssemblyName = assemblyName;

        // give <sourceFile> a unique numeric ID
        sourceFile.SourceFileTopicId = GenerateTopicId();

        // add <sourceFile> to the documentation set
        m_sourceFiles.Add(sourceFile);

        // load and parse <sourceFile>
        sourceFile.ExceptionOnInvalidXmlComment = false;
        sourceFile.InvalidXmlComment += delegate(ParsingException exception)
        {
            if (ParsingError != null)
                ParsingError(exception);
        };
        sourceFile.Load(DefinedSymbols);

        // notify the application that a source file has been successfully
        // added to the documentation set
        if (AddedSourceFile != null)
            AddedSourceFile(sourceFile);

        // done
        return sourceFile;
    }

    
DocumentationSet.AddSourceFilesInDirectoryTree Method

Adds to this documentation set each source file with a supported file name extension (for example, ".cs") within a given directory tree.

Parameters

treePath

The full path to the root of the directory tree.

relativePath

For each source file in the tree, its path relative to treePath, e.g. "Def\Ghi.cs", is appended to relativePath, e.g. "Abc", resulting in a compound relative path, e.g. "Abc\Def\Ghi.cs", that's used as the SourceFile.SourceFileRelativePath property. Use "" for no prefix.

excludeFilePaths

For each source file in the tree, if its path is matched by any regular expression in this list, the source file is ignored. This parameter is ignored if null.

assemblyName

Specifies the name of the assembly that contains the code compiled from these source files, or null to specify none. If specified, the assembly name is included in topic HTML.

    public void AddSourceFilesInDirectoryTree(string treePath,
        string relativePath, List<Regex> excludeFilePaths, string assemblyName)
    {
        // don't allow source files to be added if operations have begun that
        // make use of those source files
        if (m_begunUsingSources)
            throw new BegunUsingSourcesException(Resources.BegunUsingSources);

        // enumerate all source files in this directory
        foreach (string extension in SupportedExtensions)
        {
            foreach (string sourceFilePath in
                Directory.GetFiles(treePath, "*" + extension))
            {
                // skip this source file if it matches any regular expression
                // in <excludeFilePaths>
                if ((excludeFilePaths != null) &&
                    (excludeFilePaths.Find(
                        delegate(Regex regex)
                        {
                            return regex.Match(sourceFilePath).Success;
                        }) != null))
                {
                    continue;
                }

                // add the source file
                string sourceFileName = Path.GetFileName(sourceFilePath);
                AddSourceFile(sourceFilePath,
                    Path.Combine(relativePath, sourceFileName), assemblyName);
            }
        }

        // recurse into subdirectories
        foreach (string subdirPath in Directory.GetDirectories(treePath))
        {
            string dirName = Path.GetFileName(subdirPath);
            AddSourceFilesInDirectoryTree(subdirPath,
                Path.Combine(relativePath, dirName), excludeFilePaths,
                assemblyName);
        }
    }

    
DocumentationSet.ClearSourceFiles Method

Removes all source files from this documentation set.

    public void ClearSourceFiles()
    {
        m_externalOnly = false;
        m_begunUsingSources = false;
        m_sourceFiles = new List<SourceFile>(50);
        m_topics = new Dictionary<string, Topic>(500);
        m_namespaceTopics = new Dictionary<string, NamespaceTopic>(50);
        m_overloadListTopics = new Dictionary<string, OverloadListTopic>(50);
    }

    
DocumentationSet.DefineSymbol Method

Adds a conditional compilation symbol to DefinedSymbols.

Parameters

symbol

The symbol to define; for example, "DEBUG".

See Also
    public void DefineSymbol(string symbol)
    {
        m_definedSymbols.Add(symbol);
    }

    
DocumentationSet.ClearSymbols Method

Empties the list of conditional complilation symbols contained in DefinedSymbols so that the list can be changed for subsequently-loaded source files (using DefineSymbol).

See Also
    public void ClearSymbols()
    {
        m_definedSymbols.Clear();
    }

    
DocumentationSet.AllowXmlElement Method

Adds an XML element names (e.g. "b" for "<b>") that is allowed within XML comment sections; elements not listed are still allowed but a warning is generated, since such an element may be a mistake (e.g. "<r>" mistyped).

Parameters

elementName

The XML element name; for example "b".

Remarks

The <AllowedXmlElements> section of the CodeDoc XML project file contains allowed XML element names.

See Also
    public void AllowXmlElement(string elementName)
    {
        m_allowedXmlElements[elementName] = true;
    }

    
DocumentationSet.ClearAllowedXmlElements Method

Empties the list of XML element names that are allowed within XML comment sections, so that the list can be changed for subsequently-loaded source files (using AllowXmlElement).

See Also
    public void ClearAllowedXmlElements()
    {
        m_allowedXmlElements.Clear();
    }

    
DocumentationSet.IsAllowedXmlElement Method

Returns true if a given element name is allowed within XML comment sections.

Parameters

elementName

The XML element name; for example "b".

Remarks

See AllowXmlElement for more information.

    public bool IsAllowedXmlElement(string elementName)
    {
        bool unused;
        return m_allowedXmlElements.TryGetValue(elementName, out unused);
    }

    
DocumentationSet.FindNamespaceTopic Method

Returns the NamespaceTopic corresponding to a given namespace name (e.g. "Foo.Bar"), or null if that namespace isn't currently defined in NamespaceTopics.

Parameters

namespaceName

The namespace name; for example, "Foo.Bar".

    public NamespaceTopic FindNamespaceTopic(string namespaceName)
    {
        NamespaceTopic topic;
        if (m_namespaceTopics.TryGetValue(namespaceName, out topic))
            return topic;
        else
            return null;
    }

    
DocumentationSet.FindDocumentedTopicByCanonicalName Method (string)

Finds a topic that has Topic.IsDocumented equal to true given a topic's canonical name. Returns null if the topic isn't found.

Parameters

canonicalName

The canonical name of the topic; for example, "Foo.Bar.Abc".

Remarks

If a topic with the given canonical name is found but has Topic.IsDocumented equal to false, then the following occurs: If the topic is a TopicKind.EnumValueTopic and the parent of the topic is a TopicKind.EnumTopic and the parent topic is documented, the parent topic is returned. Otherwise, null is returned.

    public Topic FindDocumentedTopicByCanonicalName(string canonicalName)
    {
        Topic topic;
        if (m_topics.TryGetValue(canonicalName, out topic))
        {
            if (topic.IsDocumented)
                return topic;
            else
            if (topic.CanLinkTo && (topic.ParentTopic != null))
                return topic.ParentTopic;
            else
                return null;
        }
        else
            return null;
    }

    
DocumentationSet.FindDocumentedTopicByCanonicalName Method (string, FindTopicSettings)

Finds a topic that has Topic.IsDocumented equal to true given a topic's canonical name, with specific search conditions. Returns null if the topic isn't found.

Parameters

canonicalName

The canonical name of the topic; for example, "Foo.Bar.Abc".

settings

Settings that control the method.

    public Topic FindDocumentedTopicByCanonicalName(string canonicalName,
        FindTopicSettings settings)
    {
        // look for the topic specified by <canonicalName>; return null if not
        // found
        Topic targetTopic = FindDocumentedTopicByCanonicalName(canonicalName);
        if (targetTopic == null)
            return null;

        // if the caller only wants a type topic, return null if <targetTopic>
        // isn't one
        if (((settings & FindTopicSettings.TypeOnly) != 0) &&
                !targetTopic.IsTypeTopic)
            return null;

        return targetTopic;
    }

    
DocumentationSet.FindDocumentedTopic Method

Finds a topic that has Topic.IsDocumented equal to true given a member name of the sort that might be found in XML comments. Returns null if the topic isn't found.

Parameters

memberName

The qualified or unqualified member name. Examples: "Bar", "Foo.Bar", "Foo<T>.Bar".

contextTopic

The topic containing the member name reference. For example, if contextTopic is "Foo.Abc", i.e. method "Abc" in class "Foo", then if memberName is "Bar" and "Bar" is another method in "Foo" then the return value will refer to topic "Bar".

lineNumber

The line number of the reference, within the source file containing contextTopic.

settings

Settings that control the method.

    public Topic FindDocumentedTopic(string memberName, Topic contextTopic,
        int lineNumber, FindTopicSettings settings)
    {
        // set <canonicalName> to the canonicalized version of <memberName>
        string canonicalName;
        if ((settings & FindTopicSettings.Precanonicalized) != 0)
            canonicalName = memberName; // <memberName> already canonicalized
        else
        {
            canonicalName = Topic.CanonicalizeMemberName(memberName,
                false);
        }

        // keep track of whether we failed due to the referenced topic being
        // the same as <contextTopic>
        bool isSame = false;

        // fail without firing the ReferenceNotFound event if <canonicalName>
        // is a type parameter
        if (contextTopic.TypeParameters != null)
        {
            if (Array.Find(contextTopic.TypeParameters, delegate(TokenList t)
                    {
                        return (t.ToString() == canonicalName);
                    }) != null)
                return null;
        }

        // if specified by the caller, try looking up <canonicalName> within
        // all ancestors (base types, base types of base types, etc.) of T,
        // where T is either <contextTopic>, if <contextTopic> is a type topic,
        // or the type topic containing <contextTopic>
        Topic targetTopic;
        if (((settings & FindTopicSettings.SearchAncestors) != 0) ||
            ((settings & FindTopicSettings.SearchOnlyAncestors) != 0))
        {
            // set <typeTopic> to T (see above)
            Topic typeTopic = contextTopic;
            while ((typeTopic != null) && !typeTopic.IsTypeTopic)
                typeTopic = typeTopic.ParentTopic;

            // if we found T, search ancestors of T
            if (typeTopic != null)
            {
                if (((targetTopic = FindDocumentedTopicInAncestors(
                        canonicalName, contextTopic, lineNumber, typeTopic))
                            != null) &&
                    !IsSame(targetTopic, contextTopic, ref isSame))
                    return targetTopic;
            }

            // if SearchOnlyAncestors is specified, don't look any further
            if ((settings & FindTopicSettings.SearchOnlyAncestors) != 0)
            {
                if ((ReferenceNotFound != null) && !isSame)
                {
                    ReferenceNotFound(canonicalName, contextTopic, lineNumber,
                        settings);
                }
                return null;
            }
        }

        // note: the following is based loosely on section 3.8 of the C#
        // Language Specification 1.2...

        // try looking up <canonicalName> within the enclosing type(s) (if
        // any), i.e. types that <contextTopic> is nested within
        Topic otherTopic = contextTopic;
        TokenList otherName;
        while (true)
        {
#if false
            if ((otherTopic = otherTopic.ParentTopic) == null)
                break;
#endif
            if (otherTopic.TopicKind == TopicKind.NamespaceTopic)
                break;
            if (otherTopic.TypeParameters != null)
            {
                if (Array.Find(otherTopic.TypeParameters, delegate(TokenList t)
                        {
                            // check if <canonicalName> is a type parameter
                            return (t.ToString() == canonicalName);
                        }) != null)
                    return null;
            }
            if ((otherName = otherTopic.CanonicalName) != null)
            {
                if (((targetTopic = FindDocumentedTopicByCanonicalName(
                        otherName + "." + canonicalName,
                        settings)) != null) &&
                    !IsSame(targetTopic, contextTopic, ref isSame))
                    return targetTopic;
            }
#if true
            if ((otherTopic = otherTopic.ParentTopic) == null)
                break;
#endif
        }

        // try looking up <canonicalName> within the namespace of
        // <contextTopic> (if any), and any ancestor namespaces (see section
        // 3.7 of "C# Language Specification 1.2")
        if (((otherTopic = contextTopic.NamespaceTopic) != null) &&
            ((otherName = otherTopic.CanonicalName) != null))
        {
            string namespaceName = otherName.ToString();
            while (true)
            {
                // look up <canonicalName> within <namespaceName>
                if (((targetTopic = FindDocumentedTopicByCanonicalName(
                        namespaceName + "." + canonicalName,
                        settings)) != null) &&
                    !IsSame(targetTopic, contextTopic, ref isSame))
                    return targetTopic;

                // set <namespaceName> to be its own parent
                int ich = namespaceName.LastIndexOf('.');
                if (ich < 0)
                    break;
                namespaceName = namespaceName.Substring(0, ich);
            }
        }

        // try looking up <canonicalName> within aliased namespaces
        Match match, match2 = null;
        if (!(match = match2 =
                s_namespaceAliasQualifierRegex.Match(canonicalName)).Success)
            match = s_aliasPrefixRegex.Match(canonicalName);
        if (match.Success)
        {
            string alias = match.Groups[1].Value;
            string remainder = match.Groups[2].Value;
            if ((alias == "global") && (match2 != null))
            {
                // e.g. "global::MyType"
                if (((targetTopic = FindDocumentedTopicByCanonicalName(
                        remainder, settings)) != null) &&
                    !IsSame(targetTopic, contextTopic, ref isSame))
                    return targetTopic;
            }
            else
            {
                // e.g. "RE.RegularExpression"
                TokenList namespaceOrTypeName =
                    contextTopic.UsingContext.LookupAlias(alias);
                if (namespaceOrTypeName != null)
                {
                    if (((targetTopic = FindDocumentedTopicByCanonicalName(
                            namespaceOrTypeName + "." + remainder,
                            settings)) != null) &&
                        !IsSame(targetTopic, contextTopic, ref isSame))
                        return targetTopic;
                }
            }
        }

        // see if <canonicalName> is a class alias
        TokenList typeName = contextTopic.UsingContext.LookupAlias(
            canonicalName);
        if (typeName != null)
        {
            if (((targetTopic = FindDocumentedTopicByCanonicalName(
                    Topic.CanonicalizeMemberName(typeName, false)
                        .ToString(), settings)) != null) &&
                !IsSame(targetTopic, contextTopic, ref isSame))
                return targetTopic;
        }

        // try looking up <canonicalName> within unaliased namespaces
        foreach (TokenList namespaceName in contextTopic.UsingContext.Unaliased)
        {
            if (((targetTopic = FindDocumentedTopicByCanonicalName(
                    namespaceName + "." + canonicalName,
                    settings)) != null) &&
                !IsSame(targetTopic, contextTopic, ref isSame))
                return targetTopic;
        }

        // try looking up <canonicalName> in the global namespace
        if (((targetTopic = FindDocumentedTopicByCanonicalName(canonicalName,
                settings)) != null) &&
              !IsSame(targetTopic, contextTopic, ref isSame))
            return targetTopic;

        // <canonicalName> not found -- tell the application, unless this is
        // due to the referenced topic being the same as the context topic,
        // which happens normally in cases like "class Foo : IComparable<Foo>"
        if ((ReferenceNotFound != null) && !isSame)
        {
            ReferenceNotFound(canonicalName, contextTopic, lineNumber,
                settings);
        }

        return null;
    }

    
DocumentationSet.FindDocumentedTopicInAncestors Method

Search the base types and ancestors (i.e. base types of base types, and so on) of a given type for a given member. Returns null if the topic isn't found.

Parameters

memberName

The qualified or unqualified member name. Examples: "Bar", "Foo.Bar", "Foo<T>.Bar". Topic.CanonicalizeMemberName(memberName) should have already been called.

contextTopic

The topic containing the member name reference.

lineNumber

The line number of the reference, within the source file containing contextTopic.

typeTopic

All ancestors of typeTopic are searched for memberName.

    Topic FindDocumentedTopicInAncestors(string memberName, Topic contextTopic,
        int lineNumber, Topic typeTopic)
    {
        // do nothing if <typeTpic> has no base types
        if (typeTopic.BaseTypes == null)
            return null;

        // loop once for each base type of <typeTopic>
        foreach (TokenList baseType in typeTopic.BaseTypes)
        {
            // check if <memberName> is a member of <baseType>
            Topic baseTypeTopic = FindDocumentedTopic(baseType.ToString(),
                typeTopic, lineNumber, FindTopicSettings.TypeOnly);
            Topic targetTopic;
            if (baseTypeTopic != null)
            {
                targetTopic = FindDocumentedTopicByCanonicalName(
                    baseTypeTopic + "." + memberName);
                if (targetTopic != null)
                    return targetTopic;
            }

            // recurse
            if (baseTypeTopic != null)
            {
                if ((targetTopic = FindDocumentedTopicInAncestors(memberName,
                        contextTopic, lineNumber, baseTypeTopic)) != null)
                    return targetTopic;
            }
        }

        // not found
        return null;
    }

    
DocumentationSet.BeginUsingSources Method

Indicates that an operation that uses the list of source files is about to begin. At this point, any processing of the list of source files that needs to be done should be done. If that processing was previously performed, nothing is done. Any attempt to add source files after this point generates an exception.

Remarks

Code that renders documentation should call this method before starting the rendering process. Among other things, this method ensures that all topics are given unique HTML file names.

    public void BeginUsingSources()
    {
        // do nothing if this method was previously called
        if (m_begunUsingSources)
            return;
        else
            m_begunUsingSources = true;

        // perform processing on the set of source files that's required before
        // generating documentation etc...

        // create a unique Topic.Name property for each topic; see
        // MakeUniqueTopicName() for more information about the former
        Dictionary<string, List<Topic>> topicsPerBaseName =
            new Dictionary<string, List<Topic>>(1000);
        foreach (Topic topic in Topics)
            MakeUniqueTopicName(topic, topicsPerBaseName);

        // add each topic in <OverloadListTopics> to various lists
        foreach (OverloadListTopic topic in OverloadListTopics)
        {
            if (topic.ParentTopic != null)
                topic.ParentTopic.AddChildTopic(topic);
            if (topic.IsTypeTopic)
                topic.NamespaceTopic.AddTypeTopic(topic);
        }

        // add all topics to <m_topics>, and give each a unique integer Id
        // property; also, see if any two topics have the same <LongTitle>
        Dictionary<string, Topic> duplicateTitles =
            new Dictionary<string,Topic>(500);
        foreach (Topic topic in Topics)
        {
            // add <topic> to <m_topics>
            Topic otherTopic;
            string canonicalName = topic.CanonicalName.ToString();
            if (m_topics.TryGetValue(canonicalName, out otherTopic))
            {
                // fire DuplicateTopicTitle event
                if (DuplicateCanonicalName != null)
                    DuplicateCanonicalName(topic, otherTopic);
            }
            else
                m_topics.Add(canonicalName, topic);

            // give <topic> a unique Id property
            topic.Id = GenerateTopicId();

            // see if <topic> has the same <LongTitle> as another topic
            if (topic.SourceFile == null) // e.g. overload list topic
                continue;
            string longTitle = topic.LongTitle;
            if (duplicateTitles.TryGetValue(longTitle, out otherTopic))
            {
                // fire DuplicateTopicTitle event
                if (DuplicateTopicTitle != null)
                    DuplicateTopicTitle(topic, otherTopic);
            }
            else
                duplicateTitles.Add(longTitle, topic);
        }
    }

    
DocumentationSet.AfterSettingIsDocumented Method

Performs general processing on the list of topics, such as determining the base class of each documented topic, that must occur after the application has set Topic.IsDocumented to true for all topics the applicatin wants documented.

    public void AfterSettingIsDocumented()
    {
        // determine the base class of each documented class topic
        foreach (Topic topic in Topics)
        {
            // skip undocumented topics
            if (!topic.IsDocumented)
                continue;

            // skip topics that don't document classes
            if (topic.TopicKind != TopicKind.ClassTopic)
                continue;

            // skip overload list topics
            if (topic is OverloadListTopic)
                continue;

            // skip topics without base types
            if (topic.BaseTypes == null)
                continue;
            
            // attempt to find the base class among the base types -- there
            // should only be one type among the base types that's a class
            Topic baseClassTopic = null;
            foreach (TokenList baseType in topic.BaseTypes)
            {
                Topic targetTopic = FindDocumentedTopic(baseType.ToString(),
                    topic, 0, FindTopicSettings.TypeOnly);
                if ((targetTopic != null) &&
                    (targetTopic.TopicKind == TopicKind.ClassTopic))
                {
                    if (baseClassTopic != null)
                    {
                        ParsingError(topic.NewParsingException(
                            Resources.CannotDetermineBaseClass,
                            topic.QualifiedMemberName));
                        break;
                    }
                    else
                        baseClassTopic = targetTopic;
                }
            }
            if (baseClassTopic == null)
                continue;

            // set the base class of this topic
            topic.BaseClassTopic = baseClassTopic;

            // for debugging purposes
#if false && DEBUG
            Console.WriteLine("Base class of {0} is {1}", topic,
                baseClassTopic);
#endif
        }
    }

    
DocumentationSet.MakeUniqueTopicName Method

Creates a unique Topic.Name for a given topic. Creates an overload list topic if necessary.

Parameters

topic

The topic to set the Topic.Name property of.

topicsPerBaseName

A dictionary that maps a given Topic.BaseName property to a set of topics that have that same base name.

    void MakeUniqueTopicName(Topic topic,
        Dictionary<string, List<Topic>> topicsPerBaseName)
    {
        string baseName = topic.BaseName;
        List<Topic> topicsForThisBaseName;
        if (!topicsPerBaseName.TryGetValue(baseName, out topicsForThisBaseName))
        {
            // <topic> is the first topic with <baseName> -- create a
            // new list for this base name and add it to the dictionary
            topicsForThisBaseName = new List<Topic>();
            topic.Name = baseName;
            topicsForThisBaseName.Add(topic);
            topicsPerBaseName.Add(baseName, topicsForThisBaseName);
        }
        else
        {
            OverloadListTopic overloadListTopic;
            if (topicsForThisBaseName.Count == 1)
            {
                // <topic> is the second topic with <baseName> -- change
                // the name of the first topic with <baseName> to have a ".1"
                // suffix
                Topic firstTopic = topicsForThisBaseName[0];
                firstTopic.Name = baseName + ".1";

                // create an overload list topic for <baseName>
                overloadListTopic = new OverloadListTopic();
                overloadListTopic.DocumentationSet = this;
                overloadListTopic.NamespaceTopic = firstTopic.NamespaceTopic;
                overloadListTopic.ParentTopic = firstTopic.ParentTopic;
                overloadListTopic.IsTypeTopic = firstTopic.IsTypeTopic;
                overloadListTopic.TopicKind = firstTopic.TopicKind;
                overloadListTopic.NamespaceName = firstTopic.NamespaceName;
                overloadListTopic.MemberName = firstTopic.MemberName;
                overloadListTopic.MethodKind = firstTopic.MethodKind;
                overloadListTopic.PropertyKind = firstTopic.PropertyKind;
                overloadListTopic.Name = baseName;

                // add <firstTopic> to <overloadListTopic>
                overloadListTopic.AddOverloadTopic(firstTopic);
                firstTopic.OverloadListTopic = overloadListTopic;
                m_overloadListTopics.Add(baseName, overloadListTopic);
            }
            else
                overloadListTopic = m_overloadListTopics[baseName];

            // add this topic to <overloadListTopic>
            overloadListTopic.AddOverloadTopic(topic);
            topic.OverloadListTopic = overloadListTopic;

            // this topic gets the next available numeric suffix
            topicsForThisBaseName.Add(topic);
            topic.Name = String.Format("{0}.{1}", baseName,
                topicsForThisBaseName.Count);
        }
    }

    
DocumentationSet.GenerateTopicId Method

Returns an automatically generated topic ID.

    public int GenerateTopicId()
    {
        return ++m_lastGeneratedTopicId;
    }

    
DocumentationSet.CreateExternalObject Method

Loads an assembly DLL, locates a type within it, and creates and returns an instance of that type.

Type Parameters

T

The type to cast the returned object to.

Parameters

assemblyName

The name of the assembly; for example, "MyAssembly". This DLL should be located in the same directory as the application .exe file.

typeName

The fully-qualified name of the type to locate; for example, "MyNamespace.MyType".

Exceptions
Exception type Condition
CreateExternalObjectException

Caused by one of the following conditions:

  • the assembly could not be loaded;
  • the assembly doesn't contain the specified type;
  • an instance of the specified type could not be created;
  • the created instance could not be cast to type T..

    public static T CreateExternalObject<T>(string assemblyName,
            string typeName)
        where T : class
    {
        // load the assembly; set <assembly> to refer to it
        Assembly assembly;
        try
        {
            assembly = Assembly.Load(assemblyName);
        }
        catch (FileNotFoundException ex)
        {
            throw new CreateExternalObjectException(ex,
                Resources.CannotLoadAssembly, assemblyName, ex.Message);
        }

        // load the type; set <type> to refer to it
        Type type = assembly.GetType(typeName);
        if (type == null)
        {
            throw new CreateExternalObjectException(null,
                Resources.TypeNotFoundInAssembly, assemblyName, typeName);
        }

        // set <constructorInfo> to information about the default constructor
        // of <type>
        ConstructorInfo constructorInfo = type.GetConstructor(Type.EmptyTypes);
        if (constructorInfo == null)
        {
            throw new CreateExternalObjectException(null,
                Resources.ConstructorNotFoundInAssembly, assemblyName,
                    typeName);
        }

        // set <obj> to a new instance of <type>
        object obj;
        try
        {
            obj = constructorInfo.Invoke(null);
        }
        catch (TargetInvocationException ex)
        {
            throw new CreateExternalObjectException(ex,
                Resources.ConstructorException, assemblyName, typeName);
        }

        // cast <obj> to type T
        T result = obj as T;
        if (result == null)
        {
            throw new CreateExternalObjectException(null,
                Resources.ExternalObjectWrongType, assemblyName, typeName,
                obj.GetType(), typeof(T));
        }

        // done
        return result;
    }

    //////////////////////////////////////////////////////////////////////////
    // Internal Methods
    //

    
DocumentationSet.FindOrAddNamespaceTopic Method

Returns the NamespaceTopic corresponding to a given namespace name (e.g. "Foo.Bar"). If none is found, one is created and added to NamespaceTopics.

Parameters

namespaceName

The namespace name; for example, "Foo.Bar".

    internal NamespaceTopic FindOrAddNamespaceTopic(string namespaceName)
    {
        NamespaceTopic topic = FindNamespaceTopic(namespaceName);
        if (topic == null)
        {
            topic = new NamespaceTopic(null);
            topic.DocumentationSet = this;
            topic.TopicKind = TopicKind.NamespaceTopic;
            topic.MemberName = namespaceName;
            m_namespaceTopics.Add(namespaceName, topic);
        }
        return topic;
    }

    //////////////////////////////////////////////////////////////////////////
    // Internal Methods
    //

    
DocumentationSet.IsSame Method

Checks if two Topic objects refer to the same topic. to true

Parameters

topic1

The first topic.

topic2

The second topic.

isSame

If the two topics are the same, isSame is set to true. Otherwise, the value of isSame is unchanged by this method.

    bool IsSame(Topic topic1, Topic topic2, ref bool isSame)
    {
        if (topic1 == topic2)
        {
            isSame |= true;
            return true;
        }
        else
            return false;
    }
}

AddingSourceFileEventDelegate Delegate

The type of an event handler for an event which is fired to indicate that a source file is about to be added to the documentation set.

Parameters

sourceFileAbsolutePath

The value of the SourceFile.SourceFileAbsolutePath property of the new SourceFile object.

sourceFileRelativePath

The value of the SourceFile.SourceFileRelativePath property of the new SourceFile object.

Remarks
public delegate void AddingSourceFileEventDelegate(
    string sourceFileAbsolutePath, string sourceFileRelativePath);

AddedSourceFileEventDelegate Delegate

The type of an event handler for an event which is fired to indicate that a source file has been successfully added to the documentation set.

Parameters

sourceFile

The newly-added SourceFile object.

Remarks
public delegate void AddedSourceFileEventDelegate(SourceFile sourceFile);

ParsingErrorEventDelegate Delegate

The type of an event handler for an event which is fired to indicate that an error occurred while trying to parse a source file.

Parameters

exception

Information about the error.

Remarks
public delegate void ParsingErrorEventDelegate(ParsingException exception);

DuplicateTopicTitleEventDelegate Delegate

The type of an event handler for an event which is fired to indicate that two topics have the same Topic.LongTitle. This is typically a warning, displayed to the user -- it may indicate that the same source file was added twice to the documentation set.

Parameters

topic1

The first topic.

topic2

The second topic.

Remarks
public delegate void DuplicateTopicTitleEventDelegate(Topic topic1,
    Topic topic2);

DuplicateCanonicalNameEventDelegate Delegate

The type of an event handler for an event which is fired to indicate that two topics have the same Topic.CanonicalName. This is purely informational -- it does not indicate an error. (Typially it indicates that one or more overload list topics are being generated.)

Parameters

topic1

The first topic.

topic2

The second topic.

Remarks
public delegate void DuplicateCanonicalNameEventDelegate(Topic topic1,
    Topic topic2);

ReferenceNotFoundEventDelegate Delegate

The type of an event handler for an event which is fired to indicate that a topic made a potential reference to another topic which wasn't found. This is typically an informational message, displayed to the user after sorting all such messages and removing duplicates. This event does not necessarily indicate an error.

Parameters

reference

The presumed (but failed) qualified or unqualified member name of the topic being referenced.

contextTopic

The topic containing the reference.

lineNumber

The line number of the reference, within the source file containing contextTopic.

settings

Settings used to resolve the reference.

public delegate void ReferenceNotFoundEventDelegate(string reference,
    Topic contextTopic, int lineNumber, FindTopicSettings settings);

BegunUsingSourcesException Class

Indicates that the application attempt to add a source file to the documention set after an operation that used the collection of source files.

public class BegunUsingSourcesException : Exception
{
    
BegunUsingSourcesException Constructor

Initializes an instance of this class.

Parameters

message

The error message.

    public BegunUsingSourcesException(string message)
        : base(message)
    {
    }
}

CreateExternalObjectException Class

Indicates an error that occurred while attempting to create an instance of an object in another assembly.

Remarks
public class CreateExternalObjectException : Exception
{
    
CreateExternalObjectException Constructor

Initializes an instance of this class.

Parameters

innerException

The inner exception, or null if none.

format

A formatting string for an error message to include with the exception.

args

Formatting arguments for the error message.

    public CreateExternalObjectException(Exception innerException,
            string format, params object[] args) :
        base(String.Format(format, args), innerException)
    {
    }
}

FindTopicSettings Enumeration
Members
Name Description
None

No options specified.

Precanonicalized

Specifies that the input member name is already canonicalized (i.e. no type parameter names, and any trailing set of type parameters is removed), so DocumentationSet.FindDocumentedTopic doesn't need to perform canonicalization.

SearchAncestors

Search ancestor types of the current topic (i.e. base types, base types of base types, etc.) This can be significantly slower.

SearchOnlyAncestors

Search only ancestor types of the current topic (i.e. base types, base types of base types, etc.), i.e. don't search anywhere else.

TypeOnly

Only find topics that document types.

[Flags]
public enum FindTopicSettings
{
    
FindTopicSettings.None Enumeration Value

No options specified.

    None = 0x0,

    
FindTopicSettings.TypeOnly Enumeration Value

Only find topics that document types.

    TypeOnly = 0x1,

    
FindTopicSettings.SearchAncestors Enumeration Value

Search ancestor types of the current topic (i.e. base types, base types of base types, etc.) This can be significantly slower.

    SearchAncestors = 0x2,

    
FindTopicSettings.SearchOnlyAncestors Enumeration Value

Search only ancestor types of the current topic (i.e. base types, base types of base types, etc.), i.e. don't search anywhere else.

    SearchOnlyAncestors = 0x4,

    
FindTopicSettings.Precanonicalized Enumeration Value

Specifies that the input member name is already canonicalized (i.e. no type parameter names, and any trailing set of type parameters is removed), so DocumentationSet.FindDocumentedTopic doesn't need to perform canonicalization.

    Precanonicalized = 0x8,
}

}