<html><head><meta name="color-scheme" content="light dark"></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;">###############################################################################
#
#   Package: NaturalDocs::SourceDB
#
###############################################################################
#
#   SourceDB is an experimental package meant to unify the tracking of various elements in the source code.
#
#   Requirements:
#
#       - All extension packages must call &lt;RegisterExtension()&gt; before they can be used.
#
#
#   Architecture: The Idea
#
#       For quite a while Natural Docs only needed &lt;SymbolTable&gt;.  However, 1.3 introduced the &lt;ClassHierarchy&gt; package
#       which duplicated some of its functionality to track classes and parent references.  1.4 now needs &lt;ImageReferenceTable&gt;,
#       so this package was an attempt to isolate the common functionality so the wheel doesn't have to keep being rewritten as
#       the scope of Natural Docs expands.
#
#       SourceDB is designed around &lt;Extensions&gt; and items.  The purposefully vague "items" are anything in the source code
#       that we need to track the definitions of.  Extensions are the packages to track them, only they're derived from
#       &lt;NaturalDocs::SourceDB::Extension&gt; and registered with this package instead of being free standing and duplicating
#       functionality such as watched files.
#
#       The architecture on this package isn't comprehensive yet.  As more extensions are added or previously made free standing
#       packages are migrated to it it will expand to encompass them.  However, it's still experimental so this concept may
#       eventually be abandoned for something better instead.
#
#
#   Architecture: Assumptions
#
#       SourceDB is built around certain assumptions.
#
#       One item per file:
#
#           SourceDB assumes that only the first item per file with a particular item string is relevant.  For example, if two functions
#           have the exact same name, there's no way to link to the second one either in HTML or internally so it doesn't matter for
#           our purposes.  Likewise, if two references are exactly the same they go to the same target, so it doesn't matter whether
#           there's one or two or a thousand.  All that matters is that at least one reference exists in this file because you only need
#           to determine whether the entire file gets rebuilt.  If two items are different in some meaningful way, they should generate
#           different item strings.
#
#       Watched file parsing:
#
#           SourceDB assumes the parse method is that the information that was stored from Natural Docs' previous run is loaded, a
#           file is watched, that file is reparsed, and then &lt;AnalyzeWatchedFileChanges()&gt; is called.  When the file is reparsed all
#           items within it are added the same as if the file was never parsed before.
#
#           If there's a new item this time around, that's fine no matter what.  However, a changed item wouldn't normally be
#           recorded because the previous run's definition is seen as the first one and subsequent ones are ignored.  Also, deleted
#           items would normally not be recorded either because we're only adding.
#
#           The watched file method fixes this because everything is also added to a second, clean database specifically for the
#           watched file.  Because it starts clean, it always gets the first definition from the current parse which can then be
#           compared to the original by &lt;AnalyzeWatchedFileChanges()&gt;.  Because it starts clean you can also compare it to the
#           main database to see if anything was deleted, because it would appear in the main database but not the watched one.
#
#           This means that functions like &lt;ChangeDefinition()&gt; and &lt;DeleteDefinition()&gt; should only be called by
#           &lt;AnalyzeWatchedFileChanges()&gt;.  Externally only &lt;AddDefinition()&gt; should be called.  &lt;DeleteItem()&gt; is okay to be
#           called externally because entire items aren't managed by the watched file database, only definitions.
#
#
###############################################################################

# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
# Natural Docs is licensed under the GPL

use strict;
use integer;


use NaturalDocs::SourceDB::Extension;
use NaturalDocs::SourceDB::Item;
use NaturalDocs::SourceDB::ItemDefinition;
use NaturalDocs::SourceDB::File;
use NaturalDocs::SourceDB::WatchedFileDefinitions;


package NaturalDocs::SourceDB;


###############################################################################
# Group: Types


#
#   Type: ExtensionID
#
#   A unique identifier for each &lt;NaturalDocs::SourceDB&gt; extension as given out by &lt;RegisterExtension()&gt;.
#



###############################################################################
# Group: Variables


#
#   array: extensions
#
#   An array of &lt;NaturalDocs::SourceDB::Extension&gt;-derived extensions, as added with &lt;RegisterExtension()&gt;.  The indexes
#   are the &lt;ExtensionIDs&gt; and the values are package references.
#
my @extensions;

