Commit 080c80a5 authored by Pádraig Ó Conbhuí's avatar Pádraig Ó Conbhuí
Browse files

Added resolve_include.

parent 6c9aa733
......@@ -9,6 +9,7 @@
#include <cstdio>
#include <dirent.h>
#include <unistd.h>
namespace megadep {
......@@ -51,6 +52,8 @@ std::vector<FileName> list_files(const DirectoryName &directory) {
return file_names;
}
namespace {
// Wrap fopen and fclose in an RAII class.
class raii_fopen {
FILE *m_file_ptr = nullptr;
......@@ -58,8 +61,13 @@ class raii_fopen {
public:
FILE *file_ptr() { return m_file_ptr; }
explicit raii_fopen(const char *file_name)
: m_file_ptr(fopen(file_name, "re")) {}
explicit raii_fopen(std::string_view file_name) {
std::array<char, filename_max> file_name_buf{};
file_name.copy(file_name_buf.data(), file_name.size());
file_name_buf[file_name.size()] = '\0';
m_file_ptr = fopen(file_name_buf.data(), "re");
}
~raii_fopen() {
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
......@@ -73,10 +81,12 @@ public:
raii_fopen &operator=(raii_fopen &&) = delete;
};
std::vector<std::string> read_includes(const std::string &file_name) {
} // namespace
std::vector<std::string> read_includes(std::string_view file_name) {
std::vector<std::string> includes;
raii_fopen file{file_name.c_str()};
raii_fopen file{file_name};
auto file_ptr = file.file_ptr();
std::array<char, filename_max> buffer{};
......@@ -90,10 +100,9 @@ std::vector<std::string> read_includes(const std::string &file_name) {
// Strip '#include' and any surrounding spaces.
const size_t include_start = line.find_first_not_of(' ', 8);
line.remove_prefix(include_start);
const size_t include_end = ([&] {
switch (line[0]) {
switch (line[include_start]) {
case '<':
return line.find_last_not_of('>', std::string_view::npos);
case '\'':
......@@ -105,13 +114,88 @@ std::vector<std::string> read_includes(const std::string &file_name) {
return size_t(0);
}());
line.remove_suffix(line.size() - include_end);
// Add found include to includes.
includes.emplace_back(line);
includes.emplace_back(
line.substr(include_start, include_end - include_start));
}
return includes;
}
namespace {
// Remove the filename from the path, leaving the directory.
// This expects a path to a file just strips the path back to the last slash.
std::string_view directory_from_file_name(std::string_view file_name) {
const auto last_slash_position = file_name.find_last_of('/');
// No slash found.
if (last_slash_position == std::string_view::npos) {
return ".";
}
return file_name.substr(0, last_slash_position);
}
} // namespace
std::string
resolve_include(std::string_view include, std::string_view source_file,
const std::vector<std::string> &search_directories) {
std::array<char, filename_max> filename_buffer{};
// The include name without <, >, ", or '.
std::string_view stripped_include = include.substr(1, include.size() - 2);
// Build the test file name and return a pointer to it.
// This uses filename_buffer as a buffer.
const auto build_file_name = [&](std::string_view search_directory) {
// Add search directory
search_directory.copy(filename_buffer.data(), search_directory.size());
// Add directory separator
filename_buffer[search_directory.size()] = '/';
// Add include name
stripped_include.copy(filename_buffer.data() + search_directory.size() + 1,
stripped_include.size());
// Make sure it's null terminated
filename_buffer[search_directory.size() + stripped_include.size() + 1] =
'\0';
return filename_buffer.data();
};
// Check the file name exists on the disk / is accessible.
const auto file_exists = [&](const char *file_name) {
return access(filename_buffer.data(), F_OK) == 0;
};
// For #include "filename.hpp", look in the source file directory first.
switch (include[0]) {
case '"': // fallthrough
case '\'':
if (file_exists(build_file_name(directory_from_file_name(source_file)))) {
return filename_buffer.data();
}
}
// For everything else, look in the supplied include directories
for (const auto &search_directory : search_directories) {
if (file_exists(build_file_name(search_directory))) {
return filename_buffer.data();
}
}
// If nothing is found, return the original include as <filename.hpp>
filename_buffer[0] = '<';
stripped_include.copy(filename_buffer.data() + 1, stripped_include.size());
filename_buffer[stripped_include.size() + 1] = '>';
filename_buffer[stripped_include.size() + 2] = '\0';
return filename_buffer.data();
}
} // namespace megadep
......@@ -4,6 +4,7 @@
#include <megadep/common_types.hpp>
#include <string>
#include <string_view>
#include <vector>
#include <cstdio>
......@@ -12,7 +13,11 @@ namespace megadep {
std::vector<FileName> list_files(const DirectoryName &directory);
std::vector<std::string> read_includes(const std::string &file_name);
std::vector<std::string> read_includes(std::string_view file_name);
std::string resolve_include(std::string_view include,
std::string_view source_file,
const std::vector<std::string> &search_directories);
} // namespace megadep
......
......@@ -14,6 +14,24 @@
// These tests assume the test runner is run in the project root directory!
//
constexpr std::string_view this_directory = "src/megadep";
constexpr std::string_view this_file = "src/megadep/filesystem.test.cpp";
TEST_CASE("this_file is a path to this file.", "[filesystem][sanity][unit]") {
constexpr std::string_view this_file_macro = __FILE__;
// Ensure `this_directory` is at the start of `this_file`
STATIC_REQUIRE(this_file.compare(0, this_directory.size(), this_directory) ==
0);
// Ensure the `this_file` path is a suffix of __FILE__
// in case of name changes in future.
STATIC_REQUIRE(this_file_macro.size() >= this_file.size());
STATIC_REQUIRE(
this_file_macro.compare(this_file_macro.size() - this_file.size(),
this_file.size(), this_file) == 0);
}
TEST_CASE("megadep::list_files recursively lists files in a directory",
"[list_files][filesystem][unit]") {
......@@ -58,15 +76,6 @@ TEST_CASE("megadep::read_includes reads all the #include statements",
"[read_includes][filesystem][unit]") {
// read the includes from *this* file
std::string this_file = "src/megadep/filesystem.test.cpp";
std::string this_file_macro = __FILE__;
// Ensure the `this_file` path is a suffix of __FILE__
// in case of name changes in future.
REQUIRE(this_file_macro.size() >= this_file.size());
REQUIRE(this_file_macro.compare(this_file_macro.size() - this_file.size(),
this_file.size(), this_file) == 0);
auto includes = megadep::read_includes(this_file);
// includes copy/pasted from the top of this file!!
......@@ -89,3 +98,49 @@ TEST_CASE("megadep::read_includes reads all the #include statements",
REQUIRE(includes[i] == expected_includes[i]);
}
}
TEST_CASE("megadep::resolve_include resolves the #include statements",
"[resolve_include][filesystem][unit]") {
auto includes = megadep::read_includes(this_file);
// Also include:
// #include "filesystem.hpp"
// #include "megadep/filesystem.hpp"
// #include "cstdio"
includes.push_back("\"filesystem.hpp\"");
includes.push_back("\"megadep/filesystem.hpp\"");
includes.push_back("\"cstdio\"");
std::string megadep_include = "src";
std::string catch2_include = "external/catch2/single_include";
std::vector<std::string> search_directories = {megadep_include,
catch2_include};
for (auto &include : includes) {
auto resolved_include =
megadep::resolve_include(include, this_file, search_directories);
// remove <, >, ", '.
std::string stripped_include{include.data() + 1, include.size() - 2};
// expect <megadep/...> and "megadep/..." to resolve to the src/megadep
// directory.
if (stripped_include.compare(0, 8, "megadep/") == 0) {
REQUIRE(resolved_include == megadep_include + "/" + stripped_include);
}
// expect "fileystem.hpp" to resolve to the src/megadep directory.
else if (stripped_include == "filesystem.hpp") {
REQUIRE(resolved_include ==
std::string{this_directory} + "/" + stripped_include);
}
// expect <catch2/...> to resolve to the external/catch2/... directory.
else if (stripped_include.compare(0, 7, "catch2/") == 0) {
REQUIRE(resolved_include == catch2_include + "/" + stripped_include);
}
// expect everything else to be unresolved.
else {
REQUIRE(resolved_include == std::string{'<'} + stripped_include + ">");
}
}
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment