예제 #1
0
        /* Function: Remove
         * Removes the contents of an entire set from this one.
         */
        public void Remove(NumberSet setToRemove)
        {
            int position            = 0;
            int setToRemovePosition = 0;

            while (position < usedRanges && setToRemovePosition < setToRemove.usedRanges)
            {
                // Remember that these are structs, so to update the list you have to update the original struct, not this one.
                NumberRange range         = ranges[position];
                NumberRange rangeToRemove = setToRemove.ranges[setToRemovePosition];

                // If the lower bounds is less than the removal lower bounds...
                if (range.Low < rangeToRemove.Low)
                {
                    // If the upper bounds is also less than the removal lower bounds, advance the position.
                    if (range.High < rangeToRemove.Low)
                    {
                        position++;
                    }

                    // The upper bounds is somewhere in or past the removal range.  If it is less than or equal to the removal
                    // upper bounds, we can just truncate this range.
                    else if (range.High <= rangeToRemove.High)
                    {
                        ranges[position].High = rangeToRemove.Low - 1;
                        position++;
                    }

                    // The upper bounds is past the removal range.  Split it.
                    else
                    {
                        InsertAtIndex(position + 1);
                        ranges[position + 1].High = range.High;
                        ranges[position + 1].Low  = rangeToRemove.High + 1;
                        ranges[position].High     = rangeToRemove.Low - 1;

                        position++;
                        setToRemovePosition++;
                    }
                }

                // If the lower bounds is equal to the removal lower bounds...
                else if (range.Low == rangeToRemove.Low)
                {
                    // If the upper bounds is less than or equal to the removal upper bounds, remove the range entirely.
                    if (range.High <= rangeToRemove.High)
                    {
                        RemoveAtIndex(position);
                    }

                    // The upper bounds is greater than the removal upper bounds, truncate the range.
                    else
                    {
                        ranges[position].Low = rangeToRemove.High + 1;
                        setToRemovePosition++;
                    }
                }

                // If the lower bounds is greater than the removal lower bounds...
                else
                {
                    // If the lower bounds is also greater than the removal upper bounds, advance the removal.
                    if (range.Low > rangeToRemove.High)
                    {
                        setToRemovePosition++;
                    }

                    // The removal upper bounds is in or past the range.  If it's greater than or equal to the upper bounds,
                    // remove the range.
                    else if (range.High <= rangeToRemove.High)
                    {
                        RemoveAtIndex(position);
                    }

                    // Since it's less than the upper bounds, truncate the range.
                    else
                    {
                        ranges[position].Low = rangeToRemove.High + 1;
                        setToRemovePosition++;
                    }
                }
            }
        }
