mirror of
https://github.com/gnustep/libs-base.git
synced 2025-04-19 14:31:29 +00:00
5323 lines
120 KiB
Objective-C
5323 lines
120 KiB
Objective-C
/*
|
|
Copyright (C) 2001 Free Software Foundation, Inc.
|
|
|
|
Written By: Richard Frith-Macdonald <richard@brainstorm.co.uk>
|
|
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 3 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 COPYINGv3.
|
|
If not, write to the Free Software Foundation,
|
|
31 Milk Street #960789 Boston, MA 02196 USA.
|
|
|
|
*/
|
|
|
|
#import "common.h"
|
|
|
|
#import "Foundation/NSArray.h"
|
|
#import "Foundation/NSAutoreleasePool.h"
|
|
#import "Foundation/NSCharacterSet.h"
|
|
#import "Foundation/NSData.h"
|
|
#import "Foundation/NSDictionary.h"
|
|
#import "Foundation/NSEnumerator.h"
|
|
#import "Foundation/NSException.h"
|
|
#import "Foundation/NSFileManager.h"
|
|
#import "Foundation/NSUserDefaults.h"
|
|
#import "Foundation/NSScanner.h"
|
|
#import "Foundation/NSSet.h"
|
|
#import "Foundation/NSValue.h"
|
|
#import "AGSParser.h"
|
|
#import "GNUstepBase/NSString+GNUstepBase.h"
|
|
#import "GNUstepBase/NSMutableString+GNUstepBase.h"
|
|
|
|
#define ENDBRACE 0x7D // '}' character
|
|
|
|
/**
|
|
* The AGSParser class parses Objective-C header and source files
|
|
* to produce a property-list which can be handled by [AGSOutput].
|
|
*/
|
|
@implementation AGSParser
|
|
|
|
static NSString *
|
|
concreteType(NSString *t)
|
|
{
|
|
static NSString *gClass = @"GS_GENERIC_CLASS";
|
|
static NSString *gType = @"GS_GENERIC_TYPE";
|
|
NSMutableString *m = nil;
|
|
NSRange r;
|
|
|
|
r = [t rangeOfString: gClass];
|
|
while (r.length > 0)
|
|
{
|
|
unsigned end;
|
|
unsigned len;
|
|
unsigned pos;
|
|
|
|
if (t != m)
|
|
{
|
|
t = m = AUTORELEASE([t mutableCopy]);
|
|
}
|
|
r = NSMakeRange(0, [gClass length]);
|
|
[m deleteCharactersInRange: r];
|
|
len = [m length];
|
|
for (pos = r.location; pos < len; pos++)
|
|
{
|
|
unichar c = [m characterAtIndex: pos];
|
|
|
|
if (c != '(' && !isspace(c))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
if (pos > r.location)
|
|
{
|
|
r.length = pos - r.location;
|
|
[m deleteCharactersInRange: r];
|
|
len -= r.length;
|
|
}
|
|
/* Having skipped the macro opening bracket and any white space
|
|
* we now expect the true type.
|
|
*/
|
|
for (pos = r.location; pos < len; pos++)
|
|
{
|
|
unichar c = [m characterAtIndex: pos];
|
|
|
|
if (',' == c || ')' == c || isspace(c))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
end = pos;
|
|
if (pos > r.location)
|
|
{
|
|
while (pos < len)
|
|
{
|
|
unichar c = [m characterAtIndex: pos++];
|
|
|
|
if (')' == c)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
/* Stripping everything from the end of the class name to the
|
|
* closing bracket of the macro.
|
|
*/
|
|
[m deleteCharactersInRange: NSMakeRange(end, pos - end)];
|
|
}
|
|
r = [t rangeOfString: gClass];
|
|
}
|
|
|
|
r = [t rangeOfString: gType];
|
|
while (r.length > 0)
|
|
{
|
|
unsigned len = [t length];
|
|
unsigned pos = r.location;
|
|
BOOL found = NO;
|
|
|
|
if (t != m)
|
|
{
|
|
t = m = AUTORELEASE([t mutableCopy]);
|
|
}
|
|
while (pos < len)
|
|
{
|
|
unichar c = [m characterAtIndex: pos++];
|
|
|
|
if (',' == c)
|
|
{
|
|
found = YES;
|
|
break;
|
|
}
|
|
else if (')' == c)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
r.length = pos - r.location;
|
|
if (found)
|
|
{
|
|
int nest = 0;
|
|
|
|
/* We have a type specification as the second argument.
|
|
*/
|
|
[m deleteCharactersInRange: r];
|
|
len = [m length];
|
|
pos = r.location;
|
|
while (pos < len)
|
|
{
|
|
unichar c = [m characterAtIndex: pos++];
|
|
|
|
if ('(' == c)
|
|
{
|
|
nest++;
|
|
}
|
|
else if (')' == c)
|
|
{
|
|
if (--nest < 0)
|
|
{
|
|
/* Remove the closing bracket.
|
|
*/
|
|
[m replaceCharactersInRange: NSMakeRange(pos - 1, 1)
|
|
withString: @""];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* No type specification ... use id
|
|
*/
|
|
[m replaceCharactersInRange: r withString: @"id"];
|
|
}
|
|
r = [t rangeOfString: gType];
|
|
}
|
|
|
|
if ([t hasPrefix: @"nullable "])
|
|
{
|
|
if (t != m)
|
|
{
|
|
t = m = AUTORELEASE([t mutableCopy]);
|
|
}
|
|
[m replaceCharactersInRange: NSMakeRange(0, 9) withString: @""];
|
|
}
|
|
return t;
|
|
}
|
|
|
|
static BOOL
|
|
equalTypes(NSArray *t1, NSArray *t2)
|
|
{
|
|
unsigned count;
|
|
|
|
count = (unsigned)[t1 count];
|
|
if ([t2 count] != count)
|
|
{
|
|
return NO;
|
|
}
|
|
while (count-- > 0)
|
|
{
|
|
NSString *c1 = concreteType([t1 objectAtIndex: count]);
|
|
NSString *c2 = concreteType([t2 objectAtIndex: count]);
|
|
|
|
if ([c1 isEqual: c2] == NO)
|
|
{
|
|
return NO;
|
|
}
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
/**
|
|
* 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 (nil == chap)
|
|
{
|
|
createSec = NO;
|
|
m = [NSMutableString stringWithFormat:
|
|
@"<chapter id=\"_main\"><heading>%@</heading></chapter>", toolName];
|
|
}
|
|
else
|
|
{
|
|
createSec = YES;
|
|
m = AUTORELEASE([chap mutableCopy]);
|
|
}
|
|
|
|
/* Check for a pre-existing <chapter> elemment and add the markup to say
|
|
* it's for a tool if necessary (also update any <section>).
|
|
*/
|
|
r = [m rangeOfString: @"<chapter>"];
|
|
if (r.length > 0)
|
|
{
|
|
[m replaceCharactersInRange: r withString: @"<chapter id=\"_main\">"];
|
|
r = [m rangeOfString: @"<section>"];
|
|
if (r.length > 0)
|
|
{
|
|
[m replaceCharactersInRange: r
|
|
withString: @"<section id=\"_main\">"];
|
|
}
|
|
}
|
|
|
|
r = [m rangeOfString: @"</chapter>"];
|
|
r.length = 0;
|
|
if (createSec)
|
|
{
|
|
[m replaceCharactersInRange: r withString: @"</section>\n"];
|
|
}
|
|
[m replaceCharactersInRange: r withString: c];
|
|
if (createSec)
|
|
{
|
|
secHeading = [NSString stringWithFormat:
|
|
@"<section id=\"_main\">\n<heading>%@</heading>\n", toolName];
|
|
//The %@ tool
|
|
[m replaceCharactersInRange: r withString: secHeading];
|
|
}
|
|
[info setObject: m forKey: @"chapter"];
|
|
}
|
|
|
|
/**
|
|
* Append a comment (with leading and trailing space stripped)
|
|
* to an information dictionary.<br />
|
|
* If the dictionary is nil, accumulate in the comment ivar instead.<br />
|
|
* If the comment is empty, ignore it.<br />
|
|
* If there is no comment in the dictionary, simply set the new value.<br />
|
|
* If a comment already exists then the new comment text is appended to
|
|
* it with a separating line break inserted if necessary.<br />
|
|
*/
|
|
- (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: @"</p>"] == NO
|
|
&& [old hasSuffix: @"<br />"] == NO
|
|
&& [old hasSuffix: @"<br/>"] == NO)
|
|
{
|
|
s = [old stringByAppendingFormat: @"<br />%@", s];
|
|
}
|
|
else
|
|
{
|
|
s = [old stringByAppendingString: s];
|
|
}
|
|
}
|
|
if (d == nil)
|
|
{
|
|
ASSIGN(comment, s);
|
|
}
|
|
else
|
|
{
|
|
[d setObject: s forKey: @"Comment"];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (NSString*) comment
|
|
{
|
|
return comment;
|
|
}
|
|
|
|
/** Returns the current debug setting for the parser.
|
|
*/
|
|
- (BOOL) debug
|
|
{
|
|
return debug;
|
|
}
|
|
|
|
- (void) dealloc
|
|
{
|
|
[self reset];
|
|
DESTROY(wordMap);
|
|
DESTROY(ifStack);
|
|
DESTROY(declared);
|
|
DESTROY(info);
|
|
DESTROY(orderedSymbolDeclsByUnit);
|
|
DESTROY(comment);
|
|
DESTROY(identifier);
|
|
DESTROY(identStart);
|
|
DESTROY(spaces);
|
|
DESTROY(spacenl);
|
|
DESTROY(source);
|
|
DESTROY(itemName);
|
|
DESTROY(unitName);
|
|
[super dealloc];
|
|
}
|
|
|
|
- (NSString*) description
|
|
{
|
|
NSString *string;
|
|
|
|
if (pos >= length)
|
|
{
|
|
string = [NSString stringWithFormat: @"%@ in %@ pos %u of %u\n",
|
|
[super description], fileName, (unsigned)pos, (unsigned) length];
|
|
}
|
|
else
|
|
{
|
|
NSString *remaining;
|
|
|
|
remaining = [[NSString alloc] initWithCharactersNoCopy: buffer + pos
|
|
length: length - pos
|
|
freeWhenDone: NO];
|
|
string = [NSString stringWithFormat: @"%@ in %@ pos %u of %u: %@\n",
|
|
[super description], fileName, (unsigned)pos, (unsigned)length,
|
|
remaining];
|
|
RELEASE(remaining);
|
|
}
|
|
return string;
|
|
}
|
|
|
|
- (NSMutableDictionary*) info
|
|
{
|
|
return info;
|
|
}
|
|
|
|
/** Returns the methods, functions and C data types in their header
|
|
* declaration order, by organizing them into arrays as described below.
|
|
*
|
|
* Methods are grouped by class, category or protocol references.
|
|
* For example, valid keys could be <em>ClassName</em>,
|
|
* <em>ClassName(CategoryName)</em> and <em>(ProtocolName)</em>.
|
|
*
|
|
* Functions and C data types are grouped by header file names.
|
|
* For example, <em>AGParser.h</em> would a valid key.
|
|
* TODO: Collect functions and C data types. Only methods are currently
|
|
* included in the returned dictionary.
|
|
*/
|
|
- (NSDictionary *) orderedSymbolDeclarationsByUnit
|
|
{
|
|
return orderedSymbolDeclsByUnit;
|
|
}
|
|
|
|
- (id) init
|
|
{
|
|
NSMutableCharacterSet *m;
|
|
|
|
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];
|
|
orderedSymbolDeclsByUnit = [[NSMutableDictionary alloc] init];
|
|
source = [NSMutableArray new];
|
|
verbose = [[NSUserDefaults standardUserDefaults] boolForKey: @"Verbose"];
|
|
warn = [[NSUserDefaults standardUserDefaults] boolForKey: @"Warn"];
|
|
documentInstanceVariables = YES;
|
|
ifStack = [[NSMutableArray alloc] initWithCapacity: 4];
|
|
[ifStack addObject: [NSDictionary dictionary]];
|
|
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 = nil;
|
|
|
|
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
|
|
{
|
|
NSArray *declarations = [self parseDeclarations];
|
|
NSEnumerator *e = [declarations objectEnumerator];
|
|
NSDictionary *m;
|
|
|
|
if ([declarations count] == 0)
|
|
{
|
|
break;
|
|
}
|
|
while (nil != (m = [e nextObject]))
|
|
{
|
|
if ([[m objectForKey: @"BaseType"] isEqual: @"void"]
|
|
&& [m objectForKey: @"Prefix"] == nil)
|
|
{
|
|
// C++ style empty arg list. eg. 'int foo(void);'
|
|
continue;
|
|
}
|
|
[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;
|
|
}
|
|
|
|
/* When the paragraph string contains a GSDoc block element which is not a text
|
|
element (in the GSDoc DTD sense), we return NO, otherwise we return YES.
|
|
|
|
A GSDoc or HTML paragraph content is limited to text elements (see GSDoc DTD).
|
|
e.g. 'list' or 'example' cannot belong to a 'p' element.
|
|
|
|
Any other non-block elements are considered valid. Whether or not they can be
|
|
embedded within a paragraph in the final output is the doc writer
|
|
responsability.
|
|
|
|
For 'item' and 'answer' which can contain arbitrary block elements, explicit
|
|
'p' tags should be used, because we won't wrap 'patata' and 'patati' as two
|
|
paragraphs in the example below:
|
|
<list>
|
|
<item>patata
|
|
|
|
patati</item>
|
|
</list>
|
|
|
|
When <example> starts a paragraph, \n\n sequence are allowed in the example.
|
|
In the example below, bla<example> or bla\n<example> wouldn't be handled
|
|
correctly unlike:
|
|
bla
|
|
|
|
<example>
|
|
patati
|
|
|
|
patata
|
|
</example> */
|
|
- (BOOL) canWrapWithParagraphMarkup: (NSString *)para
|
|
{
|
|
NSScanner *scanner = [NSScanner scannerWithString: para];
|
|
NSSet *blockTags = [NSSet setWithObjects: @"list", @"enum", @"item",
|
|
@"deflist", @"term", @"qalist", @"question", @"answer",
|
|
@"p", @"example", @"embed", @"index", nil];
|
|
NSMutableCharacterSet *skippedChars =
|
|
(id)[NSMutableCharacterSet punctuationCharacterSet];
|
|
|
|
if (inUnclosedExample)
|
|
{
|
|
/* We don't need to check block element presence within an example,
|
|
since an example content is limited to PCDATA. */
|
|
[scanner scanUpToString: @"</example>" intoString: NULL];
|
|
if ([scanner scanString: @"</example>" intoString: NULL])
|
|
{
|
|
inUnclosedExample = NO;
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
/* Set up the scanner to treat opening and closing tags in the same way.
|
|
Punctuation character set includes '/' but not '<' and '>' */
|
|
[skippedChars formUnionWithCharacterSet: [scanner charactersToBeSkipped]];
|
|
[scanner setCharactersToBeSkipped: AUTORELEASE([skippedChars copy])];
|
|
|
|
while (![scanner isAtEnd])
|
|
{
|
|
NSString *tag = @"";
|
|
BOOL foundBlockTag = NO;
|
|
|
|
[scanner scanUpToString: @"<" intoString: NULL];
|
|
if (![scanner scanString: @"<" intoString: NULL])
|
|
return YES;
|
|
|
|
[scanner scanUpToString: @">" intoString: &tag];
|
|
foundBlockTag = [blockTags containsObject: tag];
|
|
|
|
if (foundBlockTag)
|
|
{
|
|
/* When the first block tag is <example> and the example is unclosed in
|
|
the current paragraph, we stop to insert <p> tags in the next
|
|
paragraphs until we reach </example> */
|
|
if ([tag isEqualToString: @"example"])
|
|
{
|
|
[scanner setCharactersToBeSkipped:
|
|
[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
[scanner scanUpToString: @"</example>" intoString: NULL];
|
|
inUnclosedExample
|
|
= ([scanner scanString: @"</example>" intoString: NULL] == NO);
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
// NOTE: We could be able to eliminate that if -parseComment processes the
|
|
// first comment tags before calling -generateParagraphMarkups:
|
|
- (BOOL) containsSpecialMarkup: (NSString *)aComment
|
|
{
|
|
NSArray *firstCommentTags = [NSArray arrayWithObjects:
|
|
@"<abstract>",
|
|
@"<author>",
|
|
@"<back>",
|
|
@"<chapter>",
|
|
@"<copy>",
|
|
@"<date>",
|
|
@"<front>",
|
|
@"<title>",
|
|
@"<unit>",
|
|
@"<version>",
|
|
@"Author:",
|
|
@"By:",
|
|
@"Copyright (C)", nil];
|
|
NSEnumerator *e = [firstCommentTags objectEnumerator];
|
|
NSString *tag = nil;
|
|
|
|
while ((tag = [e nextObject]) != nil)
|
|
{
|
|
if ([aComment rangeOfString: tag
|
|
options: NSCaseInsensitiveSearch].location != NSNotFound)
|
|
{
|
|
return YES;
|
|
}
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
|
|
- (NSString *) generateParagraphMarkupForString: (NSString *)aComment
|
|
{
|
|
NSMutableString *formattedComment;
|
|
NSString *para;
|
|
NSEnumerator *e;
|
|
|
|
if (NO == commentsRead
|
|
&& [self containsSpecialMarkup: aComment])
|
|
{
|
|
return aComment;
|
|
}
|
|
|
|
formattedComment = [NSMutableString
|
|
stringWithCapacity: [aComment length] + 100];
|
|
e = [[aComment componentsSeparatedByString: @"\n\n"] objectEnumerator];
|
|
|
|
while ((para = [e nextObject]) != nil)
|
|
{
|
|
NSString *newPara = para;
|
|
/* -canWrapWithParagraph: can change its value */
|
|
BOOL wasInUnclosedExample = inUnclosedExample;
|
|
|
|
if ([self canWrapWithParagraphMarkup: para])
|
|
{
|
|
newPara = [NSString stringWithFormat: @"<p>%@</p>", para];
|
|
}
|
|
else if (wasInUnclosedExample)
|
|
{
|
|
newPara = [NSString stringWithFormat: @"\n\n%@", para];
|
|
}
|
|
[formattedComment appendString: newPara];
|
|
}
|
|
|
|
return formattedComment;
|
|
}
|
|
|
|
/**
|
|
* 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.<br />
|
|
* 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.<br />
|
|
* 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 (pos >= length)
|
|
{
|
|
return length;
|
|
}
|
|
NSAssert('/' == buffer[pos], NSInternalInconsistencyException);
|
|
if (buffer[pos + 1] == '/')
|
|
{
|
|
return [self skipRemainderOfLine];
|
|
}
|
|
else if (buffer[pos + 1] == '*')
|
|
{
|
|
unichar *start = 0;
|
|
BOOL isDocumentation = NO;
|
|
BOOL skippedFirstLine = NO;
|
|
NSRange r;
|
|
BOOL ignore = NO;
|
|
|
|
|
|
/* Jump back here if we have ignored data up to a new comment.
|
|
*/
|
|
comment:
|
|
|
|
pos += 2; /* Skip opening part */
|
|
|
|
/*
|
|
* Only comments starting with slash and TWO asterisks are documentation.
|
|
*/
|
|
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)
|
|
{
|
|
unichar *end = &buffer[pos - 1];
|
|
unichar *ptr = start;
|
|
unichar *newLine = ptr;
|
|
BOOL stripAsterisks = NO;
|
|
BOOL special = 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]])
|
|
{
|
|
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)
|
|
{
|
|
/*
|
|
* 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;
|
|
NSRange r;
|
|
NSUserDefaults *defs = [NSUserDefaults standardUserDefaults];
|
|
|
|
tmp = [NSString stringWithCharacters: start length: end - start];
|
|
|
|
/* The first documentation comment in a file may be special
|
|
* containing markup not permitted elsewhere.
|
|
*/
|
|
if (NO == commentsRead)
|
|
{
|
|
special = [self containsSpecialMarkup: tmp];
|
|
}
|
|
|
|
/*
|
|
* If the comment does not contain block markup already and we
|
|
* were asked to generate it, we insert <p> tags to get an
|
|
* explicit paragraph structure.
|
|
*/
|
|
if (special && [defs boolForKey: @"GenerateParagraphMarkup"])
|
|
{
|
|
// FIXME: Should follow <ignore> processing and be called
|
|
// just before using -appendComment:to:
|
|
tmp = [self generateParagraphMarkupForString: tmp];
|
|
}
|
|
recheck:
|
|
if (YES == ignore)
|
|
{
|
|
r = [tmp rangeOfString: @"</ignore>"];
|
|
if (r.length > 0)
|
|
{
|
|
tmp = [tmp substringFromIndex: NSMaxRange(r)];
|
|
ignore = NO;
|
|
}
|
|
}
|
|
if (NO == ignore)
|
|
{
|
|
r = [tmp rangeOfString: @"<ignore>"];
|
|
if (r.length > 0)
|
|
{
|
|
[self appendComment: [tmp substringToIndex: r.location]
|
|
to: nil];
|
|
tmp = [tmp substringFromIndex: NSMaxRange(r)];
|
|
ignore = YES;
|
|
goto recheck;
|
|
}
|
|
[self appendComment: tmp to: nil];
|
|
}
|
|
}
|
|
|
|
/* For the first comment of a file we may perform special processing.
|
|
*/
|
|
if (special)
|
|
{
|
|
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: @"<author "
|
|
options: NSLiteralSearch
|
|
range: r];
|
|
if (r.length > 0)
|
|
{
|
|
unsigned i = r.location;
|
|
|
|
r = NSMakeRange(i, commentLength - i);
|
|
r = [comment rangeOfString: @"</author>"
|
|
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 <author> in comment"];
|
|
}
|
|
}
|
|
}
|
|
/*
|
|
* In addition to fully specified author elements in the
|
|
* comment, we look for lines of the formats -
|
|
* Author: name <email>
|
|
* Author: name
|
|
* By: name <email>
|
|
* 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:
|
|
@"<author name=\"%@\"><email address=\"%@\">"
|
|
@"%@</email></author>", name, mail, mail];
|
|
}
|
|
else
|
|
{
|
|
author = [NSString stringWithFormat:
|
|
@"<author name=\"%@\"></author>", 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])
|
|
{
|
|
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])
|
|
{
|
|
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])
|
|
{
|
|
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.
|
|
*/
|
|
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
|
|
@"unit", // Unit for document body
|
|
@"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 complete element 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 <copy> ... </copy> 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:
|
|
@"<copy>%@</copy>", line];
|
|
[info setObject: line forKey: @"copy"];
|
|
}
|
|
}
|
|
}
|
|
DESTROY(comment);
|
|
}
|
|
commentsRead = YES;
|
|
}
|
|
if (YES == ignore)
|
|
{
|
|
while (pos < length)
|
|
{
|
|
switch (buffer[pos])
|
|
{
|
|
case '\'':
|
|
case '"':
|
|
[self skipLiteral];
|
|
break;
|
|
|
|
case '/':
|
|
if (pos + 1 < length)
|
|
{
|
|
if (buffer[pos + 1] == '/')
|
|
{
|
|
[self skipRemainderOfLine];
|
|
break;
|
|
}
|
|
else if (buffer[pos + 1] == '*')
|
|
{
|
|
goto comment;
|
|
}
|
|
}
|
|
pos++;
|
|
break;
|
|
|
|
default:
|
|
pos++;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (ignore)
|
|
{
|
|
[self log: @"unmatched <ignore> tag"];
|
|
}
|
|
}
|
|
return pos;
|
|
}
|
|
|
|
- (void) parseDeclaratorInto: (NSMutableDictionary*)d
|
|
{
|
|
NSMutableString *p = nil;
|
|
NSMutableString *s = nil;
|
|
|
|
while ([self parseSpace] < length)
|
|
{
|
|
if (pos < length && buffer[pos] == '_')
|
|
{
|
|
[self skipIfAttribute];
|
|
}
|
|
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 (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 skipGeneric];
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
- (NSMutableArray*) parseDeclarations
|
|
{
|
|
IF_NO_ARC(NSAutoreleasePool *arp = [NSAutoreleasePool new];)
|
|
NSMutableArray *declarations = [NSMutableArray array];
|
|
static NSSet *qualifiers = nil;
|
|
static NSSet *keep = nil;
|
|
NSString *baseName = nil;
|
|
NSString *baseType = nil;
|
|
NSString *s;
|
|
NSMutableDictionary *d;
|
|
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];
|
|
IF_NO_ARC(qualifiers = [qualifiers retain];)
|
|
keep = [NSSet setWithObjects:
|
|
@"const",
|
|
@"long",
|
|
@"short",
|
|
@"signed",
|
|
@"unsigned",
|
|
@"volatile",
|
|
nil];
|
|
IF_NO_ARC(keep = [keep retain];)
|
|
}
|
|
|
|
{
|
|
NSMutableArray *a = [NSMutableArray array];
|
|
NSMutableString *t = nil;
|
|
|
|
while ((s = [self parseIdentifier]) != nil)
|
|
{
|
|
if (inHeader == NO && [s isEqualToString: @"static"])
|
|
{
|
|
/*
|
|
* We don't want to document static declarations unless they
|
|
* occur in a public header.
|
|
*/
|
|
[self skipStatementLine];
|
|
goto fail;
|
|
}
|
|
if (([s isEqual: @"__attribute__"])
|
|
|| ([s isEqual: @"__asm__"]))
|
|
{
|
|
[self skipAttribute: s];
|
|
continue;
|
|
}
|
|
if ([s isEqualToString: @"GS_EXPORT"])
|
|
{
|
|
s = @"extern";
|
|
}
|
|
if ([qualifiers member: s] == nil)
|
|
{
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
if ([s isEqualToString: @"extern"]
|
|
&& [self skipSpaces] < length - 3 && buffer[pos] == '\"'
|
|
&& buffer[pos+1] == 'C' && buffer[pos+2] == '\"')
|
|
{
|
|
/*
|
|
* Found 'extern "C" ...'
|
|
* Which is for C++ and should be ignored
|
|
*/
|
|
pos += 3;
|
|
if ([self skipSpaces] < length && buffer[pos] == '{')
|
|
{
|
|
pos++;
|
|
[self skipSpaces];
|
|
}
|
|
IF_NO_ARC([arp release];)
|
|
return nil;
|
|
}
|
|
|
|
if ([s isEqualToString: @"typedef"])
|
|
{
|
|
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"]
|
|
|| [s isEqualToString: @"union"]
|
|
|| [s isEqualToString: @"enum"]
|
|
|| [s isEqualToString: @"NS_ENUM"]
|
|
|| [s isEqualToString: @"NS_OPTIONS"])
|
|
{
|
|
BOOL isEnum = NO;
|
|
NSString *tmp = s;
|
|
|
|
if ([s isEqualToString: @"NS_ENUM"]
|
|
|| [s isEqualToString: @"NS_OPTIONS"])
|
|
{
|
|
if ([self parseSpace] < length && buffer[pos] == '(')
|
|
{
|
|
pos++;
|
|
[self parseSpace];
|
|
s = [self parseIdentifier];
|
|
if (s)
|
|
{
|
|
tmp = [tmp stringByAppendingFormat: @"(%@", s];
|
|
while ([self parseSpace] < length
|
|
&& (s = [self parseIdentifier]) != nil)
|
|
{
|
|
tmp = [tmp stringByAppendingFormat: @" %@", s];
|
|
}
|
|
if (pos < length && buffer[pos] == ',')
|
|
{
|
|
tmp = [tmp stringByAppendingString: @")"];
|
|
pos++;
|
|
[self parseSpace];
|
|
s = [self parseIdentifier];
|
|
if (nil != s && [self parseSpace] < length
|
|
&& buffer[pos] == ')')
|
|
{
|
|
isEnum = YES;
|
|
pos++;
|
|
baseName = s;
|
|
s = tmp;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (NO == isEnum)
|
|
{
|
|
[self log: @"messed up NS_ENUM/NS_OPTIONS declaration"];
|
|
IF_NO_ARC([arp release];)
|
|
return nil;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
isEnum = [s isEqualToString: @"enum"];
|
|
|
|
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.
|
|
*/
|
|
baseName = s;
|
|
}
|
|
}
|
|
|
|
/* We parse enum and options comment of the form:
|
|
* <introComment> enum { <comment1> field1, <comment2> field2 } bla;
|
|
*/
|
|
if (isEnum && [self parseSpace] < length && buffer[pos] == '{')
|
|
{
|
|
NSString *ident;
|
|
NSString *introComment;
|
|
NSMutableString *fieldComments = [NSMutableString string];
|
|
BOOL foundFieldComment = NO;
|
|
|
|
/* We want to be able to parse new comments while retaining the
|
|
originally parsed comment for the enum/union/struct. */
|
|
introComment = AUTORELEASE([comment copy]);
|
|
DESTROY(comment);
|
|
|
|
pos++; /* Skip '{' */
|
|
|
|
[fieldComments appendString: @"<deflist>"];
|
|
|
|
// TODO: We should put the parsed field into the doc index and
|
|
// let AGSOutput generate the deflist.
|
|
while (buffer[pos] != ENDBRACE)
|
|
{
|
|
/*
|
|
A comment belongs with the declaration following it,
|
|
unless it begins on the same line as a declaration.
|
|
Six combinations can be parsed:
|
|
- fieldDecl,
|
|
- <comment> fieldDecl,
|
|
- fieldDecl, <comment>
|
|
- <comment> fieldDecl, <comment>
|
|
- fieldDecl }
|
|
- <comment> fieldDecl }
|
|
*/
|
|
|
|
/* Parse any space and comments before the identifier into
|
|
* 'comment' and get the identifier in 'ident'.
|
|
*/
|
|
ident = [self parseIdentifier];
|
|
|
|
/* Skip the left-hand side such as ' = aValue'
|
|
*/
|
|
while (pos < length
|
|
&& buffer[pos] != ',' && buffer[pos] != ENDBRACE)
|
|
{
|
|
pos++;
|
|
}
|
|
if (buffer[pos] == ',')
|
|
{
|
|
/* Parse any more space on the same line as the identifier
|
|
* appending it to the 'comment' ivar
|
|
*/
|
|
[self parseSpace: spaces];
|
|
pos++;
|
|
}
|
|
|
|
if (ident != nil)
|
|
{
|
|
foundFieldComment = YES;
|
|
[fieldComments appendString: @"<term><em>"];
|
|
[fieldComments appendString: ident];
|
|
[fieldComments appendString: @"</em></term>"];
|
|
[fieldComments appendString: @"<desc>"];
|
|
// NOTE: We could add a 'Description forthcoming' if nil
|
|
if (comment != nil)
|
|
{
|
|
[fieldComments appendString: comment];
|
|
}
|
|
[fieldComments appendString: @"</desc>\n"];
|
|
}
|
|
DESTROY(comment);
|
|
}
|
|
|
|
[fieldComments appendString: @"</deflist>"];
|
|
|
|
/* Restore the comment as initially parsed before
|
|
* -parseDeclaration was called and add the comments
|
|
* parsed per field into a deflist. */
|
|
ASSIGN(comment, introComment);
|
|
if (foundFieldComment)
|
|
{
|
|
NSString *enumComment =
|
|
[comment stringByAppendingFormat: @"\n\n%@", fieldComments];
|
|
|
|
ASSIGN(comment, enumComment);
|
|
}
|
|
|
|
pos++; /* Skip closing curly brace */
|
|
}
|
|
[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
|
|
&& [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 string];
|
|
|
|
if ([a containsObject: @"const"])
|
|
{
|
|
[t appendString: @"const"];
|
|
[t appendString: @" "];
|
|
[a removeObject: @"const"];
|
|
baseConstant = YES;
|
|
}
|
|
else if ([a containsObject: @"volatile"])
|
|
{
|
|
[t appendString: @"volatile"];
|
|
[t appendString: @" "];
|
|
[a removeObject: @"volatile"];
|
|
}
|
|
|
|
if ([a containsObject: @"signed"])
|
|
{
|
|
[t appendString: @"signed"];
|
|
[t appendString: @" "];
|
|
[a removeObject: @"signed"];
|
|
}
|
|
else if ([a containsObject: @"unsigned"])
|
|
{
|
|
[t appendString: @"unsigned"];
|
|
[t appendString: @" "];
|
|
[a removeObject: @"unsigned"];
|
|
}
|
|
|
|
if ([a containsObject: @"short"])
|
|
{
|
|
[t appendString: @"short"];
|
|
[t appendString: @" "];
|
|
[a removeObject: @"short"];
|
|
}
|
|
else if ([a containsObject: @"long"])
|
|
{
|
|
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"])
|
|
{
|
|
[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 or generic specification if necessary
|
|
*/
|
|
if ([self parseSpace] < length && buffer[pos] == '<')
|
|
{
|
|
NSArray *protocols = [self parseProtocolList];
|
|
|
|
if (protocols)
|
|
{
|
|
[a addObjectsFromArray: protocols];
|
|
[a sortUsingSelector: @selector(compare:)];
|
|
[t appendString: @"<"];
|
|
[t appendString: [a componentsJoinedByString: @","]];
|
|
[t appendString: @">"];
|
|
[a removeAllObjects];
|
|
}
|
|
else
|
|
{
|
|
[self skipGeneric];
|
|
}
|
|
}
|
|
baseType = t;
|
|
}
|
|
|
|
another:
|
|
d = [NSMutableDictionary dictionary];
|
|
[declarations addObject: d];
|
|
[d setObject: baseType forKey: @"BaseType"];
|
|
if (baseName)
|
|
{
|
|
[d setObject: baseName forKey: @"Name"];
|
|
}
|
|
|
|
/*
|
|
* Set the 'Kind' of declaration ... one of 'Types', 'Functions',
|
|
* 'Variables', or 'Constants'
|
|
* We may override this later.
|
|
*/
|
|
if (isTypedef)
|
|
{
|
|
[d setObject: @"Types" forKey: @"Kind"];
|
|
[d setObject: @"YES" forKey: @"Implemented"];
|
|
}
|
|
else if (baseConstant)
|
|
{
|
|
[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] == '<')
|
|
{
|
|
[self skipGeneric];
|
|
}
|
|
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)
|
|
{
|
|
if (buffer[pos] == ')' || buffer[pos] == ',')
|
|
{
|
|
IF_NO_ARC(declarations = [declarations retain]; [arp release];)
|
|
return AUTORELEASE(declarations);
|
|
}
|
|
else
|
|
{
|
|
[self log: @"Unexpected char (%c) in arg list", buffer[pos]];
|
|
[self skipStatement];
|
|
goto fail;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
NSString *ident;
|
|
|
|
while (isFunction && (ident = [self parseIdentifier]) != nil)
|
|
{
|
|
if ([ident isEqual: @"GS_UNIMPLEMENTED"])
|
|
{
|
|
[d setObject: @"YES" forKey: @"Unimplemented"];
|
|
[self appendComment: @"<em>Warning</em> this is "
|
|
@"<em>unimplemented</em> but may be implemented "
|
|
@"in future versions"
|
|
to: nil];
|
|
}
|
|
else if ([ident isEqual: @"__attribute__"])
|
|
{
|
|
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)
|
|
{
|
|
[d setObject: @"YES" forKey: @"Deprecated"];
|
|
[self appendComment: @"<em>Warning</em> this is "
|
|
@"<em>deprecated</em> and may be removed in "
|
|
@"future versions"
|
|
to: nil];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
[self log: @"strange format function attributes"];
|
|
}
|
|
}
|
|
else if ([ident length] > 0)
|
|
{
|
|
[self log: @"ignoring '%@' in function declaration", ident];
|
|
}
|
|
[self skipSpaces];
|
|
}
|
|
if (NO == isFunction)
|
|
{
|
|
/* If there's an attribute in the variable declaration, skip it
|
|
*/
|
|
[self skipIfAttribute];
|
|
}
|
|
if (pos >= length)
|
|
{
|
|
[self log: @"Unexpected end of file in declaration"];
|
|
goto fail;
|
|
}
|
|
if (buffer[pos] == ';')
|
|
{
|
|
[self skipStatement];
|
|
}
|
|
else if (buffer[pos] == ',')
|
|
{
|
|
pos++; // Step past the comma
|
|
s = nil; // Need to parse idenfier etc
|
|
goto another;
|
|
}
|
|
else if (buffer[pos] == '=')
|
|
{
|
|
[self skipStatement];
|
|
}
|
|
else if (buffer[pos] == '{')
|
|
{
|
|
/*
|
|
* Inline functions may be implemented in the header.
|
|
*/
|
|
if (isFunction)
|
|
{
|
|
[d setObject: @"YES" forKey: @"Implemented"];
|
|
}
|
|
[self skipBlock];
|
|
}
|
|
else if (buffer[pos] == ENDBRACE)
|
|
{
|
|
pos++; // Ignore extraneous closing brace
|
|
[self skipSpaces];
|
|
}
|
|
else if (buffer[pos] == '#')
|
|
{
|
|
pos++;
|
|
[self parsePreprocessor]; // Ignore preprocessor directive.
|
|
DESTROY(comment);
|
|
}
|
|
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);
|
|
|
|
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 && [t isEqual: @"enum ..."] == NO)
|
|
{
|
|
[self log: @"parse declaration with no name - %@", d];
|
|
}
|
|
IF_NO_ARC([arp release];)
|
|
return nil;
|
|
}
|
|
}
|
|
[self setStandards: declarations];
|
|
IF_NO_ARC(declarations = [declarations retain]; [arp release];)
|
|
return AUTORELEASE(declarations);
|
|
}
|
|
else
|
|
{
|
|
[self log: @"unexpected end of data parsing declaration"];
|
|
}
|
|
fail:
|
|
DESTROY(comment);
|
|
IF_NO_ARC([arp release];)
|
|
return nil;
|
|
}
|
|
|
|
- (NSMutableDictionary*) parseFile: (NSString*)name isSource: (BOOL)isSource
|
|
{
|
|
NSString *token;
|
|
NSMutableArray *declarations;
|
|
NSEnumerator *enumerator;
|
|
NSMutableDictionary *nDecl;
|
|
|
|
if (debug)
|
|
{
|
|
NSLog(@"-parseFile:isSource: '%@' %@", name, (isSource ? @"YES" : @"NO"));
|
|
}
|
|
if (isSource)
|
|
{
|
|
inHeader = NO;
|
|
}
|
|
else
|
|
{
|
|
inHeader = YES;
|
|
}
|
|
commentsRead = NO;
|
|
ASSIGNCOPY(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];
|
|
}
|
|
}
|
|
DESTROY(unitName);
|
|
DESTROY(itemName);
|
|
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.
|
|
*/
|
|
[self parsePreprocessor];
|
|
break;
|
|
|
|
case '@':
|
|
token = [self parseIdentifier];
|
|
if (token != nil)
|
|
{
|
|
if ([token isEqual: @"interface"])
|
|
{
|
|
if (isSource)
|
|
{
|
|
[self skipUnit];
|
|
DESTROY(comment);
|
|
}
|
|
else
|
|
{
|
|
[self parseInterface];
|
|
}
|
|
}
|
|
else if ([token isEqual: @"protocol"])
|
|
{
|
|
if (isSource)
|
|
{
|
|
[self skipUnit];
|
|
DESTROY(comment);
|
|
}
|
|
else
|
|
{
|
|
[self parseProtocol];
|
|
}
|
|
}
|
|
else if ([token isEqual: @"implementation"])
|
|
{
|
|
if (isSource)
|
|
{
|
|
[self parseImplementation];
|
|
}
|
|
else
|
|
{
|
|
[self skipUnit];
|
|
DESTROY(comment);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
[self skipStatementLine];
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
/*
|
|
* Must be some sort of declaration ...
|
|
*/
|
|
pos--;
|
|
declarations = [self parseDeclarations];
|
|
if (debug)
|
|
{
|
|
NSLog(@"top level declaration: %@", declarations);
|
|
}
|
|
enumerator = [declarations objectEnumerator];
|
|
while ((nDecl = [enumerator nextObject]) != 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"])
|
|
{
|
|
NSArray *a1 = [oDecl objectForKey: @"Args"];
|
|
NSArray *a2 = [nDecl objectForKey: @"Args"];
|
|
|
|
if ([a1 isEqual: a2] == NO)
|
|
{
|
|
[self log: @"Function %@ args mismatch - "
|
|
@"%@ %@", 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.
|
|
* We may also need to patch up the initial chapter
|
|
* and section to indicate that this is a tool.
|
|
*/
|
|
if ([name isEqual: @"main"])
|
|
{
|
|
NSString *c;
|
|
|
|
if (nil == (c = [oDecl objectForKey: @"Comment"]))
|
|
{
|
|
c = @"";
|
|
}
|
|
[self addMain: c];
|
|
[dict removeObjectForKey: name];
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* If no <date> ... </date> then use the date/time at which the file
|
|
* was parsed.
|
|
*/
|
|
if ([info objectForKey: @"date"] == nil)
|
|
{
|
|
static NSString *generated = nil;
|
|
|
|
if (nil == generated)
|
|
{
|
|
generated = [[NSString alloc] initWithFormat:
|
|
@"<date>Generated at %@</date>", [NSDate date]];
|
|
}
|
|
[info setObject: generated forKey: @"date"];
|
|
}
|
|
|
|
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;
|
|
}
|
|
ASSIGNCOPY(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];
|
|
ASSIGN(unitName, name);
|
|
}
|
|
else if (buffer[pos] == ':')
|
|
{
|
|
pos++;
|
|
if ((base = [self parseIdentifier]) == nil
|
|
|| [self parseSpaceOrGeneric] >= 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);
|
|
IF_NO_ARC([arp release];)
|
|
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];
|
|
|
|
DESTROY(unitName);
|
|
DESTROY(comment);
|
|
IF_NO_ARC([arp release];)
|
|
return dict;
|
|
|
|
fail:
|
|
DESTROY(unitName);
|
|
DESTROY(comment);
|
|
IF_NO_ARC([arp release];)
|
|
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 parseSpaceOrGeneric] >= length)
|
|
{
|
|
[self log: @"interface with bad name"];
|
|
goto fail;
|
|
}
|
|
ASSIGNCOPY(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];
|
|
ASSIGN(unitName, name);
|
|
[dict setObject: @"category" forKey: @"Type"];
|
|
if ([category length] >= 7
|
|
&& [category compare: @"Private"
|
|
options: NSCaseInsensitiveSearch
|
|
range: NSMakeRange(0, 7)] == NSOrderedSame)
|
|
{
|
|
NSString *c;
|
|
|
|
c = @"<em>Warning</em> this category is <em>private</em>, 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)
|
|
{
|
|
unsigned saved = pos;
|
|
|
|
if ([self skipGeneric] > saved)
|
|
{
|
|
[self parseSpace];
|
|
}
|
|
else
|
|
{
|
|
[self log: @"bad protocol list"];
|
|
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];
|
|
|
|
DESTROY(unitName);
|
|
DESTROY(comment);
|
|
IF_NO_ARC([arp release];)
|
|
return dict;
|
|
|
|
fail:
|
|
DESTROY(unitName);
|
|
DESTROY(comment);
|
|
IF_NO_ARC([arp release];)
|
|
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.
|
|
* If we read a GS_GENERIC... macro, we return its first argument.
|
|
*/
|
|
- (NSString*) parseIdentifier
|
|
{
|
|
unsigned start;
|
|
|
|
try:
|
|
[self parseSpace: (inPreprocessorDirective ? spaces : spacenl)];
|
|
if (pos >= length || [identStart characterIsMember: buffer[pos]] == NO)
|
|
{
|
|
return nil;
|
|
}
|
|
start = pos;
|
|
while (pos < length)
|
|
{
|
|
if ([identifier characterIsMember: buffer[pos]] == NO)
|
|
{
|
|
NSString *tmp;
|
|
|
|
tmp = [[NSString alloc] initWithCharacters: &buffer[start]
|
|
length: pos - start];
|
|
if (inPreprocessorDirective)
|
|
{
|
|
/* No word mapping or special processing is done inside a
|
|
* preprocessor directive.
|
|
*/
|
|
return AUTORELEASE(tmp);
|
|
}
|
|
else
|
|
{
|
|
NSString *val;
|
|
|
|
if ([tmp isEqual: @"GS_GENERIC_CLASS"])
|
|
{
|
|
[self skipSpaces];
|
|
if (pos < length && buffer[pos] == '(')
|
|
{
|
|
pos++;
|
|
/* Found a GS_GENERIC_CLASS macro ... the first
|
|
* identifier inside the macro arguments is the
|
|
* name we want to return.
|
|
*/
|
|
RELEASE(tmp);
|
|
tmp = RETAIN([self parseIdentifier]);
|
|
while (pos < length)
|
|
{
|
|
if (buffer[pos++] == ')')
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if ([tmp isEqual: @"GS_GENERIC_TYPE"])
|
|
{
|
|
[self skipSpaces];
|
|
if (pos < length && buffer[pos] == '(')
|
|
{
|
|
pos++;
|
|
/* Found a GS_GENERIC_TYPE macro ... the second
|
|
* argument inside the macro is the name we want
|
|
* to return (or 'id' if there is no second arg).
|
|
*/
|
|
DESTROY(tmp);
|
|
while (pos < length)
|
|
{
|
|
unichar c = buffer[pos++];
|
|
|
|
if (')' == c)
|
|
{
|
|
break;
|
|
}
|
|
else if (',' == c)
|
|
{
|
|
tmp = RETAIN([self parseMethodType]);
|
|
[self skipSpaces];
|
|
if (')' == buffer[pos])
|
|
{
|
|
pos++;
|
|
}
|
|
//NSLog(@"Parsed generic type as '%@'", tmp);
|
|
break;
|
|
}
|
|
}
|
|
if (nil == tmp)
|
|
{
|
|
tmp = @"id";
|
|
}
|
|
}
|
|
}
|
|
val = [wordMap objectForKey: tmp];
|
|
if (val == nil)
|
|
{
|
|
return AUTORELEASE(tmp); // No mapping found.
|
|
}
|
|
RELEASE(tmp);
|
|
if ([val length] > 0)
|
|
{
|
|
if ([val isEqualToString: @"//"])
|
|
{
|
|
[self skipToEndOfLine];
|
|
return [self parseIdentifier];
|
|
}
|
|
else if ([val isEqualToString: @"()"])
|
|
{
|
|
if ([self skipSpaces] < length && buffer[pos] == '(')
|
|
{
|
|
[self skipBlock]; // Skip the attributes
|
|
[self skipSpaces];
|
|
}
|
|
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] != ENDBRACE)
|
|
{
|
|
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"])
|
|
{
|
|
validity = AUTORELEASE(RETAIN(token));
|
|
shouldDocument = documentInstanceVariables
|
|
&& documentAllInstanceVariables;
|
|
}
|
|
else if ([token isEqual: @"protected"])
|
|
{
|
|
validity = AUTORELEASE(RETAIN(token));
|
|
shouldDocument = documentInstanceVariables;
|
|
}
|
|
else if ([token isEqual: @"package"])
|
|
{
|
|
validity = AUTORELEASE(RETAIN(token));
|
|
shouldDocument = documentInstanceVariables;
|
|
}
|
|
else if ([token isEqual: @"public"])
|
|
{
|
|
validity = AUTORELEASE(RETAIN(token));
|
|
shouldDocument = documentInstanceVariables;
|
|
}
|
|
else
|
|
{
|
|
[self log: @"interface with bad validity (%@)", token];
|
|
goto fail;
|
|
}
|
|
}
|
|
else if (buffer[pos] == '#')
|
|
{
|
|
pos++;
|
|
[self parsePreprocessor]; // Ignore preprocessor directive.
|
|
DESTROY(comment);
|
|
}
|
|
else if (shouldDocument)
|
|
{
|
|
NSMutableArray *declarations = [self parseDeclarations];
|
|
NSEnumerator *enumerator = [declarations objectEnumerator];
|
|
NSMutableDictionary *iv;
|
|
|
|
while ((iv = [enumerator nextObject]) != nil)
|
|
{
|
|
if ([validity isEqual: @"private"] == NO)
|
|
{
|
|
NSString *n = [iv objectForKey: @"Name"];
|
|
|
|
if ([n hasPrefix: @"_"])
|
|
{
|
|
NSString *c;
|
|
|
|
c = @"<em>Warning</em> the underscore at the start of "
|
|
@"the name of this instance variable indicates that, "
|
|
@"even though it is not technically <em>private</em>, "
|
|
@"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 = AUTORELEASE([[NSMutableDictionary alloc] initWithCapacity: 4]);
|
|
name = [self parseIdentifier];
|
|
if (nil == name)
|
|
{
|
|
[self log: @"Warning - missing name in #define"];
|
|
return nil;
|
|
}
|
|
[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 preceded 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
|
|
{
|
|
dict = nil;
|
|
}
|
|
RELEASE(a);
|
|
[self setStandards: dict];
|
|
return dict;
|
|
}
|
|
|
|
- (NSMutableDictionary*) parseMethodIsDeclaration: (BOOL)flag
|
|
{
|
|
IF_NO_ARC(CREATE_AUTORELEASE_POOL(arp);)
|
|
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 stringWithUTF8String: "-"];
|
|
}
|
|
else
|
|
{
|
|
mname = [NSMutableString stringWithUTF8String: "+"];
|
|
}
|
|
[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)
|
|
{
|
|
term = ';';
|
|
}
|
|
else
|
|
{
|
|
term = '{';
|
|
}
|
|
|
|
while (buffer[pos] != term)
|
|
{
|
|
token = [self parseIdentifier];
|
|
while ([token isEqual: @"GS_UNIMPLEMENTED"]
|
|
|| [token isEqual: @"__attribute__"])
|
|
{
|
|
if ([token isEqual: @"GS_UNIMPLEMENTED"])
|
|
{
|
|
[method setObject: @"YES" forKey: @"Unimplemented"];
|
|
[self appendComment: @"<em>Warning</em> this is "
|
|
@"<em>unimplemented</em> but may be implemented "
|
|
@"in future versions"
|
|
to: nil];
|
|
[self skipSpaces];
|
|
}
|
|
else if ([token isEqual: @"__attribute__"])
|
|
{
|
|
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)
|
|
{
|
|
[method setObject: @"YES" forKey: @"Deprecated"];
|
|
[self appendComment: @"<em>Warning</em> this is "
|
|
@"<em>deprecated</em> and may be removed in "
|
|
@"future versions"
|
|
to: nil];
|
|
}
|
|
[self skipSpaces];
|
|
}
|
|
else
|
|
{
|
|
[self log: @"strange format function attributes"];
|
|
}
|
|
}
|
|
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: @"Warning - incorrect semicolon"
|
|
@" after name in method definition"];
|
|
}
|
|
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)
|
|
{
|
|
[self setStandards: method];
|
|
}
|
|
ASSIGNCOPY(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 == '{')
|
|
{
|
|
BOOL isEmpty;
|
|
|
|
[self skipBlock: &isEmpty];
|
|
if (isEmpty)
|
|
{
|
|
[method setObject: @"YES" forKey: @"Empty"];
|
|
}
|
|
else
|
|
{
|
|
[method setObject: @"NO" forKey: @"Empty"];
|
|
}
|
|
}
|
|
|
|
/*
|
|
* 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
|
|
&& [itemName length] > 1 && [itemName characterAtIndex: 1] == '_')
|
|
{
|
|
NSString *c;
|
|
|
|
c = @"<em>Warning</em> 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];
|
|
}
|
|
|
|
DESTROY(itemName);
|
|
IF_NO_ARC([arp release];)
|
|
IF_NO_ARC([method autorelease];)
|
|
return method;
|
|
|
|
fail:
|
|
DESTROY(itemName);
|
|
DESTROY(comment);
|
|
IF_NO_ARC([arp release];)
|
|
RELEASE(method);
|
|
return nil;
|
|
}
|
|
|
|
- (void) addOrderedSymbolDeclaration: (NSString *)aMethodOrFunc toUnit: (NSString *)aUnitName
|
|
{
|
|
NSMutableArray *orderedSymbolDecls = [orderedSymbolDeclsByUnit objectForKey: aUnitName];
|
|
|
|
if (orderedSymbolDecls == nil)
|
|
{
|
|
orderedSymbolDecls = [NSMutableArray array];
|
|
[orderedSymbolDeclsByUnit setObject: orderedSymbolDecls
|
|
forKey: aUnitName];
|
|
}
|
|
[orderedSymbolDecls addObject: aMethodOrFunc];
|
|
}
|
|
|
|
static unsigned
|
|
countAttributes(NSSet *keys, NSDictionary *a)
|
|
{
|
|
NSEnumerator *e = [keys objectEnumerator];
|
|
NSString *k;
|
|
unsigned count = 0;
|
|
|
|
while ((k = [e nextObject]) != nil)
|
|
{
|
|
if ([a objectForKey: k])
|
|
{
|
|
count++;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
- (NSMutableDictionary*) parsePropertyGetter: (NSMutableDictionary**)g
|
|
andSetter: (NSMutableDictionary**)s
|
|
{
|
|
static NSSet *atomicity = nil;
|
|
static NSSet *writability = nil;
|
|
static NSSet *semantics = nil;
|
|
NSMutableDictionary *gd;
|
|
NSMutableDictionary *prop;
|
|
NSMutableDictionary *attr;
|
|
NSString *type;
|
|
NSString *name;
|
|
NSString *sel;
|
|
NSString *get;
|
|
NSString *set;
|
|
NSString *token;
|
|
unsigned count;
|
|
|
|
if (nil == atomicity)
|
|
{
|
|
atomicity = [[NSSet alloc] initWithObjects:
|
|
@"atomic",
|
|
@"nonatomic",
|
|
nil];
|
|
}
|
|
if (nil == writability)
|
|
{
|
|
writability = [[NSSet alloc] initWithObjects:
|
|
@"readonly",
|
|
@"readwrite",
|
|
nil];
|
|
}
|
|
if (nil == semantics)
|
|
{
|
|
semantics = [[NSSet alloc] initWithObjects:
|
|
@"assign",
|
|
@"copy",
|
|
@"retain",
|
|
@"strong",
|
|
@"weak",
|
|
nil];
|
|
}
|
|
|
|
attr = [NSMutableDictionary dictionary];
|
|
if ([self parseSpace] < length && '(' == buffer[pos])
|
|
{
|
|
pos++;
|
|
while ([self parseSpace] < length)
|
|
{
|
|
if (',' == buffer[pos])
|
|
{
|
|
pos++;
|
|
continue;
|
|
}
|
|
if (')' == buffer[pos])
|
|
{
|
|
pos++;
|
|
break; // End of property attributes
|
|
}
|
|
if (nil == (token = [self parseIdentifier]))
|
|
{
|
|
[self log: @"@property bad attributes"];
|
|
return nil;
|
|
}
|
|
if ([token isEqual: @"getter"]
|
|
|| [token isEqual: @"setter"])
|
|
{
|
|
NSString *key = token;
|
|
|
|
if ([self parseSpace] >= length
|
|
|| buffer[pos] != '=')
|
|
{
|
|
[self log: @"@property bad %@ spec", key];
|
|
return nil;
|
|
}
|
|
pos++;
|
|
if ([self parseSpace] >= length
|
|
|| (nil == (token = [self parseIdentifier])))
|
|
{
|
|
[self log: @"@property bad %@ spec", key];
|
|
return nil;
|
|
}
|
|
[attr setObject: token forKey: key];
|
|
}
|
|
else
|
|
{
|
|
if ([writability member: token])
|
|
{
|
|
[attr setObject: token forKey: token];
|
|
}
|
|
else if ([atomicity member: token])
|
|
{
|
|
[attr setObject: token forKey: token];
|
|
}
|
|
else if ([semantics member: token])
|
|
{
|
|
[attr setObject: token forKey: token];
|
|
}
|
|
else
|
|
{
|
|
[self log: @"@property unknown attribute %@",
|
|
token];
|
|
return nil;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ((count = countAttributes(writability, attr)) > 1)
|
|
{
|
|
[self log: @"@property with multiple writablity attributes"];
|
|
return nil;
|
|
}
|
|
else if (0 == count)
|
|
{
|
|
[attr setObject: @"readwrite" forKey: @"readwrite"];
|
|
}
|
|
if ((count = countAttributes(atomicity, attr)) > 1)
|
|
{
|
|
[self log: @"@property with multiple atomicity attributes"];
|
|
return nil;
|
|
}
|
|
else if (0 == count)
|
|
{
|
|
[attr setObject: @"atomic" forKey: @"atomic"];
|
|
}
|
|
if ((count = countAttributes(semantics, attr)) > 1)
|
|
{
|
|
[self log: @"@property with multiple setter semantics attributes"];
|
|
return nil;
|
|
}
|
|
else if (0 == count)
|
|
{
|
|
[attr setObject: @"assign" forKey: @"assign"];
|
|
}
|
|
if ([attr objectForKey: @"readonly"] && [attr objectForKey: @"setter"])
|
|
{
|
|
[self log: @"@property with setter is marked readonly"];
|
|
return nil;
|
|
}
|
|
|
|
if (nil == (prop = [[self parseDeclarations] firstObject]))
|
|
{
|
|
return nil;
|
|
}
|
|
|
|
/* Get the property name (will use it in the setter)
|
|
*/
|
|
name = [prop objectForKey: @"Name"];
|
|
|
|
/* Use the declaration of the property to set up its type information.
|
|
*/
|
|
type = [prop objectForKey: @"BaseType"];
|
|
if ((token = [prop objectForKey: @"Prefix"]) != nil)
|
|
{
|
|
type = [type stringByAppendingString: token];
|
|
}
|
|
if ((token = [prop objectForKey: @"Suffix"]) != nil)
|
|
{
|
|
type = [type stringByAppendingString: token];
|
|
}
|
|
|
|
[prop setObject: attr forKey: @"Attributes"];
|
|
|
|
if (nil == (sel = [attr objectForKey: @"getter"]))
|
|
{
|
|
sel = name;
|
|
}
|
|
while ([sel hasSuffix: @":"])
|
|
{
|
|
sel = [sel substringToIndex: [sel length] - 1];
|
|
}
|
|
get = sel; // The getter selector
|
|
|
|
if ([attr objectForKey: @"readonly"])
|
|
{
|
|
*s = nil;
|
|
set = nil;
|
|
}
|
|
else
|
|
{
|
|
NSMutableDictionary *sd = [NSMutableDictionary dictionary];
|
|
|
|
*s = sd;
|
|
[sd setObject: @"Methods" forKey: @"Kind"];
|
|
[sd setObject: @"void" forKey: @"ReturnType"];
|
|
if (nil == (sel = [attr objectForKey: @"setter"]))
|
|
{
|
|
unichar c;
|
|
|
|
sel = name;
|
|
c = [sel characterAtIndex: 0];
|
|
if (c >= 'a' && c <= 'z')
|
|
{
|
|
c += 'A' - 'a';
|
|
}
|
|
sel = [NSString stringWithFormat: @"set%c%@:",
|
|
c, [sel substringFromIndex: 1]];
|
|
}
|
|
if ([sel hasSuffix: @":"] == NO)
|
|
{
|
|
sel = [sel stringByAppendingString: @":"];
|
|
}
|
|
set = sel; // The setter selector
|
|
|
|
[sd setObject: [@"-" stringByAppendingString: set] forKey: @"Name"];
|
|
[sd setObject: [NSMutableArray arrayWithObject: set]
|
|
forKey: @"Sels"];
|
|
[sd setObject: [NSMutableArray arrayWithObject: name]
|
|
forKey: @"Args"];
|
|
[sd setObject: [NSMutableArray arrayWithObject: type]
|
|
forKey: @"Types"];
|
|
token = [NSString stringWithFormat:
|
|
@"Setter for property '%@' with attributes %@."
|
|
@" See also <ref type=\"method\" id=\"-%@\""
|
|
@" class=\"%@\">[%@ -%@]</ref>\n",
|
|
name, [attr allKeys], get, unitName, unitName, get];
|
|
if (comment != nil)
|
|
{
|
|
token = [token stringByAppendingString: comment];
|
|
}
|
|
[sd setObject: token forKey: @"Comment"];
|
|
[sd setObject: @"YES" forKey: @"Implemented"];
|
|
}
|
|
|
|
gd = [NSMutableDictionary dictionary];
|
|
*g = gd;
|
|
[gd setObject: @"Methods" forKey: @"Kind"];
|
|
[gd setObject: type forKey: @"ReturnType"];
|
|
[gd setObject: [NSMutableArray arrayWithObject: get]
|
|
forKey: @"Sels"];
|
|
[gd setObject: [@"-" stringByAppendingString: get] forKey: @"Name"];
|
|
token = [NSString stringWithFormat:
|
|
@"Getter for property '%@' with attributes %@.",
|
|
name, [attr allKeys]];
|
|
if (set)
|
|
{
|
|
token = [token stringByAppendingFormat:
|
|
@" See also <ref type=\"method\" id=\"-%@\""
|
|
@" class=\"%@\">[%@ -%@]</ref>\n",
|
|
set, unitName, unitName, set];
|
|
if (comment != nil)
|
|
{
|
|
token = [token stringByAppendingString: comment];
|
|
}
|
|
}
|
|
[gd setObject: token forKey: @"Comment"];
|
|
[gd setObject: @"YES" forKey: @"Implemented"];
|
|
|
|
[prop setObject: @"Properties" forKey: @"Kind"];
|
|
DESTROY(comment);
|
|
return prop;
|
|
}
|
|
|
|
- (NSMutableDictionary*) parseMethodsAreDeclarations: (BOOL)flag
|
|
{
|
|
NSMutableDictionary *methods;
|
|
NSMutableDictionary *method;
|
|
NSMutableDictionary *exist;
|
|
NSString *token;
|
|
BOOL optionalMethods = NO;
|
|
|
|
if (flag)
|
|
{
|
|
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;
|
|
}
|
|
if (YES == optionalMethods)
|
|
{
|
|
[method setObject: @"YES" forKey: @"Optional"];
|
|
}
|
|
token = [method objectForKey: @"Name"];
|
|
if (flag)
|
|
{
|
|
/*
|
|
* Just record the method.
|
|
*/
|
|
[methods setObject: method forKey: token];
|
|
[self addOrderedSymbolDeclaration: token toUnit: unitName];
|
|
}
|
|
else if ((exist = [methods objectForKey: token]) != nil)
|
|
{
|
|
NSArray *a0;
|
|
NSArray *a1;
|
|
NSString *c0;
|
|
NSString *c1;
|
|
NSString *e;
|
|
|
|
/*
|
|
* Merge info from implementation into existing version.
|
|
*/
|
|
|
|
a0 = [exist objectForKey: @"Types"];
|
|
a1 = [method objectForKey: @"Types"];
|
|
if (a0 != nil)
|
|
{
|
|
if (equalTypes(a0, a1) == NO)
|
|
{
|
|
ASSIGNCOPY(itemName, token);
|
|
[self log: @"method types in interface %@ don't match "
|
|
@"those in implementation %@", a0, a1];
|
|
DESTROY(itemName);
|
|
[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"];
|
|
|
|
/*
|
|
* Record if the implementation is not empty.
|
|
*/
|
|
e = [method objectForKey: @"Empty"];
|
|
if (e != nil)
|
|
{
|
|
[exist setObject: e forKey: @"Empty"];
|
|
}
|
|
}
|
|
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"])
|
|
{
|
|
return methods;
|
|
}
|
|
else if ([token isEqual: @"optional"])
|
|
{
|
|
/* marking remaining methods as optional.
|
|
*/
|
|
optionalMethods = YES;
|
|
continue;
|
|
}
|
|
else if ([token isEqual: @"required"])
|
|
{
|
|
/* marking remaining methods as required.
|
|
*/
|
|
optionalMethods = NO;
|
|
continue;
|
|
}
|
|
else if ([token isEqual: @"class"])
|
|
{
|
|
/* Pre-declaration of one or more classes ... rather like a
|
|
* normal C statement, it ends with a semicolon.
|
|
*/
|
|
[self skipStatementLine];
|
|
}
|
|
else if (NO == flag
|
|
&& ([token isEqual: @"dynamic"]
|
|
|| [token isEqual: @"synthesize"]))
|
|
{
|
|
/* In implementation @dynamic and @synthesize defines how
|
|
* one or more properties are generated.
|
|
* The lists of property names end with a semicolon.
|
|
*/
|
|
[self skipStatementLine];
|
|
}
|
|
else if ([token isEqual: @"property"])
|
|
{
|
|
NSMutableDictionary *g = nil;
|
|
NSMutableDictionary *p = nil;
|
|
NSMutableDictionary *s = nil;
|
|
|
|
if (nil == [self parsePropertyGetter: &g andSetter: &s])
|
|
{
|
|
[self log: @"@property declaration invalid"];
|
|
[self skipStatementLine];
|
|
}
|
|
else
|
|
{
|
|
NSAssert(nil == p
|
|
|| [[p objectForKey: @"Kind"] isEqual: @"Properties"],
|
|
NSInternalInconsistencyException);
|
|
/* FIXME ... need to handle properties
|
|
token = [p objectForKey: @"Name"];
|
|
[methods setObject: p forKey: token];
|
|
[self addOrderedSymbolDeclaration: token toUnit: unitName];
|
|
*/
|
|
|
|
token = [g objectForKey: @"Name"];
|
|
[methods setObject: g forKey: token];
|
|
[self addOrderedSymbolDeclaration: token toUnit: unitName];
|
|
|
|
if (s)
|
|
{
|
|
token = [s objectForKey: @"Name"];
|
|
[methods setObject: s forKey: token];
|
|
[self addOrderedSymbolDeclaration: token
|
|
toUnit: unitName];
|
|
}
|
|
}
|
|
}
|
|
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)
|
|
{
|
|
[self log: @"interface with bogus line ... we expect methods"];
|
|
[self skipStatementLine];
|
|
}
|
|
else
|
|
{
|
|
pos--;
|
|
[self parseDeclarations];
|
|
}
|
|
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]])
|
|
{
|
|
*ptr++ = ' ';
|
|
}
|
|
}
|
|
}
|
|
|
|
if ([self parseSpace] >= length)
|
|
{
|
|
return nil;
|
|
}
|
|
|
|
/*
|
|
* Strip trailing space ... leading space we never copied in the
|
|
* first place.
|
|
*/
|
|
if (ptr > start && [spacenl characterIsMember: ptr[-1]])
|
|
{
|
|
ptr--;
|
|
}
|
|
|
|
if (ptr > start)
|
|
{
|
|
NSString *tmp;
|
|
|
|
tmp = [NSString stringWithCharacters: start length: ptr - start];
|
|
tmp = concreteType(tmp);
|
|
return tmp;
|
|
}
|
|
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
|
|
{
|
|
NSString *directive;
|
|
// NSString *where = [self where];
|
|
|
|
NSAssert(pos > 0 && '#' == buffer[pos - 1], NSInternalInconsistencyException);
|
|
|
|
inPreprocessorDirective = YES;
|
|
directive = [self parseIdentifier];
|
|
if ([directive isEqual: @"define"])
|
|
{
|
|
/* Macro definition inside source is ignored since it is not
|
|
* visible to the outside world.
|
|
*/
|
|
if (inHeader)
|
|
{
|
|
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"])
|
|
{
|
|
if ([ifStack count] <= 1)
|
|
{
|
|
[self log: @"Unexpected #endif (no matching #if)"];
|
|
}
|
|
else
|
|
{
|
|
// NSLog(@"Pop %@", where);
|
|
[ifStack removeLastObject];
|
|
}
|
|
}
|
|
else if ([directive isEqual: @"else"])
|
|
{
|
|
if ([ifStack count] <= 1)
|
|
{
|
|
[self log: @"Unexpected #else (no matching #if)"];
|
|
}
|
|
else
|
|
{
|
|
[ifStack removeLastObject];
|
|
[ifStack addObject: [ifStack lastObject]];
|
|
}
|
|
}
|
|
else if ([directive isEqual: @"if"] || [directive isEqual: @"elif"])
|
|
{
|
|
NSMutableDictionary *top;
|
|
NSString *arg;
|
|
BOOL hadOstep = NO;
|
|
BOOL hadGstep = NO;
|
|
|
|
if ([directive isEqual: @"elif"])
|
|
{
|
|
if ([ifStack count] <= 1)
|
|
{
|
|
[self log: @"Unexpected #elif (no matching #if"];
|
|
}
|
|
else
|
|
{
|
|
// NSLog(@"Pop %@", where);
|
|
[ifStack removeLastObject];
|
|
}
|
|
}
|
|
|
|
top = [[ifStack lastObject] mutableCopy];
|
|
|
|
while ((arg = [self parseIdentifier]) != nil)
|
|
{
|
|
BOOL openstep;
|
|
NSString *ver;
|
|
|
|
if ([arg isEqual: @"OS_API_VERSION"])
|
|
{
|
|
openstep = YES;
|
|
if (hadOstep)
|
|
{
|
|
[self log: @"multiple grouped OS_API_VERSION() calls"];
|
|
RELEASE(top);
|
|
return [self skipRemainderOfLine];
|
|
}
|
|
hadOstep = YES;
|
|
[top removeObjectForKey: @"ovadd"];
|
|
[top removeObjectForKey: @"ovdep"];
|
|
[top removeObjectForKey: @"ovrem"];
|
|
}
|
|
else if ([arg isEqual: @"GS_API_VERSION"])
|
|
{
|
|
openstep = NO;
|
|
if (hadGstep)
|
|
{
|
|
[self log: @"multiple grouped GS_API_VERSION() calls"];
|
|
RELEASE(top);
|
|
return [self skipRemainderOfLine];
|
|
}
|
|
hadGstep = YES;
|
|
[top removeObjectForKey: @"gvadd"];
|
|
[top removeObjectForKey: @"gvdep"];
|
|
[top removeObjectForKey: @"gvrem"];
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
|
|
[self parseSpace: spaces];
|
|
if (pos < length && buffer[pos] == '(')
|
|
{
|
|
pos++;
|
|
}
|
|
ver = [self parseVersion];
|
|
if ([ver length] == 0)
|
|
{
|
|
ver = @"1.0.0";
|
|
}
|
|
if (openstep)
|
|
{
|
|
[top setObject: ver forKey: @"ovadd"];
|
|
}
|
|
else
|
|
{
|
|
[top setObject: ver forKey: @"gvadd"];
|
|
}
|
|
|
|
[self parseSpace: spaces];
|
|
if (pos < length && buffer[pos] == ',')
|
|
{
|
|
pos++;
|
|
}
|
|
ver = [self parseVersion];
|
|
if ([ver length] == 0)
|
|
{
|
|
ver = @"99.99.99";
|
|
}
|
|
if ([ver isEqualToString: @"99.99.99"] == NO)
|
|
{
|
|
if (openstep)
|
|
{
|
|
[top setObject: ver forKey: @"ovrem"];
|
|
}
|
|
else
|
|
{
|
|
[top setObject: ver forKey: @"gvrem"];
|
|
}
|
|
}
|
|
|
|
[self parseSpace: spaces];
|
|
if (pos < length && buffer[pos] == ',')
|
|
{
|
|
pos++;
|
|
ver = [self parseVersion];
|
|
if ([ver length] == 0)
|
|
{
|
|
ver = @"99.99.99";
|
|
}
|
|
if ([ver isEqualToString: @"99.99.99"] == NO)
|
|
{
|
|
if (openstep)
|
|
{
|
|
[top setObject: ver forKey: @"ovdep"];
|
|
}
|
|
else
|
|
{
|
|
[top setObject: ver forKey: @"gvdep"];
|
|
}
|
|
}
|
|
[self parseSpace: spaces];
|
|
}
|
|
|
|
if (pos < length && buffer[pos] == ')')
|
|
{
|
|
pos++;
|
|
}
|
|
|
|
[self parseSpace: spaces];
|
|
if (pos < length-1 && buffer[pos] == '&' && buffer[pos+1] == '&')
|
|
{
|
|
pos += 2;
|
|
}
|
|
else
|
|
{
|
|
break; // may only join version macros with &&
|
|
}
|
|
}
|
|
[ifStack addObject: top];
|
|
// NSLog(@"Push %@", where);
|
|
RELEASE(top);
|
|
}
|
|
else if ([directive isEqual: @"ifdef"])
|
|
{
|
|
NSMutableDictionary *top = [[ifStack lastObject] mutableCopy];
|
|
NSString *arg = [self parseIdentifier];
|
|
|
|
if ([arg isEqual: @"NO_GNUSTEP"])
|
|
{
|
|
[self log: @"Unexpected #ifdef NO_GNUSTEP (nonsense)"];
|
|
}
|
|
else if ([arg isEqual: @"STRICT_MACOS_X"])
|
|
{
|
|
[top removeObjectForKey: @"NotMacOS-X"];
|
|
[top setObject: @"MacOS-X" forKey: @"MacOS-X"];
|
|
}
|
|
else if ([arg isEqual: @"STRICT_OPENSTEP"])
|
|
{
|
|
[top removeObjectForKey: @"NotOpenStep"];
|
|
[top setObject: @"OpenStep" forKey: @"OpenStep"];
|
|
}
|
|
|
|
[ifStack addObject: top];
|
|
// NSLog(@"Push %@", where);
|
|
RELEASE(top);
|
|
}
|
|
else if ([directive isEqual: @"ifndef"])
|
|
{
|
|
NSMutableDictionary *top = [[ifStack lastObject] mutableCopy];
|
|
NSString *arg = [self parseIdentifier];
|
|
|
|
if ([arg isEqual: @"NO_GNUSTEP"])
|
|
{
|
|
[top removeObjectForKey: @"MacOS-X"];
|
|
[top setObject: @"NotMacOS-X" forKey: @"NotMacOS-X"];
|
|
[top removeObjectForKey: @"OpenStep"];
|
|
[top setObject: @"NotOpenStep" forKey: @"NotOpenStep"];
|
|
}
|
|
else if ([arg isEqual: @"STRICT_MACOS_X"])
|
|
{
|
|
[top removeObjectForKey: @"MacOS-X"];
|
|
[top setObject: @"NotMacOS-X" forKey: @"NotMacOS-X"];
|
|
}
|
|
else if ([arg isEqual: @"STRICT_OPENSTEP"])
|
|
{
|
|
[top removeObjectForKey: @"OpenStep"];
|
|
[top setObject: @"NotOpenStep" forKey: @"NotOpenStep"];
|
|
}
|
|
[ifStack addObject: top];
|
|
// NSLog(@"Push %@", where);
|
|
RELEASE(top);
|
|
}
|
|
else if ([directive isEqual: @"error"]
|
|
|| [directive isEqual: @"import"]
|
|
|| [directive isEqual: @"include"]
|
|
|| [directive isEqual: @"line"]
|
|
|| [directive isEqual: @"pragma"]
|
|
|| [directive isEqual: @"undef"]
|
|
|| [directive isEqual: @"warning"])
|
|
{
|
|
}
|
|
else if ([directive length] == 0)
|
|
{
|
|
[self log: @"Warning - empty preprocessor directive"];
|
|
}
|
|
else
|
|
{
|
|
[self log: @"Warning - unknown preprocessor directive %@", directive];
|
|
}
|
|
[self skipRemainderOfLine];
|
|
inPreprocessorDirective = NO;
|
|
return pos;
|
|
}
|
|
|
|
- (NSMutableDictionary*) parseProtocol
|
|
{
|
|
NSString *name;
|
|
NSDictionary *methods = nil;
|
|
NSMutableDictionary *dict;
|
|
NSMutableDictionary *d;
|
|
IF_NO_ARC(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];
|
|
DESTROY(dict);
|
|
return nil;
|
|
}
|
|
|
|
[dict setObject: name forKey: @"Name"];
|
|
[self setStandards: dict];
|
|
DESTROY(unitName);
|
|
unitName = [[NSString alloc] initWithFormat: @"(%@)", name];
|
|
|
|
/*
|
|
* Protocols may themselves conform to protocols.
|
|
*/
|
|
if (buffer[pos] == '<')
|
|
{
|
|
NSArray *protocols = [self parseProtocolList];
|
|
|
|
if (protocols == nil)
|
|
{
|
|
[self log: @"bad protocol list"];
|
|
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];
|
|
|
|
DESTROY(unitName);
|
|
DESTROY(comment);
|
|
IF_NO_ARC([arp release];)
|
|
IF_NO_ARC([dict autorelease];)
|
|
return dict;
|
|
|
|
fail:
|
|
DESTROY(unitName);
|
|
DESTROY(comment);
|
|
IF_NO_ARC([arp release];)
|
|
RELEASE(dict);
|
|
return nil;
|
|
}
|
|
|
|
- (NSMutableArray*) parseProtocolList
|
|
{
|
|
NSMutableArray *protocols;
|
|
NSString *p;
|
|
unsigned start = pos;
|
|
|
|
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)
|
|
{
|
|
pos = start;
|
|
return nil;
|
|
}
|
|
return protocols;
|
|
}
|
|
|
|
/**
|
|
* Skip past any whitespace characters (as defined by the supplied set)
|
|
* including comments.<br />
|
|
* 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 (NO == inPreprocessorDirective)
|
|
{
|
|
if (pos < length && [identifier characterIsMember: buffer[pos]])
|
|
{
|
|
while (pos < length)
|
|
{
|
|
if ([identifier characterIsMember: buffer[pos]] == NO)
|
|
{
|
|
NSString *tmp;
|
|
NSString *val;
|
|
|
|
tmp = [[NSString alloc] initWithCharacters: &buffer[start]
|
|
length: pos - start];
|
|
if ([tmp isEqualToString: @"NS_FORMAT_ARGUMENT"]
|
|
|| [tmp isEqualToString: @"NS_FORMAT_FUNCTION"]
|
|
|| [tmp isEqualToString: @"NS_DEPRECATED"])
|
|
{
|
|
if (inPreprocessorDirective)
|
|
{
|
|
val = tmp;
|
|
}
|
|
else
|
|
{
|
|
/* These macros need to be skipped as they appear
|
|
* inside method declarations.
|
|
*/
|
|
val = @"";
|
|
[self skipSpaces];
|
|
[self skipBlock];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
val = [wordMap objectForKey: tmp];
|
|
}
|
|
RELEASE(tmp);
|
|
if (val == nil)
|
|
{
|
|
pos = start; // No mapping found
|
|
}
|
|
else if ([val length] > 0)
|
|
{
|
|
if ([val isEqualToString: @"//"])
|
|
{
|
|
[self skipToEndOfLine];
|
|
tryAgain = YES;
|
|
}
|
|
else
|
|
{
|
|
pos = start; // Not mapped to a comment.
|
|
}
|
|
}
|
|
else
|
|
{
|
|
tryAgain = YES; // Identifier ignored.
|
|
}
|
|
break;
|
|
}
|
|
pos++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
while (tryAgain);
|
|
|
|
return pos;
|
|
}
|
|
|
|
- (unsigned) parseSpace
|
|
{
|
|
[self parseSpace: spacenl];
|
|
return pos;
|
|
}
|
|
|
|
- (unsigned) parseSpaceOrGeneric
|
|
{
|
|
[self parseSpace: spacenl];
|
|
|
|
if (pos < length && '<' == buffer[pos])
|
|
{
|
|
unsigned saved = pos;
|
|
|
|
if ([self skipGeneric] > saved)
|
|
{
|
|
[self parseSpace];
|
|
}
|
|
else
|
|
{
|
|
[self log: @"bad generic"];
|
|
}
|
|
}
|
|
return pos;
|
|
}
|
|
|
|
- (NSString*) parseVersion
|
|
{
|
|
static NSDictionary *known = nil;
|
|
unsigned i;
|
|
NSString *str;
|
|
NSString *tmp;
|
|
|
|
while (pos < length && [spaces characterIsMember: buffer[pos]])
|
|
{
|
|
pos++;
|
|
}
|
|
if (pos >= length || buffer[pos] == '\n')
|
|
{
|
|
return nil;
|
|
}
|
|
if (!isdigit(buffer[pos]))
|
|
{
|
|
str = [self parseIdentifier];
|
|
}
|
|
else
|
|
{
|
|
i = pos;
|
|
while (pos < length)
|
|
{
|
|
if (!isdigit(buffer[pos]))
|
|
{
|
|
break;
|
|
}
|
|
pos++;
|
|
}
|
|
str = [NSString stringWithCharacters: &buffer[i] length: pos - i];
|
|
}
|
|
|
|
if (nil == known)
|
|
{
|
|
known = [[NSDictionary alloc] initWithObjectsAndKeys:
|
|
@"0", @"GS_API_NONE",
|
|
@"999999", @"GS_API_LATEST",
|
|
@"10000", @"GS_API_OSSPEC",
|
|
@"40000", @"GS_API_OPENSTEP",
|
|
@"100000", @"GS_API_MACOSX",
|
|
@"100100", @"MAC_OS_X_VERSION_10_1",
|
|
@"100200", @"MAC_OS_X_VERSION_10_2",
|
|
@"100300", @"MAC_OS_X_VERSION_10_3",
|
|
@"100400", @"MAC_OS_X_VERSION_10_4",
|
|
@"100500", @"MAC_OS_X_VERSION_10_5",
|
|
@"100600", @"MAC_OS_X_VERSION_10_6",
|
|
@"100700", @"MAC_OS_X_VERSION_10_7",
|
|
@"100800", @"MAC_OS_X_VERSION_10_8",
|
|
@"100900", @"MAC_OS_X_VERSION_10_9",
|
|
@"101000", @"MAC_OS_X_VERSION_10_10",
|
|
@"101100", @"MAC_OS_X_VERSION_10_11",
|
|
@"101200", @"MAC_OS_X_VERSION_10_12",
|
|
@"101300", @"MAC_OS_X_VERSION_10_13",
|
|
@"101400", @"MAC_OS_X_VERSION_10_14",
|
|
@"101500", @"MAC_OS_X_VERSION_10_15",
|
|
nil];
|
|
}
|
|
tmp = [known objectForKey: str];
|
|
if (nil != tmp)
|
|
{
|
|
str = tmp;
|
|
}
|
|
|
|
i = [str intValue];
|
|
return [NSString stringWithFormat: @"%d.%d.%d",
|
|
i/10000, (i/100)%100, i%100];
|
|
}
|
|
|
|
- (void) reset
|
|
{
|
|
[source removeAllObjects];
|
|
[info removeAllObjects];
|
|
haveOutput = NO;
|
|
haveSource = NO;
|
|
DESTROY(declared);
|
|
DESTROY(comment);
|
|
DESTROY(fileName);
|
|
DESTROY(unitName);
|
|
DESTROY(itemName);
|
|
DESTROY(lines);
|
|
buffer = 0;
|
|
length = 0;
|
|
pos = 0;
|
|
}
|
|
|
|
/** Turns debug on/off
|
|
*/
|
|
- (void) setDebug: (BOOL)aFlag
|
|
{
|
|
debug = (aFlag ? YES : NO);
|
|
}
|
|
|
|
/**
|
|
* 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.<br />
|
|
* You should only turn this on while parsing the GNUstep source code.
|
|
*/
|
|
- (void) setGenerateStandards: (BOOL)flag
|
|
{
|
|
if (flag)
|
|
{
|
|
[ifStack replaceObjectAtIndex: 0 withObject:
|
|
[NSDictionary dictionaryWithObjectsAndKeys:
|
|
@"OpenStep", @"OpenStep",
|
|
@"MacOS-X", @"MacOS-X",
|
|
@"GNUstep", @"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: (id)dst
|
|
{
|
|
if (standards)
|
|
{
|
|
NSDictionary *top = [ifStack lastObject];
|
|
|
|
if ([top count] > 0)
|
|
{
|
|
NSString *vInfo = nil;
|
|
NSString *gvadd = [top objectForKey: @"gvadd"];
|
|
NSString *ovadd = [top objectForKey: @"ovadd"];
|
|
|
|
if (ovadd != nil || gvadd != nil)
|
|
{
|
|
NSMutableString *m = [NSMutableString stringWithCapacity: 64];
|
|
NSString *s;
|
|
|
|
if (ovadd != nil)
|
|
{
|
|
[m appendFormat: @" ovadd=\"%@\"", ovadd];
|
|
if ((s = [top objectForKey: @"ovdep"]) != nil)
|
|
{
|
|
[m appendFormat: @" ovdep=\"%@\"", s];
|
|
}
|
|
if ((s = [top objectForKey: @"ovrem"]) != nil)
|
|
{
|
|
[m appendFormat: @" ovrem=\"%@\"", s];
|
|
}
|
|
}
|
|
if (gvadd != nil)
|
|
{
|
|
[m appendFormat: @" gvadd=\"%@\"", gvadd];
|
|
if ((s = [top objectForKey: @"gvdep"]) != nil)
|
|
{
|
|
[m appendFormat: @" gvdep=\"%@\"", s];
|
|
}
|
|
if ((s = [top objectForKey: @"gvrem"]) != nil)
|
|
{
|
|
[m appendFormat: @" gvrem=\"%@\"", s];
|
|
}
|
|
}
|
|
vInfo = m;
|
|
}
|
|
else if ([top objectForKey: @"NotOpenStep"]
|
|
&& [top objectForKey: @"NotMacOS-X"])
|
|
{
|
|
vInfo = @" gvadd=\"0.0.0\""; // GNUstep
|
|
}
|
|
else if ([top objectForKey: @"NotOpenStep"]
|
|
&& ![top objectForKey: @"NotMacOS-X"])
|
|
{
|
|
vInfo = @" ovadd=\"10.0.0\""; // MacOS-X
|
|
}
|
|
else if (![top objectForKey: @"NotOpenStep"]
|
|
&& [top objectForKey: @"NotMacOS-X"])
|
|
{
|
|
vInfo = @" ovadd=\"1.0.0\" ovrem=\"4.0.0\""; // OpenStep only
|
|
}
|
|
else if ([top objectForKey: @"OpenStep"]
|
|
&& ![top objectForKey: @"NotMacOS-X"])
|
|
{
|
|
vInfo = @" ovadd=\"1.0.0\""; // OpenStep
|
|
}
|
|
if (vInfo != nil)
|
|
{
|
|
if ([dst isKindOfClass: [NSMutableDictionary class]])
|
|
{
|
|
[(NSMutableDictionary*)dst setObject: vInfo
|
|
forKey: @"Versions"];
|
|
}
|
|
else
|
|
{
|
|
NSEnumerator *e = [(NSArray*)dst objectEnumerator];
|
|
|
|
while ((dst = [e nextObject]) != nil)
|
|
{
|
|
if ([dst isKindOfClass: [NSMutableDictionary class]])
|
|
{
|
|
[(NSMutableDictionary*)dst setObject: vInfo
|
|
forKey: @"Versions"];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
IF_NO_ARC(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)
|
|
{
|
|
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;
|
|
ASSIGN(lines, [NSArray arrayWithArray: a]);
|
|
IF_NO_ARC([arp release];)
|
|
IF_NO_ARC([data autorelease];)
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
- (unsigned) skipAttribute: (NSString*)s
|
|
{
|
|
if ([self skipSpaces] < length && buffer[pos] == '(')
|
|
{
|
|
unsigned start = pos;
|
|
|
|
[self skipBlock]; // Skip the attributes/asm
|
|
if (YES == verbose)
|
|
{
|
|
NSString *attr;
|
|
|
|
attr = [NSString stringWithCharacters: buffer + start
|
|
length: pos - start];
|
|
[self log: @"skip %@ %@", s, attr];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
[self log: @"strange format %@", s];
|
|
}
|
|
[self skipSpaces];
|
|
return pos;
|
|
}
|
|
|
|
- (unsigned) skipIfAttribute
|
|
{
|
|
if (pos < length && '_' == buffer[pos])
|
|
{
|
|
unsigned saved = pos;
|
|
NSString *s = [self parseIdentifier];
|
|
|
|
if ([s isEqualToString: @"__attribute__"]
|
|
|| [s isEqualToString: @"__asm__"])
|
|
{
|
|
[self skipAttribute: s];
|
|
}
|
|
else
|
|
{
|
|
pos = saved;
|
|
}
|
|
}
|
|
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
|
|
{
|
|
return [self skipBlock: 0];
|
|
}
|
|
|
|
- (unsigned) skipBlock: (BOOL*)isEmpty
|
|
{
|
|
unichar term = ENDBRACE;
|
|
BOOL empty = YES;
|
|
|
|
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 '"':
|
|
empty = NO;
|
|
pos--;
|
|
[self skipLiteral];
|
|
break;
|
|
|
|
case '{':
|
|
empty = NO;
|
|
pos--;
|
|
[self skipBlock];
|
|
break;
|
|
|
|
case '(':
|
|
empty = NO;
|
|
pos--;
|
|
[self skipBlock];
|
|
break;
|
|
|
|
case '[':
|
|
empty = NO;
|
|
pos--;
|
|
[self skipBlock];
|
|
break;
|
|
|
|
default:
|
|
if (c == term)
|
|
{
|
|
if (isEmpty != 0)
|
|
{
|
|
*isEmpty = empty;
|
|
}
|
|
return pos;
|
|
}
|
|
empty = NO;
|
|
}
|
|
}
|
|
if (isEmpty != 0)
|
|
{
|
|
*isEmpty = empty;
|
|
}
|
|
return pos;
|
|
}
|
|
|
|
- (unsigned) skipGeneric
|
|
{
|
|
unsigned depth = 0;
|
|
unsigned save = pos;
|
|
|
|
NSAssert(buffer[pos] == '<', NSInternalInconsistencyException);
|
|
while (pos < length)
|
|
{
|
|
unichar c = buffer[pos++];
|
|
|
|
if (c == '\\')
|
|
{
|
|
pos++;
|
|
}
|
|
else if ('<' == c)
|
|
{
|
|
depth++;
|
|
}
|
|
else if ('>' == c && --depth == 0)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
if (depth > 0
|
|
|| (pos < length && buffer[pos - 1] != '>'))
|
|
{
|
|
return save;
|
|
}
|
|
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 ('\n' == buffer[pos++])
|
|
{
|
|
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 ENDBRACE:
|
|
[self log: @"Argh ... read end brace 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] == ENDBRACE)
|
|
{
|
|
[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]);
|
|
}
|
|
|
|
- (NSString*) where
|
|
{
|
|
int index;
|
|
int start = 0;
|
|
int end;
|
|
NSString *l;
|
|
NSString *s;
|
|
|
|
for (index = [lines count] - 1; index >= 0; index--)
|
|
{
|
|
NSNumber *num = [lines objectAtIndex: index];
|
|
|
|
if ((start = [num intValue]) <= (int)pos)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
if (index >= [lines count] || index < 0)
|
|
{
|
|
start = 0;
|
|
index = -1;
|
|
}
|
|
|
|
if (index + 1 < [lines count])
|
|
{
|
|
end = [[lines objectAtIndex: index + 1] intValue];
|
|
}
|
|
else
|
|
{
|
|
end = length;
|
|
}
|
|
l = [[NSString alloc] initWithCharactersNoCopy: buffer + start
|
|
length: end - start
|
|
freeWhenDone: NO];
|
|
s = [NSString stringWithFormat: @"Character %d in line %d:%@",
|
|
pos - start, index + 2, l];
|
|
RELEASE(l);
|
|
return s;
|
|
}
|
|
|
|
@end
|
|
|