]>
Commit | Line | Data |
---|---|---|
f329fa92 | 1 | #!/usr/bin/env python |
2 | ||
06ccae0f | 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 | ||
f329fa92 | 35 | import sys |
36 | import os | |
37 | import re | |
06ccae0f | 38 | import logging |
39 | import getopt | |
40 | import clang.cindex | |
41 | ||
42 | ||
43 | ## Brain-dead color output for terminal. | |
44 | class Colt(str): | |
45 | ||
46 | def red(self): | |
47 | return self.color('\033[31m') | |
48 | ||
49 | def green(self): | |
50 | return self.color('\033[32m') | |
51 | ||
52 | def yellow(self): | |
53 | return self.color('\033[33m') | |
54 | ||
55 | def blue(self): | |
56 | return self.color('\033[34m') | |
57 | ||
58 | def magenta(self): | |
59 | return self.color('\033[35m') | |
60 | ||
61 | def cyan(self): | |
62 | return self.color('\033[36m') | |
63 | ||
64 | def color(self, c): | |
65 | return c + self + '\033[m' | |
66 | ||
f329fa92 | 67 | |
06ccae0f | 68 | ## Comment. |
69 | class Comment: | |
70 | ||
71 | def __init__(self, lines, first_line, first_col, last_line, last_col, indent, func): | |
3896b0ea | 72 | assert first_line > 0 and last_line >= first_line, 'Wrong line numbers' |
06ccae0f | 73 | self.lines = lines |
74 | self.first_line = first_line | |
75 | self.first_col = first_col | |
76 | self.last_line = last_line | |
77 | self.last_col = last_col | |
78 | self.indent = indent | |
79 | self.func = func | |
80 | ||
81 | def has_comment(self, line): | |
82 | return line >= self.first_line and line <= self.last_line | |
83 | ||
84 | def __str__(self): | |
85 | return "<Comment for %s: [%d,%d:%d,%d] %s>" % (self.func, self.first_line, self.first_col, self.last_line, self.last_col, self.lines) | |
86 | ||
87 | ||
88 | ## A data member comment. | |
89 | class MemberComment: | |
90 | ||
91 | def __init__(self, text, is_transient, array_size, first_line, first_col, func): | |
3896b0ea | 92 | assert first_line > 0, 'Wrong line number' |
06ccae0f | 93 | self.lines = [ text ] |
94 | self.is_transient = is_transient | |
95 | self.array_size = array_size | |
96 | self.first_line = first_line | |
97 | self.first_col = first_col | |
98 | self.func = func | |
99 | ||
100 | def has_comment(self, line): | |
101 | return line == self.first_line | |
102 | ||
103 | def __str__(self): | |
104 | ||
105 | if self.is_transient: | |
106 | tt = '!transient! ' | |
107 | else: | |
108 | tt = '' | |
109 | ||
110 | if self.array_size is not None: | |
111 | ars = '[%s] ' % self.array_size | |
112 | else: | |
113 | ars = '' | |
114 | ||
115 | return "<MemberComment for %s: [%d,%d] %s%s%s>" % (self.func, self.first_line, self.first_col, tt, ars, self.lines[0]) | |
116 | ||
117 | ||
118 | ## A dummy comment that removes comment lines. | |
119 | class RemoveComment(Comment): | |
120 | ||
121 | def __init__(self, first_line, last_line): | |
3896b0ea | 122 | assert first_line > 0 and last_line >= first_line, 'Wrong line numbers' |
06ccae0f | 123 | self.first_line = first_line |
124 | self.last_line = last_line | |
125 | self.func = '<remove>' | |
126 | ||
127 | def __str__(self): | |
128 | return "<RemoveComment: [%d,%d]>" % (self.first_line, self.last_line) | |
129 | ||
130 | ||
131 | ## Parses method comments. | |
f329fa92 | 132 | # |
06ccae0f | 133 | # @param cursor Current libclang parser cursor |
134 | # @param comments Array of comments: new ones will be appended there | |
135 | def comment_method(cursor, comments): | |
136 | ||
137 | # we are looking for the following structure: method -> compound statement -> comment, i.e. we | |
138 | # need to extract the first comment in the compound statement composing the method | |
139 | ||
140 | in_compound_stmt = False | |
141 | expect_comment = False | |
142 | emit_comment = False | |
143 | ||
144 | comment = [] | |
145 | comment_function = cursor.spelling or cursor.displayname | |
146 | comment_line_start = -1 | |
147 | comment_line_end = -1 | |
148 | comment_col_start = -1 | |
149 | comment_col_end = -1 | |
150 | comment_indent = -1 | |
151 | ||
152 | for token in cursor.get_tokens(): | |
153 | ||
154 | if token.cursor.kind == clang.cindex.CursorKind.COMPOUND_STMT: | |
155 | if not in_compound_stmt: | |
156 | in_compound_stmt = True | |
157 | expect_comment = True | |
158 | comment_line_end = -1 | |
159 | else: | |
160 | if in_compound_stmt: | |
161 | in_compound_stmt = False | |
162 | emit_comment = True | |
163 | ||
164 | # tkind = str(token.kind)[str(token.kind).index('.')+1:] | |
165 | # ckind = str(token.cursor.kind)[str(token.cursor.kind).index('.')+1:] | |
166 | ||
167 | if in_compound_stmt: | |
168 | ||
169 | if expect_comment: | |
170 | ||
171 | extent = token.extent | |
172 | line_start = extent.start.line | |
173 | line_end = extent.end.line | |
174 | ||
175 | if token.kind == clang.cindex.TokenKind.PUNCTUATION and token.spelling == '{': | |
176 | pass | |
177 | ||
178 | 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)): | |
179 | comment_line_end = line_end | |
180 | comment_col_end = extent.end.column | |
181 | ||
182 | if comment_indent == -1 or (extent.start.column-1) < comment_indent: | |
183 | comment_indent = extent.start.column-1 | |
184 | ||
185 | if comment_line_start == -1: | |
186 | comment_line_start = line_start | |
187 | comment_col_start = extent.start.column | |
188 | comment.extend( token.spelling.split('\n') ) | |
189 | ||
190 | # multiline comments are parsed in one go, therefore don't expect subsequent comments | |
191 | if line_end - line_start > 0: | |
192 | emit_comment = True | |
193 | expect_comment = False | |
194 | ||
195 | else: | |
196 | emit_comment = True | |
197 | expect_comment = False | |
198 | ||
199 | if emit_comment: | |
200 | ||
6f0e3bf3 | 201 | if comment_line_start > 0: |
06ccae0f | 202 | |
6f0e3bf3 | 203 | comment = refactor_comment( comment ) |
204 | ||
205 | if len(comment) > 0: | |
206 | logging.debug("Comment found for function %s" % Colt(comment_function).magenta()) | |
207 | comments.append( Comment(comment, comment_line_start, comment_col_start, comment_line_end, comment_col_end, comment_indent, comment_function) ) | |
208 | else: | |
209 | logging.debug('Empty comment for function %s marked for removal' % Colt(comment_function).magenta()) | |
210 | comments.append(RemoveComment(comment_line_start, comment_line_end)) | |
211 | ||
212 | else: | |
213 | logging.warning('No comment found for function %s' % Colt(comment_function).magenta()) | |
06ccae0f | 214 | |
215 | comment = [] | |
216 | comment_line_start = -1 | |
217 | comment_line_end = -1 | |
218 | comment_col_start = -1 | |
219 | comment_col_end = -1 | |
220 | comment_indent = -1 | |
221 | ||
222 | emit_comment = False | |
223 | break | |
224 | ||
225 | ||
226 | ## Parses comments to class data members. | |
227 | # | |
228 | # @param cursor Current libclang parser cursor | |
229 | # @param comments Array of comments: new ones will be appended there | |
230 | def comment_datamember(cursor, comments): | |
231 | ||
232 | # Note: libclang 3.5 seems to have problems parsing a certain type of FIELD_DECL, so we revert | |
233 | # to a partial manual parsing. When parsing fails, the cursor's "extent" is not set properly, | |
234 | # returning a line range 0-0. We therefore make the not-so-absurd assumption that the datamember | |
235 | # definition is fully on one line, and we take the line number from cursor.location. | |
236 | ||
237 | line_num = cursor.location.line | |
238 | raw = None | |
239 | prev = None | |
240 | found = False | |
241 | ||
242 | # Huge overkill | |
243 | with open(str(cursor.location.file)) as fp: | |
244 | cur_line = 0 | |
245 | for raw in fp: | |
246 | cur_line = cur_line + 1 | |
247 | if cur_line == line_num: | |
248 | found = True | |
249 | break | |
250 | prev = raw | |
251 | ||
252 | assert found, 'A line that should exist was not found in file' % cursor.location.file | |
253 | ||
254 | recomm = r'(//(!)|///?)(\[(.*?)\])?<?\s*(.*?)\s*$' | |
255 | recomm_doxyary = r'^\s*///\s*(.*?)\s*$' | |
256 | ||
257 | mcomm = re.search(recomm, raw) | |
258 | if mcomm: | |
54203c62 | 259 | # If it does not match, we do not have a comment |
06ccae0f | 260 | member_name = cursor.spelling; |
261 | is_transient = mcomm.group(2) is not None | |
262 | array_size = mcomm.group(4) | |
263 | text = mcomm.group(5) | |
264 | ||
265 | col_num = mcomm.start()+1; | |
266 | ||
267 | if array_size is not None and prev is not None: | |
268 | # ROOT arrays with comments already converted to Doxygen have the member description on the | |
269 | # previous line | |
270 | mcomm_doxyary = re.search(recomm_doxyary, prev) | |
271 | if mcomm_doxyary: | |
272 | text = mcomm_doxyary.group(1) | |
273 | comments.append(RemoveComment(line_num-1, line_num-1)) | |
274 | ||
275 | logging.debug('Comment found for member %s' % Colt(member_name).magenta()) | |
276 | ||
277 | comments.append( MemberComment( | |
278 | text, | |
279 | is_transient, | |
280 | array_size, | |
281 | line_num, | |
282 | col_num, | |
283 | member_name )) | |
284 | ||
06ccae0f | 285 | |
286 | ## Parses class description (beginning of file). | |
287 | # | |
288 | # The clang parser does not work in this case so we do it manually, but it is very simple: we keep | |
289 | # the first consecutive sequence of single-line comments (//) we find - provided that it occurs | |
290 | # before any other comment found so far in the file (the comments array is inspected to ensure | |
291 | # this). | |
f329fa92 | 292 | # |
06ccae0f | 293 | # Multi-line comments (/* ... */) are not considered as they are commonly used to display |
294 | # copyright notice. | |
f329fa92 | 295 | # |
06ccae0f | 296 | # @param filename Name of the current file |
297 | # @param comments Array of comments: new ones will be appended there | |
298 | def comment_classdesc(filename, comments): | |
299 | ||
300 | recomm = r'^\s*///?(\s*.*?)\s*/*\s*$' | |
301 | ||
302 | reclass_doxy = r'(?i)^\s*\\class:?\s*(.*?)\s*$' | |
303 | class_name_doxy = None | |
304 | ||
305 | reauthor = r'(?i)^\s*\\?authors?:?\s*(.*?)\s*(,?\s*([0-9./-]+))?\s*$' | |
306 | redate = r'(?i)^\s*\\?date:?\s*([0-9./-]+)\s*$' | |
307 | author = None | |
308 | date = None | |
309 | ||
310 | comment_lines = [] | |
311 | ||
312 | start_line = -1 | |
313 | end_line = -1 | |
314 | ||
315 | line_num = 0 | |
316 | ||
317 | with open(filename, 'r') as fp: | |
318 | ||
319 | for raw in fp: | |
320 | ||
321 | line_num = line_num + 1 | |
322 | ||
424eef90 | 323 | if raw.strip() == '' and start_line > 0: |
06ccae0f | 324 | # Skip empty lines |
325 | end_line = line_num - 1 | |
326 | continue | |
327 | ||
328 | stripped = strip_html(raw) | |
329 | mcomm = re.search(recomm, stripped) | |
330 | if mcomm: | |
331 | ||
424eef90 | 332 | if start_line == -1: |
06ccae0f | 333 | |
334 | # First line. Check that we do not overlap with other comments | |
335 | comment_overlaps = False | |
336 | for c in comments: | |
337 | if c.has_comment(line_num): | |
338 | comment_overlaps = True | |
339 | break | |
340 | ||
341 | if comment_overlaps: | |
342 | # No need to look for other comments | |
343 | break | |
344 | ||
345 | start_line = line_num | |
346 | ||
347 | append = True | |
348 | ||
349 | mclass_doxy = re.search(reclass_doxy, mcomm.group(1)) | |
350 | if mclass_doxy: | |
351 | class_name_doxy = mclass_doxy.group(1) | |
352 | append = False | |
353 | else: | |
354 | mauthor = re.search(reauthor, mcomm.group(1)) | |
355 | if mauthor: | |
356 | author = mauthor.group(1) | |
357 | if date is None: | |
358 | # Date specified in the standalone \date field has priority | |
359 | date = mauthor.group(2) | |
360 | append = False | |
361 | else: | |
362 | mdate = re.search(redate, mcomm.group(1)) | |
363 | if mdate: | |
364 | date = mdate.group(1) | |
365 | append = False | |
366 | ||
367 | if append: | |
368 | comment_lines.append( mcomm.group(1) ) | |
369 | ||
370 | else: | |
424eef90 | 371 | if start_line > 0: |
06ccae0f | 372 | # End of our comment |
373 | if end_line == -1: | |
374 | end_line = line_num - 1 | |
375 | break | |
376 | ||
377 | if class_name_doxy is None: | |
378 | ||
379 | # No \class specified: guess it from file name | |
380 | reclass = r'^(.*/)?(.*?)(\..*)?$' | |
381 | mclass = re.search( reclass, filename ) | |
382 | if mclass: | |
383 | class_name_doxy = mclass.group(2) | |
384 | else: | |
385 | assert False, 'Regexp unable to extract classname from file' | |
386 | ||
424eef90 | 387 | if start_line > 0: |
06ccae0f | 388 | |
424eef90 | 389 | # Prepend \class specifier (and an empty line) |
390 | comment_lines[:0] = [ '\\class ' + class_name_doxy ] | |
06ccae0f | 391 | |
424eef90 | 392 | # Append author and date if they exist |
393 | comment_lines.append('') | |
06ccae0f | 394 | |
424eef90 | 395 | if author is not None: |
396 | comment_lines.append( '\\author ' + author ) | |
06ccae0f | 397 | |
424eef90 | 398 | if date is not None: |
399 | comment_lines.append( '\\date ' + date ) | |
400 | ||
401 | comment_lines = refactor_comment(comment_lines, do_strip_html=False) | |
402 | logging.debug('Comment found for class %s' % Colt(class_name_doxy).magenta()) | |
403 | comments.append(Comment( | |
404 | comment_lines, | |
405 | start_line, 1, end_line, 1, | |
406 | 0, class_name_doxy | |
407 | )) | |
408 | ||
409 | else: | |
410 | ||
411 | logging.warning('No comment found for class %s' % Colt(class_name_doxy).magenta()) | |
06ccae0f | 412 | |
413 | ||
414 | ## Traverse the AST recursively starting from the current cursor. | |
415 | # | |
416 | # @param cursor A Clang parser cursor | |
417 | # @param filename Name of the current file | |
418 | # @param comments Array of comments: new ones will be appended there | |
419 | # @param recursion Current recursion depth | |
420 | def traverse_ast(cursor, filename, comments, recursion=0): | |
421 | ||
422 | # libclang traverses included files as well: we do not want this behavior | |
423 | if cursor.location.file is not None and str(cursor.location.file) != filename: | |
424 | logging.debug("Skipping processing of included %s" % cursor.location.file) | |
425 | return | |
426 | ||
427 | text = cursor.spelling or cursor.displayname | |
428 | kind = str(cursor.kind)[str(cursor.kind).index('.')+1:] | |
429 | ||
430 | indent = '' | |
431 | for i in range(0, recursion): | |
432 | indent = indent + ' ' | |
433 | ||
434 | if cursor.kind == clang.cindex.CursorKind.CXX_METHOD or cursor.kind == clang.cindex.CursorKind.CONSTRUCTOR or cursor.kind == clang.cindex.CursorKind.DESTRUCTOR: | |
435 | ||
436 | # cursor ran into a C++ method | |
437 | logging.debug( "%5d %s%s(%s)" % (cursor.location.line, indent, Colt(kind).magenta(), Colt(text).blue()) ) | |
438 | comment_method(cursor, comments) | |
439 | ||
440 | elif cursor.kind == clang.cindex.CursorKind.FIELD_DECL: | |
441 | ||
442 | # cursor ran into a data member declaration | |
443 | logging.debug( "%5d %s%s(%s)" % (cursor.location.line, indent, Colt(kind).magenta(), Colt(text).blue()) ) | |
444 | comment_datamember(cursor, comments) | |
445 | ||
446 | else: | |
447 | ||
448 | logging.debug( "%5d %s%s(%s)" % (cursor.location.line, indent, kind, text) ) | |
449 | ||
450 | for child_cursor in cursor.get_children(): | |
451 | traverse_ast(child_cursor, filename, comments, recursion+1) | |
452 | ||
453 | if recursion == 0: | |
454 | comment_classdesc(filename, comments) | |
455 | ||
456 | ||
457 | ## Strip some HTML tags from the given string. Returns clean string. | |
458 | # | |
459 | # @param s Input string | |
460 | def strip_html(s): | |
461 | rehtml = r'(?i)</?(P|H[0-9]|BR)/?>' | |
462 | return re.sub(rehtml, '', s) | |
463 | ||
464 | ||
465 | ## Remove garbage from comments and convert special tags from THtml to Doxygen. | |
466 | # | |
467 | # @param comment An array containing the lines of the original comment | |
468 | def refactor_comment(comment, do_strip_html=True): | |
469 | ||
470 | recomm = r'^(/{2,}|/\*)? ?(\s*.*?)\s*((/{2,})?\s*|\*/)$' | |
471 | regarbage = r'^(?i)\s*([\s*=-_#]+|(Begin|End)_Html)\s*$' | |
472 | ||
473 | new_comment = [] | |
474 | insert_blank = False | |
475 | wait_first_non_blank = True | |
476 | for line_comment in comment: | |
477 | ||
478 | # Strip some HTML tags | |
479 | if do_strip_html: | |
480 | line_comment = strip_html(line_comment) | |
481 | ||
482 | mcomm = re.search( recomm, line_comment ) | |
483 | if mcomm: | |
484 | new_line_comment = mcomm.group(2) | |
485 | mgarbage = re.search( regarbage, new_line_comment ) | |
486 | ||
487 | if new_line_comment == '' or mgarbage is not None: | |
488 | insert_blank = True | |
489 | else: | |
490 | if insert_blank and not wait_first_non_blank: | |
491 | new_comment.append('') | |
492 | insert_blank = False | |
493 | wait_first_non_blank = False | |
494 | new_comment.append( new_line_comment ) | |
495 | ||
496 | else: | |
497 | assert False, 'Comment regexp does not match' | |
498 | ||
499 | return new_comment | |
500 | ||
501 | ||
502 | ## Rewrites all comments from the given file handler. | |
503 | # | |
504 | # @param fhin The file handler to read from | |
505 | # @param fhout The file handler to write to | |
506 | # @param comments Array of comments | |
507 | def rewrite_comments(fhin, fhout, comments): | |
508 | ||
509 | line_num = 0 | |
510 | in_comment = False | |
511 | skip_empty = False | |
512 | comm = None | |
513 | prev_comm = None | |
514 | ||
515 | rindent = r'^(\s*)' | |
516 | ||
517 | for line in fhin: | |
518 | ||
519 | line_num = line_num + 1 | |
520 | ||
521 | # Find current comment | |
522 | prev_comm = comm | |
523 | comm = None | |
524 | for c in comments: | |
525 | if c.has_comment(line_num): | |
526 | comm = c | |
527 | ||
528 | if comm: | |
529 | ||
530 | if isinstance(comm, MemberComment): | |
531 | non_comment = line[ 0:comm.first_col-1 ] | |
532 | ||
533 | if comm.array_size is not None: | |
534 | ||
535 | mindent = re.search(rindent, line) | |
536 | if comm.is_transient: | |
537 | tt = '!' | |
538 | else: | |
539 | tt = '' | |
540 | ||
541 | # Special case: we need multiple lines not to confuse ROOT's C++ parser | |
542 | fhout.write('%s/// %s\n%s//%s[%s]\n' % ( | |
543 | mindent.group(1), | |
544 | comm.lines[0], | |
545 | non_comment, | |
546 | tt, | |
547 | comm.array_size | |
548 | )) | |
549 | ||
550 | else: | |
551 | ||
552 | if comm.is_transient: | |
553 | tt = '!' | |
554 | else: | |
555 | tt = '/' | |
556 | ||
557 | fhout.write('%s//%s< %s\n' % ( | |
558 | non_comment, | |
559 | tt, | |
560 | comm.lines[0] | |
561 | )) | |
562 | ||
563 | elif isinstance(comm, RemoveComment): | |
564 | # Do nothing: just skip line | |
565 | pass | |
566 | ||
567 | elif prev_comm is None: | |
568 | # Beginning of a new comment block of type Comment | |
569 | in_comment = True | |
570 | ||
571 | # Extract the non-comment part and print it if it exists | |
572 | non_comment = line[ 0:comm.first_col-1 ].rstrip() | |
573 | if non_comment != '': | |
574 | fhout.write( non_comment + '\n' ) | |
575 | ||
576 | else: | |
577 | ||
578 | if in_comment: | |
579 | ||
580 | # We have just exited a comment block of type Comment | |
581 | in_comment = False | |
582 | ||
583 | # Dump revamped comment, if applicable | |
584 | text_indent = '' | |
585 | for i in range(0,prev_comm.indent): | |
586 | text_indent = text_indent + ' ' | |
587 | ||
588 | for lc in prev_comm.lines: | |
589 | fhout.write( "%s/// %s\n" % (text_indent, lc) ); | |
590 | fhout.write('\n') | |
591 | skip_empty = True | |
592 | ||
593 | line_out = line.rstrip('\n') | |
594 | if skip_empty: | |
595 | skip_empty = False | |
596 | if line_out.strip() != '': | |
597 | fhout.write( line_out + '\n' ) | |
598 | else: | |
599 | fhout.write( line_out + '\n' ) | |
600 | ||
f329fa92 | 601 | |
602 | ## The main function. | |
603 | # | |
06ccae0f | 604 | # Return value is the executable's return value. |
f329fa92 | 605 | def main(argv): |
606 | ||
06ccae0f | 607 | # Setup logging on stderr |
608 | log_level = logging.INFO | |
609 | logging.basicConfig( | |
610 | level=log_level, | |
611 | format='%(levelname)-8s %(funcName)-20s %(message)s', | |
612 | stream=sys.stderr | |
613 | ) | |
f329fa92 | 614 | |
06ccae0f | 615 | # Parse command-line options |
616 | output_on_stdout = False | |
617 | try: | |
618 | opts, args = getopt.getopt( argv, 'od', [ 'debug=', 'stdout' ] ) | |
619 | for o, a in opts: | |
620 | if o == '--debug': | |
621 | log_level = getattr( logging, a.upper(), None ) | |
622 | if not isinstance(log_level, int): | |
623 | raise getopt.GetoptError('log level must be one of: DEBUG, INFO, WARNING, ERROR, CRITICAL') | |
624 | elif o == '-d': | |
625 | log_level = logging.DEBUG | |
626 | elif o == '-o' or o == '--stdout': | |
627 | logging.debug('Output on stdout instead of replacing original files') | |
628 | output_on_stdout = True | |
629 | else: | |
630 | assert False, 'Unhandled argument' | |
631 | except getopt.GetoptError as e: | |
632 | logging.fatal('Invalid arguments: %s' % e) | |
633 | return 1 | |
f329fa92 | 634 | |
06ccae0f | 635 | logging.getLogger('').setLevel(log_level) |
f329fa92 | 636 | |
06ccae0f | 637 | # Attempt to load libclang from a list of known locations |
638 | libclang_locations = [ | |
639 | '/usr/lib/llvm-3.5/lib/libclang.so.1', | |
640 | '/usr/lib/libclang.so', | |
641 | '/Library/Developer/CommandLineTools/usr/lib/libclang.dylib' | |
642 | ] | |
643 | libclang_found = False | |
f329fa92 | 644 | |
06ccae0f | 645 | for lib in libclang_locations: |
646 | if os.path.isfile(lib): | |
647 | clang.cindex.Config.set_library_file(lib) | |
648 | libclang_found = True | |
649 | break | |
f329fa92 | 650 | |
06ccae0f | 651 | if not libclang_found: |
652 | logging.fatal('Cannot find libclang') | |
653 | return 1 | |
654 | ||
655 | # Loop over all files | |
656 | for fn in args: | |
657 | ||
658 | logging.info('Input file: %s' % Colt(fn).magenta()) | |
659 | index = clang.cindex.Index.create() | |
660 | translation_unit = index.parse(fn, args=['-x', 'c++']) | |
661 | ||
662 | comments = [] | |
663 | traverse_ast( translation_unit.cursor, fn, comments ) | |
664 | for c in comments: | |
665 | ||
666 | logging.debug("Comment found for entity %s:" % Colt(c.func).magenta()) | |
f329fa92 | 667 | |
06ccae0f | 668 | if isinstance(c, MemberComment): |
669 | ||
670 | if c.is_transient: | |
671 | transient_text = Colt('transient ').yellow() | |
672 | else: | |
673 | transient_text = '' | |
674 | ||
675 | if c.array_size is not None: | |
676 | array_text = Colt('arraysize=%s ' % c.array_size).yellow() | |
677 | else: | |
678 | array_text = '' | |
679 | ||
680 | logging.debug( | |
681 | "%s %s%s{%s}" % ( \ | |
682 | Colt("[%d,%d]" % (c.first_line, c.first_col)).green(), | |
683 | transient_text, | |
684 | array_text, | |
685 | Colt(c.lines[0]).cyan() | |
686 | )) | |
687 | ||
688 | elif isinstance(c, RemoveComment): | |
689 | ||
690 | logging.debug( Colt('[%d,%d]' % (c.first_line, c.last_line)).green() ) | |
691 | ||
692 | else: | |
693 | for l in c.lines: | |
694 | logging.debug( | |
695 | Colt("[%d,%d:%d,%d] " % (c.first_line, c.first_col, c.last_line, c.last_col)).green() + | |
696 | "{%s}" % Colt(l).cyan() | |
697 | ) | |
f329fa92 | 698 | |
699 | try: | |
06ccae0f | 700 | |
701 | if output_on_stdout: | |
702 | with open(fn, 'r') as fhin: | |
703 | rewrite_comments( fhin, sys.stdout, comments ) | |
704 | else: | |
705 | fn_back = fn + '.thtml2doxy_backup' | |
706 | os.rename( fn, fn_back ) | |
707 | ||
708 | with open(fn_back, 'r') as fhin, open(fn, 'w') as fhout: | |
709 | rewrite_comments( fhin, fhout, comments ) | |
710 | ||
711 | os.remove( fn_back ) | |
712 | logging.info("File %s converted to Doxygen: check differences before committing!" % Colt(fn).magenta()) | |
713 | except (IOError,OSError) as e: | |
714 | logging.error('File operation failed: %s' % e) | |
f329fa92 | 715 | |
716 | return 0 | |
717 | ||
06ccae0f | 718 | |
f329fa92 | 719 | if __name__ == '__main__': |
06ccae0f | 720 | sys.exit( main( sys.argv[1:] ) ) |