#
#   array: extensionUsesDefinitionObjects
#
#   An array where the indexes are &lt;ExtensionIDs&gt; and the values are whether that extension uses its own definition class
#   derived from &lt;NaturalDocs::SourceDB::ItemDefinition&gt; or it just tracks their existence.
#
my @extensionUsesDefinitionObjects;



#
#   array: items
#
#   The array of source items.  The &lt;ExtensionIDs&gt; are the indexes, and the values are hashrefs mapping the item
#   string to &lt;NaturalDocs::SourceDB::Item&gt;-derived objects.  Hashrefs may be undef.
#
my @items;


#
#   hash: files
#
#   A hashref mapping source &lt;FileNames&gt; to &lt;NaturalDocs::SourceDB::Files&gt;.
#
my %files;


#
#   object: watchedFile
#
#   When a file is being watched for changes, will be a &lt;NaturalDocs::SourceDB::File&gt; for that file.  Is undef otherwise.
#
#   When the file is parsed, items are added to both this and the version in &lt;files&gt;.  Thus afterwards we can compare the two to
#   see if any were deleted since the last time Natural Docs was run, because they would be in the &lt;files&gt; version but not this
#   one.
#
my $watchedFile;


#
#   string: watchedFileName
#
#   When a file is being watched for changes, will be the &lt;FileName&gt; of the file being watched.  Is undef otherwise.
#
my $watchedFileName;


#
#   object: watchedFileDefinitions
#
#   When a file is being watched for changes, will be a &lt;NaturalDocs::SourceDB::WatchedFileDefinitions&gt; object.  Is undef
#   otherwise.
#
#   When the file is parsed, items are added to both this and the version in &lt;items&gt;.  Since only the first definition is kept, this
#   will always have the definition info from the file whereas the version in &lt;items&gt; will have the first definition as of the last time
#   Natural Docs was run.  Thus they can be compared to see if the definitions of items that existed the last time around have
#   changed.
#
my $watchedFileDefinitions;



###############################################################################
# Group: Extension Functions


#
#   Function: RegisterExtension
#
#   Registers a &lt;NaturalDocs::SourceDB::Extension&gt;-derived package and returns a unique &lt;ExtensionID&gt; for it.  All extensions
#   must call this before they can be used.
#
#   Registration Order:
#
#       The order in which extensions register is important.  Whenever possible, items are added in the order their extensions
#       registered.  However, items are changed and deleted in the reverse order.  Take advantage of this to minimize
#       churn between extensions that are dependent on each other.
#
#       For example, when symbols are added or deleted they may cause references to be retargeted and thus their files need to
#       be rebuilt.  However, adding or deleting references never causes the symbols' files to be rebuilt.  So it makes sense that
#       symbols should be created before references, and that references should be deleted before symbols.
#
#   Parameters:
#
#       extension - The package or object of the extension.  Must be derived from &lt;NaturalDocs::SourceDB::Extension&gt;.
#       usesDefinitionObjects - Whether the extension uses its own class derived from &lt;NaturalDocs::SourceDB::ItemDefinition&gt;
#                                         or simply tracks each definitions existence.
#
#   Returns:
#
#       An &lt;ExtensionID&gt; unique to the extension.  This should be saved because it's required in functions such as &lt;AddItem()&gt;.
#
sub RegisterExtension #(package extension, bool usesDefinitionObjects) =&gt; ExtensionID
    {
    my ($self, $extension, $usesDefinitionObjects) = @_;

    push @extensions, $extension;
    push @extensionUsesDefinitionObjects, $usesDefinitionObjects;

    return scalar @extensions - 1;
    };




###############################################################################
# Group: File Functions


#
#   Function: Load
#
#   Loads the data of the source database and all the extensions.  Will call &lt;NaturalDocs::SourceDB::Extension-&gt;Load()&gt; for
#   all of them, unless there's a situation where all the source files are going to be reparsed anyway in which case it's not needed.
#
sub Load
    {
    my $self = shift;

    # No point loading if RebuildData is set.
    if (!NaturalDocs::Settings-&gt;RebuildData())
        {
        # If any load fails, stop loading the rest and just reparse all the source files.
        my $success = 1;

        for (my $extension = 0; $extension &lt; scalar @extensions &amp;&amp; $success; $extension++)
            {
            $success = $extensions[$extension]-&gt;Load();
            };

        if (!$success)
            {  NaturalDocs::Project-&gt;ReparseEverything();  };
        };
    };


