Skip to content

Commit 6f3032c

Browse files
committed
Refactor canonical_path users; reduce std::string usage
Add append_component() and parent() to canonical_path so users of canonical_path don't need to convert to strings manually. This commit should not change behavior.
1 parent 3feca5c commit 6f3032c

File tree

5 files changed

+205
-56
lines changed

5 files changed

+205
-56
lines changed

src/configuration-loader.cpp

Lines changed: 12 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,10 @@ configuration* configuration_loader::find_and_load_config_file(
6363
return nullptr;
6464
}
6565

66-
std::string parent_directory =
67-
input_path ? parent_path(std::move(canonical_input_path).path())
68-
: std::move(canonical_input_path).path();
66+
canonical_path parent_directory = std::move(canonical_input_path).canonical();
67+
if (input_path) {
68+
parent_directory.parent();
69+
}
6970

7071
// TODO(strager): Directory -> config to reduce lookups in cases like the
7172
// following:
@@ -74,14 +75,12 @@ configuration* configuration_loader::find_and_load_config_file(
7475
// config path: ./quick-lint-js.config
7576

7677
for (;;) {
77-
std::string config_path = parent_directory;
78-
for (const std::string_view& suffix : {
79-
QLJS_PREFERRED_PATH_DIRECTORY_SEPARATOR "quick-lint-js.config"sv,
80-
QLJS_PREFERRED_PATH_DIRECTORY_SEPARATOR ".quick-lint-js.config"sv,
78+
for (const std::string_view& file_name : {
79+
"quick-lint-js.config"sv,
80+
".quick-lint-js.config"sv,
8181
}) {
82-
config_path.resize(parent_directory.size());
83-
QLJS_ASSERT(config_path == parent_directory);
84-
config_path.append(suffix);
82+
canonical_path config_path = parent_directory;
83+
config_path.append_component(file_name);
8584

8685
if (configuration* config = this->get_loaded_config(config_path)) {
8786
return config;
@@ -90,12 +89,11 @@ configuration* configuration_loader::find_and_load_config_file(
9089
read_file_result config_json = read_file(config_path.c_str());
9190
if (config_json.ok()) {
9291
auto [config_it, inserted] = this->loaded_config_files_.emplace(
93-
std::piecewise_construct,
94-
std::forward_as_tuple(canonical_path(std::string(config_path))),
92+
std::piecewise_construct, std::forward_as_tuple(config_path),
9593
std::forward_as_tuple());
9694
QLJS_ASSERT(inserted);
9795
configuration* config = &config_it->second;
98-
config->set_config_file_path(std::move(config_path));
96+
config->set_config_file_path(std::move(config_path).path());
9997
config->load_from_json(&config_json.content);
10098
return config;
10199
}
@@ -108,33 +106,17 @@ configuration* configuration_loader::find_and_load_config_file(
108106
}
109107

110108
// Loop, looking in parent directories.
111-
std::string new_parent_directory =
112-
parent_path(std::string(parent_directory));
113-
if (new_parent_directory == parent_directory) {
109+
if (!parent_directory.parent()) {
114110
// We searched the root directory which has no parent.
115111
break;
116112
}
117-
parent_directory = std::move(new_parent_directory);
118113
}
119114

120115
return &this->default_config_;
121116
}
122117

123118
QLJS_WARNING_POP
124119

125-
configuration* configuration_loader::get_loaded_config(
126-
const std::string& path) noexcept {
127-
#if QLJS_HAVE_STD_TRANSPARENT_KEYS
128-
auto existing_config_it = this->loaded_config_files_.find(path);
129-
#else
130-
auto existing_config_it =
131-
this->loaded_config_files_.find(canonical_path(std::string(path)));
132-
#endif
133-
return existing_config_it == this->loaded_config_files_.end()
134-
? nullptr
135-
: &existing_config_it->second;
136-
}
137-
138120
configuration* configuration_loader::get_loaded_config(
139121
const canonical_path& path) noexcept {
140122
auto existing_config_it = this->loaded_config_files_.find(path);

src/file-canonical.cpp

Lines changed: 76 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,35 @@
4444

4545
namespace quick_lint_js {
4646
namespace {
47+
#if QLJS_PATHS_WIN32
48+
using path_char = wchar_t;
49+
#elif QLJS_PATHS_POSIX
50+
using path_char = char;
51+
#else
52+
#error "Unsupported platform"
53+
#endif
54+
using path_string = std::basic_string<path_char>;
55+
using path_string_view = std::basic_string_view<path_char>;
56+
57+
#if QLJS_PATHS_WIN32
58+
constexpr path_char preferred_component_separator = L'\\';
59+
constexpr path_char component_separators[] = L"/\\";
60+
constexpr path_string_view dot = L".";
61+
constexpr path_string_view dot_dot = L"..";
62+
#elif QLJS_PATHS_POSIX
63+
constexpr path_char preferred_component_separator = '/';
64+
constexpr path_char component_separators[] = "/";
65+
constexpr path_string_view dot = ".";
66+
constexpr path_string_view dot_dot = "..";
67+
#else
68+
#error "Unsupported platform"
69+
#endif
70+
71+
// TODO(strager): Avoid hard-coding std::string. Use std::wstring always on
72+
// Windows.
73+
constexpr char preferred_component_separator_char =
74+
static_cast<char>(preferred_component_separator);
75+
4776
#if QLJS_HAVE_UNISTD_H
4877
int read_symbolic_link(const char *symlink_path, std::string *out);
4978
#endif
@@ -58,7 +87,9 @@ std::string string_for_error_message(std::wstring_view);
5887
#endif
5988
}
6089

61-
canonical_path::canonical_path(std::string &&path) : path_(std::move(path)) {}
90+
canonical_path::canonical_path(std::string &&path) : path_(std::move(path)) {
91+
QLJS_ASSERT(!this->path_.empty());
92+
}
6293

6394
std::string_view canonical_path::path() const &noexcept { return this->path_; }
6495

@@ -94,6 +125,50 @@ bool operator!=(const canonical_path &lhs, std::string_view rhs) noexcept {
94125
return !(lhs == rhs);
95126
}
96127

128+
void canonical_path::append_component(std::string_view component) {
129+
if (this->path_[this->path_.size() - 1] !=
130+
preferred_component_separator_char) {
131+
this->path_.push_back(preferred_component_separator_char);
132+
}
133+
this->path_.append(component);
134+
}
135+
136+
bool canonical_path::parent() {
137+
#if QLJS_PATHS_POSIX
138+
if (this->path_[this->path_.size() - 1] == '/') {
139+
return false;
140+
}
141+
std::size_t slash_index = this->path_.find_last_of("/");
142+
QLJS_ASSERT(slash_index != std::string::npos);
143+
if (slash_index == 0 || slash_index == 1) {
144+
// Preserve the slash for resulting root paths.
145+
this->path_.resize(slash_index + 1);
146+
} else {
147+
this->path_.resize(slash_index);
148+
}
149+
return true;
150+
#elif QLJS_PATHS_WIN32
151+
// TODO(strager): Avoid string conversions and copying.
152+
std::optional<std::wstring> wpath = mbstring_to_wstring(this->path_.c_str());
153+
QLJS_ASSERT(wpath.has_value());
154+
HRESULT result = ::PathCchRemoveFileSpec(wpath->data(), wpath->size() + 1);
155+
switch (result) {
156+
case S_OK:
157+
wpath->resize(std::wcslen(wpath->data()));
158+
this->path_ = std::filesystem::path(*wpath).string();
159+
return true;
160+
case S_FALSE:
161+
// Path is a root path already.
162+
return false;
163+
default:
164+
QLJS_UNIMPLEMENTED();
165+
break;
166+
}
167+
#else
168+
#error "Unsupported platform"
169+
#endif
170+
}
171+
97172
canonical_path_result::canonical_path_result() {}
98173

99174
canonical_path_result::canonical_path_result(std::string &&path)
@@ -141,30 +216,6 @@ canonical_path_result canonical_path_result::failure(std::string &&error) {
141216
namespace {
142217
class path_canonicalizer {
143218
public:
144-
#if QLJS_PATHS_WIN32
145-
using path_char = wchar_t;
146-
#elif QLJS_PATHS_POSIX
147-
using path_char = char;
148-
#else
149-
#error "Unsupported platform"
150-
#endif
151-
using path_string = std::basic_string<path_char>;
152-
using path_string_view = std::basic_string_view<path_char>;
153-
154-
#if QLJS_PATHS_WIN32
155-
static constexpr path_char preferred_component_separator = L'\\';
156-
static constexpr path_char component_separators[] = L"/\\";
157-
static constexpr path_string_view dot = L".";
158-
static constexpr path_string_view dot_dot = L"..";
159-
#elif QLJS_PATHS_POSIX
160-
static constexpr path_char preferred_component_separator = '/';
161-
static constexpr path_char component_separators[] = "/";
162-
static constexpr path_string_view dot = ".";
163-
static constexpr path_string_view dot_dot = "..";
164-
#else
165-
#error "Unsupported platform"
166-
#endif
167-
168219
explicit path_canonicalizer(path_string_view path) : original_path_(path) {}
169220

170221
canonical_path_result result() {

src/quick-lint-js/configuration-loader.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ class configuration_loader {
2222
configuration* load_config_file(const char* config_path);
2323
configuration* find_and_load_config_file(const char* input_path);
2424

25-
configuration* get_loaded_config(const std::string& path) noexcept;
2625
configuration* get_loaded_config(const canonical_path& path) noexcept;
2726

2827
configuration default_config_;

src/quick-lint-js/file-canonical.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,22 @@ class canonical_path {
3030
std::string &&path() && noexcept;
3131
const char *c_str() const noexcept;
3232

33+
// Add a new component to the end of the path.
34+
//
35+
// This function does not consult the filesystem.
36+
void append_component(std::string_view component);
37+
38+
// Remove the last component of the path.
39+
//
40+
// If the path is a root path, this function returns false does not modify
41+
// *this.
42+
//
43+
// If the path is not a root path, this function returns true and modifies
44+
// *this.
45+
//
46+
// This function does not consult the filesystem.
47+
bool parent();
48+
3349
friend bool operator==(const canonical_path &,
3450
const canonical_path &) noexcept;
3551
friend bool operator!=(const canonical_path &,

test/test-file-canonical.cpp

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,107 @@ TEST_F(test_file_canonical, canonical_path_win32_root) {
701701
}
702702
}
703703
#endif
704+
705+
#if defined(_POSIX_VERSION) && _POSIX_VERSION >= 200112L
706+
TEST_F(test_file_canonical, parent_of_root_is_root_posix) {
707+
for (const char* root : {"/", "//"}) {
708+
canonical_path p(root);
709+
EXPECT_FALSE(p.parent());
710+
EXPECT_EQ(p.path(), root);
711+
}
712+
}
713+
714+
TEST_F(test_file_canonical, parent_of_non_root_removes_component_posix) {
715+
struct test_case {
716+
const std::string_view before;
717+
const std::string_view after;
718+
};
719+
720+
for (const test_case& tc : {
721+
test_case{"/hello", "/"},
722+
test_case{"/dir/subdir", "/dir"},
723+
test_case{"/a/b/c/d", "/a/b/c"},
724+
test_case{"//implementation-defined", "//"},
725+
test_case{"//implementation/defined", "//implementation"},
726+
}) {
727+
canonical_path p(std::string(tc.before));
728+
EXPECT_TRUE(p.parent());
729+
EXPECT_EQ(p.path(), tc.after) << "before = " << tc.before;
730+
}
731+
}
732+
733+
TEST_F(test_file_canonical, append_component_posix) {
734+
struct test_case {
735+
const std::string_view before;
736+
const std::string_view component;
737+
const std::string_view after;
738+
};
739+
740+
for (const test_case& tc : {
741+
test_case{"/", "hello", "/hello"},
742+
test_case{"/dir", "subdir", "/dir/subdir"},
743+
test_case{"//", "hello", "//hello"},
744+
test_case{"//dir", "subdir", "//dir/subdir"},
745+
}) {
746+
canonical_path p(std::string(tc.before));
747+
p.append_component(tc.component);
748+
EXPECT_EQ(p.path(), tc.after)
749+
<< "before = " << tc.before << "\ncomponent = " << tc.component;
750+
}
751+
}
752+
#endif
753+
754+
#if defined(_WIN32)
755+
TEST_F(test_file_canonical, parent_of_root_is_root_win32) {
756+
for (const char* root : {R"(C:\)", R"(\\?\X:\)", R"(\\server\share)"}) {
757+
canonical_path p(root);
758+
EXPECT_FALSE(p.parent());
759+
EXPECT_EQ(p.path(), root);
760+
}
761+
}
762+
763+
TEST_F(test_file_canonical, parent_of_non_root_removes_component_win32) {
764+
struct test_case {
765+
const std::string_view before;
766+
const std::string_view after;
767+
};
768+
769+
for (const test_case& tc : {
770+
test_case{R"(C:\hello)", R"(C:\)"},
771+
test_case{R"(C:\dir\subdir)", R"(C:\dir)"},
772+
test_case{R"(C:\a\b\c\d)", R"(C:\a\b\c)"},
773+
test_case{R"(\\?\X:\hello)", R"(\\?\X:\)"},
774+
test_case{R"(\\?\X:\dir\subdir)", R"(\\?\X:\dir)"},
775+
test_case{R"(\\?\X:\a\b\c\d)", R"(\\?\X:\a\b\c)"},
776+
test_case{R"(\\server\share\file)", R"(\\server\share)"},
777+
}) {
778+
canonical_path p(std::string(tc.before));
779+
EXPECT_TRUE(p.parent());
780+
EXPECT_EQ(p.path(), tc.after) << "before = " << tc.before;
781+
}
782+
}
783+
784+
TEST_F(test_file_canonical, append_component_win32) {
785+
struct test_case {
786+
const std::string_view before;
787+
const std::string_view component;
788+
const std::string_view after;
789+
};
790+
791+
for (const test_case& tc : {
792+
test_case{R"(C:\)", R"(hello)", R"(C:\hello)"},
793+
test_case{R"(C:\dir)", R"(subdir)", R"(C:\dir\subdir)"},
794+
test_case{R"(\\?\X:\)", R"(hello)", R"(\\?\X:\hello)"},
795+
test_case{R"(\\?\X:\dir)", R"(subdir)", R"(\\?\X:\dir\subdir)"},
796+
test_case{R"(\\server\share)", R"(dir)", R"(\\server\share\dir)"},
797+
}) {
798+
canonical_path p(std::string(tc.before));
799+
p.append_component(tc.component);
800+
EXPECT_EQ(p.path(), tc.after)
801+
<< "before = " << tc.before << "\ncomponent = " << tc.component;
802+
}
803+
}
804+
#endif
704805
}
705806
}
706807

0 commit comments

Comments
 (0)