예제 #2
0
        /* Function: Add
         * Adds the contents of an entire set from this one.
         */
        public void Add(NumberSet setToAdd)
        {
            int position         = 0;
            int setToAddPosition = 0;

            while (position < usedRanges && setToAddPosition < setToAdd.usedRanges)
            {
                // Remember that these are structs, so to update the list you have to update the original struct, not this one.
                NumberRange range      = ranges[position];
                NumberRange rangeToAdd = setToAdd.ranges[setToAddPosition];

                // If the range starts below or on the range to add...
                if (range.Low <= rangeToAdd.Low)
                {
                    // If the entire range is below the range to add, we can just advance.
                    if (range.High < rangeToAdd.Low - 1)
                    {
                        position++;
                    }

                    // If the entire range to add is within the existing range, we can just advance that.
                    else if (range.High >= rangeToAdd.High)
                    {
                        setToAddPosition++;
                    }

                    // The range to add extends past the existing one.  If it covers the gap between it and the next existing one,
                    // merge them.
                    else if (position + 1 < usedRanges && ranges[position + 1].Low <= rangeToAdd.High + 1)
                    {
                        ranges[position].High = ranges[position + 1].High;
                        RemoveAtIndex(position + 1);
                        // Go through the loop again without advancing since the range to add may merge multiple ranges into
                        // this one.
                    }

                    // There are no more existing ranges or it doesn't cause them to connect.  Extend the existing one.
                    else
                    {
                        ranges[position].High = rangeToAdd.High;
                        setToAddPosition++;

                        // We can advance this too.  The range we just added won't intersect with the next range to add, if there is one.
                        // The range we just altered won't either because it now has the same high value.
                        position++;
                    }
                }

                // If the range starts above the range to add...
                else                 // range.Low > rangeToAdd.Low
                {
                    // If the range to add extends into the existing range, extend it.
                    if (rangeToAdd.High >= range.Low - 1)
                    {
                        ranges[position].Low = rangeToAdd.Low;
                        // Go through the loop again without advancing.
                    }

                    // The range to add is below the existing range, insert it.
                    else
                    {
                        InsertAtIndex(position);
                        ranges[position].Low  = rangeToAdd.Low;
                        ranges[position].High = rangeToAdd.High;

                        position++;
                        setToAddPosition++;
                    }
                }
            }

            // If there's still more ranges left to add, add them to the end.

            while (setToAddPosition < setToAdd.usedRanges)
            {
                NumberRange rangeToAdd = setToAdd.ranges[setToAddPosition];
                position = usedRanges;

                InsertAtIndex(position);
                ranges[position].Low  = rangeToAdd.Low;
                ranges[position].High = rangeToAdd.High;

                setToAddPosition++;
            }
        }
예제 #3
0
 /* Function: ReadNumberSet
  * An extension method to <BinaryFile> which reads a number set from it.  Call with "numberSet = binaryFile.ReadNumberSet();"
  */
 static public NumberSet ReadNumberSet(this BinaryFile binaryFile)
 {
     return(NumberSet.FromBinaryFile(binaryFile));
 }
예제 #4
0
 /* Function: WriteNumberSet
  * An extension method to <BinaryFile> which writes the number set to it.  Call with "binaryFile.WriteNumberSet(numberSet);"
  */
 static public void WriteNumberSet(this BinaryFile binaryFile, NumberSet numberSet)
 {
     numberSet.ToBinaryFile(binaryFile);
 }
