#!/usr/bin/env python ## @package thtml2doxy_clang # Translates THtml C++ comments to Doxygen using libclang as parser. # # This code relies on Python bindings for libclang: libclang's interface is pretty unstable, and # its Python bindings are unstable as well. # # AST (Abstract Source Tree) traversal is performed entirely using libclang used as a C++ parser, # instead of attempting to write a parser ourselves. # # This code (expecially AST traversal) was inspired by: # # - [Implementing a code generator with libclang](http://szelei.me/code-generator/) # (this refers to API calls used here) # - [Parsing C++ in Python with Clang](http://eli.thegreenplace.net/2011/07/03/parsing-c-in-python-with-clang) # (outdated, API calls described there do not work anymore, but useful to understand some basic # concepts) # # Usage: # # `thtml2doxy_clang file1 [file2 [file3...]]` # # @author Dario Berzano # @date 2014-12-05 import sys import os import re import logging import getopt import clang.cindex ## Brain-dead color output for terminal. class Colt(str): def red(self): return self.color('\033[31m') def green(self): return self.color('\033[32m') def yellow(self): return self.color('\033[33m') def blue(self): return self.color('\033[34m') def magenta(self): return self.color('\033[35m') def cyan(self): return self.color('\033[36m') def color(self, c): return c + self + '\033[m' ## Comment. class Comment: def __init__(self, lines, first_line, first_col, last_line, last_col, indent, func): self.lines = lines self.first_line = first_line self.first_col = first_col self.last_line = last_line self.last_col = last_col self.indent = indent self.func = func def has_comment(self, line): return line >= self.first_line and line <= self.last_line def __str__(self): return "" % (self.func, self.first_line, self.first_col, self.last_line, self.last_col, self.lines) ## A data member comment. class MemberComment: def __init__(self, text, is_transient, array_size, first_line, first_col, func): self.lines = [ text ] self.is_transient = is_transient self.array_size = array_size self.first_line = first_line self.first_col = first_col self.func = func def has_comment(self, line): return line == self.first_line def __str__(self): if self.is_transient: tt = '!transient! ' else: tt = '' if self.array_size is not None: ars = '[%s] ' % self.array_size else: ars = '' return "" % (self.func, self.first_line, self.first_col, tt, ars, self.lines[0]) ## A dummy comment that removes comment lines. class RemoveComment(Comment): def __init__(self, first_line, last_line): self.first_line = first_line self.last_line = last_line self.func = '' def __str__(self): return "" % (self.first_line, self.last_line) ## Parses method comments. # # @param cursor Current libclang parser cursor # @param comments Array of comments: new ones will be appended there def comment_method(cursor, comments): # we are looking for the following structure: method -> compound statement -> comment, i.e. we # need to extract the first comment in the compound statement composing the method in_compound_stmt = False expect_comment = False emit_comment = False comment = [] comment_function = cursor.spelling or cursor.displayname comment_line_start = -1 comment_line_end = -1 comment_col_start = -1 comment_col_end = -1 comment_indent = -1 for token in cursor.get_tokens(): if token.cursor.kind == clang.cindex.CursorKind.COMPOUND_STMT: if not in_compound_stmt: in_compound_stmt = True expect_comment = True comment_line_end = -1 else: if in_compound_stmt: in_compound_stmt = False emit_comment = True # tkind = str(token.kind)[str(token.kind).index('.')+1:] # ckind = str(token.cursor.kind)[str(token.cursor.kind).index('.')+1:] if in_compound_stmt: if expect_comment: extent = token.extent line_start = extent.start.line line_end = extent.end.line if token.kind == clang.cindex.TokenKind.PUNCTUATION and token.spelling == '{': pass 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)): comment_line_end = line_end comment_col_end = extent.end.column if comment_indent == -1 or (extent.start.column-1) < comment_indent: comment_indent = extent.start.column-1 if comment_line_start == -1: comment_line_start = line_start comment_col_start = extent.start.column comment.extend( token.spelling.split('\n') ) # multiline comments are parsed in one go, therefore don't expect subsequent comments if line_end - line_start > 0: emit_comment = True expect_comment = False else: emit_comment = True expect_comment = False if emit_comment: comment = refactor_comment( comment ) if len(comment) > 0: logging.debug("Comment found for function %s" % Colt(comment_function).magenta()) comments.append( Comment(comment, comment_line_start, comment_col_start, comment_line_end, comment_col_end, comment_indent, comment_function) ) comment = [] comment_line_start = -1 comment_line_end = -1 comment_col_start = -1 comment_col_end = -1 comment_indent = -1 emit_comment = False break ## Parses comments to class data members. # # @param cursor Current libclang parser cursor # @param comments Array of comments: new ones will be appended there def comment_datamember(cursor, comments): # Note: libclang 3.5 seems to have problems parsing a certain type of FIELD_DECL, so we revert # to a partial manual parsing. When parsing fails, the cursor's "extent" is not set properly, # returning a line range 0-0. We therefore make the not-so-absurd assumption that the datamember # definition is fully on one line, and we take the line number from cursor.location. line_num = cursor.location.line raw = None prev = None found = False # Huge overkill with open(str(cursor.location.file)) as fp: cur_line = 0 for raw in fp: cur_line = cur_line + 1 if cur_line == line_num: found = True break prev = raw assert found, 'A line that should exist was not found in file' % cursor.location.file recomm = r'(//(!)|///?)(\[(.*?)\])?