Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*.pyc
.idea/
.DS_Store
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----
Expand Down Expand Up @@ -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
```

Expand Down
63 changes: 42 additions & 21 deletions src/CodeDependencyVisualizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand All @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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)
Expand All @@ -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)


Expand Down Expand Up @@ -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']})
Expand All @@ -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())
44 changes: 28 additions & 16 deletions src/DotGenerator.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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:
Expand All @@ -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" +
Expand Down Expand Up @@ -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 ""

Expand Down Expand Up @@ -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 != "":
Expand All @@ -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 != "":
Expand Down
22 changes: 22 additions & 0 deletions test/dummyCppProject/multiple_type_template.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#include <utility> // std::pair, std::make_pair
#include <string> // std::string

class TestType1 {
public:
int i;
};

class TestType2 {
public:
int i;
};

class TestPair {
public:
std::pair<TestType1, TestType2> test_pair;
};

int main () {
TestPair product; // default constructor
return 0;
}