[cindex.py] add CompilationDatabase support

git-svn-id: https://llvm.org/svn/llvm-project/cfe/trunk@159485 91177308-0d34-0410-b5e6-96231b3b80d8
diff --git a/bindings/python/clang/cindex.py b/bindings/python/clang/cindex.py
index 02cec2d..a3da421 100644
--- a/bindings/python/clang/cindex.py
+++ b/bindings/python/clang/cindex.py
@@ -2096,6 +2096,120 @@
         """True if the included file is the input file."""
         return self.depth == 0
 
+class CompilationDatabaseError(Exception):
+    """Represents an error that occurred when working with a CompilationDatabase
+
+    Each error is associated to an enumerated value, accessible under
+    e.cdb_error. Consumers can compare the value with one of the ERROR_
+    constants in this class.
+    """
+
+    # An unknown error occured
+    ERROR_UNKNOWN = 0
+
+    # The database could not be loaded
+    ERROR_CANNOTLOADDATABASE = 1
+
+    def __init__(self, enumeration, message):
+        assert isinstance(enumeration, int)
+
+        if enumeration > 1:
+            raise Exception("Encountered undefined CompilationDatabase error "
+                            "constant: %d. Please file a bug to have this "
+                            "value supported." % enumeration)
+
+        self.cdb_error = enumeration
+        Exception.__init__(self, 'Error %d: %s' % (enumeration, message))
+
+class CompileCommand(object):
+    """Represents the compile command used to build a file"""
+    def __init__(self, cmd, ccmds):
+        self.cmd = cmd
+        # Keep a reference to the originating CompileCommands
+        # to prevent garbage collection
+        self.ccmds = ccmds
+
+    @property
+    def directory(self):
+        """Get the working directory for this CompileCommand"""
+        return CompileCommand_getDirectory(self.cmd).spelling
+
+    @property
+    def arguments(self):
+        """
+        Get an iterable object providing each argument in the
+        command line for the compiler invocation as a _CXString.
+
+        Invariants :
+          - the first argument is the compiler executable
+          - the last argument is the file being compiled
+        """
+        length = CompileCommand_getNumArgs(self.cmd)
+        for i in xrange(length):
+          yield CompileCommand_getArg(self.cmd, i)
+
+class CompileCommands(object):
+    """
+    CompileCommands is an iterable object containing all CompileCommand
+    that can be used for building a specific file.
+    """
+    def __init__(self, ccmds):
+        self.ccmds = ccmds
+
+    def __del__(self):
+        CompileCommands_dispose(self.ccmds)
+
+    def __len__(self):
+        return int(CompileCommands_getSize(self.ccmds))
+
+    def __getitem__(self, i):
+        cc = CompileCommands_getCommand(self.ccmds, i)
+        if cc is None:
+            raise IndexError
+        return CompileCommand(cc, self)
+
+    @staticmethod
+    def from_result(res, fn, args):
+        if not res:
+            return None
+        return CompileCommands(res)
+
+class CompilationDatabase(ClangObject):
+    """
+    The CompilationDatabase is a wrapper class around
+    clang::tooling::CompilationDatabase
+
+    It enables querying how a specific source file can be built.
+    """
+
+    def __del__(self):
+        CompilationDatabase_dispose(self)
+
+    @staticmethod
+    def from_result(res, fn, args):
+        if not res:
+            raise CompilationDatabaseError(0,
+                                           "CompilationDatabase loading failed")
+        return CompilationDatabase(res)
+
+    @staticmethod
+    def fromDirectory(buildDir):
+        """Builds a CompilationDatabase from the database found in buildDir"""
+        errorCode = c_uint()
+        try:
+          cdb = CompilationDatabase_fromDirectory(buildDir, byref(errorCode))
+        except CompilationDatabaseError as e:
+          raise CompilationDatabaseError(int(errorCode.value),
+                                         "CompilationDatabase loading failed")
+        return cdb
+
+    def getCompileCommands(self, filename):
+        """
+        Get an iterable object providing all the CompileCommands available to
+        build filename. Raise KeyError if filename is not found in the database.
+        """
+        return CompilationDatabase_getCompileCommands(self, filename)
+
 # Additional Functions and Types
 
 # String Functions