예제 #5
0
        // Group: Functions
        // __________________________________________________________________________


        /* Function: Merge
         *
         * Takes a list of <Topics> that come from the same class but multiple source files and rearranges them into a
         * single coherent list.  Some topics may be removed or merged with others.  The original topic list will be changed.
         *
         * Each file's topics should appear consecutively in the list and ideally in source order.  The order of the files is not
         * important but should ideally be consistent from one run to the next.
         *
         * It's possible for this function to reduce the number of topics to zero.  For example, if defining classes with a list
         * topic, the list topic itself will be removed.  You should be able to handle this and treat it as if the topic list had
         * no content.
         */
        public static void Merge(ref List <Topic> topics, Engine.Instance engineInstance)
        {
            try
            {
                var files        = engineInstance.Files;
                var commentTypes = engineInstance.CommentTypes;


                // Filter out any list topics that define members of a hierarchy.  If someone documents classes as part of a list,
                // we only want pages for the individual members, not the list topic.

                for (int i = 0; i < topics.Count; /* no auto-increment */)
                {
                    bool remove = false;

                    if (topics[i].IsList)
                    {
                        var commentType = commentTypes.FromID(topics[i].CommentTypeID);

                        if (commentType.InClassHierarchy || commentType.InDatabaseHierarchy)
                        {
                            remove = true;
                        }
                    }

                    if (remove)
                    {
                        topics.RemoveAt(i);
                    }
                    else
                    {
                        i++;
                    }
                }

                if (topics.Count == 0)
                {
                    return;
                }


                // Validate that they're all from the same class and that all of a file's topics are consecutive.

                                #if DEBUG
                int         classID     = topics[0].ClassID;
                ClassString classString = topics[0].ClassString;

                if (classID == 0)
                {
                    throw new Exception("All topics passed to Merge() must have a class ID set.");
                }

                int currentFileID = topics[0].FileID;
                IDObjects.NumberSet previousFileIDs = new IDObjects.NumberSet();

                for (int i = 1; i < topics.Count; i++)
                {
                    if (topics[i].ClassID != classID || topics[i].ClassString != classString)
                    {
                        throw new Exception("All topics passed to Merge() must have the same class string and ID.");
                    }

                    if (topics[i].FileID != currentFileID)
                    {
                        if (previousFileIDs.Contains(topics[i].FileID))
                        {
                            throw new Exception("Merge() requires all topics that share a file ID be consecutive.");
                        }

                        previousFileIDs.Add(currentFileID);
                        currentFileID = topics[i].FileID;
                    }
                }
                                #endif


                // See if there's multiple source files by comparing the first and last topics' file IDs.  If there's only one source file we'll be
                // able to skip some steps.

                bool multipleSourceFiles = (topics[0].FileID != topics[topics.Count - 1].FileID);


                List <Topic> remainingTopics = null;

                if (multipleSourceFiles)
                {
                    // First we have to sort the topic list by file name.  This ensures that the merge occurs consistently no matter
                    // what order the files in the list are in or how the file IDs were assigned.

                    List <Topic> sortedTopics = new List <Topic>(topics.Count);

                    do
                    {
                        var lowestFile      = files.FromID(topics[0].FileID);
                        var lowestFileIndex = 0;
                        var lastCheckedID   = lowestFile.ID;

                        for (int i = 1; i < topics.Count; i++)
                        {
                            if (topics[i].FileID != lastCheckedID)
                            {
                                var file = files.FromID(topics[i].FileID);

                                if (Path.Compare(file.FileName, lowestFile.FileName) < 0)
                                {
                                    lowestFile      = file;
                                    lowestFileIndex = i;
                                }

                                lastCheckedID = file.ID;
                            }
                        }

                        int count = 0;
                        for (int i = lowestFileIndex; i < topics.Count && topics[i].FileID == lowestFile.ID; i++)
                        {
                            count++;
                        }

                        sortedTopics.AddRange(topics.GetRange(lowestFileIndex, count));
                        topics.RemoveRange(lowestFileIndex, count);
                    }while (topics.Count > 0);


                    // The topics are all in sortedTopics now, and "topics" is empty.  For clarity going forward, let's rename sortedTopics
                    // to remainingTopics, since we have to move them back into topics now.

                    remainingTopics = sortedTopics;
                    sortedTopics    = null;                   // for safety


                    // Find the best topic to serve as the class definition.

                    Topic bestDefinition      = remainingTopics[0];
                    int   bestDefinitionIndex = 0;

                    for (int i = 1; i < remainingTopics.Count; i++)
                    {
                        Topic topic = remainingTopics[i];

                        if (topic.DefinesClass && engineInstance.Links.IsBetterClassDefinition(bestDefinition, topic))
                        {
                            bestDefinition      = topic;
                            bestDefinitionIndex = i;
                        }
                    }


                    // Copy the best definition in and everything that follows it in the file.  That will serve as the base for merging.

                    int bestDefinitionTopicCount = 1;

                    for (int i = bestDefinitionIndex + 1; i < remainingTopics.Count && remainingTopics[i].FileID == bestDefinition.FileID; i++)
                    {
                        bestDefinitionTopicCount++;
                    }

                    topics.AddRange(remainingTopics.GetRange(bestDefinitionIndex, bestDefinitionTopicCount));
                    remainingTopics.RemoveRange(bestDefinitionIndex, bestDefinitionTopicCount);
                }                         // if multipleSourceFiles


                // Make sure the first topic isn't embedded so that classes documented in lists still appear correctly.

                if (topics[0].IsEmbedded)
                {
                    topics[0]            = topics[0].Duplicate();
                    topics[0].IsEmbedded = false;
                }


                // Delete all the other topics that define the class.  We don't need them anymore.

                for (int i = 1; i < topics.Count; /* don't auto increment */)
                {
                    if (topics[i].DefinesClass)
                    {
                        topics.RemoveAt(i);
                    }
                    else
                    {
                        i++;
                    }
                }

                if (multipleSourceFiles)
                {
                    for (int i = 0; i < remainingTopics.Count; /* don't auto increment */)
                    {
                        if (remainingTopics[i].DefinesClass)
                        {
                            remainingTopics.RemoveAt(i);
                        }
                        else
                        {
                            i++;
                        }
                    }


                    // Now merge the remaining topics into the main list.

                    // We loop through this process one file at a time in case some topics have to be merged that aren't present in the
                    // base we chose.  For example, File A has FunctionA but not FunctionZ.  File B and File C both have FunctionZ and
                    // they need to be merged with each other.  If we only did one pass comparing all the remaining topics to the base
                    // we wouldn't see that.

                    while (remainingTopics.Count > 0)
                    {
                        int fileID = remainingTopics[0].FileID;


                        // First pick out and merge duplicates.  This is used for things like combining header and source definitions in C++.

                        for (int remainingTopicIndex = 0;
                             remainingTopicIndex < remainingTopics.Count && remainingTopics[remainingTopicIndex].FileID == fileID;
                             /* no auto-increment */)
                        {
                            var remainingTopic = remainingTopics[remainingTopicIndex];

                            // We're ignoring group topics for now.  They stay in remainingTopics.
                            if (remainingTopic.IsGroup)
                            {
                                remainingTopicIndex++;
                                continue;
                            }

                            int embeddedTopicCount = CountEmbeddedTopics(remainingTopics, remainingTopicIndex);


                            // If we're merging enums, the one with the most embedded topics (documented values) wins.  In practice one
                            // should be documented and one shouldn't be, so this should usually be any number versus zero.

                            if (remainingTopic.IsEnum)
                            {
                                int duplicateIndex = FindDuplicateTopic(remainingTopic, topics, engineInstance);

                                if (duplicateIndex == -1)
                                {
                                    remainingTopicIndex += 1 + embeddedTopicCount;
                                }
                                else
                                {
                                    int duplicateEmbeddedTopicCount = CountEmbeddedTopics(topics, duplicateIndex);

                                    if (embeddedTopicCount > duplicateEmbeddedTopicCount ||
                                        (embeddedTopicCount == duplicateEmbeddedTopicCount &&
                                         engineInstance.Links.IsBetterTopicDefinition(remainingTopic, topics[duplicateIndex]) == false))
                                    {
                                        topics.RemoveRange(duplicateIndex, 1 + duplicateEmbeddedTopicCount);
                                        topics.InsertRange(duplicateIndex, remainingTopics.GetRange(remainingTopicIndex, 1 + embeddedTopicCount));
                                    }

                                    remainingTopics.RemoveRange(remainingTopicIndex, 1 + embeddedTopicCount);
                                }
                            }


                            // If it's not an enum and it's a standalone topic, the one with the best score wins.

                            else if (embeddedTopicCount == 0)
                            {
                                int duplicateIndex = FindDuplicateTopic(remainingTopic, topics, engineInstance);

                                if (duplicateIndex == -1)
                                {
                                    remainingTopicIndex++;
                                }
                                else if (engineInstance.Links.IsBetterTopicDefinition(remainingTopic, topics[duplicateIndex]) == false)
                                {
                                    if (topics[duplicateIndex].IsEmbedded)
                                    {
                                        // Just leave them both in
                                        remainingTopicIndex++;
                                    }
                                    else
                                    {
                                        topics[duplicateIndex] = remainingTopic;
                                        remainingTopics.RemoveAt(remainingTopicIndex);
                                    }
                                }
                                else
                                {
                                    remainingTopics.RemoveAt(remainingTopicIndex);
                                }
                            }


                            // If it's not an enum and we're at a list topic, leave it for now.  We only want to remove it if EVERY member has
                            // a better definition, and those definitions can be in different files, so wait until the list is fully combined.

                            else
                            {
                                remainingTopicIndex += 1 + embeddedTopicCount;
                            }
                        }


                        // Generate groups from the topic lists.

                        // Start at 1 to skip the class topic.
                        var topicGroups = GetTopicGroups(topics, startingIndex: 1);

                        var remainingTopicGroups = GetTopicGroups(remainingTopics, limitToFileID: fileID);


                        // Now merge groups.

                        int remainingGroupIndex = 0;
                        while (remainingGroupIndex < remainingTopicGroups.Groups.Count)
                        {
                            var  remainingGroup = remainingTopicGroups.Groups[remainingGroupIndex];
                            bool merged         = false;

                            // If the group is empty because all its members were merged as duplicates, just delete it.
                            if (remainingGroup.IsEmpty)
                            {
                                remainingTopicGroups.RemoveGroupAndTopics(remainingGroupIndex);
                                merged = true;
                            }

                            // If it matches the title of an existing group, move its members to the end of the existing group.
                            else if (remainingGroup.Title != null)
                            {
                                for (int groupIndex = 0; groupIndex < topicGroups.Groups.Count; groupIndex++)
                                {
                                    if (topicGroups.Groups[groupIndex].Title == remainingGroup.Title)
                                    {
                                        remainingTopicGroups.MergeGroupInto(remainingGroupIndex, topicGroups, groupIndex);
                                        merged = true;
                                        break;
                                    }
                                }

                                // If the group had a title but didn't match one on the other list, insert it after the last group of the same
                                // dominant type so function groups stay with other function groups, variable groups stay with other variable
                                // groups, etc.
                                if (merged == false)
                                {
                                    int bestMatchIndex = -1;

                                    // Walk the list backwards because we want it to be after the last group of the type, not the first.
                                    for (int i = topicGroups.Groups.Count - 1; i >= 0; i--)
                                    {
                                        if (topicGroups.Groups[i].DominantTypeID == remainingGroup.DominantTypeID)
                                        {
                                            bestMatchIndex = i;
                                            break;
                                        }
                                    }

                                    if (bestMatchIndex == -1)
                                    {
                                        // Just add the group to the end if nothing matches.
                                        remainingTopicGroups.MoveGroupTo(remainingGroupIndex, topicGroups);
                                    }
                                    else
                                    {
                                        remainingTopicGroups.MoveGroupTo(remainingGroupIndex, topicGroups, bestMatchIndex + 1);
                                    }

                                    merged = true;
                                }
                            }

                            if (!merged)
                            {
                                remainingGroupIndex++;
                            }
                        }


                        // Now we're left with topics that are not in titled groups, meaning the file itself had no group topics or there were
                        // topics that appeared before the first one.  See if the base contains any titled groups.

                        bool hasGroupsWithTitles = false;

                        foreach (var group in topicGroups.Groups)
                        {
                            if (group.Title != null)
                            {
                                hasGroupsWithTitles = true;
                                break;
                            }
                        }


                        // If there's no titles we can just append the remaining topics as is.

                        if (hasGroupsWithTitles == false)
                        {
                            int fileIDLimit = 0;

                            while (fileIDLimit < remainingTopics.Count && remainingTopics[fileIDLimit].FileID == fileID)
                            {
                                fileIDLimit++;
                            }

                            if (fileIDLimit > 0)
                            {
                                topics.AddRange(remainingTopics.GetRange(0, fileIDLimit));
                                remainingTopics.RemoveRange(0, fileIDLimit);
                            }
                        }


                        // If there are titled groups, see if we can add them to the end of existing groups.  However, only do
                        // this if TitleMatchesType is set.  It's okay to put random functions into the group "Functions" but
                        // not into something more specific.  If there aren't appropriate groups to do this with, create new ones.

                        else
                        {
                            while (remainingTopics.Count > 0 && remainingTopics[0].FileID == fileID)
                            {
                                int type = remainingTopics[0].CommentTypeID;
                                int matchingGroupIndex = -1;

                                for (int i = topicGroups.Groups.Count - 1; i >= 0; i--)
                                {
                                    if (topicGroups.Groups[i].DominantTypeID == type &&
                                        topicGroups.Groups[i].TitleMatchesType)
                                    {
                                        matchingGroupIndex = i;
                                        break;
                                    }
                                }

                                // Create a new group if there's no existing one we can use.
                                if (matchingGroupIndex == -1)
                                {
                                    Topic generatedTopic = new Topic(engineInstance.CommentTypes);
                                    generatedTopic.TopicID       = 0;
                                    generatedTopic.Title         = engineInstance.CommentTypes.FromID(type).PluralDisplayName;
                                    generatedTopic.Symbol        = SymbolString.FromPlainText_NoParameters(generatedTopic.Title);
                                    generatedTopic.ClassString   = topics[0].ClassString;
                                    generatedTopic.ClassID       = topics[0].ClassID;
                                    generatedTopic.CommentTypeID = engineInstance.CommentTypes.IDFromKeyword("group", topics[0].LanguageID);
                                    generatedTopic.FileID        = topics[0].FileID;
                                    generatedTopic.LanguageID    = topics[0].LanguageID;

                                    // In case there's nothing that defines the "group" keyword.
                                    if (generatedTopic.CommentTypeID != 0)
                                    {
                                        topicGroups.Topics.Add(generatedTopic);
                                        topicGroups.CreateGroup(topicGroups.Topics.Count - 1, 1);
                                    }

                                    matchingGroupIndex = topicGroups.Groups.Count - 1;
                                }

                                do
                                {
                                    int topicsToMove = 1 + CountEmbeddedTopics(remainingTopics, 0);

                                    while (topicsToMove > 0)
                                    {
                                        topicGroups.AppendToGroup(matchingGroupIndex, remainingTopics[0]);
                                        remainingTopics.RemoveAt(0);
                                        topicsToMove--;
                                    }
                                }while (remainingTopics.Count > 0 && remainingTopics[0].CommentTypeID == type);
                            }
                        }
                    }


                    // Now that everything's merged into one list, make another pass to merge list topics.

                    for (int topicIndex = 0; topicIndex < topics.Count; /* no auto-increment */)
                    {
                        var topic = topics[topicIndex];

                        // Ignore group topics
                        if (topic.IsGroup)
                        {
                            topicIndex++;
                            continue;
                        }

                        int embeddedTopicCount = CountEmbeddedTopics(topics, topicIndex);

                        // Ignore single topics and enums.  Enums have embedded topics but we already handled them earlier.
                        if (embeddedTopicCount == 0 || topic.IsEnum)
                        {
                            topicIndex += 1 + embeddedTopicCount;
                            continue;
                        }


                        // If we're here we're at a list topic.  Compare its members with every other member in the list.  Remove standalone
                        // topics if the list contains a better definition, but only remove the list if EVERY member has a better definition
                        // somewhere else.  If only some do we'll leave in the whole thing and have duplicates instead of trying to pluck out
                        // individual embedded topics.

                        bool embeddedContainsBetterDefinitions = false;
                        bool embeddedContainsNonDuplicates     = false;

                        for (int embeddedTopicIndex = topicIndex + 1;
                             embeddedTopicIndex < topicIndex + 1 + embeddedTopicCount;
                             embeddedTopicIndex++)
                        {
                            var embeddedTopic         = topics[embeddedTopicIndex];
                            var embeddedTopicLanguage = engineInstance.Languages.FromID(embeddedTopic.LanguageID);
                            var foundDuplicate        = false;

                            for (int potentialDuplicateTopicIndex = 0; potentialDuplicateTopicIndex < topics.Count; /* no auto-increment */)
                            {
                                /* Skip ones in the list topic */
                                if (potentialDuplicateTopicIndex == topicIndex)
                                {
                                    potentialDuplicateTopicIndex += 1 + embeddedTopicCount;
                                    continue;
                                }

                                var potentialDuplicateTopic = topics[potentialDuplicateTopicIndex];

                                if (embeddedTopicLanguage.Parser.IsSameCodeElement(embeddedTopic, potentialDuplicateTopic))
                                {
                                    foundDuplicate = true;

                                    // If the current embedded topic is the better definition
                                    if (engineInstance.Links.IsBetterTopicDefinition(potentialDuplicateTopic, embeddedTopic))
                                    {
                                        embeddedContainsBetterDefinitions = true;

                                        // If the duplicate is also embedded, leave it alone.  Either the duplicate is going to be allowed to exist
                                        // because neither list can be completely removed, or it will be removed later when its own list is checked
                                        // for duplicates.
                                        if (potentialDuplicateTopic.IsEmbedded)
                                        {
                                            potentialDuplicateTopicIndex++;
                                        }

                                        // If the duplicate is not embedded we can remove it.
                                        else
                                        {
                                            topics.RemoveAt(potentialDuplicateTopicIndex);

                                            if (potentialDuplicateTopicIndex < topicIndex)
                                            {
                                                topicIndex--;
                                                embeddedTopicIndex--;
                                            }
                                        }
                                    }

                                    // If the potential duplicate is the better definition.  We don't need to do anything here because we're just
                                    // looking to see if all of them have better definitions elsewhere, which can be determined by whether this
                                    // group contains any better definitions or non-duplicates.
                                    else
                                    {
                                        potentialDuplicateTopicIndex++;
                                    }
                                }

                                // Not the same code element
                                else
                                {
                                    potentialDuplicateTopicIndex++;
                                }
                            }

                            if (!foundDuplicate)
                            {
                                embeddedContainsNonDuplicates = true;
                            }
                        }


                        // Now that we've checked every embedded topic against every other topic, remove the entire list only if EVERY
                        // member has a better definition somewhere else, which is the same as saying it doesn't contain any better
                        // topic definitions or non-duplicates.

                        if (embeddedContainsBetterDefinitions == false && embeddedContainsNonDuplicates == false)
                        {
                            topics.RemoveRange(topicIndex, 1 + embeddedTopicCount);
                        }
                        else
                        {
                            topicIndex += 1 + embeddedTopicCount;
                        }
                    }
                }                          // if multipleSourceFiles


                // Now that everything's merged, delete any empty groups.  We do this on the main group list for consistency,
                // since we were doing it on the remaining group list during merging.  Also, there may be new empty groups after
                // merging the list topics.

                // Start at 1 to skip the class topic.
                var groupedTopics = GetTopicGroups(topics, startingIndex: 1);

                for (int i = 0; i < groupedTopics.Groups.Count; /* don't auto increment */)
                {
                    if (groupedTopics.Groups[i].IsEmpty)
                    {
                        groupedTopics.RemoveGroupAndTopics(i);
                    }
                    else
                    {
                        i++;
                    }
                }
            }

            catch (Exception e)
            {
                // Build a message to show the class we crashed on
                if (topics != null && topics.Count >= 1 && topics[0].ClassString != null)
                {
                    var           topic = topics[0];
                    StringBuilder task  = new StringBuilder("Building class view for");

                    // Hierarchy
                    if (topic.ClassString.Hierarchy == Hierarchy.Database)
                    {
                        task.Append(" database");
                    }
                    else
                    {
                        // Language name
                        var language = (topics[0].LanguageID > 0 ? engineInstance.Languages.FromID(topics[0].LanguageID) : null);

                        if (language == null)
                        {
                            task.Append(" language ID " + topics[0].LanguageID + " class");
                        }
                        else
                        {
                            task.Append(" " + language.Name + " class");
                        }
                    }

                    // Class name
                    task.Append(" " + topic.ClassString.Symbol.FormatWithSeparator('.'));

                    e.AddNaturalDocsTask(task.ToString());
                }

                throw;
            }
        }