Commit 15a8e46f authored by Pádraig Ó Conbhuí's avatar Pádraig Ó Conbhuí
Browse files

resolve_include: Add relative pathname resolution

parent 94b5e96d
#include <megadep/filesystem.hpp>
#include <algorithm>
#include <array>
#include <iostream>
#include <iterator>
#include <string>
......@@ -8,7 +9,7 @@
#include <vector>
// List of file suffixes the program will consider
std::vector<std::string> suffixes = {
constexpr std::array<std::string_view, 8> suffixes = {
".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx",
};
......@@ -30,17 +31,15 @@ int main(int argc, char *argv[]) {
std::vector<std::string> include_directories;
for (int i = 1; i < argc; i++) {
std::string_view argv_str = argv[i];
std::string_view argv_str = argv[i]; // NOLINT
if (argv_str.compare(0, 2, "-I") == 0) {
// -I for include directory
argv_str.remove_prefix(2);
include_directories.emplace_back(argv_str);
continue;
} else {
// Regular positional argument for project directory
project_directories.emplace_back(argv_str);
continue;
}
}
......
......@@ -131,7 +131,7 @@ 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) {
std::string_view get_directory(std::string_view file_name) {
const auto last_slash_position = file_name.find_last_of('/');
// No slash found.
......@@ -142,15 +142,100 @@ std::string_view directory_from_file_name(std::string_view file_name) {
return file_name.substr(0, last_slash_position);
}
// Rewrite path in-place removing and ./, /./, or /../ .
// Note: this won't work if the path starts with ../ .
// Returns the new one-past-the-end iterator.
char *resolve_relative_path(char *path_begin, char *path_end) {
// Get the start of the next path entity.
// e.g.
// some/ENTITY/other/stuff
// b r e
// where `b` and `e` denote the `begin` and `end` pointers,
// and `r` denotes the returned pointer.
const auto next_entity_start = [](char *begin, char *end) {
auto pos = std::find(begin, end, '/');
if (pos != end) {
pos++;
}
return pos;
};
// Get the last entity.
// e.g
// /other/stuff/ENTITY
// b r e
// where `b` and `e` denote the `begin` and `end` pointers,
// and `r` denotes the returned pointer.
const auto last_entity_start = [](char *begin, char *end) {
if (begin == end) {
return begin;
}
for (auto it = std::prev(end); it != begin; it--) {
if (*it == '/') {
return std::next(it);
}
}
return begin;
};
char *write_pos = path_begin;
for (char *read_pos = path_begin; read_pos < path_end;) {
// Find the start of the next entity, and make a string_view of
// the current entity, including the '/'.
auto next_pos = next_entity_start(read_pos, path_end);
std::string_view next_entity =
std::string_view{read_pos, static_cast<size_t>(next_pos - read_pos)};
// Match './' .
// Do nothing, don't copy to write_pos, don't update write_pos.
if (next_entity == "./") {
read_pos = next_pos;
continue;
}
// Match '../' .
// Don't copy to write_pos, and rewind write_pos to a previous entity.
if (next_entity == "../") {
// Only parse ../ if it's not at the start of the string.
if (write_pos != path_begin) {
auto prev_pos = last_entity_start(path_begin, std::prev(write_pos));
write_pos = prev_pos;
read_pos = next_pos;
continue;
}
}
// Copy the entity into the write_pos.
// Move write_pos and read_pos along to the next entity.
std::copy(read_pos, next_pos, write_pos);
write_pos = std::next(write_pos, (next_pos - read_pos));
read_pos = next_pos;
}
return write_pos;
}
} // namespace
std::string
resolve_include(std::string_view include, std::string_view source_file,
const std::vector<std::string> &search_directories) {
// Buffer holding the current filename.
std::array<char, filename_max> filename_buffer{};
assert(include.size() + 1 < filename_buffer.size());
// filename_buffer length without trailing '\0'.
size_t filename_buffer_strlen = 0;
assert(include.size() + 1 < filename_buffer.size() &&
"include is too big for filename_buffer!");
// The include name without <, >, ", or '.
std::string_view stripped_include = include.substr(1, include.size() - 2);
......@@ -159,7 +244,8 @@ resolve_include(std::string_view include, std::string_view source_file,
// This uses filename_buffer as a buffer.
const auto build_file_name = [&](std::string_view search_directory) {
assert(search_directory.size() + 1 + stripped_include.size() + 1 <
filename_buffer.size());
filename_buffer.size() &&
"include + search_directory is too big for filaneme_buffer!");
// Add search directory
search_directory.copy(filename_buffer.begin(), search_directory.size());
......@@ -175,27 +261,41 @@ resolve_include(std::string_view include, std::string_view source_file,
filename_buffer[search_directory.size() + 1 + stripped_include.size()] =
'\0';
filename_buffer_strlen =
search_directory.size() + 1 + stripped_include.size();
return filename_buffer.data();
};
// Check the file name exists on the disk / is accessible.
const auto file_exists = [&](const char *file_name) {
return access(file_name, F_OK) == 0;
const auto file_exists = [&] {
return access(filename_buffer.data(), F_OK) == 0;
};
const auto resolve_filename = [&]() {
auto new_end = resolve_relative_path(filename_buffer.begin(),
filename_buffer.begin() +
filename_buffer_strlen + 1);
filename_buffer_strlen = new_end - filename_buffer.begin() - 1;
};
// 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();
build_file_name(get_directory(source_file));
if (file_exists()) {
resolve_filename();
return {filename_buffer.data(), filename_buffer_strlen};
}
}
// 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();
build_file_name(search_directory);
if (file_exists()) {
resolve_filename();
return {filename_buffer.data(), filename_buffer_strlen};
}
}
......@@ -206,7 +306,7 @@ resolve_include(std::string_view include, std::string_view source_file,
filename_buffer[stripped_include.size() + 1] = '>';
filename_buffer[stripped_include.size() + 2] = '\0';
return filename_buffer.data();
return {filename_buffer.data(), stripped_include.size() + 2};
}
} // namespace megadep
......@@ -102,51 +102,71 @@ TEST_CASE("megadep::read_includes reads all the #include statements",
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.emplace_back("\"filesystem.hpp\"");
includes.emplace_back("\"megadep/filesystem.hpp\"");
includes.emplace_back("\"cstdio\"");
const std::string megadep_include = "src";
const std::string catch2_include = "external/catch2/single_include";
const 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);
SECTION("read includes from this_file.") {
auto includes = megadep::read_includes(this_file);
for (auto &include : includes) {
auto resolved_include =
megadep::resolve_include(include, this_file, search_directories);
// remove <, >, ", '.
const std::string stripped_include =
include.substr(1, include.size() - 2);
std::string_view megadep_prefix = "megadep/";
std::string_view catch2_prefix = "catch2/";
// expect <megadep/...> and "megadep/..." to resolve to the src/megadep
// directory.
if (stripped_include.substr(0, megadep_prefix.size()) == megadep_prefix) {
// NOLINTNEXTLINE(performance-inefficient-string-concatenation)
REQUIRE(resolved_include == megadep_include + "/" + stripped_include);
}
// expect <catch2/...> to resolve to the external/catch2/... directory.
else if (stripped_include.substr(0, catch2_prefix.size()) ==
catch2_prefix) {
// NOLINTNEXTLINE(performance-inefficient-string-concatenation)
REQUIRE(resolved_include == catch2_include + "/" + stripped_include);
}
// expect everything else to be unresolved.
else {
REQUIRE(resolved_include == std::string{'<'} + stripped_include + ">");
}
}
}
// remove <, >, ", '.
const std::string stripped_include = include.substr(1, include.size() - 2);
SECTION("quoted includes and relative directories") {
const auto resolve_include = [&](std::string_view include) {
return megadep::resolve_include(include, this_file, search_directories);
};
std::string_view megadep_prefix = "megadep/";
std::string_view catch2_prefix = "catch2/";
// #include "filesystem.hpp" -> src/megadep/filesystem.hpp
REQUIRE(resolve_include("\"filesystem.hpp\"") ==
"src/megadep/filesystem.hpp");
// expect <megadep/...> and "megadep/..." to resolve to the src/megadep
// directory.
if (stripped_include.substr(0, megadep_prefix.size()) == megadep_prefix) {
// NOLINTNEXTLINE(performance-inefficient-string-concatenation)
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.substr(0, catch2_prefix.size()) ==
catch2_prefix) {
// NOLINTNEXTLINE(performance-inefficient-string-concatenation)
REQUIRE(resolved_include == catch2_include + "/" + stripped_include);
}
// expect everything else to be unresolved.
else {
REQUIRE(resolved_include == std::string{'<'} + stripped_include + ">");
}
// #include "./filesystem.hpp" -> src/megadep/filesystem.hpp
REQUIRE(resolve_include("\"./filesystem.hpp\"") ==
"src/megadep/filesystem.hpp");
// #include "../megadep/filesystem.hpp" -> src/megadep/filesystem.hpp
REQUIRE(resolve_include("\"../megadep/filesystem.hpp\"") ==
"src/megadep/filesystem.hpp");
// #include "megadep/filesystem.hpp" -> src/megadep/filesystem.hpp
REQUIRE(resolve_include("\"megadep/filesystem.hpp\"") ==
"src/megadep/filesystem.hpp");
// #include "megadep/../megadep/filesystem.hpp"
// -> src/megadep/filesystem.hpp
REQUIRE(resolve_include("\"megadep/../megadep/filesystem.hpp\"") ==
"src/megadep/filesystem.hpp");
// #include "cstdio" -> <cstdio>
REQUIRE(resolve_include("\"cstdio\"") == "<cstdio>");
}
}
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