#
#   Function: Save
#
#   Saves the data of the source database and all its extensions.  Will call &lt;NaturalDocs::SourceDB::Extension-&gt;Save()&gt; for all
#   of them.
#
sub Save
    {
    my $self = shift;

    for (my $extension = scalar @extensions - 1; $extension &gt;= 0; $extension--)
        {
        $extensions[$extension]-&gt;Save();
        };
    };


#
#   Function: PurgeDeletedSourceFiles
#
#   Removes all data associated with deleted source files.
#
sub PurgeDeletedSourceFiles
    {
    my $self = shift;

    my $filesToPurge = NaturalDocs::Project-&gt;FilesToPurge();

    # Extension is the outermost loop because we want the extensions added last to have their definitions removed first to cause
    # the least amount of churn between interdependent extensions.
    for (my $extension = scalar @extensions - 1; $extension &gt;= 0; $extension--)
        {
        foreach my $file (keys %$filesToPurge)
            {
            if (exists $files{$file})
                {
                my @items = $files{$file}-&gt;ListItems($extension);

                foreach my $item (@items)
                    {
                    $self-&gt;DeleteDefinition($extension, $item, $file);
                    };
                }; # file exists
            }; # each file
        }; # each extension
    };





###############################################################################
# Group: Item Functions


#
#   Function: AddItem
#
#   Adds the passed item to the database.  This will not work if the item string already exists.  The item added should *not*
#   already have definitions attached.  Only use this to add blank items and then call &lt;AddDefinition()&gt; instead.
#
#   Parameters:
#
#       extension - An &lt;ExtensionID&gt;.
#       itemString - The string serving as the item identifier.
#       item - An object derived from &lt;NaturalDocs::SourceDB::Item&gt;.
#
#   Returns:
#
#       Whether the item was added, that is, whether it was the first time this item was added.
#
sub AddItem #(ExtensionID extension, string itemString, NaturalDocs::SourceDB::Item item) =&gt; bool
    {
    my ($self, $extension, $itemString, $item) = @_;

    if (!defined $items[$extension])
        {  $items[$extension] = { };  };

    if (!exists $items[$extension]-&gt;{$itemString})
        {
        if ($item-&gt;HasDefinitions())
            {  die "Tried to add an item to SourceDB that already had definitions.";  };

        $items[$extension]-&gt;{$itemString} = $item;
        return 1;
        };

    return 0;
    };


#
#   Function: GetItem
#
#   Returns the &lt;NaturalDocs::SourceDB::Item&gt;-derived object for the passed &lt;ExtensionID&gt; and item string, or undef if there
#   is none.
#
sub GetItem #(ExtensionID extension, string itemString) =&gt; bool
    {
    my ($self, $extensionID, $itemString) = @_;

    if (defined $items[$extensionID])
        {  return $items[$extensionID]-&gt;{$itemString};  }
    else
        {  return undef;  };
    };


#
#   Function: DeleteItem
#
#   Deletes the record of the passed &lt;ExtensionID&gt; and item string.  Do *not* delete items that still have definitions.  Use
#   &lt;DeleteDefinition()&gt; first.
#
#   Parameters:
#
#       extension - The &lt;ExtensionID&gt;.
#       itemString - The item's identifying string.
#
#   Returns:
#
#       Whether it was successful, meaning whether an entry existed for it.
#
sub DeleteItem #(ExtensionID extension, string itemString) =&gt; bool
    {
    my ($self, $extension, $itemString) = @_;

    if (defined $items[$extension] &amp;&amp; exists $items[$extension]-&gt;{$itemString})
        {
        if ($items[$extension]-&gt;{$itemString}-&gt;HasDefinitions())
            {  die "Tried to delete an item from SourceDB that still has definitions.";  };

        delete $items[$extension]-&gt;{$itemString};
        return 1;
        }
    else
        {  return 0;  };
    };


#
#   Function: HasItem
#
#   Returns whether there is an item defined for the passed &lt;ExtensionID&gt; and item string.
#
sub HasItem #(ExtensionID extension, string itemString) =&gt; bool
    {
    my ($self, $extension, $itemString) = @_;

    if (defined $items[$extension])
        {  return (exists $items[$extension]-&gt;{$itemString});  }
    else
        {  return 0;  };
    };


