############################################################################### # # Class: NaturalDocs::Languages::ActionScript # ############################################################################### # # A subclass to handle the language variations of Flash ActionScript. # # # Topic: Language Support # # Supported: # # Not supported yet: # ############################################################################### # 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; package NaturalDocs::Languages::ActionScript; use base 'NaturalDocs::Languages::Advanced'; ################################################################################ # Group: Package Variables # # hash: classModifiers # An existence hash of all the acceptable class modifiers. The keys are in all lowercase. # my %classModifiers = ( 'dynamic' => 1, 'intrinsic' => 1 ); # # hash: memberModifiers # An existence hash of all the acceptable class member modifiers. The keys are in all lowercase. # my %memberModifiers = ( 'public' => 1, 'private' => 1, 'static' => 1 ); # # hash: declarationEnders # An existence hash of all the tokens that can end a declaration. This is important because statements don't require a semicolon # to end. The keys are in all lowercase. # my %declarationEnders = ( ';' => 1, '}' => 1, '{' => 1, 'public' => 1, 'private' => 1, 'static' => 1, 'class' => 1, 'interface' => 1, 'var' => 1, 'function' => 1, 'import' => 1 ); ################################################################################ # Group: Interface Functions # # Function: PackageSeparator # Returns the package separator symbol. # sub PackageSeparator { return '.'; }; # # Function: EnumValues # Returns the that describes how the language handles enums. # sub EnumValues { return ::ENUM_GLOBAL(); }; # # Function: ParseParameterLine # Parses a prototype parameter line and returns it as a object. # sub ParseParameterLine #(line) { my ($self, $line) = @_; return $self->ParsePascalParameterLine($line); }; # # Function: TypeBeforeParameter # Returns whether the type appears before the parameter in prototypes. # sub TypeBeforeParameter { return 0; }; # # Function: ParseFile # # Parses the passed source file, sending comments acceptable for documentation to OnComment()>. # # Parameters: # # sourceFile - The to parse. # topicList - A reference to the list of being built by the file. # # Returns: # # The array ( autoTopics, scopeRecord ). # # autoTopics - An arrayref of automatically generated topics from the file, or undef if none. # scopeRecord - An arrayref of , or undef if none. # sub ParseFile #(sourceFile, topicsList) { my ($self, $sourceFile, $topicsList) = @_; $self->ParseForCommentsAndTokens($sourceFile, [ '//' ], [ '/*', '*/' ] ); my $tokens = $self->Tokens(); my $index = 0; my $lineNumber = 1; while ($index < scalar @$tokens) { if ($self->TryToSkipWhitespace(\$index, \$lineNumber) || $self->TryToGetImport(\$index, \$lineNumber) || $self->TryToGetClass(\$index, \$lineNumber) || $self->TryToGetFunction(\$index, \$lineNumber) || $self->TryToGetVariable(\$index, \$lineNumber) ) { # The functions above will handle everything. } elsif ($tokens->[$index] eq '{') { $self->StartScope('}', $lineNumber, undef, undef, undef); $index++; } elsif ($tokens->[$index] eq '}') { if ($self->ClosingScopeSymbol() eq '}') { $self->EndScope($lineNumber); }; $index++; } else { $self->SkipToNextStatement(\$index, \$lineNumber); }; }; # Don't need to keep these around. $self->ClearTokens(); my $autoTopics = $self->AutoTopics(); my $scopeRecord = $self->ScopeRecord(); if (defined $scopeRecord && !scalar @$scopeRecord) { $scopeRecord = undef; }; return ( $autoTopics, $scopeRecord ); }; ################################################################################ # Group: Statement Parsing Functions # All functions here assume that the current position is at the beginning of a statement. # # Note for developers: I am well aware that the code in these functions do not check if we're past the end of the tokens as # often as it should. We're making use of the fact that Perl will always return undef in these cases to keep the code simpler. # # Function: TryToGetIdentifier # # Determines whether the position is at an identifier, and if so, skips it and returns the complete identifier as a string. Returns # undef otherwise. # # Parameters: # # indexRef - A reference to the current token index. # lineNumberRef - A reference to the current line number. # allowStar - If set, allows the last identifier to be a star. # sub TryToGetIdentifier #(indexRef, lineNumberRef, allowStar) { my ($self, $indexRef, $lineNumberRef, $allowStar) = @_; my $tokens = $self->Tokens(); my $index = $$indexRef; use constant MODE_IDENTIFIER_START => 1; use constant MODE_IN_IDENTIFIER => 2; use constant MODE_AFTER_STAR => 3; my $identifier; my $mode = MODE_IDENTIFIER_START; while ($index < scalar @$tokens) { if ($mode == MODE_IDENTIFIER_START) { if ($tokens->[$index] =~ /^[a-z\$\_]/i) { $identifier .= $tokens->[$index]; $index++; $mode = MODE_IN_IDENTIFIER; } elsif ($allowStar && $tokens->[$index] eq '*') { $identifier .= '*'; $index++; $mode = MODE_AFTER_STAR; } else { return undef; }; } elsif ($mode == MODE_IN_IDENTIFIER) { if ($tokens->[$index] eq '.') { $identifier .= '.'; $index++; $mode = MODE_IDENTIFIER_START; } elsif ($tokens->[$index] =~ /^[a-z0-9\$\_]/i) { $identifier .= $tokens->[$index]; $index++; } else { last; }; } else #($mode == MODE_AFTER_STAR) { if ($tokens->[$index] =~ /^[a-z0-9\$\_\.]/i) { return undef; } else { last; }; }; }; # We need to check again because we may have run out of tokens after a dot. if ($mode != MODE_IDENTIFIER_START) { $$indexRef = $index; return $identifier; } else { return undef; }; }; # # Function: TryToGetImport # # Determines whether the position is at a import statement, and if so, adds it as a Using statement to the current scope, skips # it, and returns true. # sub TryToGetImport #(indexRef, lineNumberRef) { my ($self, $indexRef, $lineNumberRef) = @_; my $tokens = $self->Tokens(); my $index = $$indexRef; my $lineNumber = $$lineNumberRef; if ($tokens->[$index] ne 'import') { return undef; }; $index++; $self->TryToSkipWhitespace(\$index, \$lineNumber); my $identifier = $self->TryToGetIdentifier(\$index, \$lineNumber, 1); if (!$identifier) { return undef; }; # Currently we implement importing by stripping the last package level and treating it as a using. So "import p1.p2.p3" makes # p1.p2 the using path, which is over-tolerant but that's okay. "import p1.p2.*" is treated the same way, but in this case it's # not over-tolerant. If there's no dot, there's no point to including it. if (index($identifier, '.') != -1) { $identifier =~ s/\.[^\.]+$//; $self->AddUsing( NaturalDocs::SymbolString->FromText($identifier) ); }; $$indexRef = $index; $$lineNumberRef = $lineNumber; return 1; }; # # Function: TryToGetClass # # Determines whether the position is at a class declaration statement, and if so, generates a topic for it, skips it, and # returns true. # # Supported Syntaxes: # # - Classes # - Interfaces # - Classes and interfaces with _global # sub TryToGetClass #(indexRef, lineNumberRef) { my ($self, $indexRef, $lineNumberRef) = @_; my $tokens = $self->Tokens(); my $index = $$indexRef; my $lineNumber = $$lineNumberRef; my @modifiers; while ($tokens->[$index] =~ /^[a-z]/i && exists $classModifiers{lc($tokens->[$index])} ) { push @modifiers, lc($tokens->[$index]); $index++; $self->TryToSkipWhitespace(\$index, \$lineNumber); }; my $type; if ($tokens->[$index] eq 'class' || $tokens->[$index] eq 'interface') { $type = $tokens->[$index]; $index++; $self->TryToSkipWhitespace(\$index, \$lineNumber); } else { return undef; }; my $className = $self->TryToGetIdentifier(\$index, \$lineNumber); if (!$className) { return undef; }; $self->TryToSkipWhitespace(\$index, \$lineNumber); my @parents; if ($tokens->[$index] eq 'extends') { $index++; $self->TryToSkipWhitespace(\$index, \$lineNumber); my $parent = $self->TryToGetIdentifier(\$index, \$lineNumber); if (!$parent) { return undef; }; push @parents, $parent; $self->TryToSkipWhitespace(\$index, \$lineNumber); }; if ($type eq 'class' && $tokens->[$index] eq 'implements') { $index++; $self->TryToSkipWhitespace(\$index, \$lineNumber); for (;;) { my $parent = $self->TryToGetIdentifier(\$index, \$lineNumber); if (!$parent) { return undef; }; push @parents, $parent; $self->TryToSkipWhitespace(\$index, \$lineNumber); if ($tokens->[$index] ne ',') { last; } else { $index++; $self->TryToSkipWhitespace(\$index, \$lineNumber); }; }; }; if ($tokens->[$index] ne '{') { return undef; }; $index++; # If we made it this far, we have a valid class declaration. my $topicType; if ($type eq 'interface') { $topicType = ::TOPIC_INTERFACE(); } else { $topicType = ::TOPIC_CLASS(); }; $className =~ s/^_global.//; my $autoTopic = NaturalDocs::Parser::ParsedTopic->New($topicType, $className, undef, $self->CurrentUsing(), undef, undef, undef, $$lineNumberRef); $self->AddAutoTopic($autoTopic); NaturalDocs::Parser->OnClass($autoTopic->Package()); foreach my $parent (@parents) { NaturalDocs::Parser->OnClassParent($autoTopic->Package(), NaturalDocs::SymbolString->FromText($parent), undef, $self->CurrentUsing(), ::RESOLVE_ABSOLUTE()); }; $self->StartScope('}', $lineNumber, $autoTopic->Package()); $$indexRef = $index; $$lineNumberRef = $lineNumber; return 1; }; # # Function: TryToGetFunction # # Determines if the position is on a function declaration, and if so, generates a topic for it, skips it, and returns true. # # Supported Syntaxes: # # - Functions # - Constructors # - Properties # - Functions with _global # sub TryToGetFunction #(indexRef, lineNumberRef) { my ($self, $indexRef, $lineNumberRef) = @_; my $tokens = $self->Tokens(); my $index = $$indexRef; my $lineNumber = $$lineNumberRef; my $startIndex = $index; my $startLine = $lineNumber; my @modifiers; while ($tokens->[$index] =~ /^[a-z]/i && exists $memberModifiers{lc($tokens->[$index])} ) { push @modifiers, lc($tokens->[$index]); $index++; $self->TryToSkipWhitespace(\$index, \$lineNumber); }; if ($tokens->[$index] ne 'function') { return undef; }; $index++; $self->TryToSkipWhitespace(\$index, \$lineNumber); my $type; if ($tokens->[$index] eq 'get' || $tokens->[$index] eq 'set') { # This can either be a property ("function get Something()") or a function name ("function get()"). my $nextIndex = $index; my $nextLineNumber = $lineNumber; $nextIndex++; $self->TryToSkipWhitespace(\$nextIndex, \$nextLineNumber); if ($tokens->[$nextIndex] eq '(') { $type = ::TOPIC_FUNCTION(); # Ignore the movement and let the code ahead pick it up as the name. } else { $type = ::TOPIC_PROPERTY(); $index = $nextIndex; $lineNumber = $nextLineNumber; }; } else { $type = ::TOPIC_FUNCTION(); }; my $name = $self->TryToGetIdentifier(\$index, \$lineNumber); if (!$name) { return undef; }; $self->TryToSkipWhitespace(\$index, \$lineNumber); if ($tokens->[$index] ne '(') { return undef; }; $index++; $self->GenericSkipUntilAfter(\$index, \$lineNumber, ')'); $self->TryToSkipWhitespace(\$index, \$lineNumber); if ($tokens->[$index] eq ':') { $index++; $self->TryToSkipWhitespace(\$index, \$lineNumber); $self->TryToGetIdentifier(\$index, \$lineNumber); $self->TryToSkipWhitespace(\$index, \$lineNumber); }; my $prototype = $self->NormalizePrototype( $self->CreateString($startIndex, $index) ); if ($tokens->[$index] eq '{') { $self->GenericSkip(\$index, \$lineNumber); } elsif (!exists $declarationEnders{$tokens->[$index]}) { return undef; }; my $scope = $self->CurrentScope(); if ($name =~ s/^_global.//) { $scope = undef; }; $self->AddAutoTopic(NaturalDocs::Parser::ParsedTopic->New($type, $name, $scope, $self->CurrentUsing(), $prototype, undef, undef, $startLine)); # We succeeded if we got this far. $$indexRef = $index; $$lineNumberRef = $lineNumber; return 1; }; # # Function: TryToGetVariable # # Determines if the position is on a variable declaration statement, and if so, generates a topic for each variable, skips the # statement, and returns true. # # Supported Syntaxes: # # - Variables # - Variables with _global # sub TryToGetVariable #(indexRef, lineNumberRef) { my ($self, $indexRef, $lineNumberRef) = @_; my $tokens = $self->Tokens(); my $index = $$indexRef; my $lineNumber = $$lineNumberRef; my $startIndex = $index; my $startLine = $lineNumber; my @modifiers; while ($tokens->[$index] =~ /^[a-z]/i && exists $memberModifiers{lc($tokens->[$index])} ) { push @modifiers, lc($tokens->[$index]); $index++; $self->TryToSkipWhitespace(\$index, \$lineNumber); }; if ($tokens->[$index] ne 'var') { return undef; }; $index++; $self->TryToSkipWhitespace(\$index, \$lineNumber); my $endTypeIndex = $index; my @names; my @types; for (;;) { my $name = $self->TryToGetIdentifier(\$index, \$lineNumber); if (!$name) { return undef; }; $self->TryToSkipWhitespace(\$index, \$lineNumber); my $type; if ($tokens->[$index] eq ':') { $index++; $self->TryToSkipWhitespace(\$index, \$lineNumber); $type = ': ' . $self->TryToGetIdentifier(\$index, \$lineNumber); $self->TryToSkipWhitespace(\$index, \$lineNumber); }; if ($tokens->[$index] eq '=') { do { $self->GenericSkip(\$index, \$lineNumber); } while ($tokens->[$index] ne ',' && !exists $declarationEnders{$tokens->[$index]} && $index < scalar @$tokens); }; push @names, $name; push @types, $type; if ($tokens->[$index] eq ',') { $index++; $self->TryToSkipWhitespace(\$index, \$lineNumber); } elsif (exists $declarationEnders{$tokens->[$index]}) { last; } else { return undef; }; }; # We succeeded if we got this far. my $prototypePrefix = $self->CreateString($startIndex, $endTypeIndex); for (my $i = 0; $i < scalar @names; $i++) { my $prototype = $self->NormalizePrototype( $prototypePrefix . ' ' . $names[$i] . $types[$i]); my $scope = $self->CurrentScope(); if ($names[$i] =~ s/^_global.//) { $scope = undef; }; $self->AddAutoTopic(NaturalDocs::Parser::ParsedTopic->New(::TOPIC_VARIABLE(), $names[$i], $scope, $self->CurrentUsing(), $prototype, undef, undef, $startLine)); }; $$indexRef = $index; $$lineNumberRef = $lineNumber; return 1; }; ################################################################################ # Group: Low Level Parsing Functions # # Function: GenericSkip # # Advances the position one place through general code. # # - If the position is on a string, it will skip it completely. # - If the position is on an opening symbol, it will skip until the past the closing symbol. # - If the position is on whitespace (including comments), it will skip it completely. # - Otherwise it skips one token. # # Parameters: # # indexRef - A reference to the current index. # lineNumberRef - A reference to the current line number. # sub GenericSkip #(indexRef, lineNumberRef) { my ($self, $indexRef, $lineNumberRef) = @_; my $tokens = $self->Tokens(); # We can ignore the scope stack because we're just skipping everything without parsing, and we need recursion anyway. if ($tokens->[$$indexRef] eq '{') { $$indexRef++; $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, '}'); } elsif ($tokens->[$$indexRef] eq '(') { $$indexRef++; $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, ')'); } elsif ($tokens->[$$indexRef] eq '[') { $$indexRef++; $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, ']'); } elsif ($self->TryToSkipWhitespace($indexRef, $lineNumberRef) || $self->TryToSkipString($indexRef, $lineNumberRef)) { } else { $$indexRef++; }; }; # # Function: GenericSkipUntilAfter # # Advances the position via until a specific token is reached and passed. # sub GenericSkipUntilAfter #(indexRef, lineNumberRef, token) { my ($self, $indexRef, $lineNumberRef, $token) = @_; my $tokens = $self->Tokens(); while ($$indexRef < scalar @$tokens && $tokens->[$$indexRef] ne $token) { $self->GenericSkip($indexRef, $lineNumberRef); }; if ($tokens->[$$indexRef] eq "\n") { $$lineNumberRef++; }; $$indexRef++; }; # # Function: SkipToNextStatement # # Advances the position via until the next statement, which is defined as anything in not # appearing in brackets or strings. It will always advance at least one token. # sub SkipToNextStatement #(indexRef, lineNumberRef) { my ($self, $indexRef, $lineNumberRef) = @_; my $tokens = $self->Tokens(); do { $self->GenericSkip($indexRef, $lineNumberRef); } while ( $$indexRef < scalar @$tokens && !exists $declarationEnders{$tokens->[$$indexRef]} ); }; # # Function: TryToSkipString # If the current position is on a string delimiter, skip past the string and return true. # # Parameters: # # indexRef - A reference to the index of the position to start at. # lineNumberRef - A reference to the line number of the position. # # Returns: # # Whether the position was at a string. # # Syntax Support: # # - Supports quotes, apostrophes, and at-quotes. # sub TryToSkipString #(indexRef, lineNumberRef) { my ($self, $indexRef, $lineNumberRef) = @_; return ($self->SUPER::TryToSkipString($indexRef, $lineNumberRef, '\'') || $self->SUPER::TryToSkipString($indexRef, $lineNumberRef, '"') ); }; # # Function: TryToSkipWhitespace # If the current position is on a whitespace token, a line break token, or a comment, it skips them and returns true. If there are # a number of these in a row, it skips them all. # sub TryToSkipWhitespace #(indexRef, lineNumberRef) { my ($self, $indexRef, $lineNumberRef) = @_; my $tokens = $self->Tokens(); my $result; while ($$indexRef < scalar @$tokens) { if ($tokens->[$$indexRef] =~ /^[ \t]/) { $$indexRef++; $result = 1; } elsif ($tokens->[$$indexRef] eq "\n") { $$indexRef++; $$lineNumberRef++; $result = 1; } elsif ($self->TryToSkipComment($indexRef, $lineNumberRef)) { $result = 1; } else { last; }; }; return $result; }; # # Function: TryToSkipComment # If the current position is on a comment, skip past it and return true. # sub TryToSkipComment #(indexRef, lineNumberRef) { my ($self, $indexRef, $lineNumberRef) = @_; return ( $self->TryToSkipLineComment($indexRef, $lineNumberRef) || $self->TryToSkipMultilineComment($indexRef, $lineNumberRef) ); }; # # Function: TryToSkipLineComment # If the current position is on a line comment symbol, skip past it and return true. # sub TryToSkipLineComment #(indexRef, lineNumberRef) { my ($self, $indexRef, $lineNumberRef) = @_; my $tokens = $self->Tokens(); if ($tokens->[$$indexRef] eq '/' && $tokens->[$$indexRef+1] eq '/') { $self->SkipRestOfLine($indexRef, $lineNumberRef); return 1; } else { return undef; }; }; # # Function: TryToSkipMultilineComment # If the current position is on an opening comment symbol, skip past it and return true. # sub TryToSkipMultilineComment #(indexRef, lineNumberRef) { my ($self, $indexRef, $lineNumberRef) = @_; my $tokens = $self->Tokens(); if ($tokens->[$$indexRef] eq '/' && $tokens->[$$indexRef+1] eq '*') { $self->SkipUntilAfter($indexRef, $lineNumberRef, '*', '/'); return 1; } else { return undef; }; }; 1;