/* Copyright (C) 2001 Free Software Foundation, Inc. Written by: Richard Frith-Macdonald Created: October 2001 This file is part of the GNUstep Project This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. You should have received a copy of the GNU General Public License along with this program; see the file COPYING.LIB. If not, write to the Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #include "AGSParser.h" #include "GNUstepBase/GNUstep.h" #include "GNUstepBase/GSCategories.h" /** * The AGSParser class parses Objective-C header and source files * to produce a property-list which can be handled by [AGSOutput]. */ @implementation AGSParser /** * Method to add the comment from the main() function to the end * of the initial chapter in the output document. We do this to * support the use of autogsdoc to document tools. */ - (void) addMain: (NSString*)c { NSString *chap; NSString *toolName; NSString *secHeading; BOOL createSec = NO; NSMutableString *m; NSRange r; chap = [info objectForKey: @"chapter"]; toolName = [[fileName lastPathComponent] stringByDeletingPathExtension]; if (chap == nil) { chap = [NSString stringWithFormat: @"%@", toolName]; } else { createSec = YES; } m = [chap mutableCopy]; r = [m rangeOfString: @""]; r.length = 0; if (createSec) { [m replaceCharactersInRange: r withString: @"\n"]; } [m replaceCharactersInRange: r withString: c]; if (createSec) { secHeading = [NSString stringWithFormat: @"
\n\n", toolName]; //The %@ tool [m replaceCharactersInRange: r withString: secHeading]; } [info setObject: m forKey: @"chapter"]; RELEASE(m); } /** * Append a comment (with leading and trailing space stripped) * to an information dictionary.
* If the dictionary is nil, accumulate in the comment ivar instead.
* If the comment is empty, ignore it.
* If there is no comment in the dictionary, simply set the new value.
* If a comment already exists then the new comment text is appended to * it with a separating line break inserted if necessary.
*/ - (void) appendComment: (NSString*)s to: (NSMutableDictionary*)d { s = [s stringByTrimmingSpaces]; if ([s length] > 0) { NSString *old; if (d == nil) { old = comment; } else { old = [d objectForKey: @"Comment"]; } if (old != nil) { if ([old hasSuffix: @"

"] == NO && [old hasSuffix: @"
"] == NO && [old hasSuffix: @"
"] == NO) { s = [old stringByAppendingFormat: @"
%@", s]; } else { s = [old stringByAppendingString: s]; } } if (d == nil) { ASSIGN(comment, s); } else { [d setObject: s forKey: @"Comment"]; } } } - (void) dealloc { DESTROY(wordMap); DESTROY(ifStack); DESTROY(declared); DESTROY(info); DESTROY(comment); DESTROY(identifier); DESTROY(identStart); DESTROY(spaces); DESTROY(spacenl); DESTROY(source); [super dealloc]; } - (NSMutableDictionary*) info { return info; } - (id) init { NSMutableCharacterSet *m; NSMutableSet *s; m = [[NSCharacterSet controlCharacterSet] mutableCopy]; [m addCharactersInString: @" "]; spacenl = [m copy]; [m removeCharactersInString: @"\n"]; spaces = [m copy]; RELEASE(m); identifier = RETAIN([NSCharacterSet characterSetWithCharactersInString: @"_0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"]); identStart = RETAIN([NSCharacterSet characterSetWithCharactersInString: @"_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"]); info = [[NSMutableDictionary alloc] initWithCapacity: 6]; source = [NSMutableArray new]; verbose = [[NSUserDefaults standardUserDefaults] boolForKey: @"Verbose"]; warn = [[NSUserDefaults standardUserDefaults] boolForKey: @"Warn"]; documentInstanceVariables = YES; ifStack = [[NSMutableArray alloc] initWithCapacity: 4]; s = [NSMutableSet new]; [ifStack addObject: s]; RELEASE(s); return self; } - (void) log: (NSString*)fmt arguments: (va_list)args { const char *msg; int where; /* * Take the current position in the character buffer and * step through the lines array to find which line of the * original document it was on. * NB. Each item in the array represents the position *after* * a newline in the original data - so the zero'th array * element contains the character position of the start of * line two in human readable numbering (ie starting from 1). */ for (where = [lines count] - 1; where >= 0; where--) { NSNumber *num = [lines objectAtIndex: where]; if ([num intValue] <= (int)pos) { break; } } where += 2; if (unitName != nil) { if (itemName != nil) { fmt = [NSString stringWithFormat: @"%@:%u %@(%@): %@", fileName, where, unitName, itemName, fmt]; } else { fmt = [NSString stringWithFormat: @"%@:%u %@: %@", fileName, where, unitName, fmt]; } } else { fmt = [NSString stringWithFormat: @"%@:%u %@", fileName, where, fmt]; } fmt = AUTORELEASE([[NSString alloc] initWithFormat: fmt arguments: args]); if ([fmt hasSuffix: @"\n"] == NO) { fmt = [fmt stringByAppendingString: @"\n"]; } msg = [fmt lossyCString]; fwrite(msg, strlen(msg), 1, stderr); } - (void) log: (NSString*)fmt, ... { va_list ap; va_start (ap, fmt); [self log: fmt arguments: ap]; va_end (ap); } - (void) parseArgsInto: (NSMutableDictionary*)d { BOOL wasInArgList = inArgList; NSMutableArray *a = [d objectForKey: @"Args"]; NSAssert([d objectForKey: @"Args"] == nil, NSInternalInconsistencyException); a = [[NSMutableArray alloc] initWithCapacity: 4]; [d setObject: a forKey: @"Args"]; RELEASE(a); inArgList = YES; pos++; // Step past opening '(' while ([self parseSpace] < length && buffer[pos] != ')') { if (buffer[pos] == ',') { pos++; } else if (buffer[pos] == '.') { pos += 3; // Skip '...' [d setObject: @"YES" forKey: @"VarArgs"]; } else { NSMutableDictionary *m; m = [self parseDeclaration]; if (m == nil) { break; } if ([[m objectForKey: @"BaseType"] isEqual: @"void"] == YES && [m objectForKey: @"Prefix"] == nil) { continue; // C++ style empty arg list. eg. 'int foo(void);' } [a addObject: m]; } } if (pos < length) { pos++; // Step past closing ')' } inArgList = wasInArgList; } /** * Return the list of known output files depending on this source/header. */ - (NSMutableArray*) outputs { NSUserDefaults *defs = [NSUserDefaults standardUserDefaults]; NSMutableArray *output = [NSMutableArray arrayWithCapacity: 6]; NSString *basic = [info objectForKey: @"Header"]; NSString *names[5] = { @"Functions", @"Typedefs", @"Variables", @"Macros", @"Constants" }; unsigned i; basic = [basic lastPathComponent]; basic = [basic stringByDeletingPathExtension]; basic = [basic stringByAppendingPathExtension: @"gsdoc"]; /** * If there are any classes, categories, or protocols, there will be * an output file for them whose name is based on the name of the header. */ if ([[info objectForKey: @"Classes"] count] > 0 || [[info objectForKey: @"Categories"] count] > 0 || [[info objectForKey: @"Protocols"] count] > 0) { [output addObject: basic]; } /** * If there are any constants, variables, typedefs or functions, there * will either be a shared output file for them (defined by a template * name set in the user defaults system), or they will go in the same * file as classes etc. */ for (i = 0; i < sizeof(names) / sizeof(NSString*); i++) { NSString *base = names[i]; if ([[info objectForKey: base] count] > 0) { NSString *file; base = [base stringByAppendingString: @"Template"]; file = [defs stringForKey: base]; if ([file length] == 0) { if ([output containsObject: basic] == NO) { [output addObject: basic]; } } else { if ([[file pathExtension] isEqual: @"gsdoc"] == NO) { file = [file stringByAppendingPathExtension: @"gsdoc"]; } if ([output containsObject: file] == NO) { [output addObject: file]; } } } } return output; } /** * In spite of its trivial name, this is one of the key methods - * it parses and skips past comments, but it also recognizes special * comments (with an additional asterisk after the start of the block * comment) and extracts their contents, accumulating them into the * 'comment' instance variable.
* When the data provided by a comment is appended to the data * stored in the 'comment' instance variable, a line break (<br />)is * automatically forced to separate it from the proceding info.
* In addition, the first extracted documentation is checked for the * prsence of file header markup, which is extracted into the 'info' * dictionary. */ - (unsigned) parseComment { if (buffer[pos + 1] == '/') { return [self skipRemainderOfLine]; } else if (buffer[pos + 1] == '*') { unichar *start = 0; BOOL isDocumentation = NO; BOOL skippedFirstLine = NO; NSRange r; pos += 2; /* Skip opening part */ /* * Only comments starting with slash and TWO asterisks are special. */ if (pos < length - 2 && buffer[pos] == '*' && buffer[pos + 1] != '*') { isDocumentation = YES; pos++; /* * Ignore first line of comment if it is empty. */ if ([self skipSpaces] < length && buffer[pos] == '\n') { pos++; skippedFirstLine = YES; } } /* * Find end of comment. */ start = &buffer[pos]; while (pos < length) { unichar c = buffer[pos++]; if (c == '*' && pos < length && buffer[pos] == '/') { pos++; // Position after trailing slash. break; } } if (isDocumentation == YES) { unichar *end = &buffer[pos - 1]; unichar *ptr = start; unichar *newLine = ptr; BOOL stripAsterisks = NO; /* * Remove any asterisks immediately before end of comment. */ while (end > start && end[-1] == '*') { end--; } /* * Remove any trailing whitespace in the comment, but ensure that * there is a final newline. */ while (end > start && [spacenl characterIsMember: end[-1]] == YES) { end--; } *end++ = '\n'; /* * If second line in the comment starts with whitespace followed * by an asterisk, we assume all the lines in the comment start * in a similar way, and everything up to and including the * asterisk on each line should be stripped. * Otherwise we take the comment verbatim. */ if (skippedFirstLine == NO) { while (ptr < end && *ptr != '\n') { ptr++; } ptr++; // Step past the end of the first line. } while (ptr < end) { unichar c = *ptr++; if (c == '\n') { break; } else if (c == '*') { stripAsterisks = YES; break; } else if ([spaces characterIsMember: c] == NO) { break; } } if (stripAsterisks == YES) { /* * Strip parts of lines up to leading asterisks. */ ptr = start; while (ptr < end) { unichar c = *ptr++; if (c == '\n') { newLine = ptr; } else if (c == '*' && newLine != 0) { unichar *out = newLine; while (ptr < end) { *out++ = *ptr++; } end = out; ptr = newLine; newLine = 0; } else if ([spaces characterIsMember: c] == NO) { newLine = 0; } } } /* * If we have something for documentation, accumulate it in the * 'comment' ivar. */ if (end > start) { NSString *tmp; tmp = [NSString stringWithCharacters: start length: end - start]; [self appendComment: tmp to: nil]; } /* * We're in the first comment of a file; perform special processing. */ if (commentsRead == NO && comment != nil) { unsigned commentLength = [comment length]; NSMutableArray *authors; NSEnumerator *enumerator; NSArray *keys; NSString *key; authors = (NSMutableArray*)[info objectForKey: @"authors"]; /* * Scan through for more authors */ r = NSMakeRange(0, commentLength); while (r.length > 0) { r = [comment rangeOfString: @" 0) { unsigned i = r.location; r = NSMakeRange(i, commentLength - i); r = [comment rangeOfString: @"" options: NSLiteralSearch range: r]; if (r.length > 0) { NSString *author; r = NSMakeRange(i, NSMaxRange(r) - i); author = [comment substringWithRange: r]; i = NSMaxRange(r); r = NSMakeRange(i, commentLength - i); /* * There may be more than one author * of a document. */ if (authors == nil) { authors = [NSMutableArray new]; [info setObject: authors forKey: @"authors"]; RELEASE(authors); } if ([authors containsObject: author] == NO) { [authors addObject: author]; } } else { [self log: @"unterminated in comment"]; } } } /* * In addition to fully specified author elements in the * comment, we look for lines of the formats - * Author: name * Author: name * By: name * By: name */ r = NSMakeRange(0, commentLength); while (r.length > 0) { NSString *term = @"\n"; NSRange a; NSRange b; /* * Look for 'Author:' or 'By:' and use whichever we * find first. */ a = [comment rangeOfString: @"author:" options: NSCaseInsensitiveSearch range: r]; b = [comment rangeOfString: @"by:" options: NSCaseInsensitiveSearch range: r]; if (a.length > 0) { if (b.length > 0 && b.location < a.location) { r = b; } else { r = a; /* * A line '$Author$' is an RCS tag and is * terminated by the second dollar rather than * by a newline. */ if (r.location > 0 && [comment characterAtIndex: r.location-1] == '$') { term = @"$"; } } } else { r = b; } if (r.length > 0) { unsigned i = NSMaxRange(r); NSString *line; NSString *author; r = NSMakeRange(i, commentLength - i); r = [comment rangeOfString: term options: NSLiteralSearch range: r]; if (r.length == 0) { r.location = commentLength; } r = NSMakeRange(i, NSMaxRange(r) - i); line = [comment substringWithRange: r]; line = [line stringByTrimmingSpaces]; i = NSMaxRange(r); r = [line rangeOfString: @"<"]; if (r.length > 0) { NSString *name; NSString *mail; name = [line substringToIndex: r.location]; name = [name stringByTrimmingSpaces]; mail = [line substringFromIndex: r.location+1]; r = [mail rangeOfString: @">"]; if (r.length > 0) { mail = [mail substringToIndex: r.location]; } author = [NSString stringWithFormat: @"" @"%@", name, mail, mail]; } else { author = [NSString stringWithFormat: @"", line]; } r = NSMakeRange(i, commentLength - i); if (authors == nil) { authors = [NSMutableArray new]; [info setObject: authors forKey: @"authors"]; RELEASE(authors); } if ([authors containsObject: author] == NO) { [authors addObject: author]; } } } /* * Lines of the form 'AutogsdocSource: ...' are used as the * names of source files to provide documentation information. * whitespace around a filename is stripped. */ r = NSMakeRange(0, commentLength); while (r.length > 0) { /* * Look for 'AutogsdocSource:' lines. */ r = [comment rangeOfString: @"AutogsdocSource:" options: NSCaseInsensitiveSearch range: r]; if (r.length > 0) { unsigned i = NSMaxRange(r); NSString *line; r = NSMakeRange(i, commentLength - i); r = [comment rangeOfString: @"\n" options: NSLiteralSearch range: r]; if (r.length == 0) { r.location = commentLength; } r = NSMakeRange(i, NSMaxRange(r) - i); line = [comment substringWithRange: r]; line = [line stringByTrimmingSpaces]; if (haveSource == NO) { haveSource = YES; [source removeAllObjects]; // remove default. } if ([line length] > 0 && [source containsObject: line] == NO) { NSFileManager *mgr; /* * See if the path given exists, and add it to * the list of source files parsed for this * header. */ mgr = [NSFileManager defaultManager]; if ([line isAbsolutePath] == YES) { if ([mgr isReadableFileAtPath: line] == NO) { [self log: @"AutogsdocSource: %@ not found!", line]; line = nil; } } else { NSString *p; /* * Try forming a path relative to the header. */ p = [info objectForKey: @"Header"]; p = [p stringByDeletingLastPathComponent]; p = [p stringByAppendingPathComponent: line]; if ([mgr isReadableFileAtPath: p] == YES) { line = p; } else if ([mgr isReadableFileAtPath: line] == NO) { NSUserDefaults *defs; NSString *ddir; NSString *old = p; defs = [NSUserDefaults standardUserDefaults]; ddir = [defs stringForKey: @"DocumentationDirectory"]; if ([ddir length] > 0) { p = [ddir stringByAppendingPathComponent: line]; if ([mgr isReadableFileAtPath: p] == YES) { line = p; } else { [self log: @"AutogsdocSource: %@ not " @"found (tried %@ and %@ too)!", line, old, p]; line = nil; } } else { [self log: @"AutogsdocSource: %@ not " @"found (tried %@ too)!", line, old]; line = nil; } } } if (line != nil) { [source addObject: line]; } } i = NSMaxRange(r); r = NSMakeRange(i, commentLength - i); } } /** * There are various sections we can extract from the * document - at most one of each. * If date and version are not supplied RCS Date and Revision * tags will be extracted where available. */ keys = [NSArray arrayWithObjects: @"abstract", // Abstract for document head @"back", // Appendix for document body @"chapter", // Chapter at start of document @"copy", // Copyright for document head @"date", // date for document head @"front", // Forward for document body @"title", // Title for document head @"version", // Version for document head nil]; enumerator = [keys objectEnumerator]; while ((key = [enumerator nextObject]) != nil) { NSString *s = [NSString stringWithFormat: @"<%@>", key]; NSString *e = [NSString stringWithFormat: @"", key]; /* * Read date information if available */ r = [comment rangeOfString: s]; if (r.length > 0) { unsigned i = r.location; r = NSMakeRange(i, commentLength - i); r = [comment rangeOfString: e options: NSLiteralSearch range: r]; if (r.length > 0) { NSString *val; r = NSMakeRange(i, NSMaxRange(r) - i); val = [comment substringWithRange: r]; [info setObject: val forKey: key]; } else { [self log: @"unterminated %@ in comment", s]; } } } /* * If no ... then try Copyright: */ if ([info objectForKey: @"copy"] == nil) { r = NSMakeRange(0, commentLength); while (r.length > 0) { /* * Look for 'Copyright:' */ r = [comment rangeOfString: @"copyright (c)" options: NSCaseInsensitiveSearch range: r]; if (r.length > 0) { unsigned i = NSMaxRange(r); NSString *line; r = NSMakeRange(i, commentLength - i); r = [comment rangeOfString: @"\n" options: NSLiteralSearch range: r]; if (r.length == 0) { r.location = commentLength; } r = NSMakeRange(i, NSMaxRange(r) - i); line = [comment substringWithRange: r]; line = [line stringByTrimmingSpaces]; line = [NSString stringWithFormat: @"%@", line]; [info setObject: line forKey: @"copy"]; } } } /* * If no ... then try RCS info. */ if ([info objectForKey: @"date"] == nil) { r = [comment rangeOfString: @"$Date:"]; if (r.length > 0) { unsigned i = NSMaxRange(r); NSString *date; r = NSMakeRange(i, commentLength - i); r = [comment rangeOfString: @"$" options: NSLiteralSearch range: r]; if (r.length > 0) { r = NSMakeRange(i, r.location - i); date = [comment substringWithRange: r]; date = [date stringByTrimmingSpaces]; date = [NSString stringWithFormat: @"%@", date]; [info setObject: date forKey: @"date"]; } } } /* * If no ... then try RCS info. */ if ([info objectForKey: @"version"] == nil) { r = [comment rangeOfString: @"$Revision:"]; if (r.length > 0) { unsigned i = NSMaxRange(r); NSString *version; r = NSMakeRange(i, commentLength - i); r = [comment rangeOfString: @"$" options: NSLiteralSearch range: r]; if (r.length > 0) { r = NSMakeRange(i, r.location - i); version = [comment substringWithRange: r]; version = [version stringByTrimmingSpaces]; version = [NSString stringWithFormat: @"%@", version]; [info setObject: version forKey: @"version"]; } } } } commentsRead = YES; } } return pos; } - (void) parseDeclaratorInto: (NSMutableDictionary*)d { NSMutableString *p = nil; NSMutableString *s = nil; while ([self parseSpace] < length) { while (pos < length && buffer[pos] == '*') { if (p == nil && (p = [d objectForKey: @"Prefix"]) == nil) { p = [NSMutableString new]; [d setObject: p forKey: @"Prefix"]; RELEASE(p); } else if ([p hasSuffix: @"("] == NO && [p hasSuffix: @"*"] == NO) { [p appendString: @" "]; } [p appendString: @"*"]; pos++; } if (buffer[pos] == '(') { if (p == nil && (p = [d objectForKey: @"Prefix"]) == nil) { p = [NSMutableString new]; [d setObject: p forKey: @"Prefix"]; RELEASE(p); } else if ([p hasSuffix: @"("] == NO && [p hasSuffix: @"*"] == NO) { [p appendString: @" "]; } [p appendString: @"("]; pos++; [self parseDeclaratorInto: d]; if ([self parseSpace] < length && buffer[pos] == '(') { [self parseArgsInto: d]; // parse function args. } if ([self parseSpace] < length && buffer[pos] == ')') { if (s == nil && (s = [d objectForKey: @"Suffix"]) == nil) { s = [NSMutableString new]; [d setObject: s forKey: @"Suffix"]; RELEASE(s); } [s appendString: @")"]; pos++; return; } else { [self log: @"missing ')' in declarator."]; return; } } else { NSString *t; t = [self parseIdentifier]; if (t == nil) { return; } if ([t isEqualToString: @"const"] || [t isEqualToString: @"volatile"]) { if (p == nil && (p = [d objectForKey: @"Prefix"]) == nil) { p = [NSMutableString new]; [d setObject: p forKey: @"Prefix"]; RELEASE(p); } else if ([p hasSuffix: @"("] == NO) { [p appendString: @" "]; } [p appendString: t]; } else { [d setObject: t forKey: @"Name"]; return; } } } } - (NSMutableDictionary*) parseDeclaration { NSMutableDictionary *d = [NSMutableDictionary dictionary]; #if GS_WITH_GC == 0 CREATE_AUTORELEASE_POOL(arp); #endif static NSSet *qualifiers = nil; static NSSet *keep = nil; NSMutableString *t = nil; NSMutableArray *a; NSString *s; BOOL isTypedef = NO; BOOL isPointer = NO; BOOL isFunction = NO; BOOL baseConstant = NO; BOOL needScalarType = NO; if (qualifiers == nil) { qualifiers = [NSSet setWithObjects: @"auto", @"const", @"extern", @"inline", @"long", @"register", @"short", @"signed", @"static", @"typedef", @"unsigned", @"volatile", nil]; RETAIN(qualifiers); keep = [NSSet setWithObjects: @"const", @"long", @"short", @"signed", @"unsigned", @"volatile", nil]; RETAIN(keep); } a = [NSMutableArray array]; while ((s = [self parseIdentifier]) != nil) { if (inHeader == NO && [s isEqualToString: @"static"] == YES) { /* * We don't want to document static declarations unless they * occur in a public header. */ [self skipStatementLine]; goto fail; } if ([s isEqualToString: @"GS_EXPORT"] == YES) { s = @"extern"; } if ([qualifiers member: s] == nil) { break; } else { if ([s isEqualToString: @"typedef"] == YES) { isTypedef = YES; } if ([keep member: s] != nil) { [a addObject: s]; if ([s isEqual: @"const"] == NO && [s isEqual: @"volatile"] == NO) { needScalarType = YES; } } } } /** * We handle struct, union, and enum declarations by skipping the * stuff enclosed in curly braces. If there was an identifier * after the keyword we use it as the struct name, otherwise we * use '...' to denote a nameless type. */ if ([s isEqualToString: @"struct"] == YES || [s isEqualToString: @"union"] == YES || [s isEqualToString: @"enum"] == YES) { NSString *tmp = s; s = [self parseIdentifier]; if (s == nil) { s = [NSString stringWithFormat: @"%@ ...", tmp]; } else { s = [NSString stringWithFormat: @"%@ %@", tmp, s]; /* * It's possible to declare a struct, union, or enum without * giving it a name beyond after the declaration, in this case * we can use something like 'struct foo' as the name. */ [d setObject: s forKey: @"Name"]; } if ([self parseSpace] < length && buffer[pos] == '{') { [self skipBlock]; } [a addObject: s]; s = nil; } else { if (s == nil) { /* * If there is no identifier here, the line must have been * something like 'unsigned *length' so we must set the default * base type of 'int' */ [a addObject: @"int"]; } else if (needScalarType == YES && [s isEqualToString: @"char"] == NO && [s isEqualToString: @"int"] == NO) { /* * If we had something like 'unsigned' in the qualifiers, we must * have a 'char' or an 'int', and if we didn't find one we should * insert one and use what we found as the variable name. */ [a addObject: @"int"]; } else { [a addObject: s]; s = nil; // s used as baseType } } /* * Now build a string containing the base type in a standardised form. */ t = [NSMutableString new]; if ([a containsObject: @"const"] == YES) { [t appendString: @"const"]; [t appendString: @" "]; [a removeObject: @"const"]; baseConstant = YES; } else if ([a containsObject: @"volatile"] == YES) { [t appendString: @"volatile"]; [t appendString: @" "]; [a removeObject: @"volatile"]; } if ([a containsObject: @"signed"] == YES) { [t appendString: @"signed"]; [t appendString: @" "]; [a removeObject: @"signed"]; } else if ([a containsObject: @"unsigned"] == YES) { [t appendString: @"unsigned"]; [t appendString: @" "]; [a removeObject: @"unsigned"]; } if ([a containsObject: @"short"] == YES) { [t appendString: @"short"]; [t appendString: @" "]; [a removeObject: @"short"]; } else if ([a containsObject: @"long"] == YES) { unsigned c = [a count]; /* * There may be more than one 'long' in a type spec */ while (c-- > 0) { NSString *tmp = [a objectAtIndex: c]; if ([tmp isEqual: @"long"] == YES) { [t appendString: tmp]; [t appendString: @" "]; [a removeObjectAtIndex: c]; } } } if ([a count] != 1) { [self log: @"odd values in declaration base type - '%@'", a]; [t appendString: [a componentsJoinedByString: @" "]]; } else { [t appendString: [a objectAtIndex: 0]]; } [a removeAllObjects]; // Parsed base type /* * Handle protocol specification if necessary */ if ([self parseSpace] < length && buffer[pos] == '<') { NSString *p; do { pos++; p = [self parseIdentifier]; if (p != nil) { [a addObject: p]; } } while ([self parseSpace] < length && buffer[pos] == ','); pos++; [self parseSpace]; [a sortUsingSelector: @selector(compare:)]; [t appendString: @"<"]; [t appendString: [a componentsJoinedByString: @","]]; [t appendString: @">"]; [a removeAllObjects]; } [d setObject: t forKey: @"BaseType"]; RELEASE(t); /* * Set the 'Kind' of declaration ... one of 'Types', 'Functions', * 'Variables', or 'Constants' * We may override this later. */ if (isTypedef == YES) { [d setObject: @"Types" forKey: @"Kind"]; [d setObject: @"YES" forKey: @"Implemented"]; } else if (baseConstant == YES) { [d setObject: @"Constants" forKey: @"Kind"]; [d setObject: @"YES" forKey: @"Implemented"]; } else { [d setObject: @"Variables" forKey: @"Kind"]; } if (s == nil) { [self parseDeclaratorInto: d]; /* * There may have been '*' and 'const' applied to the declarator * which will change whether it is a constant or a variable, and * whether it is a pointer to something. * If the last thing to be applied was a '*' it is a variable * which points to a constant. If the last thing was 'const' * then it is a constant (and may be a pointer too). */ s = [d objectForKey: @"Prefix"]; if (s != nil) { NSRange r; r = [s rangeOfString: @"*" options: NSBackwardsSearch|NSLiteralSearch]; if (r.length > 0) { unsigned p = r.location; isPointer = YES; if (isTypedef == NO) { r = [s rangeOfString: @"const" options: NSBackwardsSearch|NSLiteralSearch]; if (r.length > 0 && r.location >= p) { [d setObject: @"Constants" forKey: @"Kind"]; } } } } } else { [d setObject: s forKey: @"Name"]; } if ([self parseSpace] < length) { if (buffer[pos] == '[') { NSMutableString *suffix; if ((suffix = [d objectForKey: @"Suffix"]) == nil) { suffix = [NSMutableString new]; [d setObject: suffix forKey: @"Suffix"]; RELEASE(suffix); } while (buffer[pos] == '[') { unsigned old = pos; if ([self skipArray] == old) { break; } [suffix appendString: @"[]"]; } } else if (buffer[pos] == '(') { [self parseArgsInto: d]; } } if ([d objectForKey: @"Args"] != nil) { /* * If the declaration looked like this int (*foo)() then * 'isPointer' will be YES and 'Suffix' will contain the * bracket after 'foo'. In this case, what we have is a * variable or constant pointer to a function. * Otherwise, we have a function declaration and the * 'Kind' should be set to 'function'. */ if (isPointer == NO || [d objectForKey: @"Suffix"] == nil) { [d setObject: @"Functions" forKey: @"Kind"]; isFunction = YES; } } if ([self parseSpace] < length) { if (inArgList == YES) { if (buffer[pos] == ')' || buffer[pos] == ',') { RELEASE(arp); return d; } else { [self log: @"Unexpected char (%c) in arg list", buffer[pos]]; [self skipStatement]; goto fail; } } else { if (isFunction == YES) { NSString *ident = [self parseIdentifier]; if ([ident isEqual: @"__attribute__"] == YES) { if ([self skipSpaces] < length && buffer[pos] == '(') { unsigned start = pos; NSString *attr; [self skipBlock]; // Skip the attributes attr = [NSString stringWithCharacters: buffer + start length: pos - start]; if ([attr rangeOfString: @"deprecated"].length > 0) { [self appendComment: @"Warning this is " @"deprecated and may be removed in " @"future versions" to: nil]; } } else { [self log: @"strange format function attributes"]; } } else if (ident != nil) { [self log: @"ignoring '%@' in function declaration", ident]; } } if (buffer[pos] == '_') { NSString *ident = [self parseIdentifier]; if ([ident isEqualToString: @"__attribute__"] == YES) { [self skipSpaces]; if (pos < length && buffer[pos] == '(') { unsigned start = pos; NSString *attr; [self skipBlock]; attr = [NSString stringWithCharacters: buffer + start length: pos - start]; if ([attr rangeOfString: @"deprecated"].length > 0) { [self appendComment: @"Warning this is " @"deprecated and may be removed in " @"future versions" to: nil]; } [self skipSpaces]; } } else { [self log: @"Underscore is not from __attribute__"]; goto fail; } if (pos >= length) { [self log: @"Unexpected end of declaration"]; goto fail; } } if (buffer[pos] == ';') { [self skipStatement]; } else if (buffer[pos] == ',') { [self log: @"ignoring multiple comma separated declarations"]; [self skipStatement]; } else if (buffer[pos] == '=') { [self skipStatement]; } else if (buffer[pos] == '{') { /* * Inline functions may be implemented in the header. */ if (isFunction == YES) { [d setObject: @"YES" forKey: @"Implemented"]; } [self skipBlock]; } else { [self log: @"Unexpected char (%c) in declaration", buffer[pos]]; [self skipStatement]; goto fail; } /* * Read in any comment on the same line in case it * contains documentation for the declaration. */ if ([self skipSpaces] < length && buffer[pos] == '/') { [self parseComment]; } } if (comment != nil) { [self appendComment: comment to: d]; } DESTROY(comment); RELEASE(arp); if (inArgList == NO) { /* * This is a top-level declaration, so let's tidy up ready for * linking into the documentation tree. */ if ([d objectForKey: @"Name"] == nil) { NSString *t = [d objectForKey: @"BaseType"]; /* * Don't bother to warn about nameless enumerations. */ if (verbose == YES && [t isEqual: @"enum ..."] == NO) { [self log: @"parse declaration with no name - %@", d]; } return nil; } } return d; } else { [self log: @"unexpected end of data parsing declaration"]; } fail: DESTROY(comment); RELEASE(arp); return nil; } - (NSMutableDictionary*) parseFile: (NSString*)name isSource: (BOOL)isSource { NSString *token; NSMutableDictionary *nDecl; if (isSource == YES) { inHeader = NO; } else { inHeader = YES; } commentsRead = NO; fileName = name; if (declared == nil) { ASSIGN(declared, [fileName lastPathComponent]); } /** * If this is parsing a header file (isSource == NO) then we reset the * list of known source files associated with the header before proceeding. */ [source removeAllObjects]; if (isSource == NO) { NSFileManager *mgr = [NSFileManager defaultManager]; NSString *path; [info setObject: fileName forKey: @"Header"]; [source removeAllObjects]; /** * We initially assume that the location of a source file is the * same as the header, but if there is no file at that location, * we expect the source to be in the documentatation directory * or the current directory instead. */ path = [fileName stringByDeletingPathExtension]; path = [path stringByAppendingPathExtension: @"m"]; if ([mgr isReadableFileAtPath: path] == NO) { path = [path lastPathComponent]; if ([mgr isReadableFileAtPath: path] == NO) { NSUserDefaults *defs; NSString *ddir; defs = [NSUserDefaults standardUserDefaults]; ddir = [defs stringForKey: @"DocumentationDirectory"]; if ([ddir length] > 0) { path = [ddir stringByAppendingPathComponent: path]; if ([mgr isReadableFileAtPath: path] == NO) { path = nil; // No default source file found. } } else { path = nil; // No default source file found. } } } if (path != nil) { [source addObject: path]; } } unitName = nil; itemName = nil; DESTROY(comment); [self setupBuffer]; while ([self parseSpace] < length) { unichar c = buffer[pos++]; switch (c) { case '#': /* * Some preprocessor directive ... must be on one line ... skip * past it and delete any comment accumulated while doing so. */ [self parsePreprocessor]; DESTROY(comment); break; case '@': token = [self parseIdentifier]; if (token != nil) { if ([token isEqual: @"interface"] == YES) { if (isSource == YES) { [self skipUnit]; DESTROY(comment); } else { [self parseInterface]; } } else if ([token isEqual: @"protocol"] == YES) { if (isSource == YES) { [self skipUnit]; DESTROY(comment); } else { [self parseProtocol]; } } else if ([token isEqual: @"implementation"] == YES) { if (isSource == YES) { [self parseImplementation]; } else { [self skipUnit]; DESTROY(comment); } } else { [self skipStatementLine]; } } break; default: /* * Must be some sort of declaration ... */ pos--; nDecl = [self parseDeclaration]; if (nDecl != nil) { NSString *name = [nDecl objectForKey: @"Name"]; NSString *kind = [nDecl objectForKey: @"Kind"]; NSMutableDictionary *dict = [info objectForKey: kind]; if (isSource == NO) { /* * Ensure that we have an entry for this declaration. */ if (dict == nil) { dict = [NSMutableDictionary new]; [info setObject: dict forKey: kind]; RELEASE(dict); } [dict setObject: nDecl forKey: name]; } else { NSMutableDictionary *oDecl = [dict objectForKey: name]; if (oDecl != nil) { NSString *oc = [oDecl objectForKey: @"Comment"]; NSString *nc = [nDecl objectForKey: @"Comment"]; /* * If the old comment from the header parsing is * the same as the new comment from the source * parsing, assume we parsed the same file as both * source and header ... otherwise append the new * comment. */ if ([oc isEqual: nc] == NO) { [self appendComment: nc to: oDecl]; } [oDecl setObject: @"YES" forKey: @"Implemented"]; if ([kind isEqualToString: @"Functions"] == YES) { NSArray *a1 = [oDecl objectForKey: @"Args"]; NSArray *a2 = [nDecl objectForKey: @"Args"]; if ([a1 isEqual: a2] == NO) { [self log: @"Function %@ args missmatch - " @"%@ %@", name, a1, a2]; } /* * A main function is not documented as a * function, but as a special case its * comments are added to the 'front' * section of the documentation. */ if ([name isEqual: @"main"] == YES) { NSString *c; c = [oDecl objectForKey: @"Comment"]; if (c != nil) { [self addMain: c]; } [dict removeObjectForKey: name]; } } } } } break; } } return info; } - (NSMutableDictionary*) parseImplementation { NSString *nc = nil; NSString *name; NSString *base = nil; NSString *category = nil; NSDictionary *methods = nil; NSMutableDictionary *d; NSMutableDictionary *dict = nil; CREATE_AUTORELEASE_POOL(arp); /* * Record any class documentation for this class */ nc = AUTORELEASE(comment); comment = nil; if ((name = [self parseIdentifier]) == nil || [self parseSpace] >= length) { [self log: @"implementation with bad name"]; goto fail; } unitName = name; /* * After the class name, we may have a category name or * a base class, but not both. */ if (buffer[pos] == '(') { pos++; if ((category = [self parseIdentifier]) == nil || [self parseSpace] >= length || buffer[pos++] != ')' || [self parseSpace] >= length) { [self log: @"interface with bad category"]; goto fail; } name = [name stringByAppendingFormat: @"(%@)", category]; unitName = name; } else if (buffer[pos] == ':') { pos++; if ((base = [self parseIdentifier]) == nil || [self parseSpace] >= length) { [self log: @"@interface with bad base class"]; goto fail; } } if (category == nil) { d = [info objectForKey: @"Classes"]; } else { d = [info objectForKey: @"Categories"]; } dict = [d objectForKey: unitName]; if (dict == nil) { /* * If the implementation found does not correspond to an * interface found in the header file, it should not be * documented, and we skip it. */ [self skipUnit]; DESTROY(comment); return [NSMutableDictionary dictionary]; } else { NSString *oc = [dict objectForKey: @"Comment"]; [dict setObject: @"YES" forKey: @"Implemented"]; /* * Append any comment we have for this ... if it's not just a copy * because we've parsed the same file twice. */ if ([oc isEqual: nc] == NO) { [self appendComment: nc to: dict]; } /* * Update base class if necessary. */ if (base != nil) { if ([base isEqual: [dict objectForKey: @"BaseClass"]] == NO) { [self log: @"implementation base class differs from interface"]; } [dict setObject: base forKey: @"BaseClass"]; } } methods = [self parseMethodsAreDeclarations: NO]; if (methods != nil && [methods count] > 0) { // [dict setObject: methods forKey: @"Methods"]; } // [self log: @"Found implementation %@", dict]; unitName = nil; DESTROY(comment); RELEASE(arp); return dict; fail: unitName = nil; DESTROY(comment); RELEASE(arp); return nil; } - (NSMutableDictionary*) parseInterface { NSString *name; NSString *base = nil; NSString *category = nil; NSDictionary *methods = nil; NSMutableDictionary *d; NSMutableDictionary *dict; CREATE_AUTORELEASE_POOL(arp); dict = [NSMutableDictionary dictionaryWithCapacity: 8]; /* * Record any class documentation for this class */ if (comment != nil) { [self appendComment: comment to: dict]; DESTROY(comment); } if ((name = [self parseIdentifier]) == nil || [self parseSpace] >= length) { [self log: @"interface with bad name"]; goto fail; } unitName = name; [dict setObject: @"class" forKey: @"Type"]; [self setStandards: dict]; /* * After the class name, we may have a category name or * a base class, but not both. */ if (buffer[pos] == '(') { pos++; if ((category = [self parseIdentifier]) == nil || [self parseSpace] >= length || buffer[pos++] != ')' || [self parseSpace] >= length) { [self log: @"interface with bad category"]; goto fail; } [dict setObject: category forKey: @"Category"]; [dict setObject: name forKey: @"BaseClass"]; name = [name stringByAppendingFormat: @"(%@)", category]; unitName = name; [dict setObject: @"category" forKey: @"Type"]; if ([category length] >= 7 && [category compare: @"Private" options: NSCaseInsensitiveSearch range: NSMakeRange(0, 7)] == NSOrderedSame) { NSString *c; c = @"Warning this category is private, which " @"means that the methods are for internal use by the package. " @"You should not use them in external code."; [self appendComment: c to: dict]; } } else if (buffer[pos] == ':') { pos++; if ((base = [self parseIdentifier]) == nil || [self parseSpace] >= length) { [self log: @"@interface with bad base class"]; goto fail; } [dict setObject: base forKey: @"BaseClass"]; } [dict setObject: name forKey: @"Name"]; /* * Interfaces or categories may conform to protocols. */ if (buffer[pos] == '<') { NSArray *protocols = [self parseProtocolList]; if (protocols == nil) { goto fail; } else if ([protocols count] > 0) { [dict setObject: protocols forKey: @"Protocols"]; } } /* * Interfaces may have instance variables, but categories may not. */ if (buffer[pos] == '{' && category == nil) { NSDictionary *ivars = [self parseInstanceVariables]; if (ivars == nil) { goto fail; } else if ([ivars count] > 0) { [dict setObject: ivars forKey: @"InstanceVariables"]; } DESTROY(comment); // Ignore any ivar comments. } methods = [self parseMethodsAreDeclarations: YES]; if (methods != nil && [methods count] > 0) { [dict setObject: methods forKey: @"Methods"]; } [dict setObject: declared forKey: @"Declared"]; if (category == nil) { d = [info objectForKey: @"Classes"]; if (d == nil) { d = [[NSMutableDictionary alloc] initWithCapacity: 4]; [info setObject: d forKey: @"Classes"]; RELEASE(d); } } else { d = [info objectForKey: @"Categories"]; if (d == nil) { d = [[NSMutableDictionary alloc] initWithCapacity: 4]; [info setObject: d forKey: @"Categories"]; RELEASE(d); } } [d setObject: dict forKey: unitName]; // [self log: @"Found interface %@", dict]; unitName = nil; DESTROY(comment); RELEASE(arp); return dict; fail: unitName = nil; DESTROY(comment); RELEASE(arp); return nil; } /** * Attempt to parse an identifier/keyword (with optional whitespace in * front of it). Perform mappings using the wordMap dictionary. If a * mapping produces an empty string, we treat it as if we had read * whitespace and try again. * If we read end of data, or anything which is invalid inside an * identifier, we return nil. */ - (NSString*) parseIdentifier { unsigned start; try: [self parseSpace]; if (pos >= length || [identStart characterIsMember: buffer[pos]] == NO) { return nil; } start = pos; while (pos < length) { if ([identifier characterIsMember: buffer[pos]] == NO) { NSString *tmp; NSString *val; tmp = [[NSString alloc] initWithCharacters: &buffer[start] length: pos - start]; val = [wordMap objectForKey: tmp]; if (val == nil) { return AUTORELEASE(tmp); // No mapping found. } RELEASE(tmp); if ([val length] > 0) { if ([val isEqualToString: @"//"] == YES) { [self skipToEndOfLine]; return [self parseIdentifier]; } return val; // Got mapped identifier. } goto try; // Mapping removed the identifier. } pos++; } return nil; } - (NSMutableDictionary*) parseInstanceVariables { NSString *validity = @"protected"; NSMutableDictionary *ivars; BOOL shouldDocument = documentInstanceVariables; DESTROY(comment); inInstanceVariables = YES; ivars = [NSMutableDictionary dictionaryWithCapacity: 8]; pos++; while ([self parseSpace] < length && buffer[pos] != '}') { if (buffer[pos] == '@') { NSString *token; pos++; if ((token = [self parseIdentifier]) == nil || [self parseSpace] >= length) { [self log: @"interface with bad validity directive"]; goto fail; } if ([token isEqual: @"private"] == YES) { ASSIGN(validity, token); shouldDocument = documentInstanceVariables && documentAllInstanceVariables; } else if ([token isEqual: @"protected"] == YES) { ASSIGN(validity, token); shouldDocument = documentInstanceVariables; } else if ([token isEqual: @"public"] == YES) { ASSIGN(validity, token); shouldDocument = documentInstanceVariables; } else { [self log: @"interface with bad validity (%@)", token]; goto fail; } } else if (buffer[pos] == '#') { [self parsePreprocessor]; // Ignore preprocessor directive. DESTROY(comment); } else if (shouldDocument == YES) { NSMutableDictionary *iv = [self parseDeclaration]; if (iv != nil) { if ([validity isEqual: @"private"] == NO) { NSString *n = [iv objectForKey: @"Name"]; if ([n hasPrefix: @"_"] == YES) { NSString *c; c = @"Warning the underscore at the start of " @"the name of this instance variable indicates that, " @"even though it is not technically private, " @"it is intended for internal use within the package, " @"and you should not use the variable in other code."; [self appendComment: c to: iv]; } } [iv setObject: validity forKey: @"Validity"]; [ivars setObject: iv forKey: [iv objectForKey: @"Name"]]; } } else { [self skipStatement]; } } inInstanceVariables = NO; if (pos >= length) { [self log: @"interface with bad instance variables"]; return nil; } pos++; // Step past closing bracket. return ivars; fail: DESTROY(comment); inInstanceVariables = NO; return nil; } /** * Parse a macro definition ... we are expected to have read #define already */ - (NSMutableDictionary*) parseMacro { NSMutableDictionary *dict; NSMutableArray *a = nil; NSString *name; dict = [[NSMutableDictionary alloc] initWithCapacity: 4]; [self parseSpace: spaces]; name = [self parseIdentifier]; [self parseSpace: spaces]; if (pos < length && buffer[pos] == '(') { a = [[NSMutableArray alloc] initWithCapacity: 4]; pos++; // Step past opening '(' while ([self parseSpace: spaces] < length && buffer[pos] != ')') { if (buffer[pos] == ',') { pos++; } else if (buffer[pos] == '.') { pos += 3; // Skip '...' [dict setObject: @"YES" forKey: @"VarArgs"]; } else { NSString *s; s = [self parseIdentifier]; if (s == nil) { break; } [a addObject: s]; } } if (pos < length) { pos++; // Step past closing ')' } } /* * Now parse macro body (to end of line) gathering any comments. */ [self parseSpace: spaces]; while (pos < length) { unsigned c = buffer[pos]; if (c == '\n') { break; } else if (c == '/') { unsigned save = pos; if ([self parseComment] == save) { pos++; // Step past '/' } } else if (c == '\'' || c == '"') { [self skipLiteral]; } else if ([spaces characterIsMember: c] == NO) { pos++; } else { [self parseSpace: spaces]; } } /** * It's common to have macros which don't need commenting ... * like the ones used to protect a header against multiple * inclusion for instance. For this reason, we ignore any * macro which is not preceeded by a documentation comment. */ if ([comment length] > 0) { [dict setObject: name forKey: @"Name"]; if (a != nil) { [dict setObject: a forKey: @"Args"]; } /* A macro is implemented as soon as it is defined. */ [dict setObject: @"YES" forKey: @"Implemented"]; [self appendComment: comment to: dict]; } else { DESTROY(dict); } RELEASE(a); return AUTORELEASE(dict); } - (NSMutableDictionary*) parseMethodIsDeclaration: (BOOL)flag { #if GS_WITH_GC == 0 CREATE_AUTORELEASE_POOL(arp); #endif NSMutableDictionary *method; NSMutableString *mname; NSString *token; NSMutableArray *types = nil; NSMutableArray *args = nil; NSMutableArray *sels = [NSMutableArray arrayWithCapacity: 2]; unichar term; method = [[NSMutableDictionary alloc] initWithCapacity: 4]; if (buffer[pos++] == '-') { mname = [NSMutableString stringWithCString: "-"]; } else { mname = [NSMutableString stringWithCString: "+"]; } [method setObject: sels forKey: @"Sels"]; // Parts of selector. /* * Parse return type ... defaults to 'id' */ if ([self parseSpace] >= length) { [self log: @"error parsing method return type"]; goto fail; } if (buffer[pos] == '(') { if ((token = [self parseMethodType]) == nil || [self parseSpace] >= length) { [self log: @"error parsing method return type"]; goto fail; } [method setObject: token forKey: @"ReturnType"]; } else { [method setObject: @"id" forKey: @"ReturnType"]; } if (flag == YES) { term = ';'; } else { term = '{'; } while (buffer[pos] != term) { token = [self parseIdentifier]; if ([self parseSpace] >= length) { [self log: @"error at method name component"]; goto fail; } if (buffer[pos] == ':') { NSString *arg; NSString *type = @"id"; pos++; if (token == nil) { [sels addObject: @":"]; } else { [mname appendString: token]; [sels addObject: [token stringByAppendingString: @":"]]; } [mname appendString: @":"]; if ([self parseSpace] >= length) { [self log: @"error parsing method argument"]; goto fail; } if (buffer[pos] == '(') { if ((type = [self parseMethodType]) == nil || [self parseSpace] >= length) { [self log: @"error parsing method arguument type"]; goto fail; } } if ((arg = [self parseIdentifier]) == nil || [self parseSpace] >= length) { [self log: @"error parsing method argument name"]; goto fail; } if (types == nil) { types = [NSMutableArray arrayWithCapacity: 2]; [method setObject: types forKey: @"Types"]; } [types addObject: type]; if (args == nil) { args = [NSMutableArray arrayWithCapacity: 2]; [method setObject: args forKey: @"Args"]; } [args addObject: arg]; if (buffer[pos] == ',') { [method setObject: @"YES" forKey: @"VarArgs"]; [mname appendString: @",..."]; while ([self parseSpace] < length) { if (buffer[pos] == term) { break; } pos++; } if (buffer[pos] != term) { [self log: @"error skipping varargs"]; goto fail; } } } else if (token != nil) { [sels addObject: token]; [mname appendString: token]; if (buffer[pos] != term) { unsigned saved = pos; /* * As a special case, try to cope with a method name separated * from its body by a semicolon ... a common bug since the * compiler doesn't pick it up! */ if (term == '{' && buffer[pos] == ';') { pos++; if ([self parseSpace] >= length || buffer[pos] != term) { pos = saved; } } if (buffer[pos] == term) { [self log: @"error in method definition ... " @"semicolon after name"]; } else { [self log: @"error parsing method name"]; goto fail; } } } else { unsigned saved = pos; /* * As a special case, try to cope with a method name separated * from its body by a semicolon ... a common bug since the * compiler doesn't pick it up! */ if (term == '{' && buffer[pos] == ';') { pos++; if ([self parseSpace] >= length || buffer[pos] != term) { pos = saved; } } if (buffer[pos] == term) { [self log: @"error in method definition ... " @"semicolon after name"]; } else { [self log: @"error parsing method name"]; goto fail; } } } [method setObject: mname forKey: @"Name"]; if (flag == YES) { [self setStandards: method]; } itemName = mname; if (term == ';') { /* * Skip past the closing semicolon of the method declaration, * and read in any comment on the same line in case it * contains documentation for the method. */ pos++; if ([self skipSpaces] < length && buffer[pos] == '/') { [self parseComment]; } } else if (term == '{') { [self skipBlock]; } /* * Store any available documentation information in the method. * If the method is already documented, append new information. */ if (comment != nil) { [self appendComment: comment to: method]; DESTROY(comment); } if (flag == YES && [itemName length] > 1 && [itemName characterAtIndex: 1] == '_') { NSString *c; c = @"Warning the underscore at the start of the name " @"of this method indicates that it is private, for internal use only, " @" and you should not use the method in your code."; [self appendComment: c to: method]; } itemName = nil; RELEASE(arp); AUTORELEASE(method); return method; fail: itemName = nil; DESTROY(comment); RELEASE(arp); RELEASE(method); return nil; } - (NSMutableDictionary*) parseMethodsAreDeclarations: (BOOL)flag { NSMutableDictionary *methods; NSMutableDictionary *method; NSMutableDictionary *exist; NSString *token; if (flag == YES) { exist = nil; // Declaration ... no existing methods. methods = [NSMutableDictionary dictionaryWithCapacity: 8]; } else { /* * Get a list of known methods. */ if ([unitName hasPrefix: @"("]) { exist = nil; // A protocol ... no method implementations. } else if ([unitName hasSuffix: @")"]) { exist = [info objectForKey: @"Categories"]; } else { exist = [info objectForKey: @"Classes"]; } exist = [exist objectForKey: unitName]; exist = [exist objectForKey: @"Methods"]; /* * If there were no methods in the interface, we can't * document any now so we may as well skip to the end. */ if (exist == nil) { [self skipUnit]; DESTROY(comment); return [NSMutableDictionary dictionary]; // Empty dictionary. } methods = exist; } while ([self parseSpace] < length) { unichar c = buffer[pos++]; switch (c) { case '-': case '+': pos--; method = [self parseMethodIsDeclaration: flag]; if (method == nil) { return nil; } token = [method objectForKey: @"Name"]; if (flag == YES) { /* * Just record the method. */ [methods setObject: method forKey: token]; } else if ((exist = [methods objectForKey: token]) != nil) { NSArray *a0; NSArray *a1; NSString *c0; NSString *c1; /* * Merge info from implementation into existing version. */ a0 = [exist objectForKey: @"Args"]; a1 = [method objectForKey: @"Args"]; if (a0 != nil) { if ([a0 isEqual: a1] == NO) { itemName = token; [self log: @"method args in interface %@ don't match " @"those in implementation %@", a0, a1]; itemName = nil; [exist setObject: a1 forKey: @"Args"]; } } a0 = [exist objectForKey: @"Types"]; a1 = [method objectForKey: @"Types"]; if (a0 != nil) { if ([a0 isEqual: a1] == NO) { itemName = token; [self log: @"method types in interface %@ don't match " @"those in implementation %@", a0, a1]; itemName = nil; [exist setObject: a1 forKey: @"Types"]; } } /* * If the old comment from the header parsing is * the same as the new comment from the source * parsing, assume we parsed the same file as both * source and header ... otherwise append the new * comment. */ c0 = [exist objectForKey: @"Comment"]; c1 = [method objectForKey: @"Comment"]; if ([c0 isEqual: c1] == NO) { [self appendComment: c1 to: exist]; } [exist setObject: @"YES" forKey: @"Implemented"]; } DESTROY(comment); // Don't want this. break; case '@': if ((token = [self parseIdentifier]) == nil) { [self log: @"method list with error after '@'"]; [self skipStatementLine]; return nil; } if ([token isEqual: @"end"] == YES) { return methods; } else if ([token isEqual: @"class"] == YES) { /* * Pre-declaration of one or more classes ... rather like a * normal C statement, it ends with a semicolon. */ [self skipStatementLine]; } else { [self log: @"@method list with unknown directive '%@'", token]; [self skipStatementLine]; } DESTROY(comment); // Don't want this. break; case '#': /* * Some preprocessor directive ... must be on one line ... skip * past it and delete any comment accumulated while doing so. */ [self parsePreprocessor]; DESTROY(comment); break; default: /* * Some statement other than a method ... skip and delete comments. */ if (flag == YES) { [self log: @"interface with bogus line ... we expect methods"]; [self skipStatementLine]; } else { pos--; [self parseDeclaration]; } DESTROY(comment); // Don't want this. break; } } [self log: @"method list prematurely ended"]; return nil; } - (NSString*) parseMethodType { unichar *start; unichar *ptr; unsigned nest = 0; pos++; if ([self parseSpace] >= length) { return nil; } ptr = start = &buffer[pos]; while (pos < length) { unichar c = buffer[pos++]; if (c == '(') { /* * Remove any whitespace before an opening bracket. */ if (ptr > start && ptr[-1] == ' ') { ptr--; } *ptr++ = '('; nest++; } else if (c == ')') { /* * Remove any whitespace before a closing bracket. */ if (ptr > start && ptr[-1] == ' ') { ptr--; } if (nest > 0) { *ptr++ = ')'; nest--; } else { break; } } else if ([spacenl characterIsMember: c] == NO) { /* * If this character is not part of a name, and the previous * character written was a space, we know we can get rid of * the space to standardise the type format to use a minimal * number of spaces. */ if (ptr > start && ptr[-1] == ' ') { if ([identifier characterIsMember: c] == NO) { ptr--; } } *ptr++ = c; } else { /* * Don't retain whitespace if we know we don't need it * because the previous character was not part of a name. */ if (ptr > start && [identifier characterIsMember: ptr[-1]] == YES) { *ptr++ = ' '; } } } if ([self parseSpace] >= length) { return nil; } /* * Strip trailing sapce ... leading space we never copied in the * first place. */ if (ptr > start && [spacenl characterIsMember: ptr[-1]] == YES) { ptr--; } if (ptr > start) { return [NSString stringWithCharacters: start length: ptr - start]; } else { return nil; } } /** * Parse a preprocessor statement, handling preprocessor * conditionals in a rudimentary way. We keep track of the * level of conditional nesting, and we also track the use of * #ifdef and #ifndef with some well-known constants to tell * us which standards are currently supported. */ - (unsigned) parsePreprocessor { [self parseSpace: spaces]; if (pos < length && buffer[pos] != '\n') { NSString *directive = [self parseIdentifier]; if ([directive isEqual: @"define"] == YES && inHeader == YES) { NSMutableDictionary *defn; defn = [self parseMacro]; if (defn != nil) { NSMutableDictionary *dict = [info objectForKey: @"Macros"]; NSString *name = [defn objectForKey: @"Name"]; NSMutableDictionary *odef; odef = [dict objectForKey: name]; if (odef == nil) { if (dict == nil) { dict = [[NSMutableDictionary alloc] initWithCapacity: 8]; [info setObject: dict forKey: @"Macros"]; RELEASE(dict); } [dict setObject: defn forKey: name]; } else { NSString *oc = [odef objectForKey: @"Comment"]; NSString *nc = [defn objectForKey: @"Comment"]; /* * If the old comment from the header parsing is * the same as the new comment from the source * parsing, assume we parsed the same file as both * source and header ... otherwise append the new * comment. */ if ([oc isEqual: nc] == NO) { [self appendComment: nc to: odef]; } } } } else if ([directive isEqual: @"endif"] == YES) { if ([ifStack count] <= 1) { [self log: @"Unexpected #endif (no matching #if)"]; } else { [ifStack removeLastObject]; } } else if ([directive isEqual: @"elif"] == YES) { if ([ifStack count] <= 1) { [self log: @"Unexpected #else (no matching #if)"]; } else { [ifStack removeLastObject]; [ifStack addObject: [ifStack lastObject]]; } } else if ([directive isEqual: @"else"] == YES) { if ([ifStack count] <= 1) { [self log: @"Unexpected #else (no matching #if)"]; } else { [ifStack removeLastObject]; [ifStack addObject: [ifStack lastObject]]; } } else if ([directive isEqual: @"if"] == YES) { [ifStack addObject: [ifStack lastObject]]; } else if ([directive hasPrefix: @"if"] == YES) { BOOL isIfDef = [directive isEqual: @"ifdef"]; while (pos < length && [spaces characterIsMember: buffer[pos]] == YES) { pos++; } if (pos < length && buffer[pos] != '\n') { NSMutableSet *set = [[ifStack lastObject] mutableCopy]; NSString *arg = [self parseIdentifier]; if ([arg isEqual: @"NO_GNUSTEP"] == YES) { if (isIfDef == YES) { [self log: @"Unexpected #ifdef NO_GNUSTEP (nonsense)"]; } else { [set removeObject: @"MacOS-X"]; [set addObject: @"NotMacOS-X"]; [set removeObject: @"OpenStep"]; [set addObject: @"NotOpenStep"]; } } else if ([arg isEqual: @"STRICT_MACOS_X"] == YES) { if (isIfDef == YES) { [set removeObject: @"NotMacOS-X"]; [set addObject: @"MacOS-X"]; } else { [set removeObject: @"MacOS-X"]; [set addObject: @"NotMacOS-X"]; } } else if ([arg isEqual: @"STRICT_OPENSTEP"] == YES) { if (isIfDef == YES) { [set removeObject: @"NotOpenStep"]; [set addObject: @"OpenStep"]; } else { [set removeObject: @"OpenStep"]; [set addObject: @"NotOpenStep"]; } } [ifStack addObject: set]; RELEASE(set); } } } return [self skipRemainderOfLine]; } - (NSMutableDictionary*) parseProtocol { NSString *name; NSDictionary *methods = nil; NSMutableDictionary *dict; NSMutableDictionary *d; CREATE_AUTORELEASE_POOL(arp); dict = [[NSMutableDictionary alloc] initWithCapacity: 8]; /* * Record any protocol documentation for this protocol */ if (comment != nil) { [dict setObject: comment forKey: @"Comment"]; DESTROY(comment); } if ((name = [self parseIdentifier]) == nil || [self parseSpace] >= length) { [self log: @"protocol with bad name"]; goto fail; } /* * If there is a comma, this must be a forward declaration of a list * of protocols ... so we can ignore it. Otherwise, if we found a * semicolon, we have a single forward declaration to ignore. */ if (pos < length && (buffer[pos] == ',' || buffer[pos] == ';')) { [self skipStatement]; return nil; } [dict setObject: name forKey: @"Name"]; [self setStandards: dict]; unitName = [NSString stringWithFormat: @"(%@)", name]; /* * Protocols may themselves conform to protocols. */ if (buffer[pos] == '<') { NSArray *protocols = [self parseProtocolList]; if (protocols == nil) { goto fail; } else if ([protocols count] > 0) { [dict setObject: protocols forKey: @"Protocols"]; } } [dict setObject: @"protocol" forKey: @"Type"]; methods = [self parseMethodsAreDeclarations: YES]; if (methods != nil && [methods count] > 0) { NSEnumerator *e = [methods objectEnumerator]; NSMutableDictionary *m; /* * Mark methods as implemented because protocol methods have no * implementation separate from their declaration. */ while ((m = [e nextObject]) != nil) { [m setObject: @"YES" forKey: @"Implemented"]; } [dict setObject: methods forKey: @"Methods"]; } [dict setObject: declared forKey: @"Declared"]; d = [info objectForKey: @"Protocols"]; if (d == nil) { d = [[NSMutableDictionary alloc] initWithCapacity: 4]; [info setObject: d forKey: @"Protocols"]; RELEASE(d); } /* * A protocol has no separate implementation, so mark it as implemented. */ [dict setObject: @"YES" forKey: @"Implemented"]; [d setObject: dict forKey: unitName]; // [self log: @"Found protocol %@", dict]; unitName = nil; DESTROY(comment); RELEASE(arp); AUTORELEASE(dict); return dict; fail: unitName = nil; DESTROY(comment); RELEASE(arp); RELEASE(dict); return nil; } - (NSMutableArray*) parseProtocolList { NSMutableArray *protocols; NSString *p; protocols = [NSMutableArray arrayWithCapacity: 2]; pos++; while ((p = [self parseIdentifier]) != nil && [self parseSpace] < length) { if ([protocols containsObject: p] == NO) { [protocols addObject: p]; } if (buffer[pos] == ',') { pos++; } else { break; } } if (pos >= length || buffer[pos] != '>' || ++pos >= length || [self parseSpace] >= length || [protocols count] == 0) { [self log: @"bad protocol list"]; return nil; } return protocols; } /** * Skip past any whitespace characters (as defined by the supplied set) * including comments.
* Calls parseComment if neccesary, ensuring that any documentation * in comments is appended to our 'comment' ivar. */ - (unsigned) parseSpace: (NSCharacterSet*)spaceSet { BOOL tryAgain; do { unsigned start; tryAgain = NO; while (pos < length) { unichar c = buffer[pos]; if (c == '/') { unsigned old = pos; if ([self parseComment] > old) { continue; // Found a comment ... act as if it was a space. } break; } if ([spaceSet characterIsMember: c] == NO) { break; // Not whitespace ... done. } pos++; // Step past space character. } start = pos; if (pos < length && [identifier characterIsMember: buffer[pos]] == YES) { while (pos < length) { if ([identifier characterIsMember: buffer[pos]] == NO) { NSString *tmp; NSString *val; tmp = [[NSString alloc] initWithCharacters: &buffer[start] length: pos - start]; val = [wordMap objectForKey: tmp]; RELEASE(tmp); if (val == nil) { pos = start; // No mapping found } else if ([val length] > 0) { if ([val isEqualToString: @"//"] == YES) { [self skipToEndOfLine]; tryAgain = YES; } else { pos = start; // Not mapped to a comment. } } else { tryAgain = YES; // Identifier ignored. } break; } pos++; } } } while (tryAgain == YES); return pos; } - (unsigned) parseSpace { return [self parseSpace: spacenl]; } - (void) reset { [source removeAllObjects]; [info removeAllObjects]; haveOutput = NO; haveSource = NO; DESTROY(declared); DESTROY(comment); fileName = nil; unitName = nil; itemName = nil; lines = nil; buffer = 0; length = 0; pos = 0; } /** * Set the name of the file in which classes are to be documented as * being declared. The default value of this is the last part of the * path of the source file being parsed. */ - (void) setDeclared: (NSString*)name { ASSIGN(declared, name); } /** * This method is used to enable (or disable) documentation of all * instance variables. If it is turned off, only those instance * variables that are explicitly declared 'public' or 'protected' * will be documented. */ - (void) setDocumentAllInstanceVariables: (BOOL)flag { documentAllInstanceVariables = flag; } /** * This method is used to enable (or disable) documentation of instance * variables. If it is turned off, instance variables will not be documented. */ - (void) setDocumentInstanceVariables: (BOOL)flag { documentInstanceVariables = flag; } /** * Turn on or off parsing of preprocessor conditional compilation info * indicating the standards complied with. When this is turned on, we * assume that all standards are complied with by default.
* You should only turn this on while parsing the GNUstep source code. */ - (void) setGenerateStandards: (BOOL)flag { if (flag == YES) { [ifStack replaceObjectAtIndex: 0 withObject: [NSSet setWithObjects: @"OpenStep", @"MacOS-X", @"GNUstep", nil]]; } standards = flag; } /** * Store the current standards information derived from preprocessor * conditionals in the supplied dictionary ... this will be used by * the AGSOutput class to put standards markup in the gsdoc output. */ - (void) setStandards: (NSMutableDictionary*)dict { if (standards == YES) { NSSet *set = [ifStack lastObject]; if ([set count] > 0) { NSMutableString *s = nil; NSEnumerator *e = [set objectEnumerator]; NSString *name; s = [NSMutableString stringWithCString: ""]; while ((name = [e nextObject]) != nil) { [s appendFormat: @"<%@ />", name]; } [s appendString: @""]; [dict setObject: s forKey: @"Standards"]; } } } /** * Sets up a dictionary used for mapping identifiers/keywords to other * words. This is used to help cope with cases where C preprocessor * definitions are confusing the parsing process. */ - (void) setWordMap: (NSDictionary*)map { ASSIGNCOPY(wordMap, map); } /** * Read in the file to be parsed and store it in a temporary unicode * buffer. Perform basic transformations on the buffer to simplify * the parsing process later - including stripping out of escaped * end-of-line sequences. Create mapping information to convert * positions in the new character buffer to line numbers in the * original data (for logging purposes). */ - (void) setupBuffer { NSString *contents; NSMutableData *data; unichar *end; unichar *inptr; unichar *outptr; NSMutableArray *a; CREATE_AUTORELEASE_POOL(arp); contents = [NSString stringWithContentsOfFile: fileName]; length = [contents length]; data = [[NSMutableData alloc] initWithLength: length * sizeof(unichar)]; buffer = [data mutableBytes]; [contents getCharacters: buffer]; outptr = buffer; end = &buffer[length]; a = [NSMutableArray arrayWithCapacity: 1024]; for (inptr = buffer; inptr < end; outptr++, inptr++) { unichar c = *inptr; *outptr = c; /* * Perform ansi trigraph substitution. * Don't know why I bothered ... will probably never be used. */ if (c == '?' && (inptr < end - 2) && inptr[1] == '?') { BOOL changed = YES; switch (inptr[2]) { case '=': *outptr = '#'; break; case '/': *outptr = '\\'; break; case '\'': *outptr = '^'; break; case '(': *outptr = '['; break; case ')': *outptr = ']'; break; case '!': *outptr = '|'; break; default: *outptr = '?'; changed = NO; break; } if (changed == YES) { inptr += 2; } } else if (c == '\\') { /* * Backslash-end-of-line sequences are removed. */ if (inptr < end - 1) { if (inptr[1] == '\n') { inptr++; outptr--; [a addObject: [NSNumber numberWithInt: outptr - buffer]]; } else if (inptr[1] == '\r') { inptr++; outptr--; if (inptr[1] == '\n') { inptr++; } [a addObject: [NSNumber numberWithInt: outptr - buffer]]; } } } else if (c == '\r') { /* * Convert cr-fl or single cr to single lf */ if (inptr < end - 1) { if (inptr[1] == '\n') { inptr++; } *outptr = '\n'; } else { outptr--; // Ignore trailing carriage return. } [a addObject: [NSNumber numberWithInt: outptr - buffer]]; } else if (c == '\n') { [a addObject: [NSNumber numberWithInt: outptr - buffer]]; } } length = outptr - buffer; [data setLength: length*sizeof(unichar)]; buffer = [data mutableBytes]; pos = 0; lines = [[NSArray alloc] initWithArray: a]; RELEASE(arp); AUTORELEASE(lines); AUTORELEASE(data); } /** * Skip until we encounter an ']' marking the end of an array. * Expect the current character position to be pointing to the * '[' at the start of an array. */ - (unsigned) skipArray { pos++; while ([self parseSpace] < length) { unichar c = buffer[pos++]; switch (c) { case '#': // preprocessor directive. [self parsePreprocessor]; break; case '\'': case '"': pos--; [self skipLiteral]; break; case '[': pos--; [self skipArray]; break; case ']': return pos; } } return pos; } /** * Skip a bracketed block. * Expect the current character position to be pointing to the * bracket at the start of a block. */ - (unsigned) skipBlock { unichar term = '}'; if (buffer[pos] == '(') { term = ')'; } else if (buffer[pos] == '[') { term = ']'; } pos++; while ([self parseSpace] < length) { unichar c = buffer[pos++]; switch (c) { case '#': // preprocessor directive. [self parsePreprocessor]; break; case '\'': case '"': pos--; [self skipLiteral]; break; case '{': pos--; [self skipBlock]; break; case '(': pos--; [self skipBlock]; break; case '[': pos--; [self skipBlock]; break; default: if (c == term) { return pos; } } } return pos; } - (unsigned) skipLiteral { unichar term = buffer[pos++]; while (pos < length) { unichar c = buffer[pos++]; if (c == '\\') { pos++; } else if (c == term) { break; } } return pos; } - (unsigned) skipRemainderOfLine { while (pos < length) { if (buffer[pos++] == '\n') { break; } } return pos; } - (unsigned) skipSpaces { while (pos < length) { unichar c = buffer[pos]; if ([spaces characterIsMember: c] == NO) { break; } pos++; } return pos; } /** * Skip until we encounter a semicolon or closing brace. * Strictly speaking, we don't skip all statements that way, * since we only skip part of an if...else statement. */ - (unsigned) skipStatement { while ([self parseSpace] < length) { unichar c = buffer[pos++]; switch (c) { case '#': // preprocessor directive. [self parsePreprocessor]; break; case '\'': case '"': pos--; [self skipLiteral]; break; case '{': pos--; [self skipBlock]; return pos; case ';': return pos; // At end of statement case '}': [self log: @"Argh ... read '}' when looking for ';'"]; return --pos; // No statement to skip. break; } } return pos; } /** * Special method to skip a statement and up to the end of the last * line it was on, discarding any comments so they don't get used by * the next construct that actually needs documenting. */ - (unsigned) skipStatementLine { [self skipStatement]; if (buffer[pos-1] == ';' || buffer[pos-1] == '}') { [self skipRemainderOfLine]; } DESTROY(comment); return pos; } - (unsigned) skipToEndOfLine { while (pos < length) { if (buffer[pos++] == '\n') { pos--; break; } } return pos; } /** * Skip until we encounter an '@end' marking the end of an interface, * implementation, or protocol. */ - (unsigned) skipUnit { while ([self parseSpace] < length) { unichar c = buffer[pos++]; switch (c) { case '#': // preprocessor directive. [self parsePreprocessor]; break; case '\'': case '"': pos--; [self skipLiteral]; break; case '@': [self parseSpace]; if (pos < length - 3) { if (buffer[pos] == 'e' && buffer[pos+1] == 'n' && buffer[pos+2] == 'd') { pos += 3; return pos; } } break; } } return pos; } - (NSMutableArray*) sources { return AUTORELEASE([source mutableCopy]); } @end