#
#   Function: GetAllItemsHashRef
#
#   Returns a hashref of all the items defined for an extension.  *Do not change the contents.*  The keys are the item strings and
#   the values are &lt;NaturalDocs::SourceDB::Items&gt; or derived classes.
#
sub GetAllItemsHashRef #(ExtensionID extension) =&gt; hashref
    {
    my ($self, $extension) = @_;
    return $items[$extension];
    };



###############################################################################
# Group: Definition Functions


#
#   Function: AddDefinition
#
#   Adds a definition to an item.  Assumes the item was already created with &lt;AddItem()&gt;.  If there's already a definition for this
#   file in the item, the new definition will be ignored.
#
#   Parameters:
#
#       extension - The &lt;ExtensionID&gt;.
#       itemString - The item string.
#       file - The &lt;FileName&gt; the definition is in.
#       definition - If you're using a custom &lt;NaturalDocs::SourceDB::ItemDefinition&gt; class, you must include an object for it here.
#                       Otherwise this parameter is ignored.
#
#   Returns:
#
#       Whether the definition was added, which is to say, whether this was the first definition for the passed &lt;FileName&gt;.
#
sub AddDefinition #(ExtensionID extension, string itemString, FileName file, optional NaturalDocs::SourceDB::ItemDefinition definition) =&gt; bool
    {
    my ($self, $extension, $itemString, $file, $definition) = @_;


    # Items

    my $item = $self-&gt;GetItem($extension, $itemString);

    if (!defined $item)
        {  die "Tried to add a definition to an undefined item in SourceDB.";  };

    if (!$extensionUsesDefinitionObjects[$extension])
        {  $definition = 1;  };

    my $result = $item-&gt;AddDefinition($file, $definition);


    # Files

    if (!exists $files{$file})
        {  $files{$file} = NaturalDocs::SourceDB::File-&gt;New();  };

    $files{$file}-&gt;AddItem($extension, $itemString);


    # Watched File

    if ($self-&gt;WatchingFileForChanges())
        {
        $watchedFile-&gt;AddItem($extension, $itemString);

        if ($extensionUsesDefinitionObjects[$extension])
            {  $watchedFileDefinitions-&gt;AddDefinition($extension, $itemString, $definition);  };
        };


    return $result;
    };


#
#   Function: ChangeDefinition
#
#   Changes the definition of an item.  This function is only used for extensions that use custom
#   &lt;NaturalDocs::SourceDB::ItemDefinition&gt;-derived classes.
#
#   Parameters:
#
#       extension - The &lt;ExtensionID&gt;.
#       itemString - The item string.
#       file - The &lt;FileName&gt; the definition is in.
#       definition - The definition, which must be an object derived from &lt;NaturalDocs::SourceDB::ItemDefinition&gt;.
#
sub ChangeDefinition #(ExtensionID extension, string itemString, FileName file, NaturalDocs::SourceDB::ItemDefinition definition)
    {
    my ($self, $extension, $itemString, $file, $definition) = @_;

    my $item = $self-&gt;GetItem($extension, $itemString);

    if (!defined $item)
        {  die "Tried to change the definition of an undefined item in SourceDB.";  };

    if (!$extensionUsesDefinitionObjects[$extension])
        {  die "Tried to change the definition of an item in an extension that doesn't use definition objects in SourceDB.";  };

    if (!$item-&gt;HasDefinition($file))
        {  die "Tried to change a definition that doesn't exist in SourceDB.";  };

    $item-&gt;ChangeDefinition($file, $definition);
    $extensions[$extension]-&gt;OnChangedDefinition($itemString, $file);
    };


#
#   Function: GetDefinition
#
#   If the extension uses custom &lt;NaturalDocs::SourceDB::ItemDefinition&gt; classes, returns it for the passed definition or undef
#   if it doesn't exist.  Otherwise returns whether it exists.
#
sub GetDefinition #(ExtensionID extension, string itemString, FileName file) =&gt; NaturalDocs::SourceDB::ItemDefinition or bool
    {
    my ($self, $extension, $itemString, $file) = @_;

    my $item = $self-&gt;GetItem($extension, $itemString);

    if (!defined $item)
        {  return undef;  };

    return $item-&gt;GetDefinition($file);
    };


