forked from tomaz/appledoc
-
Notifications
You must be signed in to change notification settings - Fork 1
/
GBProcessor.m
411 lines (356 loc) · 17.4 KB
/
GBProcessor.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
//
// GBProcessor.m
// appledoc
//
// Created by Tomaz Kragelj on 27.8.10.
// Copyright (C) 2010, Gentle Bytes. All rights reserved.
//
#import "GBStore.h"
#import "GBApplicationSettingsProvider.h"
#import "GBDataObjects.h"
#import "GBCommentsProcessor.h"
#import "GBProcessor.h"
@interface GBProcessor ()
- (void)processClasses;
- (void)processCategories;
- (void)processProtocols;
- (void)processDocuments;
- (void)processMethodsFromProvider:(GBMethodsProvider *)provider;
- (void)processCommentForObject:(GBModelBase *)object;
- (void)processParametersFromComment:(GBComment *)comment matchingMethod:(GBMethodData *)method;
- (void)processHtmlReferencesForObject:(GBModelBase *)object;
- (void)copyKnownDocumentationForMethod:(GBMethodData *)method;
- (BOOL)removeUndocumentedObject:(id)object;
- (BOOL)removeUndocumentedMember:(GBMethodData *)object;
- (void)setupKnownObjectsFromStore;
- (void)mergeKnownCategoriesFromStore;
- (void)setupSuperclassForClass:(GBClassData *)class;
- (void)setupAdoptedProtocolsFromProvider:(GBAdoptedProtocolsProvider *)provider;
- (void)validateCommentsForObjectAndMembers:(GBModelBase *)object;
- (BOOL)isCommentValid:(GBComment *)comment;
@property (retain) GBCommentsProcessor *commentsProcessor;
@property (retain) id currentContext;
@property (retain) GBStore *store;
@property (retain) GBApplicationSettingsProvider *settings;
@end
#pragma mark -
@implementation GBProcessor
#pragma mark Initialization & disposal
+ (id)processorWithSettingsProvider:(id)settingsProvider {
return [[[self alloc] initWithSettingsProvider:settingsProvider] autorelease];
}
- (id)initWithSettingsProvider:(id)settingsProvider {
NSParameterAssert(settingsProvider != nil);
GBLogDebug(@"Initializing processor with settings provider %@...", settingsProvider);
self = [super init];
if (self) {
self.settings = settingsProvider;
self.commentsProcessor = [GBCommentsProcessor processorWithSettingsProvider:self.settings];
}
return self;
}
#pragma mark Processing handling
- (void)processObjectsFromStore:(id)store {
NSParameterAssert(store != nil);
GBLogVerbose(@"Processing parsed objects...");
self.currentContext = nil;
self.store = store;
[self setupKnownObjectsFromStore];
[self mergeKnownCategoriesFromStore];
[self processClasses];
[self processCategories];
[self processProtocols];
[self processDocuments];
}
- (void)processClasses {
// No need to process ivars as they are not used for output. Note that we need to iterate over a copy of objects to prevent problems when removing undocumented ones!
NSArray *classes = [self.store.classes allObjects];
for (GBClassData *class in classes) {
GBLogInfo(@"Processing class %@...", class);
self.currentContext = class;
[self processMethodsFromProvider:class.methods];
if (![self removeUndocumentedObject:class]) {
[self processCommentForObject:class];
[self validateCommentsForObjectAndMembers:class];
[self processHtmlReferencesForObject:class];
}
GBLogDebug(@"Finished processing class %@.", class);
}
}
- (void)processCategories {
NSArray *categories = [self.store.categories allObjects];
for (GBCategoryData *category in categories) {
GBLogInfo(@"Processing category %@...", category);
self.currentContext = category;
[self processMethodsFromProvider:category.methods];
if (![self removeUndocumentedObject:category]) {
[self processCommentForObject:category];
[self validateCommentsForObjectAndMembers:category];
[self processHtmlReferencesForObject:category];
}
GBLogDebug(@"Finished processing category %@.", category);
}
}
- (void)processProtocols {
NSArray *protocols = [self.store.protocols allObjects];
for (GBProtocolData *protocol in protocols) {
GBLogInfo(@"Processing protocol %@...", protocol);
self.currentContext = protocol;
[self processMethodsFromProvider:protocol.methods];
if (![self removeUndocumentedObject:protocol]) {
[self processCommentForObject:protocol];
[self validateCommentsForObjectAndMembers:protocol];
[self processHtmlReferencesForObject:protocol];
}
GBLogDebug(@"Finished processing protocol %@.", protocol);
}
}
- (void)processDocuments {
for (GBDocumentData *document in self.store.documents) {
GBLogInfo(@"Processing static document %@...", document);
self.currentContext = document;
[self processCommentForObject:document];
GBLogDebug(@"Finished processing document %@.", document);
}
for (GBDocumentData *document in self.store.customDocuments) {
GBLogInfo(@"Processing custom document %@...", document);
self.currentContext = document;
[self processCommentForObject:document];
GBLogDebug(@"Finished processing custom document %@.", document);
}
}
#pragma mark Common data processing
- (void)processMethodsFromProvider:(GBMethodsProvider *)provider {
NSArray *methods = [provider.methods copy];
for (GBMethodData *method in methods) {
GBLogVerbose(@"Processing method %@...", method);
[self copyKnownDocumentationForMethod:method];
if (![self removeUndocumentedMember:method]) {
[self processCommentForObject:method];
[self processParametersFromComment:method.comment matchingMethod:method];
[self processHtmlReferencesForObject:method];
}
GBLogDebug(@"Finished processing method %@.", method);
}
}
- (void)processHtmlReferencesForObject:(GBModelBase *)object {
// Setups html reference name and local reference that's going to be used later on when generating HTML. This could easily be handled within the object accessors, but using a predefined value speeds up these frequently used values.
object.htmlReferenceName = [self.settings htmlReferenceNameForObject:object];
object.htmlLocalReference = [self.settings htmlReferenceForObject:object fromSource:object];
}
- (void)processCommentForObject:(GBModelBase *)object {
// Processes the comment for the given object. If the comment is not valid, it's forced to nil to make simpler work for template engine later on. Note that comment is considered invalid if the object isn't commented or has comment, but it's string value is nil or empty string.
if (![self isCommentValid:object.comment]) {
object.comment = nil;
return;
}
// Let comments processor parse comment string value into object representation.
self.commentsProcessor.alwaysRepeatFirstParagraph = object.isTopLevelObject || object.isStaticDocument;
[self.commentsProcessor processComment:object.comment withContext:self.currentContext store:self.store];
}
- (void)processParametersFromComment:(GBComment *)comment matchingMethod:(GBMethodData *)method {
// This is where we validate comment parameters and sort them in proper order.
if (!comment || [comment.stringValue length] == 0 || comment.isCopied) return;
GBLogDebug(@"Validating processed parameters...");
// Prepare names of all argument variables from the method and parameter descriptions from the comment. Note that we don't warn about issues here, we'll handle missing parameters while sorting and unkown parameters at the end.
NSMutableArray *names = [NSMutableArray arrayWithCapacity:[method.methodArguments count]];
[method.methodArguments enumerateObjectsUsingBlock:^(GBMethodArgument *argument, NSUInteger idx, BOOL *stop) {
if (!argument.argumentVar) return;
[names addObject:argument.argumentVar];
if (idx == [method.methodArguments count] - 1 && [argument isVariableArg]) [names addObject:@"..."];
}];
NSMutableDictionary *parameters = [NSMutableDictionary dictionaryWithCapacity:[comment.methodParameters count]];
[comment.methodParameters enumerateObjectsUsingBlock:^(GBCommentArgument *parameter, NSUInteger idx, BOOL *stop) {
[parameters setObject:parameter forKey:parameter.argumentName];
}];
// Sort the parameters in the same order as in the method. Warn if any parameter is not found. Also warn if there are more parameters in the comment than the method defines. Note that we still add these descriptions to the end of the sorted list!
NSMutableArray *sorted = [NSMutableArray arrayWithCapacity:[parameters count]];
[names enumerateObjectsUsingBlock:^(NSString *name, NSUInteger idx, BOOL *stop) {
GBCommentArgument *parameter = [parameters objectForKey:name];
if (!parameter) {
if (method.includeInOutput)
GBLogXWarn(comment.sourceInfo, @"%@: Description for parameter '%@' missing for %@!", comment.sourceInfo, name, method);
return;
}
[sorted addObject:parameter];
[parameters removeObjectForKey:name];
}];
if ([parameters count] > 0) {
NSMutableString *description = [NSMutableString string];
[[parameters allValues] enumerateObjectsUsingBlock:^(GBCommentArgument *parameter, NSUInteger idx, BOOL *stop) {
if ([description length] > 0) [description appendString:@", "];
[description appendString:parameter.argumentName];
[sorted addObject:parameter];
}];
if (self.settings.warnOnMissingMethodArgument && method.includeInOutput) GBLogXWarn(comment.sourceInfo, @"%@: %ld unknown parameter descriptions (%@) found for %@", comment.sourceInfo, [parameters count], description, method);
}
// Finaly re-register parameters to the comment if necessary (no need if there's only one parameter).
if ([names count] > 1) {
[comment.methodParameters removeAllObjects];
[comment.methodParameters addObjectsFromArray:sorted];
}
}
- (void)copyKnownDocumentationForMethod:(GBMethodData *)method {
// Copies method documentation from known superclasses or adopted protocols.
if (!self.settings.findUndocumentedMembersDocumentation || [self isCommentValid:method.comment]) return;
// First search within superclass hierarchy. This only works for classes.
if ([method.parentObject isKindOfClass:[GBClassData class]]) {
GBClassData *class = [(GBClassData *)method.parentObject superclass];
while (class) {
GBMethodData *superMethod = [class.methods methodBySelector:method.methodSelector];
if (superMethod.comment) {
GBLogVerbose(@"Copying documentation for %@ from superclass %@...", method, class);
superMethod.comment.originalContext = superMethod.parentObject;
method.comment = superMethod.comment;
return;
}
class = class.superclass;
}
}
// If not found on superclass, search within adopted protocols.
GBAdoptedProtocolsProvider *protocols = [method.parentObject adoptedProtocols];
for (GBProtocolData *protocol in protocols.protocols) {
GBMethodData *protocolMethod = [protocol.methods methodBySelector:method.methodSelector];
if (protocolMethod.comment) {
GBLogVerbose(@"Copying documentation for %@ from adopted protocol %@...", method, protocol);
protocolMethod.comment.originalContext = protocolMethod.parentObject;
method.comment = protocolMethod.comment;
return;
}
}
}
- (BOOL)removeUndocumentedObject:(id)object {
// Removes the given top level object if it's not commented and all of it's methods are uncommented. Returns YES if the object was removed, NO otherwise.
if (self.settings.keepUndocumentedObjects) return NO;
if ([self isCommentValid:[(GBModelBase *)object comment]]) return NO;
// Only remove if all methods are uncommented. Note that this also removes methods regardless of keepUndocumentedMembers setting, however if the object itself is commented, we'll keep methods.
GBMethodsProvider *provider = [(id<GBObjectDataProviding>)object methods];
BOOL hasCommentedMethods = NO;
for (GBMethodData *method in provider.methods) {
if ([self isCommentValid:method.comment]) {
hasCommentedMethods = YES;
break;
}
}
// Remove the object if it only has uncommented methods.
if (!hasCommentedMethods) {
GBLogVerbose(@"Removing undocumented object %@...", object);
[self.store unregisterTopLevelObject:object];
return YES;
}
return NO;
}
- (BOOL)removeUndocumentedMember:(GBMethodData *)object {
// Removes the given method if it's not commented and returns YES if removed, NO otherwise.
if (self.settings.keepUndocumentedMembers) return NO;
if ([self isCommentValid:object.comment]) return NO;
// Remove the method and all empty sections to cleanup the object for output generation.
GBLogVerbose(@"Removing undocumented method %@...", object);
GBMethodsProvider *provider = [(id<GBObjectDataProviding>)object.parentObject methods];
[provider unregisterMethod:object];
[provider unregisterEmptySections];
return YES;
}
#pragma mark Known objects handling
- (void)setupKnownObjectsFromStore {
// Setups links to superclasses and adopted protocols. This should be sent first so that the data is prepared for later processing.
GBLogInfo(@"Checking for known superclasses and adopted protocols...");
for (GBClassData *class in self.store.classes) {
[self setupSuperclassForClass:class];
[self setupAdoptedProtocolsFromProvider:class.adoptedProtocols];
}
for (GBCategoryData *category in self.store.categories) {
[self setupAdoptedProtocolsFromProvider:category.adoptedProtocols];
}
for (GBProtocolData *protocol in self.store.protocols) {
[self setupAdoptedProtocolsFromProvider:protocol.adoptedProtocols];
}
}
- (void)setupSuperclassForClass:(GBClassData *)class {
// This setups super class links for known superclasses.
if ([class.nameOfSuperclass length] == 0) return;
GBClassData *superclass = [self.store classWithName:class.nameOfSuperclass];
if (superclass) {
GBLogDebug(@"Setting superclass link of %@ to %@...", class, superclass);
class.superclass = superclass;
}
}
- (void)setupAdoptedProtocolsFromProvider:(GBAdoptedProtocolsProvider *)provider {
// This replaces known adopted protocols with real ones from the assigned store.
NSArray *registeredProtocols = [self.store.protocols allObjects];
for (GBProtocolData *adopted in [provider.protocols allObjects]) {
for (GBProtocolData *registered in registeredProtocols) {
if ([registered.nameOfProtocol isEqualToString:adopted.nameOfProtocol]) {
GBLogDebug(@"Replacing %@ placeholder with known data from store...", registered);
[provider replaceProtocol:adopted withProtocol:registered];
break;
}
}
}
}
- (void)mergeKnownCategoriesFromStore {
GBLogInfo(@"Merging known categories to classes...");
if (!self.settings.mergeCategoriesToClasses) return;
NSSet *categories = [self.store.categories copy];
for (GBCategoryData *category in categories) {
GBLogVerbose(@"Checking %@ for merging...", category);
// Get the class and continue with next category if unknown class is extended.
GBClassData *class = [self.store classWithName:category.nameOfClass];
if (!class) {
GBLogDebug(@"Category %@ extends unknown class %@, skipping merging.", category, category.nameOfClass);
continue;
}
// Merge all methods from category to the class. We can leave methods within the category as we'll delete it later on anyway.
if ([category.methods.methods count] > 0) {
// If we should merge all section into a single section per category, create it now. Note that name is different whether this is category or extension.
if (!self.settings.keepMergedCategoriesSections) {
GBLogDebug(@"Creating single section for methods merged from %@...", category);
NSString *key = category.isExtension ? @"mergedExtensionSectionTitle" : @"mergedCategorySectionTitle";
NSString *template = [self.settings.stringTemplates.objectPage objectForKey:key];
NSString *name = category.isExtension ? template : [NSString stringWithFormat:template, category.nameOfCategory];
[class.methods registerSectionWithName:name];
}
// Merge all sections and all the methods, optionally create a separate section for each section from category.
for (GBMethodSectionData *section in category.methods.sections) {
GBLogDebug(@"Merging section %@ from %@...", section, category);
if (self.settings.keepMergedCategoriesSections) {
if (self.settings.prefixMergedCategoriesSectionsWithCategoryName && !category.isExtension) {
NSString *template = [self.settings.stringTemplates.objectPage objectForKey:@"mergedPrefixedCategorySectionTitle"];
NSString *name = [NSString stringWithFormat:template, category.nameOfCategory, section.sectionName];
[class.methods registerSectionWithName:name];
} else {
[class.methods registerSectionWithName:section.sectionName];
}
}
for (GBMethodData *method in section.methods) {
GBLogDebug(@"Merging method %@ from %@...", method, category);
[class.methods registerMethod:method];
}
}
}
// Finally clean all empty sections and remove merged category from the store.
[class.methods unregisterEmptySections];
[self.store unregisterTopLevelObject:category];
}
}
#pragma mark Helper methods
- (void)validateCommentsForObjectAndMembers:(GBModelBase *)object {
if (!object.includeInOutput) return;
// Checks if the object is commented and warns if not. This validates given object and all it's members comments! The reason for doing it together is due to the fact that we first process all members and then handle the object. At that point we can even remove the object if not documented. So we can't validate members before as we don't know whether they will be deleted together with their parent object too...
if (![self isCommentValid:object.comment] && self.settings.warnOnUndocumentedObject) GBLogXWarn(object.prefferedSourceInfo, @"%@ is not documented!", object);
// Handle methods.
for (GBMethodData *method in [[(id<GBObjectDataProviding>)object methods] methods]) {
if (![self isCommentValid:method.comment] && self.settings.warnOnUndocumentedMember) {
GBLogXWarn(method.prefferedSourceInfo, @"%@ is not documented!", method);
}
}
}
- (BOOL)isCommentValid:(GBComment *)comment {
return (comment && [comment.stringValue length] > 0);
}
#pragma mark Properties
@synthesize commentsProcessor;
@synthesize currentContext;
@synthesize settings;
@synthesize store;
@end