@@ -2463,9 +2577,48 @@
 _clang_getCompletionPriority.argtypes = [c_void_p]
 _clang_getCompletionPriority.restype = c_int
 
+# Compilation Database
+CompilationDatabase_fromDirectory = lib.clang_tooling_CompilationDatabase_fromDirectory
+CompilationDatabase_fromDirectory.argtypes = [c_char_p, POINTER(c_uint)]
+CompilationDatabase_fromDirectory.restype = c_object_p
+CompilationDatabase_fromDirectory.errcheck = CompilationDatabase.from_result
+
+CompilationDatabase_dispose = lib.clang_tooling_CompilationDatabase_dispose
+CompilationDatabase_dispose.argtypes = [c_object_p]
+
+CompilationDatabase_getCompileCommands = lib.clang_tooling_CompilationDatabase_getCompileCommands
+CompilationDatabase_getCompileCommands.argtypes = [c_object_p, c_char_p]
+CompilationDatabase_getCompileCommands.restype = c_object_p
+CompilationDatabase_getCompileCommands.errcheck = CompileCommands.from_result
+
+CompileCommands_dispose = lib.clang_tooling_CompileCommands_dispose
+CompileCommands_dispose.argtypes = [c_object_p]
+
+CompileCommands_getSize = lib.clang_tooling_CompileCommands_getSize
+CompileCommands_getSize.argtypes = [c_object_p]
+CompileCommands_getSize.restype = c_uint
+
+CompileCommands_getCommand = lib.clang_tooling_CompileCommands_getCommand
+CompileCommands_getCommand.argtypes = [c_object_p, c_uint]
+CompileCommands_getCommand.restype = c_object_p
+
+CompileCommand_getDirectory = lib.clang_tooling_CompileCommand_getDirectory
+CompileCommand_getDirectory.argtypes = [c_object_p]
+CompileCommand_getDirectory.restype = _CXString
+
+CompileCommand_getNumArgs = lib.clang_tooling_CompileCommand_getNumArgs
+CompileCommand_getNumArgs.argtypes = [c_object_p]
+CompileCommand_getNumArgs.restype = c_uint
+
+CompileCommand_getArg = lib.clang_tooling_CompileCommand_getArg
+CompileCommand_getArg.argtypes = [c_object_p, c_uint]
+CompileCommand_getArg.restype = _CXString
 
 __all__ = [
     'CodeCompletionResults',
+    'CompilationDatabase',
+    'CompileCommands',
+    'CompileCommand',
     'CursorKind',
     'Cursor',
     'Diagnostic',
diff --git a/bindings/python/tests/cindex/INPUTS/compile_commands.json b/bindings/python/tests/cindex/INPUTS/compile_commands.json
new file mode 100644
index 0000000..944150b
--- /dev/null
+++ b/bindings/python/tests/cindex/INPUTS/compile_commands.json
@@ -0,0 +1,17 @@
+[
+{
+  "directory": "/home/john.doe/MyProject",
+  "command": "clang++  -o project.o -c /home/john.doe/MyProject/project.cpp",
+  "file": "/home/john.doe/MyProject/project.cpp"
+},
+{
+  "directory": "/home/john.doe/MyProjectA",
+  "command": "clang++ -o project2.o -c /home/john.doe/MyProject/project2.cpp",
+  "file": "/home/john.doe/MyProject/project2.cpp"
+},
+{
+  "directory": "/home/john.doe/MyProjectB",
+  "command": "clang++  -DFEATURE=1 -o project2-feature.o -c /home/john.doe/MyProject/project2.cpp",
+  "file": "/home/john.doe/MyProject/project2.cpp"
+}
+]
diff --git a/bindings/python/tests/cindex/test_cdb.py b/bindings/python/tests/cindex/test_cdb.py
new file mode 100644
index 0000000..84ac1f8
--- /dev/null
+++ b/bindings/python/tests/cindex/test_cdb.py
@@ -0,0 +1,81 @@
+from clang.cindex import CompilationDatabase
+from clang.cindex import CompilationDatabaseError
+from clang.cindex import CompileCommands
+from clang.cindex import CompileCommand
+import os
+import gc
+
+kInputsDir = os.path.join(os.path.dirname(__file__), 'INPUTS')
+
+def test_create_fail():
+    """Check we fail loading a database with an assertion"""
+    path = os.path.dirname(__file__)
+    try:
+      cdb = CompilationDatabase.fromDirectory(path)
+    except CompilationDatabaseError as e:
+      assert e.cdb_error == CompilationDatabaseError.ERROR_CANNOTLOADDATABASE
+    else:
+      assert False
+
+def test_create():
+    """Check we can load a compilation database"""
+    cdb = CompilationDatabase.fromDirectory(kInputsDir)
+
+def test_lookup_fail():
+    """Check an assertion is raised when file lookup failed"""
+    cdb = CompilationDatabase.fromDirectory(kInputsDir)
+    assert cdb.getCompileCommands('file_do_not_exist.cpp') == None
+
+def test_lookup_succeed():
+    """Check we get some results if the file exists in the db"""
+    cdb = CompilationDatabase.fromDirectory(kInputsDir)
+    cmds = cdb.getCompileCommands('/home/john.doe/MyProject/project.cpp')
+    assert len(cmds) != 0
+
+def test_1_compilecommand():
+    """Check file with single compile command"""
+    cdb = CompilationDatabase.fromDirectory(kInputsDir)
+    cmds = cdb.getCompileCommands('/home/john.doe/MyProject/project.cpp')
+    assert len(cmds) == 1
+    assert cmds[0].directory == '/home/john.doe/MyProject'
+    expected = [ 'clang++', '-o', 'project.o', '-c',
+                 '/home/john.doe/MyProject/project.cpp']
+    for arg, exp in zip(cmds[0].arguments, expected):
+        assert arg.spelling == exp
+
+def test_2_compilecommand():
+    """Check file with 2 compile commands"""
+    cdb = CompilationDatabase.fromDirectory(kInputsDir)
+    cmds = cdb.getCompileCommands('/home/john.doe/MyProject/project2.cpp')
+    assert len(cmds) == 2
+    expected = [
+        { 'wd': '/home/john.doe/MyProjectA',
+          'line': ['clang++', '-o', 'project2.o', '-c',
+                   '/home/john.doe/MyProject/project2.cpp']},
+        { 'wd': '/home/john.doe/MyProjectB',
+          'line': ['clang++', '-DFEATURE=1', '-o', 'project2-feature.o', '-c',
+                   '/home/john.doe/MyProject/project2.cpp']}
+        ]
+    for i in range(len(cmds)):
+        assert cmds[i].directory == expected[i]['wd']
+        for arg, exp in zip(cmds[i].arguments, expected[i]['line']):
+            assert arg.spelling == exp
+
+def test_compilationDB_references():
+    """Ensure CompilationsCommands are independent of the database"""
+    cdb = CompilationDatabase.fromDirectory(kInputsDir)
+    cmds = cdb.getCompileCommands('/home/john.doe/MyProject/project.cpp')
+    del cdb
+    gc.collect()
+    workingdir = cmds[0].directory
+
+def test_compilationCommands_references():
+    """Ensure CompilationsCommand keeps a reference to CompilationCommands"""
+    cdb = CompilationDatabase.fromDirectory(kInputsDir)
+    cmds = cdb.getCompileCommands('/home/john.doe/MyProject/project.cpp')
+    del cdb
+    cmd0 = cmds[0]
+    del cmds
+    gc.collect()
+    workingdir = cmd0.directory
+