#
#   Function: DeleteDefinition
#
#   Removes the definition for the passed item.  Returns whether it was successful, meaning whether a definition existed for that
#   file.
#
sub DeleteDefinition #(ExtensionID extension, string itemString, FileName file) =&gt; bool
    {
    my ($self, $extension, $itemString, $file) = @_;

    my $item = $self-&gt;GetItem($extension, $itemString);

    if (!defined $item)
        {  return 0;  };

    my $result = $item-&gt;DeleteDefinition($file);

    if ($result)
        {
        $files{$file}-&gt;DeleteItem($extension, $itemString);
        $extensions[$extension]-&gt;OnDeletedDefinition($itemString, $file, !$item-&gt;HasDefinitions());
        };

    return $result;
    };


#
#   Function: HasDefinitions
#
#   Returns whether there are any definitions for this item.
#
sub HasDefinitions #(ExtensionID extension, string itemString) =&gt; bool
    {
    my ($self, $extension, $itemString) = @_;

    my $item = $self-&gt;GetItem($extension, $itemString);

    if (!defined $item)
        {  return 0;  };

    return $item-&gt;HasDefinitions();
    };


#
#   Function: HasDefinition
#
#   Returns whether there is a definition for the passed &lt;FileName&gt;.
#
sub HasDefinition #(ExtensionID extension, string itemString, FileName file) =&gt; bool
    {
    my ($self, $extension, $itemString, $file) = @_;

    my $item = $self-&gt;GetItem($extension, $itemString);

    if (!defined $item)
        {  return 0;  };

    return $item-&gt;HasDefinition($file);
    };



###############################################################################
# Group: Watched File Functions


#
#   Function: WatchFileForChanges
#
#   Begins watching a file for changes.  Only one file at a time can be watched.
#
#   This should be called before a file is parsed so the file info goes both into the main database and the watched file info.
#   Afterwards you call &lt;AnalyzeWatchedFileChanges()&gt; so item deletions and definition changes can be detected.
#
#   Parameters:
#
#       filename - The &lt;FileName&gt; to watch.
#
sub WatchFileForChanges #(FileName filename)
    {
    my ($self, $filename) = @_;

    $watchedFileName = $filename;
    $watchedFile = NaturalDocs::SourceDB::File-&gt;New();
    $watchedFileDefinitions = NaturalDocs::SourceDB::WatchedFileDefinitions-&gt;New();
    };


#
#   Function: WatchingFileForChanges
#
#   Returns whether we're currently watching a file for changes or not.
#
sub WatchingFileForChanges # =&gt; bool
    {
    my $self = shift;
    return defined $watchedFileName;
    };


#
#   Function: AnalyzeWatchedFileChanges
#
#   Analyzes the watched file for changes.  Will delete and change definitions as necessary.
#
sub AnalyzeWatchedFileChanges
    {
    my $self = shift;

    if (!$self-&gt;WatchingFileForChanges())
        {  die "Tried to analyze watched file for changes in SourceDB when no file was being watched.";  };
    if (!$files{$watchedFileName})
        {  return;  };


    # Process extensions last registered to first.

    for (my $extension = scalar @extensions - 1; $extension &gt;= 0; $extension--)
        {
        my @items = $files{$watchedFileName}-&gt;ListItems($extension);

        foreach my $item (@items)
            {
            if ($watchedFile-&gt;HasItem($extension, $item))
                {
                if ($extensionUsesDefinitionObjects[$extension])
                    {
                    my $originalDefinition = $items[$extension]-&gt;GetDefinition($watchedFileName);
                    my $watchedDefinition = $watchedFileDefinitions-&gt;GetDefinition($extension, $item);

                    if (!$originalDefinition-&gt;Compare($watchedDefinition))
                        {  $self-&gt;ChangeDefinition($extension, $item, $watchedFileName, $watchedDefinition);  };
                    }
                }
            else # !$watchedFile-&gt;HasItem($item)
                {
                $self-&gt;DeleteDefinition($extension, $item, $watchedFileName);
                };
            };
        };


    $watchedFile = undef;
    $watchedFileName = undef;
    $watchedFileDefinitions = undef;
    };


1;
</pre></body></html>