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