diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a15a5be --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.pyc +.idea/ +.DS_Store diff --git a/README.md b/README.md index e0e51bd..b61f04f 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,12 @@ You might also take a look at my blogpost: http://gernotklingler.com/blog/libcla Dependencies ------------ -- python 2.7 -- clang version 3.5 +- python 2.7 and 3.6 +- clang version 3.5+ - graphvitz (for the dot tool) to be able to transform the generated dot file to an image +- ccsyspath -Just tested on Linux. +Just tested on Linux and Mac OS X. Usage ----- @@ -42,9 +43,9 @@ optional arguments: additional search path(s) for include files (seperated by space) -v, --verbose print verbose information for debugging purposes - --exclude_classes EXCLUDE_CLASSES + --excludeClasses EXCLUDE_CLASSES classes matching this pattern will be excluded - --include_classes INCLUDE_CLASSES + --includeClasses INCLUDE_CLASSES only classes matching this pattern will be included ``` diff --git a/src/CodeDependencyVisualizer.py b/src/CodeDependencyVisualizer.py index 145af3d..5d615ec 100644 --- a/src/CodeDependencyVisualizer.py +++ b/src/CodeDependencyVisualizer.py @@ -6,12 +6,19 @@ import logging import argparse import fnmatch +import ccsyspath from DotGenerator import * +if 2 == sys.version_info[0]: + text = unicode +else: + text = str + +clang_system_include_paths = [path.decode('utf-8') for path in ccsyspath.system_include_paths('/usr/bin/clang++')] index = clang.cindex.Index.create() dotGenerator = DotGenerator() - +log = logging.getLogger(__name__) def findFilesInDir(rootDir, patterns): """ Searches for files in rootDir which file names mathes the given pattern. Returns @@ -27,18 +34,21 @@ def findFilesInDir(rootDir, patterns): def processClassField(cursor): """ Returns the name and the type of the given class field. The cursor must be of kind CursorKind.FIELD_DECL""" - type = None + # the first element of types is for display purpose. Form 2nd to Nth element, they are tempalte type argurment in + # the chain list + types = list() fieldChilds = list(cursor.get_children()) if len(fieldChilds) == 0: # if there are not cursorchildren, the type is some primitive datatype - type = cursor.type.spelling + types.append(cursor.type.spelling) else: # if there are cursorchildren, the type is some non-primitive datatype (a class or class template) + types.append(cursor.type.spelling) for cc in fieldChilds: if cc.kind == clang.cindex.CursorKind.TEMPLATE_REF: - type = cc.spelling + types.append(cc.spelling) elif cc.kind == clang.cindex.CursorKind.TYPE_REF: - type = cursor.type.spelling + types.append(cc.type.spelling) name = cursor.spelling - return name, type + return name, types def processClassMemberDeclaration(umlClass, cursor): @@ -51,18 +61,20 @@ def processClassMemberDeclaration(umlClass, cursor): elif baseClass.kind == clang.cindex.CursorKind.TYPE_REF: umlClass.parents.append(baseClass.type.spelling) elif cursor.kind == clang.cindex.CursorKind.FIELD_DECL: # non static data member - name, type = processClassField(cursor) - if name is not None and type is not None: + name, types = processClassField(cursor) + if name is not None and types is not None: # clang < 3.5: needs patched cindex.py to have # clang.cindex.AccessSpecifier available: # https://gitorious.org/clang-mirror/clang-mirror/commit/e3d4e7c9a45ed9ad4645e4dc9f4d3b4109389cb7 if cursor.access_specifier == clang.cindex.AccessSpecifier.PUBLIC: - umlClass.publicFields.append((name, type)) + umlClass.publicFields.append((name, types)) elif cursor.access_specifier == clang.cindex.AccessSpecifier.PRIVATE: - umlClass.privateFields.append((name, type)) + umlClass.privateFields.append((name, types)) elif cursor.access_specifier == clang.cindex.AccessSpecifier.PROTECTED: - umlClass.protectedFields.append((name, type)) - elif cursor.kind == clang.cindex.CursorKind.CXX_METHOD: + umlClass.protectedFields.append((name, types)) + elif cursor.kind == clang.cindex.CursorKind.CXX_METHOD or\ + cursor.kind == clang.cindex.CursorKind.CONSTRUCTOR or\ + cursor.kind == clang.cindex.CursorKind.DESTRUCTOR: try: returnType, argumentTypes = cursor.type.spelling.split(' ', 1) if cursor.access_specifier == clang.cindex.AccessSpecifier.PUBLIC: @@ -72,7 +84,7 @@ def processClassMemberDeclaration(umlClass, cursor): elif cursor.access_specifier == clang.cindex.AccessSpecifier.PROTECTED: umlClass.protectedMethods.append((returnType, cursor.spelling, argumentTypes)) except: - logging.error("Invalid CXX_METHOD declaration! " + str(cursor.type.spelling)) + log.error("Invalid CXX_METHOD declaration! " + str(cursor.type.spelling)) elif cursor.kind == clang.cindex.CursorKind.FUNCTION_TEMPLATE: returnType, argumentTypes = cursor.type.spelling.split(' ', 1) if cursor.access_specifier == clang.cindex.AccessSpecifier.PUBLIC: @@ -107,6 +119,10 @@ def processClass(cursor, inclusionConfig): re.match(inclusionConfig['includeClasses'], umlClass.fqn)): return + if dotGenerator.hasClass(umlClass.fqn): + return + + log.info('Process class %s' % umlClass.fqn ) for c in cursor.get_children(): # process member variables and methods declarations processClassMemberDeclaration(umlClass, c) @@ -126,11 +142,14 @@ def traverseAst(cursor, inclusionConfig): def parseTranslationUnit(filePath, includeDirs, inclusionConfig): - clangArgs = ['-x', 'c++'] + ['-I' + includeDir for includeDir in includeDirs] + if includeDirs is None: + includeDirs = list() + includeDirs = includeDirs + clang_system_include_paths + clangArgs = ['-x', 'c++', '-std=c++11'] + ['-I' + includeDir for includeDir in includeDirs] tu = index.parse(filePath, args=clangArgs, options=clang.cindex.TranslationUnit.PARSE_SKIP_FUNCTION_BODIES) for diagnostic in tu.diagnostics: - logging.debug(diagnostic) - logging.info('Translation unit:' + tu.spelling + "\n") + log.debug(diagnostic) + log.info('Translation unit:' + tu.spelling + "\n") traverseAst(tu.cursor, inclusionConfig) @@ -158,14 +177,16 @@ def parseTranslationUnit(filePath, includeDirs, inclusionConfig): subdirectories = [x[0] for x in os.walk(args['d'])] loggingFormat = "%(levelname)s - %(module)s: %(message)s" - logging.basicConfig(format=loggingFormat, level=logging.INFO) + logging.basicConfig(format=loggingFormat) + log.setLevel(logging.INFO) if args['verbose']: - logging.basicConfig(format=loggingFormat, level=logging.DEBUG) + log.setLevel(logging.DEBUG) - logging.info("found " + str(len(filesToParse)) + " source files.") + log.info("found " + str(len(filesToParse)) + " source files.") + log.info('Use system path: ' + ' '.join(clang_system_include_paths)) for sourceFile in filesToParse: - logging.info("parsing file " + sourceFile) + log.info("parsing file " + sourceFile) parseTranslationUnit(sourceFile, args['includeDirs'], { 'excludeClasses': args['excludeClasses'], 'includeClasses': args['includeClasses']}) @@ -177,6 +198,6 @@ def parseTranslationUnit(filePath, includeDirs, inclusionConfig): dotGenerator.setShowPubMethods(args['pubMembers']) dotfileName = args['outFile'] - logging.info("generating dotfile " + dotfileName) + log.info("generating dotfile " + dotfileName) with open(dotfileName, 'w') as dotfile: dotfile.write(dotGenerator.generate()) diff --git a/src/DotGenerator.py b/src/DotGenerator.py index 1f39cab..d8f3f87 100644 --- a/src/DotGenerator.py +++ b/src/DotGenerator.py @@ -1,6 +1,9 @@ import hashlib -import logging - +import sys +if 2 == sys.version_info[0]: + text = unicode +else: + text = str class UmlClass: def __init__(self): @@ -17,7 +20,7 @@ def addParentByFQN(self, fullyQualifiedClassName): self.parents.append(fullyQualifiedClassName) def getId(self): - return "id" + str(hashlib.md5(self.fqn).hexdigest()) + return "id" + text(hashlib.md5(text(self.fqn).encode('utf-8')).hexdigest()) class DotGenerator: @@ -30,15 +33,22 @@ class DotGenerator: def __init__(self): self.classes = {} + def hasClass(self, aClass): + return self.classes.get(aClass) is not None + def addClass(self, aClass): self.classes[aClass.fqn] = aClass def _genFields(self, accessPrefix, fields): - ret = "".join([(accessPrefix + fieldName + ": " + fieldType + "\l") for fieldName, fieldType in fields]) + # sort by fieldName + sorted_fields = sorted(fields, key=lambda x: x[0]) + ret = "".join([(accessPrefix + fieldName + ": " + fieldTypes[0] + "\l") for fieldName, fieldTypes in sorted_fields]) return ret def _genMethods(self, accessPrefix, methods): - return "".join([(accessPrefix + methodName + methodArgs + " : " + returnType + "\l") for (returnType, methodName, methodArgs) in methods]) + # sort by methodName + sorted_methods = sorted(methods, key=lambda x: x[1]) + return "".join([(accessPrefix + methodName + methodArgs + " : " + returnType + "\l") for (returnType, methodName, methodArgs) in sorted_methods]) def _genClass(self, aClass, withPublicMembers=False, withProtectedMembers=False, withPrivateMembers=False): c = (aClass.getId()+" [ \n" + @@ -72,14 +82,16 @@ def _genClass(self, aClass, withPublicMembers=False, withProtectedMembers=False, def _genAssociations(self, aClass): edges = set() - for fieldName, fieldType in aClass.privateFields: - if fieldType in self.classes: - c = self.classes[fieldType] - edges.add(aClass.getId() + "->" + c.getId()) - for fieldName, fieldType in aClass.publicFields: - if fieldType in self.classes: - c = self.classes[fieldType] - edges.add(aClass.getId() + "->" + c.getId()) + for fieldName, fieldTypes in aClass.privateFields: + for fieldType in fieldTypes: + if fieldType in self.classes: + c = self.classes[fieldType] + edges.add(aClass.getId() + "->" + c.getId()) + for fieldName, fieldTypes in aClass.publicFields: + for fieldType in fieldTypes: + if fieldType in self.classes: + c = self.classes[fieldType] + edges.add(aClass.getId() + "->" + c.getId()) edgesJoined = "\n".join(edges) return edgesJoined+"\n" if edgesJoined != "" else "" @@ -121,13 +133,13 @@ def generate(self): " ]\n" ) - for key, value in self.classes.iteritems(): + for key, value in self.classes.items(): dotContent += self._genClass(value, self._showPubMembers, self._showProtMembers, self._showPrivMembers) # associations if self._drawAssociations: associations = "" - for key, aClass in self.classes.iteritems(): + for key, aClass in self.classes.items(): associations += self._genAssociations(aClass) if associations != "": @@ -137,7 +149,7 @@ def generate(self): # inheritances if self._drawInheritances: inheritances = "" - for key, aClass in self.classes.iteritems(): + for key, aClass in self.classes.items(): inheritances += self._genInheritances(aClass) if inheritances != "": diff --git a/test/dummyCppProject/multiple_type_template.cpp b/test/dummyCppProject/multiple_type_template.cpp new file mode 100644 index 0000000..9ccd303 --- /dev/null +++ b/test/dummyCppProject/multiple_type_template.cpp @@ -0,0 +1,22 @@ +#include // std::pair, std::make_pair +#include // std::string + +class TestType1 { + public: + int i; +}; + +class TestType2 { + public: + int i; +}; + +class TestPair { + public: + std::pair test_pair; +}; + +int main () { + TestPair product; // default constructor + return 0; +}