Add basic compilation database parsing

This commit is contained in:
Jonathan Müller 2017-06-06 17:23:48 +02:00
commit d14965b24e
4 changed files with 283 additions and 29 deletions

View file

@ -25,6 +25,50 @@ namespace cppast
};
} // namespace detail
/// The exception thrown when a fatal parse error occurs.
class libclang_error final : public std::runtime_error
{
public:
/// \effects Creates it with a message.
libclang_error(std::string msg) : std::runtime_error(std::move(msg))
{
}
};
/// A compilation database.
///
/// This represents a `compile_commands.json` file,
/// which stores all the commands needed to compile a set of files.
/// It can be generated by CMake using the `CMAKE_EXPORT_COMPILE_COMMANDS` option.
class libclang_compilation_database
{
public:
/// \effects Creates it giving the directory where the `compile_commands.json` file is located.
/// \throws `libclang_error` if the database could not be loaded or found.
libclang_compilation_database(const std::string& build_directory);
libclang_compilation_database(libclang_compilation_database&& other)
: database_(other.database_)
{
other.database_ = nullptr;
}
~libclang_compilation_database();
libclang_compilation_database& operator=(libclang_compilation_database&& other)
{
libclang_compilation_database tmp(std::move(other));
std::swap(tmp.database_, database_);
return *this;
}
private:
using database = void*;
database database_;
friend libclang_compile_config;
};
/// Compilation config for the [cppast::libclang_parser]().
class libclang_compile_config final : public compile_config
{
@ -36,6 +80,20 @@ namespace cppast
/// It will also define `__cppast__` with the value `"libclang"` as well as `__cppast_major__` and `__cppast_minor__`.
libclang_compile_config();
/// Creates the configuration stored in the database.
///
/// \effects It will use the options found in the database for the specified file.
/// This does not necessarily need to match the file that is going to be parsed,
/// but it should.
/// It will also add the default configuration options.
/// \notes Header files are not included in the compilation database,
/// you need to pass in the file name of the corresponding source file,
/// if you want to parse one.
/// \notes It will only consider options you could also set by the other functions.
/// \notes The file key will include the specified directory in the JSON, if it is not a full path.
libclang_compile_config(const libclang_compilation_database& database,
const std::string& file);
/// \effects Sets the path to the location of the `clang++` binary and the version of that binary.
/// \notes It will be used for preprocessing.
void set_clang_binary(std::string binary, int major, int minor, int patch)
@ -64,16 +122,6 @@ namespace cppast
friend detail::libclang_compile_config_access;
};
/// The exception thrown when a fatal parse error occurs.
class libclang_error final : public std::runtime_error
{
public:
/// \effects Creates it with a message.
libclang_error(std::string msg) : std::runtime_error(std::move(msg))
{
}
};
/// A parser that uses libclang.
class libclang_parser final : public parser
{

View file

@ -7,6 +7,8 @@
#include <cstring>
#include <vector>
#include <clang-c/CXCompilationDatabase.h>
#include "libclang_visitor.hpp"
#include "raii_wrapper.hpp"
#include "parse_error.hpp"
@ -33,6 +35,22 @@ const std::vector<std::string>& detail::libclang_compile_config_access::flags(
return config.get_flags();
}
libclang_compilation_database::libclang_compilation_database(const std::string& build_directory)
{
static_assert(std::is_same<database, CXCompilationDatabase>::value, "forgot to update type");
auto error = CXCompilationDatabase_NoError;
database_ = clang_CompilationDatabase_fromDirectory(build_directory.c_str(), &error);
if (error != CXCompilationDatabase_NoError)
throw libclang_error("unable to load compilation database");
}
libclang_compilation_database::~libclang_compilation_database()
{
if (database_)
clang_CompilationDatabase_dispose(database_);
}
namespace
{
int parse_number(const char*& str)
@ -65,6 +83,123 @@ libclang_compile_config::libclang_compile_config() : compile_config({})
define_macro("__cppast_version_minor__", CPPAST_VERSION_MINOR);
}
namespace
{
struct cxcompile_commands_deleter
{
void operator()(CXCompileCommands cmds)
{
clang_CompileCommands_dispose(cmds);
}
};
using cxcompile_commands = detail::raii_wrapper<CXCompileCommands, cxcompile_commands_deleter>;
}
namespace
{
bool is_flag(const detail::cxstring& str)
{
return str.length() > 1u && str[0] == '-';
}
const char* find_flag_arg_sep(const std::string& last_flag)
{
if (last_flag[1] == 'D')
// no separator, equal is part of the arg
return nullptr;
return std::strchr(last_flag.c_str(), '=');
}
template <typename Func>
void parse_flags(CXCompileCommand cmd, Func callback)
{
auto no_args = clang_CompileCommand_getNumArgs(cmd);
std::string last_flag;
for (auto i = 1u /* 0 is compiler executable */; i != no_args; ++i)
{
detail::cxstring str(clang_CompileCommand_getArg(cmd, i));
if (is_flag(str))
{
if (!last_flag.empty())
{
// process last flag
std::string args;
if (auto ptr = find_flag_arg_sep(last_flag))
{
auto pos = std::size_t(ptr - last_flag.c_str());
++ptr;
while (*ptr)
args += *ptr++;
last_flag.erase(pos);
}
else if (last_flag.size() > 2u)
{
// assume two character flag
args = last_flag.substr(2u);
last_flag.erase(2u);
}
callback(std::move(last_flag), std::move(args));
}
last_flag = str.std_str();
}
else if (!last_flag.empty())
{
// we have flags + args
callback(std::move(last_flag), str.std_str());
last_flag.clear();
}
// else skip argument
}
}
}
libclang_compile_config::libclang_compile_config(const libclang_compilation_database& database,
const std::string& file)
: libclang_compile_config()
{
auto cxcommands =
clang_CompilationDatabase_getCompileCommands(database.database_, file.c_str());
if (cxcommands == nullptr)
throw libclang_error(detail::format("no compile commands specified for file '", file, "'"));
cxcompile_commands commands(cxcommands);
auto size = clang_CompileCommands_getSize(commands.get());
for (auto i = 0u; i != size; ++i)
{
auto cmd = clang_CompileCommands_getCommand(commands.get(), i);
auto dir = detail::cxstring(clang_CompileCommand_getDirectory(cmd));
parse_flags(cmd, [&](std::string flag, std::string args) {
if (flag == "-I")
{
if (args.front() == '/' || args.front() == '\\')
{
add_flag(std::move(flag) + std::move(args));
}
else
{
// path relative to the directory
if (dir[dir.length() - 1] != '/' && dir[dir.length() - 1] != '\\')
add_flag(std::move(flag) + dir.std_str() + '/' + std::move(args));
else
add_flag(std::move(flag) + dir.std_str() + std::move(args));
}
}
else if (flag == "-D" || flag == "-U")
// preprocessor options
this->add_flag(std::move(flag) + std::move(args));
else if (flag == "-std")
// standard
this->add_flag(std::move(flag) + "=" + std::move(args));
else if (flag == "-f" && (args == "ms-compatibility" || args == "ms-extensions"))
// other options
this->add_flag(std::move(flag) + std::move(args));
});
}
}
void libclang_compile_config::do_set_flags(cpp_standard standard, compile_flags flags)
{
switch (standard)

View file

@ -9,25 +9,26 @@ if(NOT EXISTS ${CMAKE_CURRENT_BINARY_DIR}/catch.hpp)
endif()
set(tests
code_generator.cpp
cpp_alias_template.cpp
cpp_class.cpp
cpp_class_template.cpp
cpp_enum.cpp
cpp_friend.cpp
cpp_function.cpp
cpp_function_template.cpp
cpp_language_linkage.cpp
cpp_member_function.cpp
cpp_member_variable.cpp
cpp_namespace.cpp
cpp_preprocessor.cpp
cpp_static_assert.cpp
cpp_template_parameter.cpp
cpp_type_alias.cpp
cpp_variable.cpp
visitor.cpp
integration.cpp)
code_generator.cpp
cpp_alias_template.cpp
cpp_class.cpp
cpp_class_template.cpp
cpp_enum.cpp
cpp_friend.cpp
cpp_function.cpp
cpp_function_template.cpp
cpp_language_linkage.cpp
cpp_member_function.cpp
cpp_member_variable.cpp
cpp_namespace.cpp
cpp_preprocessor.cpp
cpp_static_assert.cpp
cpp_template_parameter.cpp
cpp_type_alias.cpp
cpp_variable.cpp
integration.cpp
libclang_parser.cpp
visitor.cpp)
add_executable(cppast_test test.cpp test_parser.hpp ${tests})
target_include_directories(cppast_test PUBLIC ${CMAKE_CURRENT_BINARY_DIR})

70
test/libclang_parser.cpp Normal file
View file

@ -0,0 +1,70 @@
// Copyright (C) 2017 Jonathan Müller <jonathanmueller.dev@gmail.com>
// This file is subject to the license terms in the LICENSE file
// found in the top-level directory of this distribution.
#include <catch.hpp>
#include <cppast/libclang_parser.hpp>
#include <fstream>
using namespace cppast;
libclang_compilation_database get_database(const char* json)
{
std::ofstream file("compile_commands.json");
file << json;
file.close();
return libclang_compilation_database(".");
}
void require_flags(const libclang_compile_config& config, const char* flags)
{
std::string result;
auto config_flags = detail::libclang_compile_config_access::flags(config);
// skip first 4, those are the default options
for (auto iter = config_flags.begin() + 4; iter != config_flags.end(); ++iter)
result += *iter + ' ';
result.pop_back();
REQUIRE(result == flags);
}
TEST_CASE("libclang_compile_config")
{
// only test database parser
auto json = R"([
{
"directory": "/foo",
"command": "/usr/bin/clang++ -Irelative -I/absolute -DA=FOO -DB(X)=X -c -o a.o a.cpp",
"file": "a.cpp"
},
{
"directory": "/bar/",
"command": "/usr/bin/clang++ -Irelative -DA=FOO -c -o b.o b.cpp",
"command": "/usr/bin/clang++ -I/absolute -DB(X)=X -c -o b.o b.cpp",
"file": "/b.cpp",
},
{
"directory": "/bar/",
"command": "/usr/bin/clang++ -I/absolute -DB(X)=X -c -o b.o b.cpp",
"file": "/b.cpp",
},
{
"directory": "",
"command": "/usr/bin/clang++ -std=c++14 -fms-extensions -fms-compatibility -c -o c.o c.cpp",
"file": "/c.cpp",
}
])";
auto database = get_database(json);
libclang_compile_config a(database, "/foo/a.cpp");
require_flags(a, "-I/foo/relative -I/absolute -DA=FOO -DB(X)=X");
libclang_compile_config b(database, "/b.cpp");
require_flags(b, "-I/bar/relative -DA=FOO -I/absolute -DB(X)=X");
libclang_compile_config c(database, "/c.cpp");
require_flags(c, "-std=c++14 -fms-extensions -fms-compatibility");
}