############################################################################### # # Package: NaturalDocs::ClassHierarchy # ############################################################################### # # A package that handles all the gory details of managing the class hierarchy. It handles the hierarchy itself, which files define # them, rebuilding the files that are affected by changes, and loading and saving them to a file. # # Usage and Dependencies: # # - and must be initialized before use. # # - must be initialized before is called. It must reflect the state as of the last time # Natural Docs was run. # # - must be called to initialize the package. At this point, the will return the state as # of the last time Natural Docs was run. You are free to resolve afterwards. # # - must be called, and then ParseForInformation()> must be called on all files that # have changed so it can fully resolve the hierarchy via the . Afterwards the # will reflect the current state of the code. # # - must be called to commit any changes to the symbol table back to disk. # ############################################################################### # This file is part of Natural Docs, which is Copyright (C) 2003-2005 Greg Valure # Natural Docs is licensed under the GPL use strict; use integer; use NaturalDocs::ClassHierarchy::Class; use NaturalDocs::ClassHierarchy::File; package NaturalDocs::ClassHierarchy; ############################################################################### # Group: Variables # # handle: CLASS_HIERARCHY_FILEHANDLE # The file handle used with . # # # hash: classes # # A hash of all the classes. The keys are the class and the values are . # my %classes; # # hash: files # # A hash of the hierarchy information referenced by file. The keys are the , and the values are # s. # my %files; # # hash: parentReferences # # A hash of all the parent reference strings and what they resolve to. The keys are the and the values are # the class that they resolve to. # my %parentReferences; # # object: watchedFile # # A object of the file being watched for changes. This is compared to the version in # to see if anything was changed since the last parse. # my $watchedFile; # # string: watchedFileName # # The of the watched file, if any. If there is no watched file, this will be undef. # my $watchedFileName; # # bool: dontRebuildFiles # # A bool to set if you don't want changes in the hierarchy to cause files to be rebuilt. # my $dontRebuildFiles; ############################################################################### # Group: Files # # File: ClassHierarchy.nd # # Stores the class hierarchy on disk. # # Format: # # > [BINARY_FORMAT] # > [VersionInt: app version] # # The standard and header. # # > [SymbolString: class or undef to end] # # Next we begin a class segment with its . These continue until the end of the file. Only defined classes are # included. # # > [UInt32: number of files] # > [AString16: file] [AString16: file] ... # # Next there is the number of files that define that class. It's a UInt32, which seems like overkill, but I could imagine every # file in a huge C++ project being under the same namespace, and thus contributing its own definition. It's theoretically # possible. # # Following the number is that many file names. You must remember the index of each file, as they will be important later. # Indexes start at one because zero has a special meaning. # # > [UInt8: number of parents] # > ( [ReferenceString (no type): parent] # > [UInt32: file index] [UInt32: file index] ... [UInt32: 0] ) ... # # Next there is the number of parents defined for this class. For each one, we define a parent segment, which consists of # its , and then a zero-terminated string of indexes of the files that define that parent as part of that class. # The indexes start at one, and are into the list of files we saw previously. # # Note that we do store class segments for classes without parents, but not for undefined classes. # # This concludes a class segment. These segments continue until an undef . # # See Also: # # # # Revisions: # # 1.22: # # - Classes and parents switched from AString16s to and . # - A ending undef was added to the end. Previously it stopped when the file ran out. # # 1.2: # # - This file was introduced in 1.2. # ############################################################################### # Group: File Functions # # Function: Load # # Loads the class hierarchy from disk. # sub Load { my ($self) = @_; $dontRebuildFiles = 1; my $fileIsOkay = 1; my $fileName = NaturalDocs::Project->ClassHierarchyFile(); if (!open(CLASS_HIERARCHY_FILEHANDLE, '<' . $fileName)) { $fileIsOkay = undef; } else { # See if it's binary. binmode(CLASS_HIERARCHY_FILEHANDLE); my $firstChar; read(CLASS_HIERARCHY_FILEHANDLE, $firstChar, 1); if ($firstChar != ::BINARY_FORMAT()) { close(CLASS_HIERARCHY_FILEHANDLE); $fileIsOkay = undef; } else { my $version = NaturalDocs::Version->FromBinaryFile(\*CLASS_HIERARCHY_FILEHANDLE); # Minor bugs were fixed in 1.33 that may affect the stored data. if ($version > NaturalDocs::Settings->AppVersion() || $version < NaturalDocs::Version->FromString('1.33')) { close(CLASS_HIERARCHY_FILEHANDLE); $fileIsOkay = undef; }; }; }; if (!$fileIsOkay) { NaturalDocs::Project->ReparseEverything(); } else { my $raw; for (;;) { # [SymbolString: class or undef to end] my $class = NaturalDocs::SymbolString->FromBinaryFile(\*CLASS_HIERARCHY_FILEHANDLE); if (!defined $class) { last; }; # [UInt32: number of files] read(CLASS_HIERARCHY_FILEHANDLE, $raw, 4); my $numberOfFiles = unpack('N', $raw); my @files; while ($numberOfFiles) { # [AString16: file] read(CLASS_HIERARCHY_FILEHANDLE, $raw, 2); my $fileLength = unpack('n', $raw); my $file; read(CLASS_HIERARCHY_FILEHANDLE, $file, $fileLength); push @files, $file; $self->AddClass($file, $class); $numberOfFiles--; }; # [UInt8: number of parents] read(CLASS_HIERARCHY_FILEHANDLE, $raw, 1); my $numberOfParents = unpack('C', $raw); while ($numberOfParents) { # [ReferenceString (no type): parent] my $parent = NaturalDocs::ReferenceString->FromBinaryFile(\*CLASS_HIERARCHY_FILEHANDLE, ::BINARYREF_NOTYPE(), ::REFERENCE_CH_PARENT()); for (;;) { # [UInt32: file index or 0] read(CLASS_HIERARCHY_FILEHANDLE, $raw, 4); my $fileIndex = unpack('N', $raw); if ($fileIndex == 0) { last; } $self->AddParentReference( $files[$fileIndex - 1], $class, $parent ); }; $numberOfParents--; }; }; close(CLASS_HIERARCHY_FILEHANDLE); }; $dontRebuildFiles = undef; }; # # Function: Save # # Saves the class hierarchy to disk. # sub Save { my ($self) = @_; open (CLASS_HIERARCHY_FILEHANDLE, '>' . NaturalDocs::Project->ClassHierarchyFile()) or die "Couldn't save " . NaturalDocs::Project->ClassHierarchyFile() . ".\n"; binmode(CLASS_HIERARCHY_FILEHANDLE); print CLASS_HIERARCHY_FILEHANDLE '' . ::BINARY_FORMAT(); NaturalDocs::Version->ToBinaryFile(\*CLASS_HIERARCHY_FILEHANDLE, NaturalDocs::Settings->AppVersion()); while (my ($class, $classObject) = each %classes) { if ($classObject->IsDefined()) { # [SymbolString: class or undef to end] NaturalDocs::SymbolString->ToBinaryFile(\*CLASS_HIERARCHY_FILEHANDLE, $class); # [UInt32: number of files] my @definitions = $classObject->Definitions(); my %definitionIndexes; print CLASS_HIERARCHY_FILEHANDLE pack('N', scalar @definitions); for (my $i = 0; $i < scalar @definitions; $i++) { # [AString16: file] print CLASS_HIERARCHY_FILEHANDLE pack('nA*', length($definitions[$i]), $definitions[$i]); $definitionIndexes{$definitions[$i]} = $i + 1; }; # [UInt8: number of parents] my @parents = $classObject->ParentReferences(); print CLASS_HIERARCHY_FILEHANDLE pack('C', scalar @parents); foreach my $parent (@parents) { # [ReferenceString (no type): parent] NaturalDocs::ReferenceString->ToBinaryFile(\*CLASS_HIERARCHY_FILEHANDLE, $parent, ::BINARYREF_NOTYPE()); # [UInt32: file index] my @parentDefinitions = $classObject->ParentReferenceDefinitions($parent); foreach my $parentDefinition (@parentDefinitions) { print CLASS_HIERARCHY_FILEHANDLE pack('N', $definitionIndexes{$parentDefinition}); }; # [UInt32: 0] print CLASS_HIERARCHY_FILEHANDLE pack('N', 0); }; }; }; # [SymbolString: class or undef to end] NaturalDocs::SymbolString->ToBinaryFile(\*CLASS_HIERARCHY_FILEHANDLE, undef); close(CLASS_HIERARCHY_FILEHANDLE); }; # # Function: Purge # # Purges the hierarchy of files that no longer have Natural Docs content. # sub Purge { my ($self) = @_; my $filesToPurge = NaturalDocs::Project->FilesToPurge(); foreach my $file (keys %$filesToPurge) { $self->DeleteFile($file); }; }; ############################################################################### # Group: Interface Functions # # Function: OnInterpretationChange # # Called by whenever a class hierarchy reference's intepretation changes, meaning it switched # from one symbol to another. # # reference - The whose current interpretation changed. # sub OnInterpretationChange #(reference) { my ($self, $reference) = @_; if (NaturalDocs::ReferenceString->TypeOf($reference) == ::REFERENCE_CH_PARENT()) { # The approach here is simply to completely delete the reference and readd it. This is less than optimal efficiency, since it's # being removed and added from %files too, even though that isn't required. However, the simpler code is worth it # considering this will only happen when a parent reference becomes defined or undefined, or on the rare languages (like C#) # that allow relative parent references. my $oldTargetSymbol = $parentReferences{$reference}; my $oldTargetObject = $classes{$oldTargetSymbol}; my @classesWithReferenceParent = $oldTargetObject->Children(); # Each entry is an arrayref of file names. Indexes are the same as classesWithReferenceParent's. my @filesDefiningReferenceParent; foreach my $classWithReferenceParent (@classesWithReferenceParent) { my $fileList = [ $classes{$classWithReferenceParent}->ParentReferenceDefinitions($reference) ]; push @filesDefiningReferenceParent, $fileList; foreach my $fileDefiningReferenceParent (@$fileList) { $self->DeleteParentReference($fileDefiningReferenceParent, $classWithReferenceParent, $reference); }; }; # This will force the reference to be reinterpreted on the next add. delete $parentReferences{$reference}; # Now we can just readd it. for (my $i = 0; $i < scalar @classesWithReferenceParent; $i++) { foreach my $file (@{$filesDefiningReferenceParent[$i]}) { $self->AddParentReference($file, $classesWithReferenceParent[$i], $reference); }; }; }; # The only way for a REFERENCE_CH_CLASS reference to change is if the symbol is deleted. That will be handled by # , so we don't need to do anything here. }; # # Function: OnTargetSymbolChange # # Called by whenever a class hierarchy reference's target symbol changes, but the reference # still resolves to the same symbol. # # Parameters: # # reference - The that was affected by the change. # sub OnTargetSymbolChange #(reference) { my ($self, $reference) = @_; my $type = NaturalDocs::ReferenceString->TypeOf($reference); my $class; if ($type == ::REFERENCE_CH_PARENT()) { $class = $parentReferences{$reference}; } else # ($type == ::REFERENCE_CH_CLASS()) { # Class references are global absolute, so we can just yank the symbol. (undef, $class, undef, undef, undef) = NaturalDocs::ReferenceString->InformationOf($reference); }; $self->RebuildFilesFor($class, 1, 0, 1); }; ############################################################################### # Group: Modification Functions # # Function: AddClass # # Adds a class to the hierarchy. # # Parameters: # # file - The the class was defined in. # class - The class . # # Note: # # The file parameter must be defined when using this function externally. It may be undef for internal use only. # sub AddClass #(file, class) { my ($self, $file, $class) = @_; if (!exists $classes{$class}) { $classes{$class} = NaturalDocs::ClassHierarchy::Class->New(); NaturalDocs::SymbolTable->AddReference($self->ClassReferenceOf($class), $file) }; if (defined $file) { # If this was the first definition for this class... if ($classes{$class}->AddDefinition($file)) { $self->RebuildFilesFor($class, 1, 1, 1); }; if (!exists $files{$file}) { $files{$file} = NaturalDocs::ClassHierarchy::File->New(); }; $files{$file}->AddClass($class); if (defined $watchedFileName) { $watchedFile->AddClass($class); }; }; }; # # Function: AddParentReference # # Adds a class-parent relationship to the hierarchy. The classes will be created if they don't already exist. # # Parameters: # # file - The the reference was defined in. # class - The class . # symbol - The parent class . # scope - The package that the reference appeared in. # using - An arrayref of package that the reference has access to via "using" statements. # resolvingFlags - Any to be used when resolving the reference. # # Alternate Parameters: # # file - The the reference was defined in. # class - The class . # reference - The parent . # sub AddParentReference #(file, class, symbol, scope, using, resolvingFlags) or (file, class, reference) { my ($self, $file, $class, $symbol, $parentReference); if (scalar @_ == 7) { my ($scope, $using, $resolvingFlags); ($self, $file, $class, $symbol, $scope, $using, $resolvingFlags) = @_; $parentReference = NaturalDocs::ReferenceString->MakeFrom(::REFERENCE_CH_PARENT(), $symbol, $scope, $using, $resolvingFlags); } else { ($self, $file, $class, $parentReference) = @_; $symbol = (NaturalDocs::ReferenceString->InformationOf($parentReference))[1]; }; # In case it doesn't already exist. $self->AddClass($file, $class); my $parent; if (exists $parentReferences{$parentReference}) { $parent = $parentReferences{$parentReference}; } else { NaturalDocs::SymbolTable->AddReference($parentReference, $file); my $parentTarget = NaturalDocs::SymbolTable->References($parentReference); if (defined $parentTarget) { $parent = $parentTarget->Symbol(); } else { $parent = $symbol; }; # In case it doesn't already exist. $self->AddClass(undef, $parent); $parentReferences{$parentReference} = $parent; }; # If this defined a new parent... if ($classes{$class}->AddParentReference($parentReference, $file, \%parentReferences)) { $classes{$parent}->AddChild($class); $self->RebuildFilesFor($class, 0, 1, 0); $self->RebuildFilesFor($parent, 0, 1, 0); }; $files{$file}->AddParentReference($class, $parentReference); if (defined $watchedFileName) { $watchedFile->AddParentReference($class, $parentReference); }; }; # # Function: WatchFileForChanges # # Watches a file for changes, which can then be applied by . Definitions are not deleted via a DeleteClass() # function. Instead, a file is watched for changes, reparsed, and then a comparison is made to look for definitions that # disappeared and any other relevant changes. # # Parameters: # # file - The to watch. # sub WatchFileForChanges #(file) { my ($self, $file) = @_; $watchedFile = NaturalDocs::ClassHierarchy::File->New(); $watchedFileName = $file; }; # # Function: AnalyzeChanges # # Checks the watched file for any changes that occured since the last time is was parsed, and updates the hierarchy as # necessary. Also sends any files that are affected to RebuildFile()>. # sub AnalyzeChanges { my ($self) = @_; # If the file didn't have any classes before, and it still doesn't, it wont be in %files. if (exists $files{$watchedFileName}) { my @originalClasses = $files{$watchedFileName}->Classes(); foreach my $originalClass (@originalClasses) { # If the class isn't there the second time around... if (!$watchedFile->HasClass($originalClass)) { $self->DeleteClass($watchedFileName, $originalClass); } else { my @originalParents = $files{$watchedFileName}->ParentReferencesOf($originalClass); foreach my $originalParent (@originalParents) { # If the parent reference wasn't there the second time around... if (!$watchedFile->HasParentReference($originalClass, $originalParent)) { $self->DeleteParentReference($watchedFileName, $originalClass, $originalParent); }; }; }; }; }; $watchedFile = undef; $watchedFileName = undef; }; ############################################################################### # Group: Information Functions # # Function: ParentsOf # Returns a array of the passed class' parents, or an empty array if none. Note that not all of them may be # defined. # sub ParentsOf #(class) { my ($self, $class) = @_; if (exists $classes{$class}) { return $classes{$class}->Parents(); } else { return ( ); }; }; # # Function: ChildrenOf # Returns a array of the passed class' children, or an empty array if none. Note that not all of them may be # defined. # sub ChildrenOf #(class) { my ($self, $class) = @_; if (exists $classes{$class}) { return $classes{$class}->Children(); } else { return ( ); }; }; ############################################################################### # Group: Support Functions # # Function: DeleteFile # # Deletes a file and everything defined in it. # # Parameters: # # file - The . # sub DeleteFile #(file) { my ($self, $file) = @_; if (!exists $files{$file}) { return; }; my @classes = $files{$file}->Classes(); foreach my $class (@classes) { $self->DeleteClass($file, $class); }; delete $files{$file}; }; # # Function: DeleteClass # # Deletes a class definition from a file. Will also delete any parent references from this class and file. Will rebuild any file # affected unless is set. # # Parameters: # # file - The that defines the class. # class - The class . # sub DeleteClass #(file, class) { my ($self, $file, $class) = @_; my @parents = $files{$file}->ParentReferencesOf($class); foreach my $parent (@parents) { $self->DeleteParentReference($file, $class, $parent); }; $files{$file}->DeleteClass($class); # If we're deleting the last definition of this class. if ($classes{$class}->DeleteDefinition($file)) { if (!$classes{$class}->HasChildren()) { delete $classes{$class}; if (!$dontRebuildFiles) { NaturalDocs::Project->RebuildFile($file); }; } else { $self->RebuildFilesFor($class, 0, 1, 1); }; }; }; # # Function: DeleteParentReference # # Deletes a class' parent reference and returns whether it resulted in the loss of a parent class. Will rebuild any file affected # unless is set. # # Parameters: # # file - The that defines the reference. # class - The class . # reference - The parent . # # Returns: # # If the class lost a parent as a result of this, it will return its . It will return undef otherwise. # sub DeleteParentReference #(file, class, reference) { my ($self, $file, $class, $reference) = @_; if (!exists $classes{$class}) { return; }; $files{$file}->DeleteParentReference($class, $reference); my $deletedParent = $classes{$class}->DeleteParentReference($reference, $file, \%parentReferences); if (defined $deletedParent) { my $deletedParentObject = $classes{$deletedParent}; $deletedParentObject->DeleteChild($class); $self->RebuildFilesFor($deletedParent, 0, 1, 0); $self->RebuildFilesFor($class, 0, 1, 0); if (!$deletedParentObject->HasChildren() && !$deletedParentObject->IsDefined()) { delete $classes{$deletedParent}; NaturalDocs::SymbolTable->DeleteReference( $self->ClassReferenceOf($class) ); }; return $deletedParent; }; return undef; }; # # Function: ClassReferenceOf # # Returns the of the passed class . # sub ClassReferenceOf #(class) { my ($self, $class) = @_; return NaturalDocs::ReferenceString->MakeFrom(::REFERENCE_CH_CLASS(), $class, undef, undef, ::RESOLVE_ABSOLUTE() | ::RESOLVE_NOPLURAL()); }; # # Function: RebuildFilesFor # # Calls RebuildFile()> for every file defining the passed class, its parents, and/or its children. # Returns without doing anything if is set. # # Parameters: # # class - The class . # rebuildParents - Whether to rebuild the class' parents. # rebuildSelf - Whether to rebuild the class. # rebuildChildren - Whether to rebuild the class' children. # sub RebuildFilesFor #(class, rebuildParents, rebuildSelf, rebuildChildren) { my ($self, $class, $rebuildParents, $rebuildSelf, $rebuildChildren) = @_; if ($dontRebuildFiles) { return; }; my @classesToBuild; if ($rebuildParents) { @classesToBuild = $classes{$class}->Parents(); }; if ($rebuildSelf) { push @classesToBuild, $class; }; if ($rebuildChildren) { push @classesToBuild, $classes{$class}->Children(); }; foreach my $classToBuild (@classesToBuild) { my @definitions = $classes{$classToBuild}->Definitions(); foreach my $definition (@definitions) { NaturalDocs::Project->RebuildFile($definition); }; }; }; 1;