Add basic compilation database parsing
This commit is contained in:
parent
1053429b00
commit
d14965b24e
4 changed files with 283 additions and 29 deletions
|
|
@ -25,6 +25,50 @@ namespace cppast
|
||||||
};
|
};
|
||||||
} // namespace detail
|
} // 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]().
|
/// Compilation config for the [cppast::libclang_parser]().
|
||||||
class libclang_compile_config final : public compile_config
|
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__`.
|
/// It will also define `__cppast__` with the value `"libclang"` as well as `__cppast_major__` and `__cppast_minor__`.
|
||||||
libclang_compile_config();
|
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.
|
/// \effects Sets the path to the location of the `clang++` binary and the version of that binary.
|
||||||
/// \notes It will be used for preprocessing.
|
/// \notes It will be used for preprocessing.
|
||||||
void set_clang_binary(std::string binary, int major, int minor, int patch)
|
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;
|
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.
|
/// A parser that uses libclang.
|
||||||
class libclang_parser final : public parser
|
class libclang_parser final : public parser
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include <clang-c/CXCompilationDatabase.h>
|
||||||
|
|
||||||
#include "libclang_visitor.hpp"
|
#include "libclang_visitor.hpp"
|
||||||
#include "raii_wrapper.hpp"
|
#include "raii_wrapper.hpp"
|
||||||
#include "parse_error.hpp"
|
#include "parse_error.hpp"
|
||||||
|
|
@ -33,6 +35,22 @@ const std::vector<std::string>& detail::libclang_compile_config_access::flags(
|
||||||
return config.get_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
|
namespace
|
||||||
{
|
{
|
||||||
int parse_number(const char*& str)
|
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);
|
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)
|
void libclang_compile_config::do_set_flags(cpp_standard standard, compile_flags flags)
|
||||||
{
|
{
|
||||||
switch (standard)
|
switch (standard)
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,9 @@ set(tests
|
||||||
cpp_template_parameter.cpp
|
cpp_template_parameter.cpp
|
||||||
cpp_type_alias.cpp
|
cpp_type_alias.cpp
|
||||||
cpp_variable.cpp
|
cpp_variable.cpp
|
||||||
visitor.cpp
|
integration.cpp
|
||||||
integration.cpp)
|
libclang_parser.cpp
|
||||||
|
visitor.cpp)
|
||||||
|
|
||||||
add_executable(cppast_test test.cpp test_parser.hpp ${tests})
|
add_executable(cppast_test test.cpp test_parser.hpp ${tests})
|
||||||
target_include_directories(cppast_test PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
|
target_include_directories(cppast_test PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
|
||||||
|
|
|
||||||
70
test/libclang_parser.cpp
Normal file
70
test/libclang_parser.cpp
Normal 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");
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue