From a8c3c3d2fde2fd1be1dbb35afe8e59787794ea5b Mon Sep 17 00:00:00 2001 From: Ricky Zhang Date: Tue, 22 May 2018 10:21:59 -0400 Subject: [PATCH 1/2] Added a couple features: 1. Sort fields and methods name. 2. Support multiple types in template. 3. Support Python 3.x 4. Use 3rd party package ccsyspath to include clang headers. This solves incorrect parsing of STL classes problem. Signed-off-by: Ricky Zhang --- .gitignore | 3 + README.md | 11 ++-- src/CodeDependencyVisualizer.py | 59 ++++++++++++------- src/DotGenerator.py | 44 +++++++++----- .../multiple_type_template.cpp | 22 +++++++ 5 files changed, 98 insertions(+), 41 deletions(-) create mode 100644 .gitignore create mode 100644 test/dummyCppProject/multiple_type_template.cpp 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..1131305 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,17 +61,17 @@ 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)) + umlClass.protectedFields.append((name, types)) elif cursor.kind == clang.cindex.CursorKind.CXX_METHOD: try: returnType, argumentTypes = cursor.type.spelling.split(' ', 1) @@ -72,7 +82,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 +117,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 +140,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 +175,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 +196,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; +} From 235f4dfeb9ac545e6205b1f93c550e1b6c46a995 Mon Sep 17 00:00:00 2001 From: Ricky Zhang Date: Tue, 22 May 2018 20:49:28 -0400 Subject: [PATCH 2/2] Add constructors and destructors Signed-off-by: Ricky Zhang --- src/CodeDependencyVisualizer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/CodeDependencyVisualizer.py b/src/CodeDependencyVisualizer.py index 1131305..5d615ec 100644 --- a/src/CodeDependencyVisualizer.py +++ b/src/CodeDependencyVisualizer.py @@ -72,7 +72,9 @@ def processClassMemberDeclaration(umlClass, cursor): umlClass.privateFields.append((name, types)) elif cursor.access_specifier == clang.cindex.AccessSpecifier.PROTECTED: umlClass.protectedFields.append((name, types)) - elif cursor.kind == clang.cindex.CursorKind.CXX_METHOD: + 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: