diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d35cb3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__ +*.pyc diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b69d16a --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2010-2016 Ahmed Abdel Aal, https://github.com/wow2006/CodeDependencyVisualizer + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/ParseCode/CodeDependencyVisualizer.py b/ParseCode/CodeDependencyVisualizer.py new file mode 100644 index 0000000..24b8058 --- /dev/null +++ b/ParseCode/CodeDependencyVisualizer.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python + +import clang.cindex +import sys +import os +import json +import logging +import argparse +import fnmatch + +from DotGenerator import * + +index = clang.cindex.Index.create() +dotGenerator = DotGenerator() + + +def splitCommand(command): + args = command.split() + + includes_list = [x for x in args if "-I" in x] + + indecies = [(i, i + 1) for i, x in enumerate(args) + if "-isystem" in x] + + for x, y in indecies: + includes_list.append(args[y]) + + return includes_list + + +def findFilesInDir(rootDir, patterns): + """ + Searches for files in rootDir which file names mathes the given pattern. Returns + a list of file paths of found files + """ + foundFiles = [] + for root, dirs, files in os.walk(rootDir): + for p in patterns: + for filename in fnmatch.filter(files, p): + foundFiles.append(os.path.join(root, filename)) + return foundFiles + + +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 + fieldChilds = list(cursor.get_children()) + if len(fieldChilds) == 0: + # if there are not cursorchildren, the type is some primitive datatype + type = cursor.type.spelling + else: + # if there are cursorchildren, the type is some non-primitive datatype (a class or class template) + for cc in fieldChilds: + if cc.kind == clang.cindex.CursorKind.TEMPLATE_REF: + type = cc.spelling + elif cc.kind == clang.cindex.CursorKind.TYPE_REF: + type = cursor.type.spelling + name = cursor.spelling + return name, type + + +def processClassMemberDeclaration(umlClass, cursor): + """ + Processes a cursor corresponding to a class member declaration and + appends the extracted information to the given umlClass + """ + if cursor.kind == clang.cindex.CursorKind.CXX_BASE_SPECIFIER: + for baseClass in cursor.get_children(): + if baseClass.kind == clang.cindex.CursorKind.TEMPLATE_REF: + umlClass.parents.append(baseClass.spelling) + 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: + # 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)) + elif cursor.access_specifier == clang.cindex.AccessSpecifier.PRIVATE: + umlClass.privateFields.append((name, type)) + elif cursor.access_specifier == clang.cindex.AccessSpecifier.PROTECTED: + umlClass.protectedFields.append((name, type)) + elif cursor.kind == clang.cindex.CursorKind.CXX_METHOD: + try: + returnType, argumentTypes = cursor.type.spelling.split(' ', 1) + if cursor.access_specifier == clang.cindex.AccessSpecifier.PUBLIC: + umlClass.publicMethods.append((returnType, cursor.spelling, + argumentTypes)) + elif cursor.access_specifier == clang.cindex.AccessSpecifier.PRIVATE: + umlClass.privateMethods.append((returnType, cursor.spelling, + argumentTypes)) + 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)) + elif cursor.kind == clang.cindex.CursorKind.FUNCTION_TEMPLATE: + returnType, argumentTypes = cursor.type.spelling.split(' ', 1) + if cursor.access_specifier == clang.cindex.AccessSpecifier.PUBLIC: + umlClass.publicMethods.append((returnType, cursor.spelling, + argumentTypes)) + elif cursor.access_specifier == clang.cindex.AccessSpecifier.PRIVATE: + umlClass.privateMethods.append((returnType, cursor.spelling, + argumentTypes)) + elif cursor.access_specifier == clang.cindex.AccessSpecifier.PROTECTED: + umlClass.protectedMethods.append((returnType, cursor.spelling, + argumentTypes)) + + + def processClass(cursor, inclusionConfig): + """ Processes an ast node that is a class. """ + # umlClass is the datastructure for the DotGenerator + umlClass = UmlClass() + + # that stores the necessary information about a single class. + # We extract this information from the clang ast hereafter ... + if cursor.kind == clang.cindex.CursorKind.CLASS_TEMPLATE: + # process declarations like: + # template class MyClass + umlClass.fqn = cursor.spelling + else: + # process declarations like: + # class MyClass ... + # struct MyStruct ... + umlClass.fqn = cursor.type.spelling # the fully qualified name + + logging.debug("Before children") + + import re + if (inclusionConfig['excludeClasses'] and + re.match(inclusionConfig['excludeClasses'], umlClass.fqn)): + return + + if (inclusionConfig['includeClasses'] and not + re.match(inclusionConfig['includeClasses'], umlClass.fqn)): + return + + logging.debug("Before children") + + for c in cursor.get_children(): + # process member variables and methods declarations + processClassMemberDeclaration(umlClass, c) + + dotGenerator.addClass(umlClass) + + +def traverseAst(cursor, inclusionConfig): + if (cursor.kind == clang.cindex.CursorKind.CLASS_DECL or + cursor.kind == clang.cindex.CursorKind.STRUCT_DECL or + cursor.kind == clang.cindex.CursorKind.CLASS_TEMPLATE): + # if the current cursor is a class, class template or struct declaration, + # we process it further ... + processClass(cursor, inclusionConfig) + + for child_node in cursor.get_children(): + traverseAst(child_node, inclusionConfig) + + +def parseTranslationUnit(filePath, includeDirs, inclusionConfig): + if(not includeDirs): + return + + clangArgs = ['-x', 'c++'] + ['-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") + traverseAst(tu.cursor, inclusionConfig) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="CodeDependencyVisualizer (CDV)") + parser.add_argument( + '-d', + required=True, + help="directory with source files to parse (searches recusively)") + parser.add_argument( + '-o', + '--outFile', + default='uml.dot', + help="output file name / name of generated dot file") + parser.add_argument( + '-u', '--withUnusedHeaders', help="parse unused header files (slow)") + parser.add_argument( + '-a', + '--associations', + action="store_true", + help="draw class member assiciations") + parser.add_argument( + '-i', + '--inheritances', + action="store_true", + help="draw class inheritances") + parser.add_argument( + '-p', + '--privMembers', + action="store_true", + help="show private members") + parser.add_argument( + '-t', + '--protMembers', + action="store_true", + help="show protected members") + parser.add_argument( + '-P', '--pubMembers', action="store_true", help="show public members") + parser.add_argument( + '-I', + '--includeDirs', + help="additional search path(s) for include files (seperated by space)", + nargs='+') + parser.add_argument( + '-v', + '--verbose', + action="store_true", + help="print verbose information for debugging purposes") + parser.add_argument( + '--excludeClasses', + help="classes matching this pattern will be excluded") + parser.add_argument( + '--includeClasses', + help="only classes matching this pattern will be included") + + args = vars(parser.parse_args(sys.argv[1:])) + jsonPath = os.path.join(args['d'], "compile_commands.json") + + if (os.path.isfile(jsonPath)): + with open(jsonPath, "r") as jsonPath: + json_data = json.load(jsonPath) + + for json in json_data: + sourceFile = json["file"] + + include_list = splitCommand(json["command"]) + + logging.info("parsing file " + sourceFile) + + parseTranslationUnit(sourceFile, + include_list, { + 'excludeClasses': args['excludeClasses'], + 'includeClasses': args['includeClasses'] + }) + else: + filesToParsePatterns = ['*.cpp', '*.cxx', '*.c', '*.cc'] + if args['withUnusedHeaders']: + filesToParsePatterns += ['*.h', '*.hxx', '*.hpp'] + filesToParse = findFilesInDir(args['d'], filesToParsePatterns) + subdirectories = [x[0] for x in os.walk(args['d'])] + + loggingFormat = "%(levelname)s - %(module)s: %(message)s" + logging.basicConfig(format=loggingFormat, level=logging.INFO) + if args['verbose']: + logging.basicConfig(format=loggingFormat, level=logging.DEBUG) + + logging.info("found " + str(len(filesToParse)) + " source files.") + + for sourceFile in filesToParse: + logging.info("parsing file " + sourceFile) + parseTranslationUnit( + sourceFile, args['includeDirs'], { + 'excludeClasses': args['excludeClasses'], + 'includeClasses': args['includeClasses'] + }) + + dotGenerator.setDrawAssociations(args['associations']) + dotGenerator.setDrawInheritances(args['inheritances']) + dotGenerator.setShowPrivMethods(args['privMembers']) + dotGenerator.setShowProtMethods(args['protMembers']) + #dotGenerator.setShowPubMethods(args['pubMembers']) + + dotfileName = args['outFile'] + logging.info("generating dotfile " + dotfileName) + with open(dotfileName, 'w') as dotfile: + dotfile.write(dotGenerator.generate()) diff --git a/src/DotGenerator.py b/ParseCode/DotGenerator.py similarity index 71% rename from src/DotGenerator.py rename to ParseCode/DotGenerator.py index 1f39cab..f4d1ec2 100644 --- a/src/DotGenerator.py +++ b/ParseCode/DotGenerator.py @@ -17,15 +17,18 @@ def addParentByFQN(self, fullyQualifiedClassName): self.parents.append(fullyQualifiedClassName) def getId(self): - return "id" + str(hashlib.md5(self.fqn).hexdigest()) + return "id" + str(hashlib.md5(self.fqn.encode('utf-8')).hexdigest()) + + def __str__(self): + return str(self.fqn) class DotGenerator: - _showPrivMembers = False - _showProtMembers = False - _showPubMembers = False + _showPrivMembers = True + _showProtMembers = True + _showPubMembers = True _drawAssociations = False - _drawInheritances = False + _drawInheritances = True def __init__(self): self.classes = {} @@ -34,15 +37,21 @@ 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]) + ret = "".join([(accessPrefix + fieldName + ": " + fieldType + "\l") + for fieldName, fieldType in fields]) return ret def _genMethods(self, accessPrefix, methods): - return "".join([(accessPrefix + methodName + methodArgs + " : " + returnType + "\l") for (returnType, methodName, methodArgs) in methods]) + return "".join([( + accessPrefix + methodName + methodArgs + " : " + returnType + "\l") + for (returnType, methodName, methodArgs) in methods]) - def _genClass(self, aClass, withPublicMembers=False, withProtectedMembers=False, withPrivateMembers=False): - c = (aClass.getId()+" [ \n" + - " label = \"{" + aClass.fqn) + def _genClass(self, + aClass, + withPublicMembers=False, + withProtectedMembers=False, + withPrivateMembers=False): + c = (aClass.getId() + " [ \n" + " label = \"{" + aClass.fqn) if withPublicMembers: pubFields = self._genFields('+ ', aClass.publicFields) @@ -81,7 +90,7 @@ def _genAssociations(self, aClass): c = self.classes[fieldType] edges.add(aClass.getId() + "->" + c.getId()) edgesJoined = "\n".join(edges) - return edgesJoined+"\n" if edgesJoined != "" else "" + return edgesJoined + "\n" if edgesJoined != "" else "" def _genInheritances(self, aClass): edges = "" @@ -107,27 +116,23 @@ def setShowPubMethods(self, enable): self._showPubMembers = enable def generate(self): - dotContent = ("digraph dependencies {\n" + - " fontname = \"Bitstream Vera Sans\"\n" + - " fontsize = 8" + - " node [" + - " fontname = \"Bitstream Vera Sans\"\n" + - " fontsize = 8\n" + - " shape = \"record\"\n" + - " ]\n" + - " edge [\n" + - " fontname = \"Bitstream Vera Sans\"\n" + - " fontsize = 8\n" + - " ]\n" - ) - - for key, value in self.classes.iteritems(): - dotContent += self._genClass(value, self._showPubMembers, self._showProtMembers, self._showPrivMembers) + dotContent = ( + "digraph dependencies {\n" + + " fontname = \"Bitstream Vera Sans\"\n" + " fontsize = 8" + + " node [" + " fontname = \"Bitstream Vera Sans\"\n" + + " fontsize = 8\n" + " shape = \"record\"\n" + " ]\n" + + " edge [\n" + " fontname = \"Bitstream Vera Sans\"\n" + + " fontsize = 8\n" + " ]\n") + + 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 +142,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/ParseCode/ParseSourceCode.py b/ParseCode/ParseSourceCode.py new file mode 100644 index 0000000..75e8f87 --- /dev/null +++ b/ParseCode/ParseSourceCode.py @@ -0,0 +1,45 @@ +from json import load +import clang.cindex + + +def ParseCommand(command): + args = command.split() + + includes_list = [x for x in args if "-I" in x] + + indecies = [(i, i+1) for i,x in enumerate(args) if "-isystem" in x] + + for x, y in indecies: + includes_list.append(args[x] + " " + args[y]) + + return includes_list + +def ParseSourceCode(json): + data = load(json) + commands = dict() + + for item in data: + key = item["file"] + value = item["command"] + + commands[key] = ParseCommand(value) + + return commands + +def CreateAST(file_info): + index = clang.cindex.Index.create() + for key in file_info: + tu = index.parse(key, args=file_info[key], + options=clang.cindex.TranslationUnit.PARSE_SKIP_FUNCTION_BODIES) + + if not tu: + raise Exception("ExpectedException not raised") + + return tu + +def TraverseAST(TransUnit): + pass + +def printClass(trans_unit, class_name): + pass + diff --git a/ParseCode/__init__.py b/ParseCode/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ParseCode/main.py b/ParseCode/main.py new file mode 100644 index 0000000..bfad562 --- /dev/null +++ b/ParseCode/main.py @@ -0,0 +1,154 @@ +import json +import clang.cindex + +from DotGenerator import * +from ParseSourceCode import * + + +def splitCommand(command): + args = command.split() + + includes_list = [x for x in args if "-I" in x] + + indecies = [(i, i+1) for i,x in enumerate(args) if "-isystem" in x] + + for x, y in indecies: + includes_list.append(args[x] + " " + args[y]) + + return includes_list + +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 + fieldChilds = list(cursor.get_children()) + + if len(fieldChilds) == 0: + # if there are not cursorchildren, the type is some primitive datatype + type = cursor.type.spelling + else: + # if there are cursorchildren, the type is some non-primitive datatype (a class or class template) + for cc in fieldChilds: + if cc.kind == clang.cindex.CursorKind.TEMPLATE_REF: + type = cc.spelling + elif cc.kind == clang.cindex.CursorKind.TYPE_REF: + type = cursor.type.spelling + + name = cursor.spelling + return name, type + +def processClassMemberDeclaration(umlClass, cursor): + """ + Processes a cursor corresponding to a class member + declaration and appends the extracted information + to the given umlClass + """ + if cursor.kind == clang.cindex.CursorKind.CXX_BASE_SPECIFIER: + for baseClass in cursor.get_children(): + if baseClass.kind == clang.cindex.CursorKind.TEMPLATE_REF: + umlClass.parents.append(baseClass.spelling) + 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: + # 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)) + elif cursor.access_specifier == clang.cindex.AccessSpecifier.PRIVATE: + umlClass.privateFields.append((name, type)) + elif cursor.access_specifier == clang.cindex.AccessSpecifier.PROTECTED: + umlClass.protectedFields.append((name, type)) + elif cursor.kind == clang.cindex.CursorKind.CXX_METHOD: + try: + returnType, argumentTypes = cursor.type.spelling.split(' ', 1) + if cursor.access_specifier == clang.cindex.AccessSpecifier.PUBLIC: + umlClass.publicMethods.append((returnType, cursor.spelling, + argumentTypes)) + elif cursor.access_specifier == clang.cindex.AccessSpecifier.PRIVATE: + umlClass.privateMethods.append((returnType, cursor.spelling, + argumentTypes)) + 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)) + elif cursor.kind == clang.cindex.CursorKind.FUNCTION_TEMPLATE: + returnType, argumentTypes = cursor.type.spelling.split(' ', 1) + if cursor.access_specifier == clang.cindex.AccessSpecifier.PUBLIC: + umlClass.publicMethods.append((returnType, cursor.spelling, + argumentTypes)) + elif cursor.access_specifier == clang.cindex.AccessSpecifier.PRIVATE: + umlClass.privateMethods.append((returnType, cursor.spelling, + argumentTypes)) + elif cursor.access_specifier == clang.cindex.AccessSpecifier.PROTECTED: + umlClass.protectedMethods.append((returnType, cursor.spelling, + argumentTypes)) + +def processClass(cursor, inclusionConfig): + """ Processes an ast node that is a class. """ + umlClass = UmlClass() + + # umlClass is the datastructure for the DotGenerator + # that stores the necessary information about a single class. + # We extract this information from the clang ast hereafter ... + if cursor.kind == clang.cindex.CursorKind.CLASS_TEMPLATE: + # process declarations like: + # template class MyClass + umlClass.fqn = cursor.spelling + else: + # process declarations like: + # class MyClass ... + # struct MyStruct ... + umlClass.fqn = cursor.type.spelling # the fully qualified name + + import re + if (inclusionConfig['excludeClasses'] and + re.match(inclusionConfig['excludeClasses'], umlClass.fqn)): + return + + if (inclusionConfig['includeClasses'] and not + re.match(inclusionConfig['includeClasses'], umlClass.fqn)): + return + + for c in cursor.get_children(): + # process member variables and methods declarations + processClassMemberDeclaration(umlClass, c) + + print(umlClass) + +def traverseAst(cursor, inclusionConfig): + if (cursor.kind == clang.cindex.CursorKind.CLASS_DECL or + cursor.kind == clang.cindex.CursorKind.STRUCT_DECL or + cursor.kind == clang.cindex.CursorKind.CLASS_TEMPLATE): + # if the current cursor is a class, class template or struct declaration, + # we process it further ... + processClass(cursor, inclusionConfig) + + for child_node in cursor.get_children(): + traverseAst(child_node, inclusionConfig) + +def parseTranslationUnit(filePath, includeDirs, inclusionConfig): + index = clang.cindex.Index.create() + + clangArgs = ['-x', 'c++', *includeDirs] + + tu = index.parse(filePath, args=clangArgs, + options=clang.cindex.TranslationUnit.PARSE_SKIP_FUNCTION_BODIES) + + traverseAst(tu.cursor, inclusionConfig) + +if __name__ == "__main__": + json_name = "compile_commands.json" + + with open(json_name, "r") as json_file: + json_data = ParseSourceCode(json_file) + + for file_name in json_data: + parseTranslationUnit(file_name, + json_data[file_name], + "") + diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..333ef05 --- /dev/null +++ b/TODO.md @@ -0,0 +1,2 @@ +1. [ ] list members in struct + diff --git a/Utility/__init__.py b/Utility/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..2b91ecc --- /dev/null +++ b/setup.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +from setuptools import setup, find_packages + + +with open('README.md') as f: + readme = f.read() + +with open('LICENSE') as f: + license = f.read() + +setup( + name='CodeDependencyVisualizer', + version='0.1.0', + description='Code Dependency Visualizer', + long_description=readme, + author='Ahmed Abdel Aal', + author_email='eng.ahmedhussein89@gmail.com', + url='https://github.com/wow2006/CodeDependencyVisualizer', + license=license, + packages=find_packages(exclude=('tests', 'docs')) +) + diff --git a/src/CodeDependencyVisualizer.py b/src/CodeDependencyVisualizer.py deleted file mode 100644 index 145af3d..0000000 --- a/src/CodeDependencyVisualizer.py +++ /dev/null @@ -1,182 +0,0 @@ -#!/usr/bin/env python - -import clang.cindex -import sys -import os -import logging -import argparse -import fnmatch - -from DotGenerator import * - -index = clang.cindex.Index.create() -dotGenerator = DotGenerator() - - -def findFilesInDir(rootDir, patterns): - """ Searches for files in rootDir which file names mathes the given pattern. Returns - a list of file paths of found files""" - foundFiles = [] - for root, dirs, files in os.walk(rootDir): - for p in patterns: - for filename in fnmatch.filter(files, p): - foundFiles.append(os.path.join(root, filename)) - return foundFiles - - -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 - fieldChilds = list(cursor.get_children()) - if len(fieldChilds) == 0: # if there are not cursorchildren, the type is some primitive datatype - type = cursor.type.spelling - else: # if there are cursorchildren, the type is some non-primitive datatype (a class or class template) - for cc in fieldChilds: - if cc.kind == clang.cindex.CursorKind.TEMPLATE_REF: - type = cc.spelling - elif cc.kind == clang.cindex.CursorKind.TYPE_REF: - type = cursor.type.spelling - name = cursor.spelling - return name, type - - -def processClassMemberDeclaration(umlClass, cursor): - """ Processes a cursor corresponding to a class member declaration and - appends the extracted information to the given umlClass """ - if cursor.kind == clang.cindex.CursorKind.CXX_BASE_SPECIFIER: - for baseClass in cursor.get_children(): - if baseClass.kind == clang.cindex.CursorKind.TEMPLATE_REF: - umlClass.parents.append(baseClass.spelling) - 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: - # 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)) - elif cursor.access_specifier == clang.cindex.AccessSpecifier.PRIVATE: - umlClass.privateFields.append((name, type)) - elif cursor.access_specifier == clang.cindex.AccessSpecifier.PROTECTED: - umlClass.protectedFields.append((name, type)) - elif cursor.kind == clang.cindex.CursorKind.CXX_METHOD: - try: - returnType, argumentTypes = cursor.type.spelling.split(' ', 1) - if cursor.access_specifier == clang.cindex.AccessSpecifier.PUBLIC: - umlClass.publicMethods.append((returnType, cursor.spelling, argumentTypes)) - elif cursor.access_specifier == clang.cindex.AccessSpecifier.PRIVATE: - umlClass.privateMethods.append((returnType, cursor.spelling, argumentTypes)) - 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)) - elif cursor.kind == clang.cindex.CursorKind.FUNCTION_TEMPLATE: - returnType, argumentTypes = cursor.type.spelling.split(' ', 1) - if cursor.access_specifier == clang.cindex.AccessSpecifier.PUBLIC: - umlClass.publicMethods.append((returnType, cursor.spelling, argumentTypes)) - elif cursor.access_specifier == clang.cindex.AccessSpecifier.PRIVATE: - umlClass.privateMethods.append((returnType, cursor.spelling, argumentTypes)) - elif cursor.access_specifier == clang.cindex.AccessSpecifier.PROTECTED: - umlClass.protectedMethods.append((returnType, cursor.spelling, argumentTypes)) - - -def processClass(cursor, inclusionConfig): - """ Processes an ast node that is a class. """ - umlClass = UmlClass() # umlClass is the datastructure for the DotGenerator - # that stores the necessary information about a single class. - # We extract this information from the clang ast hereafter ... - if cursor.kind == clang.cindex.CursorKind.CLASS_TEMPLATE: - # process declarations like: - # template class MyClass - umlClass.fqn = cursor.spelling - else: - # process declarations like: - # class MyClass ... - # struct MyStruct ... - umlClass.fqn = cursor.type.spelling # the fully qualified name - - import re - if (inclusionConfig['excludeClasses'] and - re.match(inclusionConfig['excludeClasses'], umlClass.fqn)): - return - - if (inclusionConfig['includeClasses'] and not - re.match(inclusionConfig['includeClasses'], umlClass.fqn)): - return - - for c in cursor.get_children(): - # process member variables and methods declarations - processClassMemberDeclaration(umlClass, c) - - dotGenerator.addClass(umlClass) - - -def traverseAst(cursor, inclusionConfig): - if (cursor.kind == clang.cindex.CursorKind.CLASS_DECL - or cursor.kind == clang.cindex.CursorKind.STRUCT_DECL - or cursor.kind == clang.cindex.CursorKind.CLASS_TEMPLATE): - # if the current cursor is a class, class template or struct declaration, - # we process it further ... - processClass(cursor, inclusionConfig) - for child_node in cursor.get_children(): - traverseAst(child_node, inclusionConfig) - - -def parseTranslationUnit(filePath, includeDirs, inclusionConfig): - clangArgs = ['-x', 'c++'] + ['-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") - traverseAst(tu.cursor, inclusionConfig) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="CodeDependencyVisualizer (CDV)") - parser.add_argument('-d', required=True, help="directory with source files to parse (searches recusively)") - parser.add_argument('-o', '--outFile', default='uml.dot', help="output file name / name of generated dot file") - parser.add_argument('-u', '--withUnusedHeaders', help="parse unused header files (slow)") - parser.add_argument('-a', '--associations', action="store_true", help="draw class member assiciations") - parser.add_argument('-i', '--inheritances', action="store_true", help="draw class inheritances") - parser.add_argument('-p', '--privMembers', action="store_true", help="show private members") - parser.add_argument('-t', '--protMembers', action="store_true", help="show protected members") - parser.add_argument('-P', '--pubMembers', action="store_true", help="show public members") - parser.add_argument('-I', '--includeDirs', help="additional search path(s) for include files (seperated by space)", nargs='+') - parser.add_argument('-v', '--verbose', action="store_true", help="print verbose information for debugging purposes") - parser.add_argument('--excludeClasses', help="classes matching this pattern will be excluded") - parser.add_argument('--includeClasses', help="only classes matching this pattern will be included") - - args = vars(parser.parse_args(sys.argv[1:])) - - filesToParsePatterns = ['*.cpp', '*.cxx', '*.c', '*.cc'] - if args['withUnusedHeaders']: - filesToParsePatterns += ['*.h', '*.hxx', '*.hpp'] - filesToParse = findFilesInDir(args['d'], filesToParsePatterns) - subdirectories = [x[0] for x in os.walk(args['d'])] - - loggingFormat = "%(levelname)s - %(module)s: %(message)s" - logging.basicConfig(format=loggingFormat, level=logging.INFO) - if args['verbose']: - logging.basicConfig(format=loggingFormat, level=logging.DEBUG) - - logging.info("found " + str(len(filesToParse)) + " source files.") - - for sourceFile in filesToParse: - logging.info("parsing file " + sourceFile) - parseTranslationUnit(sourceFile, args['includeDirs'], { - 'excludeClasses': args['excludeClasses'], - 'includeClasses': args['includeClasses']}) - - dotGenerator.setDrawAssociations(args['associations']) - dotGenerator.setDrawInheritances(args['inheritances']) - dotGenerator.setShowPrivMethods(args['privMembers']) - dotGenerator.setShowProtMethods(args['protMembers']) - dotGenerator.setShowPubMethods(args['pubMembers']) - - dotfileName = args['outFile'] - logging.info("generating dotfile " + dotfileName) - with open(dotfileName, 'w') as dotfile: - dotfile.write(dotGenerator.generate()) diff --git a/test/dummyCppProject/class.cpp b/test/dummyCppProject/class.cpp deleted file mode 100644 index 17d2094..0000000 --- a/test/dummyCppProject/class.cpp +++ /dev/null @@ -1,35 +0,0 @@ -#include "class.h" - -using namespace NSXX::NSX; - -template -class F { -}; - -template -class G { -}; - -class H : public X, C, A, public G{ - private: - const NSXX::NSX::C *m; - char c; - F d1; - - public: - T x; - void aPublicMethod() {} - - protected: - int y; - const NSXX::NSX::C *n; - - void aProtectedMethod(); -}; - -namespace NSXX { - namespace NSX { - class E : public H { - }; - } -} diff --git a/test/dummyCppProject/subfolder/class.h b/test/dummyCppProject/subfolder/class.h deleted file mode 100644 index e08fad6..0000000 --- a/test/dummyCppProject/subfolder/class.h +++ /dev/null @@ -1,34 +0,0 @@ -namespace NSXX { - namespace NSX { - class C { - }; - class D { - }; - } -} - - -using namespace NSXX::NSX; - - -class A { -}; - -template -class B : public A, C /* asdf */ { - private: - const NSXX::NSX::C *m; - char c; - D d1; - T varT; - - NSXX::NSX::C aprivateMethod(int a /*acomment*/, int b); - float aprivateMethod(); - public: - C x; - - void aPublicMethod() {} - - template - void aTemplateMethod(T x) {} -}; diff --git a/test/test_CodeDependencyVisualizer.sh b/test/test_CodeDependencyVisualizer.sh deleted file mode 100755 index a3de1c7..0000000 --- a/test/test_CodeDependencyVisualizer.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh - -TMP_DIR="/tmp" -CPP_SRC_DIR="dummyCppProject" -CUML_ARGS="-aiptP --verbose -d ${CPP_SRC_DIR} -I ./dummyCppProject/subfolder -o ${TMP_DIR}/uml.dot --excludeClasses G" -VIEWER="firefox" -OUTPUT_FORMAT="svg" - -python2 ../src/CodeDependencyVisualizer.py ${CUML_ARGS} && dot -T ${OUTPUT_FORMAT} -o ${TMP_DIR}/uml.${OUTPUT_FORMAT} ${TMP_DIR}/uml.dot && ${VIEWER} ${TMP_DIR}/uml.${OUTPUT_FORMAT} diff --git a/test/test_DotGenerator.py b/test/test_DotGenerator.py deleted file mode 100755 index 8045e22..0000000 --- a/test/test_DotGenerator.py +++ /dev/null @@ -1,32 +0,0 @@ -from DotGenerator import * - -import sys - -dot = DotGenerator() - -privateFields=[("aa", "int"),("bb","void*"),("cc","NS1::BClass"),("dd", "void")] -privateMethods=[("void", "privateMethod1", "(asdds, dss*)"), ("BClass", "privateMethod2", "(asdf)")] -publicFields=[("publicField1","CClass"), ("publicField2", "none")] -publicMethods=[("void", "publicMethod1", "(asdds, dss*)"), ("BClass", "publicMethod2", "(asdf)")] - -c1 = UmlClass() -c1.fqn = "NS1::AClass" -c1.privateFields = privateFields -c1.privateMethods = privateMethods -c1.publicFields = publicFields -c1.publicMethods = publicMethods -dot.addClass(c1) - -c2 = UmlClass() -c2.fqn = "NS1::BClass" -c2.parents.append(c1.fqn) -dot.addClass(c2) - -c3 = UmlClass() -c3.fqn = "CClass" -dot.addClass(c3) - -outputDotFile = ['uml2.dot', sys.argv[1]][len(sys.argv) == 2] - -with open(outputDotFile, "w") as dotfile: - dotfile.write(dot.generate()) diff --git a/test/test_DotGenerator.sh b/test/test_DotGenerator.sh deleted file mode 100755 index 32d3665..0000000 --- a/test/test_DotGenerator.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh - -TMP_DIR="/tmp" -VIEWER="eog" -export PYTHONPATH=../src - -python2 test_DotGenerator.py ${TMP_DIR}/uml2.dot && dot -T png -o ${TMP_DIR}/uml2.png ${TMP_DIR}/uml2.dot && ${VIEWER} ${TMP_DIR}/uml2.png diff --git a/tests/context.py b/tests/context.py new file mode 100644 index 0000000..81648b6 --- /dev/null +++ b/tests/context.py @@ -0,0 +1,7 @@ +import os +import sys +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +from ParseCode.ParseSourceCode import * +import Utility + diff --git a/tests/functional.py b/tests/functional.py new file mode 100644 index 0000000..03fb8f9 --- /dev/null +++ b/tests/functional.py @@ -0,0 +1,42 @@ +simple_file_name = "simple.cpp" +simple_file_string = """ +struct Simple { + int x; + int y; + int z; +}; +""" +simple_result_string = """ +- Simple + - int x + - int y + - int z +""" +import sys +from io import StringIO +from context import CreateAST, printClass + + +# 1. Create file contain "simple_file_string" called "simple.cpp" +with open(simple_file_name, "w") as f: + f.write(simple_file_string) + +# 2. Parse "simple.cpp" +tu = CreateAST({simple_file_name: []}) + +# 3. print +# - Simple +# - int x +# - int y +# - int z +old_stdout = sys.stdout +sys.stdout = temp_stdout = StringIO() +printClass(tu, "Simple") +sys.stdout = old_stdout + +if(temp_stdout.read() == simple_file_string): + print("Finish!") + +# 4. clean file +os.remove(simple_file_name) + diff --git a/tests/test_parse_code.py b/tests/test_parse_code.py new file mode 100644 index 0000000..0285364 --- /dev/null +++ b/tests/test_parse_code.py @@ -0,0 +1,57 @@ +import os +import sys +import clang +from io import StringIO +from unittest import TestCase, main +from context import CreateAST, printClass + + +class ParseCode(TestCase): + def test_create_clang_ast(self): + test_file_info = {"/tmp/xyz.cpp": ["-I/usr/include"]} + + try: + CreateAST(test_file_info) + except clang.cindex.TranslationUnitLoadError as e: + self.assertTrue("Error parsing translation unit." == str(e)) + except Exception as e: + self.fail('Unexpected exception raised:', e) + else: + self.fail('ExpectedException not raised') + + def test_simple_class_ast(self): + test_file_name = "/tmp/test.cpp" + test_file_str = "class test {int x;};" + test_file_info = {test_file_name: []} + with open(test_file_name, "w") as f: + f.write(test_file_str) + + tu = CreateAST(test_file_info) + self.assertTrue(test_file_name == tu.spelling) + children = list(tu.cursor.get_children()) + self.assertTrue(len(children) == 1) + + os.remove(test_file_name) + + def test_simple_class_print(self): + test_file_name = "/tmp/Simple.cpp" + test_file_str = "class Simple {int x;};" + test_file_info = {test_file_name: []} + with open(test_file_name, "w") as f: + f.write(test_file_str) + + tu = CreateAST(test_file_info) + + old_stdout = sys.stdout + sys.stdout = temp_stdout = StringIO() + printClass(tu, "Simple") + sys.stdout = old_stdout + + self.assertTrue("- Simple\n - int x" == temp_stdout.read()) + + os.remove(test_file_name) + + +if __name__ == '__main__': + main() + diff --git a/tests/test_parse_json.py b/tests/test_parse_json.py new file mode 100644 index 0000000..eb7169f --- /dev/null +++ b/tests/test_parse_json.py @@ -0,0 +1,105 @@ +import io +from unittest import TestCase, main +from context import ParseSourceCode, ParseCommand +from json.decoder import JSONDecodeError + + +class ParseJSON(TestCase): + def test_file_empty(self): + try: + empty_file = io.StringIO("") + parser = ParseSourceCode(json=empty_file) + except JSONDecodeError as e: + self.assertTrue(True) + except Exception as e: + self.fail('Unexpected exception raised:', e) + else: + self.fail('ExpectedException not raised') + + def test_simple_file(self): + test_file = "/tmp/test_file.json" + test_command_file = "FullSystem.cpp" + test_command_dir = "/tmp" + test_include = "-I/usr/include" + test_command = "/usr/bin/clang++ %s %s" % (test_include, test_command_file) + + test_string = io.StringIO('[{"directory": "%s","command": "%s","file": "%s"}]' % (test_command_dir, test_command, test_command_file)) + + commands = ParseSourceCode(json=test_string) + + self.assertEqual(type(commands), dict) + self.assertEqual(len(commands), 1) + self.assertIn(test_command_file, commands) + self.assertIn(test_include, commands[test_command_file]) + + def test_parse_two_files(self): + test_command_file = ["1.cpp", "2.cpp"] + test_command_dir = "/tmp" + test_command = ["/usr/bin/clang++ %s" % test_command_file[0], + "/usr/bin/clang++ %s" % test_command_file[1]] + + test_full_string = """ + [ + {"directory": "%s","command": "%s","file": "%s"}, + {"directory": "%s","command": "%s","file": "%s"} + ] + """ % (test_command_dir, test_command[0], test_command_file[0], + test_command_dir, test_command[1], test_command_file[1]) + + test_string = io.StringIO(test_full_string) + + commands = ParseSourceCode(json=test_string) + + self.assertEqual(type(commands), dict) + self.assertEqual(len(commands), 2) + + keys = list(commands.keys()) + for index, (test_file, true_file) in enumerate(zip(keys, test_command_file)): + self.assertEqual(test_file, true_file) + self.assertEqual(commands[test_file], []) + + def test_parse_empty_command(self): + test_command = "" + + command = ParseCommand(test_command) + self.assertTrue(len(command) == 0) + + def test_parse_simple_command(self): + test_command = "/usr/bin/clang++ test.cpp" + + command = ParseCommand(test_command) + self.assertTrue(len(command) == 0) + + def test_parse_include_command(self): + test_include = "-I/usr/include" + test_command = "/usr/bin/clang++ %s test.cpp" % test_include + + command = ParseCommand(test_command) + self.assertTrue(len(command) == 1) + self.assertIn(test_include, test_command) + + def test_parse_include_two_command(self): + test_include = ["-I/usr/include", + "-I/usr/local/include"] + + test_command = "/usr/bin/clang++ %s %s test.cpp" % (test_include[0], test_include[1]) + + command = ParseCommand(test_command) + self.assertTrue(len(command) == 2) + for test_ in test_include: + self.assertIn(test_, test_command) + + def test_parse_include_two_system_command(self): + test_include = ["-isystem /usr/include", + "-isystem /usr/local/include"] + + test_command = "/usr/bin/clang++ %s %s test.cpp" % (test_include[0], test_include[1]) + + command = ParseCommand(test_command) + self.assertTrue(len(command) == 2) + for test_ in test_include: + self.assertIn(test_, test_command) + +if __name__ == '__main__': + main() +