]>
Commit | Line | Data |
---|---|---|
1 | #!/usr/bin/env python | |
2 | ||
3 | ## @package thtml2doxy_clang | |
4 | # Translates THtml C++ comments to Doxygen using libclang as parser. | |
5 | # | |
6 | # This code relies on Python bindings for libclang: libclang's interface is pretty unstable, and | |
7 | # its Python bindings are unstable as well. | |
8 | # | |
9 | # AST (Abstract Source Tree) traversal is performed entirely using libclang used as a C++ parser, | |
10 | # instead of attempting to write a parser ourselves. | |
11 | # | |
12 | # This code (expecially AST traversal) was inspired by: | |
13 | # | |
14 | # - [Implementing a code generator with libclang](http://szelei.me/code-generator/) | |
15 | # (this refers to API calls used here) | |
16 | # - [Parsing C++ in Python with Clang](http://eli.thegreenplace.net/2011/07/03/parsing-c-in-python-with-clang) | |
17 | # (outdated, API calls described there do not work anymore, but useful to understand some basic | |
18 | # concepts) | |
19 | # | |
20 | # Usage: | |
21 | # | |
22 | # `thtml2doxy_clang [--stdout|-o] [-d] [--debug=DEBUG_LEVEL] file1 [file2 [file3...]]` | |
23 | # | |
24 | # Parameters: | |
25 | # | |
26 | # - `--stdout|-o`: output all on standard output instead of writing files in place | |
27 | # - `-d`: enable debug mode (very verbose output) | |
28 | # - `--debug=DEBUG_LEVEL`: set debug level to one of `DEBUG`, `INFO`, `WARNING`, `ERROR`, | |
29 | # `CRITICAL` | |
30 | # | |
31 | # @author Dario Berzano, CERN | |
32 | # @date 2014-12-05 | |
33 | ||
34 | ||
35 | import sys | |
36 | import os | |
37 | import re | |
38 | import logging | |
39 | import getopt | |
40 | import hashlib | |
41 | import clang.cindex | |
42 | ||
43 | ||
44 | ## Brain-dead color output for terminal. | |
45 | class Colt(str): | |
46 | ||
47 | def red(self): | |
48 | return self.color('\033[31m') | |
49 | ||
50 | def green(self): | |
51 | return self.color('\033[32m') | |
52 | ||
53 | def yellow(self): | |
54 | return self.color('\033[33m') | |
55 | ||
56 | def blue(self): | |
57 | return self.color('\033[34m') | |
58 | ||
59 | def magenta(self): | |
60 | return self.color('\033[35m') | |
61 | ||
62 | def cyan(self): | |
63 | return self.color('\033[36m') | |
64 | ||
65 | def color(self, c): | |
66 | return c + self + '\033[m' | |
67 | ||
68 | ||
69 | ## Comment. | |
70 | class Comment(object): | |
71 | ||
72 | def __init__(self, lines, first_line, first_col, last_line, last_col, indent, func, \ | |
73 | append_empty=True): | |
74 | ||
75 | assert first_line > 0 and last_line >= first_line, 'Wrong line numbers' | |
76 | self.lines = lines | |
77 | self.first_line = first_line | |
78 | self.first_col = first_col | |
79 | self.last_line = last_line | |
80 | self.last_col = last_col | |
81 | self.indent = indent | |
82 | self.func = func | |
83 | self.append_empty = append_empty | |
84 | ||
85 | def has_comment(self, line): | |
86 | return line >= self.first_line and line <= self.last_line | |
87 | ||
88 | def __str__(self): | |
89 | return "<%s for %s: [%d,%d:%d,%d] %s>" % ( \ | |
90 | self.__class__.__name__, self.func, | |
91 | self.first_line, self.first_col, self.last_line, self.last_col, | |
92 | self.lines) | |
93 | ||
94 | ||
95 | ## Prepend comment. | |
96 | class PrependComment(Comment): | |
97 | ||
98 | def __init__(self, lines, first_line, first_col, last_line, last_col, indent, func, \ | |
99 | append_empty=False): | |
100 | super(PrependComment, self).__init__( \ | |
101 | lines, first_line, first_col, last_line, last_col, indent, func, append_empty) | |
102 | ||
103 | ||
104 | ## A data member comment. | |
105 | class MemberComment: | |
106 | ||
107 | def __init__(self, text, comment_flag, array_size, first_line, first_col, func): | |
108 | assert first_line > 0, 'Wrong line number' | |
109 | assert comment_flag is None or comment_flag == '!' or comment_flag in [ '!', '||', '->' ] | |
110 | self.lines = [ text ] | |
111 | self.comment_flag = comment_flag | |
112 | self.array_size = array_size | |
113 | self.first_line = first_line | |
114 | self.first_col = first_col | |
115 | self.func = func | |
116 | ||
117 | def is_transient(self): | |
118 | return self.comment_flag == '!' | |
119 | ||
120 | def is_dontsplit(self): | |
121 | return self.comment_flag == '||' | |
122 | ||
123 | def is_ptr(self): | |
124 | return self.comment_flag == '->' | |
125 | ||
126 | def has_comment(self, line): | |
127 | return line == self.first_line | |
128 | ||
129 | def __str__(self): | |
130 | ||
131 | if self.is_transient(): | |
132 | tt = '!transient! ' | |
133 | elif self.is_dontsplit(): | |
134 | tt = '!dontsplit! ' | |
135 | elif self.is_ptr(): | |
136 | tt = '!ptr! ' | |
137 | else: | |
138 | tt = '' | |
139 | ||
140 | if self.array_size is not None: | |
141 | ars = '[%s] ' % self.array_size | |
142 | else: | |
143 | ars = '' | |
144 | ||
145 | return "<MemberComment for %s: [%d,%d] %s%s%s>" % (self.func, self.first_line, self.first_col, tt, ars, self.lines[0]) | |
146 | ||
147 | ||
148 | ## A dummy comment that removes comment lines. | |
149 | class RemoveComment(Comment): | |
150 | ||
151 | def __init__(self, first_line, last_line): | |
152 | assert first_line > 0 and last_line >= first_line, 'Wrong line numbers' | |
153 | self.first_line = first_line | |
154 | self.last_line = last_line | |
155 | self.func = '<remove>' | |
156 | ||
157 | def __str__(self): | |
158 | return "<RemoveComment: [%d,%d]>" % (self.first_line, self.last_line) | |
159 | ||
160 | ||
161 | ## Parses method comments. | |
162 | # | |
163 | # @param cursor Current libclang parser cursor | |
164 | # @param comments Array of comments: new ones will be appended there | |
165 | def comment_method(cursor, comments): | |
166 | ||
167 | # we are looking for the following structure: method -> compound statement -> comment, i.e. we | |
168 | # need to extract the first comment in the compound statement composing the method | |
169 | ||
170 | in_compound_stmt = False | |
171 | expect_comment = False | |
172 | emit_comment = False | |
173 | ||
174 | comment = [] | |
175 | comment_function = cursor.spelling or cursor.displayname | |
176 | comment_line_start = -1 | |
177 | comment_line_end = -1 | |
178 | comment_col_start = -1 | |
179 | comment_col_end = -1 | |
180 | comment_indent = -1 | |
181 | ||
182 | for token in cursor.get_tokens(): | |
183 | ||
184 | if token.cursor.kind == clang.cindex.CursorKind.COMPOUND_STMT: | |
185 | if not in_compound_stmt: | |
186 | in_compound_stmt = True | |
187 | expect_comment = True | |
188 | comment_line_end = -1 | |
189 | else: | |
190 | if in_compound_stmt: | |
191 | in_compound_stmt = False | |
192 | emit_comment = True | |
193 | ||
194 | # tkind = str(token.kind)[str(token.kind).index('.')+1:] | |
195 | # ckind = str(token.cursor.kind)[str(token.cursor.kind).index('.')+1:] | |
196 | ||
197 | if in_compound_stmt: | |
198 | ||
199 | if expect_comment: | |
200 | ||
201 | extent = token.extent | |
202 | line_start = extent.start.line | |
203 | line_end = extent.end.line | |
204 | ||
205 | if token.kind == clang.cindex.TokenKind.PUNCTUATION and token.spelling == '{': | |
206 | pass | |
207 | ||
208 | elif token.kind == clang.cindex.TokenKind.COMMENT and (comment_line_end == -1 or (line_start == comment_line_end+1 and line_end-line_start == 0)): | |
209 | comment_line_end = line_end | |
210 | comment_col_end = extent.end.column | |
211 | ||
212 | if comment_indent == -1 or (extent.start.column-1) < comment_indent: | |
213 | comment_indent = extent.start.column-1 | |
214 | ||
215 | if comment_line_start == -1: | |
216 | comment_line_start = line_start | |
217 | comment_col_start = extent.start.column | |
218 | comment.extend( token.spelling.split('\n') ) | |
219 | ||
220 | # multiline comments are parsed in one go, therefore don't expect subsequent comments | |
221 | if line_end - line_start > 0: | |
222 | emit_comment = True | |
223 | expect_comment = False | |
224 | ||
225 | else: | |
226 | emit_comment = True | |
227 | expect_comment = False | |
228 | ||
229 | if emit_comment: | |
230 | ||
231 | if comment_line_start > 0: | |
232 | ||
233 | comment = refactor_comment( comment, infilename=str(cursor.location.file) ) | |
234 | ||
235 | if len(comment) > 0: | |
236 | logging.debug("Comment found for function %s" % Colt(comment_function).magenta()) | |
237 | comments.append( Comment(comment, comment_line_start, comment_col_start, comment_line_end, comment_col_end, comment_indent, comment_function) ) | |
238 | else: | |
239 | logging.debug('Empty comment found for function %s: collapsing' % Colt(comment_function).magenta()) | |
240 | comments.append( Comment([''], comment_line_start, comment_col_start, comment_line_end, comment_col_end, comment_indent, comment_function) ) | |
241 | #comments.append(RemoveComment(comment_line_start, comment_line_end)) | |
242 | ||
243 | else: | |
244 | logging.warning('No comment found for function %s' % Colt(comment_function).magenta()) | |
245 | ||
246 | comment = [] | |
247 | comment_line_start = -1 | |
248 | comment_line_end = -1 | |
249 | comment_col_start = -1 | |
250 | comment_col_end = -1 | |
251 | comment_indent = -1 | |
252 | ||
253 | emit_comment = False | |
254 | break | |
255 | ||
256 | ||
257 | ## Parses comments to class data members. | |
258 | # | |
259 | # @param cursor Current libclang parser cursor | |
260 | # @param comments Array of comments: new ones will be appended there | |
261 | def comment_datamember(cursor, comments): | |
262 | ||
263 | # Note: libclang 3.5 seems to have problems parsing a certain type of FIELD_DECL, so we revert | |
264 | # to a partial manual parsing. When parsing fails, the cursor's "extent" is not set properly, | |
265 | # returning a line range 0-0. We therefore make the not-so-absurd assumption that the datamember | |
266 | # definition is fully on one line, and we take the line number from cursor.location. | |
267 | ||
268 | line_num = cursor.location.line | |
269 | raw = None | |
270 | prev = None | |
271 | found = False | |
272 | ||
273 | # Huge overkill: current line saved in "raw", previous in "prev" | |
274 | with open(str(cursor.location.file)) as fp: | |
275 | cur_line = 0 | |
276 | for raw in fp: | |
277 | cur_line = cur_line + 1 | |
278 | if cur_line == line_num: | |
279 | found = True | |
280 | break | |
281 | prev = raw | |
282 | ||
283 | assert found, 'A line that should exist was not found in file' % cursor.location.file | |
284 | ||
285 | recomm = r'(//(!|\|\||->)|///?)(\[(.+?)\])?<?\s*(.*?)\s*$' | |
286 | recomm_prevline = r'^\s*///\s*(.*?)\s*$' | |
287 | ||
288 | mcomm = re.search(recomm, raw) | |
289 | if mcomm: | |
290 | # If it does not match, we do not have a comment | |
291 | member_name = cursor.spelling; | |
292 | comment_flag = mcomm.group(2) | |
293 | array_size = mcomm.group(4) | |
294 | text = mcomm.group(5) | |
295 | ||
296 | col_num = mcomm.start()+1; | |
297 | ||
298 | if array_size is not None and prev is not None: | |
299 | # ROOT arrays with comments already converted to Doxygen have the member description on the | |
300 | # previous line | |
301 | mcomm_prevline = re.search(recomm_prevline, prev) | |
302 | if mcomm_prevline: | |
303 | text = mcomm_prevline.group(1) | |
304 | comments.append(RemoveComment(line_num-1, line_num-1)) | |
305 | ||
306 | logging.debug('Comment found for member %s' % Colt(member_name).magenta()) | |
307 | ||
308 | comments.append( MemberComment( | |
309 | text, | |
310 | comment_flag, | |
311 | array_size, | |
312 | line_num, | |
313 | col_num, | |
314 | member_name )) | |
315 | ||
316 | ||
317 | ## Parses class description (beginning of file). | |
318 | # | |
319 | # The clang parser does not work in this case so we do it manually, but it is very simple: we keep | |
320 | # the first consecutive sequence of single-line comments (//) we find - provided that it occurs | |
321 | # before any other comment found so far in the file (the comments array is inspected to ensure | |
322 | # this). | |
323 | # | |
324 | # Multi-line comments (/* ... */) that *immediately* follow a series of single-line comments | |
325 | # (*i.e.* without empty lines in-between) are also considered. A class description can eventually | |
326 | # be a series of single-line and multi-line comments, with no blank spaces between them, and always | |
327 | # starting with a single-line sequence. | |
328 | # | |
329 | # The reason why they cannot start with a multi-line sequence is that those are commonly used to | |
330 | # display a copyright notice. | |
331 | # | |
332 | # @param filename Name of the current file | |
333 | # @param comments Array of comments: new ones will be appended there | |
334 | # @param look_no_further_than_line Stop before reaching this line when looking for class comment | |
335 | def comment_classdesc(filename, comments, look_no_further_than_line): | |
336 | ||
337 | # Single-line comment | |
338 | recomm = r'^\s*///?(\s*(.*?))\s*/*\s*$' | |
339 | ||
340 | # Multi-line comment (only either /* or */ on a single line) | |
341 | remlcomm_in = r'^\s*/\*\s*$' | |
342 | remlcomm_out = r'^\s*\*/\s*$' | |
343 | in_mlcomm = False | |
344 | ||
345 | reclass_doxy = r'(?i)^\s*\\(class|file):?\s*([^.]*)' | |
346 | class_name_doxy = None | |
347 | ||
348 | reauthor = r'(?i)^\s*\\?(authors?|origin):?\s*(.*?)\s*(,?\s*([0-9./-]+))?\s*$' | |
349 | redate = r'(?i)^\s*\\?date:?\s*([0-9./-]+)\s*$' | |
350 | rebrief = r'(?i)^\s*\\brief\s*(.*)\s*$' | |
351 | author = None | |
352 | date = None | |
353 | brief = None | |
354 | brief_len_threshold = 80 | |
355 | ||
356 | comment_lines = [] | |
357 | ||
358 | start_line = -1 | |
359 | end_line = -1 | |
360 | ||
361 | line_num = 0 | |
362 | last_comm_line_num = 0 | |
363 | ||
364 | is_macro = filename.endswith('.C') | |
365 | ||
366 | with open(filename, 'r') as fp: | |
367 | ||
368 | for raw in fp: | |
369 | ||
370 | line_num = line_num + 1 | |
371 | ||
372 | if look_no_further_than_line is not None and line_num == look_no_further_than_line: | |
373 | logging.debug('Stopping at line %d while looking for class/file description' % \ | |
374 | look_no_further_than_line) | |
375 | break | |
376 | ||
377 | if in_mlcomm == False and raw.strip() == '' and start_line > 0: | |
378 | # Skip empty lines | |
379 | continue | |
380 | ||
381 | stripped = strip_html(raw) | |
382 | mcomm = None | |
383 | this_comment = None | |
384 | ||
385 | if not in_mlcomm: | |
386 | mcomm = re.search(recomm, stripped) | |
387 | ||
388 | if last_comm_line_num == 0 or last_comm_line_num == line_num-1: | |
389 | ||
390 | if mcomm and not mcomm.group(2).startswith('#'): | |
391 | # Single-line comment | |
392 | this_comment = mcomm.group(1) | |
393 | elif start_line > -1: | |
394 | # Not a single-line comment. But it cannot be the first. | |
395 | if in_mlcomm == False: | |
396 | mmlcomm = re.search(remlcomm_in, stripped) | |
397 | if mmlcomm: | |
398 | in_mlcomm = True | |
399 | this_comment = '' | |
400 | else: | |
401 | mmlcomm = re.search(remlcomm_out, stripped) | |
402 | if mmlcomm: | |
403 | in_mlcomm = False | |
404 | this_comment = '' | |
405 | else: | |
406 | this_comment = stripped | |
407 | ||
408 | if this_comment is not None: | |
409 | ||
410 | if start_line == -1: | |
411 | ||
412 | # First line. Check that we do not overlap with other comments | |
413 | comment_overlaps = False | |
414 | for c in comments: | |
415 | if c.has_comment(line_num): | |
416 | comment_overlaps = True | |
417 | break | |
418 | ||
419 | if comment_overlaps: | |
420 | # No need to look for other comments | |
421 | break | |
422 | ||
423 | start_line = line_num | |
424 | ||
425 | end_line = line_num | |
426 | append = True | |
427 | ||
428 | mclass_doxy = re.search(reclass_doxy, this_comment) | |
429 | if mclass_doxy: | |
430 | class_name_doxy = mclass_doxy.group(2) | |
431 | append = False | |
432 | else: | |
433 | mauthor = re.search(reauthor, this_comment) | |
434 | if mauthor: | |
435 | author = mauthor.group(2) | |
436 | if date is None: | |
437 | # Date specified in the standalone \date field has priority | |
438 | date = mauthor.group(4) | |
439 | append = False | |
440 | else: | |
441 | mdate = re.search(redate, this_comment) | |
442 | if mdate: | |
443 | date = mdate.group(1) | |
444 | append = False | |
445 | else: | |
446 | mbrief = re.search(rebrief, this_comment) | |
447 | if mbrief: | |
448 | brief = mbrief.group(1) | |
449 | append = False | |
450 | ||
451 | if append: | |
452 | comment_lines.append( this_comment ) | |
453 | ||
454 | else: | |
455 | if start_line > 0: | |
456 | break | |
457 | ||
458 | # This line had a valid comment | |
459 | last_comm_line_num = line_num | |
460 | ||
461 | if class_name_doxy is None: | |
462 | ||
463 | # No \class specified: guess it from file name | |
464 | reclass = r'^(.*/)?(.*?)(\..*)?$' | |
465 | mclass = re.search( reclass, filename ) | |
466 | if mclass: | |
467 | class_name_doxy = mclass.group(2) | |
468 | else: | |
469 | assert False, 'Regexp unable to extract classname from file' | |
470 | ||
471 | # Macro or class? | |
472 | if is_macro: | |
473 | file_class_line = '\\file ' + class_name_doxy + '.C' | |
474 | else: | |
475 | file_class_line = '\\class ' + class_name_doxy | |
476 | ||
477 | if start_line > 0: | |
478 | ||
479 | prepend_to_comment = [] | |
480 | ||
481 | # Prepend \class or \file specifier, then the \brief, then an empty line | |
482 | prepend_to_comment.append( file_class_line ) | |
483 | ||
484 | if brief is not None: | |
485 | prepend_to_comment.append( '\\brief ' + brief ) | |
486 | prepend_to_comment.append( '' ) | |
487 | ||
488 | comment_lines = prepend_to_comment + comment_lines # join lists | |
489 | ||
490 | # Append author and date if they exist | |
491 | if author is not None: | |
492 | comment_lines.append( '\\author ' + author ) | |
493 | ||
494 | if date is not None: | |
495 | comment_lines.append( '\\date ' + date ) | |
496 | ||
497 | # We should erase the "dumb" comments, such as "<class_name> class" | |
498 | comm_idx = 0 | |
499 | regac = r'\s*%s\s+class\.?\s*' % class_name_doxy | |
500 | mgac = None | |
501 | for comm in comment_lines: | |
502 | mgac = re.search(regac, comm) | |
503 | if mgac: | |
504 | break | |
505 | comm_idx = comm_idx + 1 | |
506 | if mgac: | |
507 | logging.debug('Removing dumb comment line: {%s}' % Colt(comment_lines[comm_idx]).magenta()) | |
508 | del comment_lines[comm_idx] | |
509 | ||
510 | comment_lines = refactor_comment(comment_lines, do_strip_html=False, infilename=filename) | |
511 | ||
512 | # Now we look for a possible \brief | |
513 | if brief is None: | |
514 | comm_idx = 0 | |
515 | for comm in comment_lines: | |
516 | if comm.startswith('\\class') or comm.startswith('\\file') or comm == '': | |
517 | pass | |
518 | else: | |
519 | if len(comm) <= brief_len_threshold: | |
520 | brief = comm | |
521 | break | |
522 | comm_idx = comm_idx + 1 | |
523 | if brief is not None: | |
524 | comment_lines = refactor_comment( | |
525 | [ comment_lines[0], '\\brief ' + brief ] + comment_lines[1:comm_idx] + comment_lines[comm_idx+1:], | |
526 | do_strip_html=False, infilename=filename) | |
527 | ||
528 | logging.debug('Comment found for class %s' % Colt(class_name_doxy).magenta()) | |
529 | comments.append(Comment( | |
530 | comment_lines, | |
531 | start_line, 1, end_line, 1, | |
532 | 0, class_name_doxy | |
533 | )) | |
534 | ||
535 | else: | |
536 | ||
537 | logging.warning('No comment found for class %s: creating a dummy entry at the beginning' % \ | |
538 | Colt(class_name_doxy).magenta()) | |
539 | ||
540 | comments.append(PrependComment( | |
541 | [ file_class_line ], | |
542 | 1, 1, 1, 1, | |
543 | 0, class_name_doxy, append_empty=True | |
544 | )) | |
545 | ||
546 | ||
547 | ## Looks for a special ROOT ClassImp() entry. | |
548 | # | |
549 | # Doxygen might get confused by `ClassImp()` entries as they are macros normally written without | |
550 | # the ending `;`: this function wraps the definition inside a condition in order to make Doxygen | |
551 | # ignore it. | |
552 | # | |
553 | # @param filename Name of the current file | |
554 | # @param comments Array of comments: new ones will be appended there | |
555 | def comment_classimp(filename, comments): | |
556 | ||
557 | recomm = r'^\s*///?(\s*.*?)\s*/*\s*$' | |
558 | ||
559 | line_num = 0 | |
560 | reclassimp = r'^(\s*)Class(Imp|Def)\((.*?)\).*$' | |
561 | ||
562 | in_classimp_cond = False | |
563 | restartcond = r'^\s*///\s*\\cond\s+CLASSIMP\s*$' | |
564 | reendcond = r'^\s*///\s*\\endcond\s*$' | |
565 | ||
566 | with open(filename, 'r') as fp: | |
567 | ||
568 | # Array of tuples: classimp, startcond, endcond | |
569 | found_classimp = [] | |
570 | ||
571 | # Reset to nothing found | |
572 | line_classimp = -1 | |
573 | line_startcond = -1 | |
574 | line_endcond = -1 | |
575 | classimp_class = None | |
576 | classimp_indent = None | |
577 | ||
578 | for line in fp: | |
579 | ||
580 | line_num = line_num + 1 | |
581 | ||
582 | mclassimp = re.search(reclassimp, line) | |
583 | if mclassimp: | |
584 | ||
585 | # Dump previous one if appropriate, and reset | |
586 | if line_classimp != -1: | |
587 | found_classimp.append( (line_classimp, line_startcond, line_endcond) ) | |
588 | line_classimp = -1 | |
589 | line_startcond = -1 | |
590 | line_endcond = -1 | |
591 | ||
592 | # Adjust indent | |
593 | classimp_indent = len( mclassimp.group(1) ) | |
594 | ||
595 | line_classimp = line_num | |
596 | classimp_class = mclassimp.group(3) | |
597 | imp_or_def = mclassimp.group(2) | |
598 | logging.debug( | |
599 | 'Comment found for ' + | |
600 | Colt( 'Class%s(' % imp_or_def ).magenta() + | |
601 | Colt( classimp_class ).cyan() + | |
602 | Colt( ')' ).magenta() ) | |
603 | ||
604 | else: | |
605 | ||
606 | mstartcond = re.search(restartcond, line) | |
607 | if mstartcond: | |
608 | ||
609 | # Dump previous one if appropriate, and reset | |
610 | if line_classimp != -1: | |
611 | found_classimp.append( (line_classimp, line_startcond, line_endcond) ) | |
612 | line_classimp = -1 | |
613 | line_startcond = -1 | |
614 | line_endcond = -1 | |
615 | ||
616 | logging.debug('Found Doxygen opening condition for ClassImp') | |
617 | in_classimp_cond = True | |
618 | line_startcond = line_num | |
619 | ||
620 | elif in_classimp_cond: | |
621 | ||
622 | mendcond = re.search(reendcond, line) | |
623 | if mendcond: | |
624 | logging.debug('Found Doxygen closing condition for ClassImp') | |
625 | in_classimp_cond = False | |
626 | line_endcond = line_num | |
627 | ||
628 | # Dump previous one if appropriate, and reset (out of the loop) | |
629 | if line_classimp != -1: | |
630 | found_classimp.append( (line_classimp, line_startcond, line_endcond) ) | |
631 | line_classimp = -1 | |
632 | line_startcond = -1 | |
633 | line_endcond = -1 | |
634 | ||
635 | for line_classimp,line_startcond,line_endcond in found_classimp: | |
636 | ||
637 | # Loop over the ClassImp conditions we've found | |
638 | ||
639 | if line_startcond != -1: | |
640 | logging.debug('Looks like we are in a condition here %d,%d,%d' % (line_classimp, line_startcond, line_endcond)) | |
641 | comments.append(Comment( | |
642 | ['\cond CLASSIMP'], | |
643 | line_startcond, 1, line_startcond, 1, | |
644 | classimp_indent, 'ClassImp/Def(%s)' % classimp_class, | |
645 | append_empty=False | |
646 | )) | |
647 | else: | |
648 | logging.debug('Looks like we are NOT NOT in a condition here %d,%d,%d' % (line_classimp, line_startcond, line_endcond)) | |
649 | comments.append(PrependComment( | |
650 | ['\cond CLASSIMP'], | |
651 | line_classimp, 1, line_classimp, 1, | |
652 | classimp_indent, 'ClassImp/Def(%s)' % classimp_class | |
653 | )) | |
654 | ||
655 | if line_endcond != -1: | |
656 | comments.append(Comment( | |
657 | ['\endcond'], | |
658 | line_endcond, 1, line_endcond, 1, | |
659 | classimp_indent, 'ClassImp/Def(%s)' % classimp_class, | |
660 | append_empty=False | |
661 | )) | |
662 | else: | |
663 | comments.append(PrependComment( | |
664 | ['\endcond'], | |
665 | line_classimp+1, 1, line_classimp+1, 1, | |
666 | classimp_indent, 'ClassImp/Def(%s)' % classimp_class | |
667 | )) | |
668 | ||
669 | ||
670 | ## Traverse the AST recursively starting from the current cursor. | |
671 | # | |
672 | # @param cursor A Clang parser cursor | |
673 | # @param filename Name of the current file | |
674 | # @param comments Array of comments: new ones will be appended there | |
675 | # @param recursion Current recursion depth | |
676 | # @param in_func True if we are inside a function or method | |
677 | # @param classdesc_line_limit Do not look for comments after this line | |
678 | # | |
679 | # @return A tuple containing the classdesc_line_limit as first item | |
680 | def traverse_ast(cursor, filename, comments, recursion=0, in_func=False, classdesc_line_limit=None): | |
681 | ||
682 | # libclang traverses included files as well: we do not want this behavior | |
683 | if cursor.location.file is not None and str(cursor.location.file) != filename: | |
684 | logging.debug("Skipping processing of included %s" % cursor.location.file) | |
685 | return | |
686 | ||
687 | text = cursor.spelling or cursor.displayname | |
688 | kind = str(cursor.kind)[str(cursor.kind).index('.')+1:] | |
689 | ||
690 | is_macro = filename.endswith('.C') | |
691 | ||
692 | indent = '' | |
693 | for i in range(0, recursion): | |
694 | indent = indent + ' ' | |
695 | ||
696 | if cursor.kind in [ clang.cindex.CursorKind.CXX_METHOD, clang.cindex.CursorKind.CONSTRUCTOR, | |
697 | clang.cindex.CursorKind.DESTRUCTOR, clang.cindex.CursorKind.FUNCTION_DECL ]: | |
698 | ||
699 | if classdesc_line_limit is None: | |
700 | classdesc_line_limit = cursor.location.line | |
701 | ||
702 | # cursor ran into a C++ method | |
703 | logging.debug( "%5d %s%s(%s)" % (cursor.location.line, indent, Colt(kind).magenta(), Colt(text).blue()) ) | |
704 | comment_method(cursor, comments) | |
705 | in_func = True | |
706 | ||
707 | elif not is_macro and not in_func and \ | |
708 | cursor.kind in [ clang.cindex.CursorKind.FIELD_DECL, clang.cindex.CursorKind.VAR_DECL ]: | |
709 | ||
710 | if classdesc_line_limit is None: | |
711 | classdesc_line_limit = cursor.location.line | |
712 | ||
713 | # cursor ran into a data member declaration | |
714 | logging.debug( "%5d %s%s(%s)" % (cursor.location.line, indent, Colt(kind).magenta(), Colt(text).blue()) ) | |
715 | comment_datamember(cursor, comments) | |
716 | ||
717 | else: | |
718 | ||
719 | logging.debug( "%5d %s%s(%s)" % (cursor.location.line, indent, kind, text) ) | |
720 | ||
721 | for child_cursor in cursor.get_children(): | |
722 | classdesc_line_limit = traverse_ast(child_cursor, filename, comments, recursion+1, in_func, classdesc_line_limit) | |
723 | ||
724 | if recursion == 0: | |
725 | comment_classimp(filename, comments) | |
726 | comment_classdesc(filename, comments, classdesc_line_limit) | |
727 | ||
728 | return classdesc_line_limit | |
729 | ||
730 | ||
731 | ## Strip some HTML tags from the given string. Returns clean string. | |
732 | # | |
733 | # @param s Input string | |
734 | def strip_html(s): | |
735 | rehtml = r'(?i)</?(P|BR)/?>' | |
736 | return re.sub(rehtml, '', s) | |
737 | ||
738 | ||
739 | ## Remove garbage from comments and convert special tags from THtml to Doxygen. | |
740 | # | |
741 | # @param comment An array containing the lines of the original comment | |
742 | def refactor_comment(comment, do_strip_html=True, infilename=None): | |
743 | ||
744 | recomm = r'^(/{2,}|/\*)? ?(\s*)(.*?)\s*((/{2,})?\s*|\*/)$' | |
745 | regarbage = r'^(?i)\s*([\s*=_#-]+|(Begin|End)_Html)\s*$' | |
746 | ||
747 | # Support for LaTeX blocks spanning on multiple lines | |
748 | relatex = r'(?i)^((.*?)\s+)?(BEGIN|END)_LATEX([.,;:\s]+.*)?$' | |
749 | in_latex = False | |
750 | latex_block = False | |
751 | ||
752 | # Support for LaTeX blocks on a single line | |
753 | reinline_latex = r'(?i)(.*)BEGIN_LATEX\s+(.*?)\s+END_LATEX(.*)$' | |
754 | ||
755 | # Match <pre> (to turn it into the ~~~ Markdown syntax) | |
756 | reblock = r'(?i)^(\s*)</?PRE>\s*$' | |
757 | ||
758 | # Macro blocks for pictures generation | |
759 | in_macro = False | |
760 | current_macro = [] | |
761 | remacro = r'(?i)^\s*(BEGIN|END)_MACRO(\((.*?)\))?\s*$' | |
762 | ||
763 | # Minimum indent level: scale back everything | |
764 | lowest_indent_level = None | |
765 | ||
766 | # Indentation threshold: if too much indented, don't indent at all | |
767 | indent_level_threshold = 7 | |
768 | ||
769 | new_comment = [] | |
770 | insert_blank = False | |
771 | wait_first_non_blank = True | |
772 | for line_comment in comment: | |
773 | ||
774 | # Strip some HTML tags | |
775 | if do_strip_html: | |
776 | line_comment = strip_html(line_comment) | |
777 | ||
778 | mcomm = re.search( recomm, line_comment ) | |
779 | if mcomm: | |
780 | new_line_comment = mcomm.group(2) + mcomm.group(3) # indent + comm | |
781 | ||
782 | # Check if we are in a macro block | |
783 | mmacro = re.search(remacro, new_line_comment) | |
784 | if mmacro: | |
785 | if in_macro: | |
786 | in_macro = False | |
787 | ||
788 | # Dump macro | |
789 | outimg = write_macro(infilename, current_macro) + '.png' | |
790 | current_macro = [] | |
791 | ||
792 | # Insert image | |
793 | new_comment.append( '![Picture from ROOT macro](%s)' % (os.path.basename(outimg)) ) | |
794 | ||
795 | logging.debug( 'Found macro for generating image %s' % Colt(outimg).magenta() ) | |
796 | ||
797 | else: | |
798 | in_macro = True | |
799 | ||
800 | continue | |
801 | elif in_macro: | |
802 | current_macro.append( new_line_comment ) | |
803 | continue | |
804 | ||
805 | mgarbage = re.search( regarbage, new_line_comment ) | |
806 | ||
807 | if mgarbage is None and not mcomm.group(3).startswith('\\') and mcomm.group(3) != '': | |
808 | # not a special command line: count indent | |
809 | indent_level = len( mcomm.group(2) ) | |
810 | if lowest_indent_level is None or indent_level < lowest_indent_level: | |
811 | lowest_indent_level = indent_level | |
812 | ||
813 | # if indentation level is too much, consider it zero | |
814 | if indent_level > indent_level_threshold: | |
815 | new_line_comment = mcomm.group(3) # remove ALL indentation | |
816 | ||
817 | if new_line_comment == '' or mgarbage is not None: | |
818 | insert_blank = True | |
819 | else: | |
820 | if insert_blank and not wait_first_non_blank: | |
821 | new_comment.append('') | |
822 | insert_blank = False | |
823 | wait_first_non_blank = False | |
824 | ||
825 | # Postprocessing: LaTeX formulas in ROOT format | |
826 | # Marked by BEGIN_LATEX ... END_LATEX and they use # in place of \ | |
827 | # There can be several ROOT LaTeX forumlas per line | |
828 | while True: | |
829 | minline_latex = re.search( reinline_latex, new_line_comment ) | |
830 | if minline_latex: | |
831 | new_line_comment = '%s\\f$%s\\f$%s' % \ | |
832 | ( minline_latex.group(1), minline_latex.group(2).replace('#', '\\'), | |
833 | minline_latex.group(3) ) | |
834 | else: | |
835 | break | |
836 | ||
837 | # ROOT LaTeX: do we have a Begin/End_LaTeX block? | |
838 | # Note: the presence of LaTeX "closures" does not exclude the possibility to have a begin | |
839 | # block here left without a corresponding ending block | |
840 | mlatex = re.search( relatex, new_line_comment ) | |
841 | if mlatex: | |
842 | ||
843 | # before and after parts have been already stripped | |
844 | l_before = mlatex.group(2) | |
845 | l_after = mlatex.group(4) | |
846 | is_begin = mlatex.group(3).upper() == 'BEGIN' # if not, END | |
847 | ||
848 | if l_before is None: | |
849 | l_before = '' | |
850 | if l_after is None: | |
851 | l_after = '' | |
852 | ||
853 | if is_begin: | |
854 | ||
855 | # Begin of LaTeX part | |
856 | ||
857 | in_latex = True | |
858 | if l_before == '' and l_after == '': | |
859 | ||
860 | # Opening tag alone: mark the beginning of a block: \f[ ... \f] | |
861 | latex_block = True | |
862 | new_comment.append( '\\f[' ) | |
863 | ||
864 | else: | |
865 | # Mark the beginning of inline: \f$ ... \f$ | |
866 | latex_block = False | |
867 | new_comment.append( | |
868 | '%s \\f$%s' % ( l_before, l_after.replace('#', '\\') ) | |
869 | ) | |
870 | ||
871 | else: | |
872 | ||
873 | # End of LaTeX part | |
874 | in_latex = False | |
875 | ||
876 | if latex_block: | |
877 | ||
878 | # Closing a LaTeX block | |
879 | if l_before != '': | |
880 | new_comment.append( l_before.replace('#', '\\') ) | |
881 | new_comment.append( '\\f]' ) | |
882 | if l_after != '': | |
883 | new_comment.append( l_after ) | |
884 | ||
885 | else: | |
886 | ||
887 | # Closing a LaTeX inline | |
888 | new_comment.append( | |
889 | '%s\\f$%s' % ( l_before.replace('#', '\\'), l_after ) | |
890 | ) | |
891 | ||
892 | # Prevent appending lines (we have already done that) | |
893 | new_line_comment = None | |
894 | ||
895 | # If we are not in a LaTeX block, look for <pre> tags and transform them into Doxygen code | |
896 | # blocks (using ~~~ ... ~~~). Only <pre> tags on a single line are supported | |
897 | if new_line_comment is not None and not in_latex: | |
898 | ||
899 | mblock = re.search( reblock, new_line_comment ) | |
900 | if mblock: | |
901 | new_comment.append( mblock.group(1)+'~~~' ) | |
902 | new_line_comment = None | |
903 | ||
904 | if new_line_comment is not None: | |
905 | if in_latex: | |
906 | new_line_comment = new_line_comment.replace('#', '\\') | |
907 | new_comment.append( new_line_comment ) | |
908 | ||
909 | else: | |
910 | assert False, 'Comment regexp does not match' | |
911 | ||
912 | # Fixing indentation level | |
913 | if lowest_indent_level is not None: | |
914 | logging.debug('Lowest indentation level found: %d' % lowest_indent_level) | |
915 | ||
916 | new_comment_indent = [] | |
917 | reblankstart = r'^\s+' | |
918 | for line in new_comment: | |
919 | if re.search(reblankstart, line): | |
920 | new_comment_indent.append( line[lowest_indent_level:] ) | |
921 | else: | |
922 | new_comment_indent.append( line ) | |
923 | ||
924 | new_comment = new_comment_indent | |
925 | ||
926 | else: | |
927 | logging.debug('No indentation scaling applied') | |
928 | ||
929 | return new_comment | |
930 | ||
931 | ||
932 | ## Dumps an image-generating macro to the correct place. Returns a string with the image path, | |
933 | # without the extension. | |
934 | # | |
935 | # @param infilename File name of the source file | |
936 | # @param macro_lines Array of macro lines | |
937 | def write_macro(infilename, macro_lines): | |
938 | ||
939 | # Calculate hash | |
940 | digh = hashlib.sha1() | |
941 | for l in macro_lines: | |
942 | digh.update(l) | |
943 | digh.update('\n') | |
944 | short_digest = digh.hexdigest()[0:7] | |
945 | ||
946 | infiledir = os.path.dirname(infilename) | |
947 | if infiledir == '': | |
948 | infiledir = '.' | |
949 | outdir = '%s/imgdoc' % infiledir | |
950 | outprefix = '%s/%s_%s' % ( | |
951 | outdir, | |
952 | os.path.basename(infilename).replace('.', '_'), | |
953 | short_digest | |
954 | ) | |
955 | outmacro = '%s.C' % outprefix | |
956 | ||
957 | # Make directory | |
958 | if not os.path.isdir(outdir): | |
959 | # do not catch: let everything die on error | |
960 | logging.debug('Creating directory %s' % Colt(outdir).magenta()) | |
961 | os.mkdir(outdir) | |
962 | ||
963 | # Create file (do not catch errors either) | |
964 | with open(outmacro, 'w') as omfp: | |
965 | logging.debug('Writing macro %s' % Colt(outmacro).magenta()) | |
966 | for l in macro_lines: | |
967 | omfp.write(l) | |
968 | omfp.write('\n') | |
969 | ||
970 | return outprefix | |
971 | ||
972 | ||
973 | ## Rewrites all comments from the given file handler. | |
974 | # | |
975 | # @param fhin The file handler to read from | |
976 | # @param fhout The file handler to write to | |
977 | # @param comments Array of comments | |
978 | def rewrite_comments(fhin, fhout, comments): | |
979 | ||
980 | line_num = 0 | |
981 | in_comment = False | |
982 | skip_empty = False | |
983 | comm = None | |
984 | prev_comm = None | |
985 | restore_lines = None | |
986 | ||
987 | rindent = r'^(\s*)' | |
988 | ||
989 | def dump_comment_block(cmt, restore=None): | |
990 | text_indent = '' | |
991 | ask_skip_empty = False | |
992 | ||
993 | for i in range(0, cmt.indent): | |
994 | text_indent = text_indent + ' ' | |
995 | ||
996 | for lc in cmt.lines: | |
997 | fhout.write('%s///' % text_indent ) | |
998 | lc = lc.rstrip() | |
999 | if len(lc) != 0: | |
1000 | fhout.write(' ') | |
1001 | fhout.write(lc) | |
1002 | fhout.write('\n') | |
1003 | ||
1004 | # Empty new line at the end of the comment | |
1005 | if cmt.append_empty: | |
1006 | fhout.write('\n') | |
1007 | ask_skip_empty = True | |
1008 | ||
1009 | # Restore lines if possible | |
1010 | if restore: | |
1011 | for lr in restore: | |
1012 | fhout.write(lr) | |
1013 | fhout.write('\n') | |
1014 | ||
1015 | # Tell the caller whether it should skip the next empty line found | |
1016 | return ask_skip_empty | |
1017 | ||
1018 | ||
1019 | for line in fhin: | |
1020 | ||
1021 | line_num = line_num + 1 | |
1022 | ||
1023 | # Find current comment | |
1024 | prev_comm = comm | |
1025 | comm = None | |
1026 | comm_list = [] | |
1027 | for c in comments: | |
1028 | if c.has_comment(line_num): | |
1029 | comm = c | |
1030 | comm_list.append(c) | |
1031 | ||
1032 | if len(comm_list) > 1: | |
1033 | ||
1034 | merged = True | |
1035 | ||
1036 | if len(comm_list) == 2: | |
1037 | c1,c2 = comm_list | |
1038 | if isinstance(c1, Comment) and isinstance(c2, Comment): | |
1039 | c1.lines = c1.lines + c2.lines # list merge | |
1040 | comm = c1 | |
1041 | logging.debug('Two adjacent comments merged. Result: {%s}' % Colt(comm).cyan()) | |
1042 | else: | |
1043 | merged = False | |
1044 | else: | |
1045 | merged = False | |
1046 | ||
1047 | if merged == False: | |
1048 | logging.warning('Too many unmergeable comments on the same line (%d), picking the last one' % len(comm_list)) | |
1049 | for c in comm_list: | |
1050 | logging.warning('>> %s' % c) | |
1051 | comm = c # considering the last one | |
1052 | ||
1053 | if comm: | |
1054 | ||
1055 | # First thing to check: are we in the same comment as before? | |
1056 | if comm is not prev_comm and \ | |
1057 | isinstance(comm, Comment) and \ | |
1058 | isinstance(prev_comm, Comment) and \ | |
1059 | not isinstance(prev_comm, RemoveComment): | |
1060 | ||
1061 | # We are NOT in the same comment as before, and this comment is dumpable | |
1062 | ||
1063 | skip_empty = dump_comment_block(prev_comm, restore_lines) | |
1064 | in_comment = False | |
1065 | restore_lines = None | |
1066 | prev_comm = None # we have just dumped it: pretend it never existed in this loop | |
1067 | ||
1068 | # | |
1069 | # Check type of comment and react accordingly | |
1070 | # | |
1071 | ||
1072 | if isinstance(comm, MemberComment): | |
1073 | ||
1074 | # end comment block | |
1075 | if in_comment: | |
1076 | skip_empty = dump_comment_block(prev_comm, restore_lines) | |
1077 | in_comment = False | |
1078 | restore_lines = None | |
1079 | ||
1080 | non_comment = line[ 0:comm.first_col-1 ] | |
1081 | ||
1082 | if comm.array_size is not None or comm.is_dontsplit() or comm.is_ptr(): | |
1083 | ||
1084 | # This is a special case: comment will be split in two lines: one before the comment for | |
1085 | # Doxygen as "member description", and the other right after the comment on the same line | |
1086 | # to be parsed by ROOT's C++ parser | |
1087 | ||
1088 | # Keep indent on the generated line of comment before member definition | |
1089 | mindent = re.search(rindent, line) | |
1090 | ||
1091 | # Get correct comment flag, if any | |
1092 | if comm.comment_flag is not None: | |
1093 | cflag = comm.comment_flag | |
1094 | else: | |
1095 | cflag = '' | |
1096 | ||
1097 | # Get correct array size, if any | |
1098 | if comm.array_size is not None: | |
1099 | asize = '[%s]' % comm.array_size | |
1100 | else: | |
1101 | asize = '' | |
1102 | ||
1103 | # Write on two lines | |
1104 | fhout.write('%s/// %s\n%s//%s%s\n' % ( | |
1105 | mindent.group(1), | |
1106 | comm.lines[0], | |
1107 | non_comment, | |
1108 | cflag, | |
1109 | asize | |
1110 | )) | |
1111 | ||
1112 | else: | |
1113 | ||
1114 | # Single-line comments with the "transient" flag can be kept on one line in a way that | |
1115 | # they are correctly interpreted by both ROOT and Doxygen | |
1116 | ||
1117 | if comm.is_transient(): | |
1118 | tt = '!' | |
1119 | else: | |
1120 | tt = '/' | |
1121 | ||
1122 | fhout.write('%s//%s< %s\n' % ( | |
1123 | non_comment, | |
1124 | tt, | |
1125 | comm.lines[0] | |
1126 | )) | |
1127 | ||
1128 | elif isinstance(comm, RemoveComment): | |
1129 | # End comment block and skip this line | |
1130 | if in_comment: | |
1131 | skip_empty = dump_comment_block(prev_comm, restore_lines) | |
1132 | in_comment = False | |
1133 | restore_lines = None | |
1134 | ||
1135 | elif restore_lines is None: | |
1136 | ||
1137 | # Beginning of a new comment block of type Comment or PrependComment | |
1138 | in_comment = True | |
1139 | ||
1140 | if isinstance(comm, PrependComment): | |
1141 | # Prepare array of lines to dump right after the comment | |
1142 | restore_lines = [ line.rstrip('\n') ] | |
1143 | logging.debug('Commencing lines to restore: {%s}' % Colt(restore_lines[0]).cyan()) | |
1144 | else: | |
1145 | # Extract the non-comment part and print it if it exists. If this is the first line of a | |
1146 | # comment, it might happen something like `valid_code; // this is a comment`. | |
1147 | if comm.first_line == line_num: | |
1148 | non_comment = line[ 0:comm.first_col-1 ].rstrip() | |
1149 | if non_comment != '': | |
1150 | fhout.write( non_comment + '\n' ) | |
1151 | ||
1152 | elif isinstance(comm, Comment): | |
1153 | ||
1154 | if restore_lines is not None: | |
1155 | # From the 2nd line on of comment to prepend | |
1156 | restore_lines.append( line.rstrip('\n') ) | |
1157 | logging.debug('Appending lines to restore. All lines: {%s}' % Colt(restore_lines).cyan()) | |
1158 | ||
1159 | else: | |
1160 | assert False, 'Unhandled parser state: line=%d comm={%s} prev_comm={%s}' % \ | |
1161 | (line_num, comm, prev_comm) | |
1162 | ||
1163 | else: | |
1164 | ||
1165 | # Not a comment line | |
1166 | ||
1167 | if in_comment: | |
1168 | ||
1169 | # We have just exited a comment block of type Comment | |
1170 | skip_empty = dump_comment_block(prev_comm, restore_lines) | |
1171 | in_comment = False | |
1172 | restore_lines = None | |
1173 | ||
1174 | # Dump the non-comment line | |
1175 | line_out = line.rstrip('\n') | |
1176 | if skip_empty: | |
1177 | skip_empty = False | |
1178 | if line_out.strip() != '': | |
1179 | fhout.write( line_out + '\n' ) | |
1180 | else: | |
1181 | fhout.write( line_out + '\n' ) | |
1182 | ||
1183 | # Is there some comment left here? | |
1184 | if restore_lines is not None: | |
1185 | dump_comment_block(comm, restore_lines) | |
1186 | ||
1187 | # Is there some other comment beyond the last line? | |
1188 | for c in comments: | |
1189 | if c.has_comment(line_num+1): | |
1190 | dump_comment_block(c, None) | |
1191 | break | |
1192 | ||
1193 | ||
1194 | ## The main function. | |
1195 | # | |
1196 | # Return value is the executable's return value. | |
1197 | def main(argv): | |
1198 | ||
1199 | # Setup logging on stderr | |
1200 | log_level = logging.INFO | |
1201 | logging.basicConfig( | |
1202 | level=log_level, | |
1203 | format='%(levelname)-8s %(funcName)-20s %(message)s', | |
1204 | stream=sys.stderr | |
1205 | ) | |
1206 | ||
1207 | # Parse command-line options | |
1208 | output_on_stdout = False | |
1209 | include_flags = [] | |
1210 | try: | |
1211 | opts, args = getopt.getopt( argv, 'odI:', [ 'debug=', 'stdout' ] ) | |
1212 | for o, a in opts: | |
1213 | if o == '--debug': | |
1214 | log_level = getattr( logging, a.upper(), None ) | |
1215 | if not isinstance(log_level, int): | |
1216 | raise getopt.GetoptError('log level must be one of: DEBUG, INFO, WARNING, ERROR, CRITICAL') | |
1217 | elif o == '-d': | |
1218 | log_level = logging.DEBUG | |
1219 | elif o == '-o' or o == '--stdout': | |
1220 | output_on_stdout = True | |
1221 | elif o == '-I': | |
1222 | if os.path.isdir(a): | |
1223 | include_flags.extend( [ '-I', a ] ) | |
1224 | else: | |
1225 | logging.fatal('Include directory not found: %s' % Colt(a).magenta()) | |
1226 | return 2 | |
1227 | else: | |
1228 | assert False, 'Unhandled argument' | |
1229 | except getopt.GetoptError as e: | |
1230 | logging.fatal('Invalid arguments: %s' % e) | |
1231 | return 1 | |
1232 | ||
1233 | logging.getLogger('').setLevel(log_level) | |
1234 | ||
1235 | # Attempt to load libclang from a list of known locations | |
1236 | libclang_locations = [ | |
1237 | '/usr/lib/llvm-3.5/lib/libclang.so.1', | |
1238 | '/usr/lib/libclang.so', | |
1239 | '/Library/Developer/CommandLineTools/usr/lib/libclang.dylib' | |
1240 | ] | |
1241 | libclang_found = False | |
1242 | ||
1243 | for lib in libclang_locations: | |
1244 | if os.path.isfile(lib): | |
1245 | clang.cindex.Config.set_library_file(lib) | |
1246 | libclang_found = True | |
1247 | break | |
1248 | ||
1249 | if not libclang_found: | |
1250 | logging.fatal('Cannot find libclang') | |
1251 | return 1 | |
1252 | ||
1253 | # Loop over all files | |
1254 | for fn in args: | |
1255 | ||
1256 | logging.info('Input file: %s' % Colt(fn).magenta()) | |
1257 | index = clang.cindex.Index.create() | |
1258 | clang_args = [ '-x', 'c++' ] | |
1259 | clang_args.extend( include_flags ) | |
1260 | translation_unit = index.parse(fn, args=clang_args) | |
1261 | ||
1262 | comments = [] | |
1263 | traverse_ast( translation_unit.cursor, fn, comments ) | |
1264 | for c in comments: | |
1265 | ||
1266 | logging.debug("Comment found for entity %s:" % Colt(c.func).magenta()) | |
1267 | ||
1268 | if isinstance(c, MemberComment): | |
1269 | ||
1270 | if c.is_transient(): | |
1271 | flag_text = Colt('transient ').yellow() | |
1272 | elif c.is_dontsplit(): | |
1273 | flag_text = Colt('dontsplit ').yellow() | |
1274 | elif c.is_ptr(): | |
1275 | flag_text = Colt('ptr ').yellow() | |
1276 | else: | |
1277 | flag_text = '' | |
1278 | ||
1279 | if c.array_size is not None: | |
1280 | array_text = Colt('arraysize=%s ' % c.array_size).yellow() | |
1281 | else: | |
1282 | array_text = '' | |
1283 | ||
1284 | logging.debug( | |
1285 | "%s %s%s{%s}" % ( \ | |
1286 | Colt("[%d,%d]" % (c.first_line, c.first_col)).green(), | |
1287 | flag_text, | |
1288 | array_text, | |
1289 | Colt(c.lines[0]).cyan() | |
1290 | )) | |
1291 | ||
1292 | elif isinstance(c, RemoveComment): | |
1293 | ||
1294 | logging.debug( Colt('[%d,%d]' % (c.first_line, c.last_line)).green() ) | |
1295 | ||
1296 | else: | |
1297 | for l in c.lines: | |
1298 | logging.debug( | |
1299 | Colt("[%d,%d:%d,%d] " % (c.first_line, c.first_col, c.last_line, c.last_col)).green() + | |
1300 | "{%s}" % Colt(l).cyan() | |
1301 | ) | |
1302 | ||
1303 | try: | |
1304 | ||
1305 | if output_on_stdout: | |
1306 | with open(fn, 'r') as fhin: | |
1307 | rewrite_comments( fhin, sys.stdout, comments ) | |
1308 | else: | |
1309 | fn_back = fn + '.thtml2doxy_backup' | |
1310 | os.rename( fn, fn_back ) | |
1311 | ||
1312 | with open(fn_back, 'r') as fhin, open(fn, 'w') as fhout: | |
1313 | rewrite_comments( fhin, fhout, comments ) | |
1314 | ||
1315 | os.remove( fn_back ) | |
1316 | logging.info("File %s converted to Doxygen: check differences before committing!" % Colt(fn).magenta()) | |
1317 | except (IOError,OSError) as e: | |
1318 | logging.error('File operation failed: %s' % e) | |
1319 | ||
1320 | return 0 | |
1321 | ||
1322 | ||
1323 | if __name__ == '__main__': | |
1324 | sys.exit( main( sys.argv[1:] ) ) |