Skip to content

Commit fef0ace

Browse files
committed
Allow finding config even if file doesn't exist
To support editors which allow editing new, not-created, unsaved files, we need to search for config files even if the file (or its parent directory) doesn't exist. Add smarts to canonicalize_path to succeed even if the file doesn't exist.
1 parent ed1859a commit fef0ace

File tree

6 files changed

+247
-39
lines changed

6 files changed

+247
-39
lines changed

src/configuration-loader.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,13 @@ configuration* configuration_loader::find_and_load_config_file(
6363
return nullptr;
6464
}
6565

66+
bool should_drop_file_name = input_path != nullptr;
67+
if (canonical_input_path.have_missing_components()) {
68+
canonical_input_path.drop_missing_components();
69+
should_drop_file_name = false;
70+
}
6671
canonical_path parent_directory = std::move(canonical_input_path).canonical();
67-
if (input_path) {
72+
if (should_drop_file_name) {
6873
parent_directory.parent();
6974
}
7075

src/file-canonical.cpp

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -171,10 +171,9 @@ bool canonical_path::parent() {
171171

172172
canonical_path_result::canonical_path_result() {}
173173

174-
canonical_path_result::canonical_path_result(std::string &&path)
175-
: path_(std::move(path)) {}
176-
177-
canonical_path_result::canonical_path_result(const char *path) : path_(path) {}
174+
canonical_path_result::canonical_path_result(std::string &&path,
175+
std::size_t existing_path_length)
176+
: path_(std::move(path)), existing_path_length_(existing_path_length) {}
178177

179178
std::string_view canonical_path_result::path() const &noexcept {
180179
QLJS_ASSERT(this->ok());
@@ -206,6 +205,16 @@ std::string &&canonical_path_result::error() && noexcept {
206205
return std::move(this->error_);
207206
}
208207

208+
bool canonical_path_result::have_missing_components() const noexcept {
209+
QLJS_ASSERT(this->ok());
210+
return this->existing_path_length_ != this->path_->path_.size();
211+
}
212+
213+
void canonical_path_result::drop_missing_components() {
214+
QLJS_ASSERT(this->ok());
215+
this->path_->path_.resize(this->existing_path_length_);
216+
}
217+
209218
canonical_path_result canonical_path_result::failure(std::string &&error) {
210219
canonical_path_result result;
211220
result.error_ = std::move(error);
@@ -224,9 +233,13 @@ class path_canonicalizer {
224233
}
225234
#if QLJS_PATHS_WIN32
226235
// HACK(strager): Convert UTF-16 to UTF-8.
227-
return canonical_path_result(std::filesystem::path(canonical_).string());
236+
// TODO(strager): existing_path_length_ is in UTF-16 code units, but it's
237+
// interpreted as UTF-8 code units! Fix by storing a std::wstring in
238+
// canonical_path.
239+
return canonical_path_result(std::filesystem::path(canonical_).string(),
240+
existing_path_length_);
228241
#else
229-
return canonical_path_result(std::move(canonical_));
242+
return canonical_path_result(std::move(canonical_), existing_path_length_);
230243
#endif
231244
}
232245

@@ -251,13 +264,18 @@ class path_canonicalizer {
251264
#endif
252265
canonical_ += preferred_component_separator;
253266
}
267+
268+
if (existing_path_length_ == 0) {
269+
existing_path_length_ = canonical_.size();
270+
}
254271
}
255272

256273
private:
257274
enum class file_type {
258275
error,
259276

260277
directory,
278+
does_not_exist,
261279
other,
262280
symlink,
263281
};
@@ -358,19 +376,41 @@ class path_canonicalizer {
358376
if (component == dot) {
359377
skip_to_next_component();
360378
} else if (component == dot_dot) {
361-
parent();
379+
if (existing_path_length_ == 0) {
380+
parent();
381+
} else {
382+
QLJS_ASSERT(!canonical_.empty());
383+
canonical_ += preferred_component_separator;
384+
canonical_ += component;
385+
}
362386
skip_to_next_component();
363387
} else {
388+
std::size_t canonical_length_without_component = canonical_.size();
389+
364390
canonical_ += preferred_component_separator;
365391
canonical_ += component;
366392
need_root_slash_ = false;
367393

394+
if (existing_path_length_ != 0) {
395+
// A parent path did not exist, so this path certainly does not exist.
396+
// Don't bother checking.
397+
skip_to_next_component();
398+
return;
399+
}
400+
368401
file_type type = get_file_type(canonical_);
369402
switch (type) {
370403
case file_type::error:
371404
QLJS_ASSERT(!error_.empty());
372405
return;
373406

407+
case file_type::does_not_exist:
408+
if (existing_path_length_ == 0) {
409+
existing_path_length_ = canonical_length_without_component;
410+
}
411+
skip_to_next_component();
412+
break;
413+
374414
case file_type::directory:
375415
skip_to_next_component();
376416
break;
@@ -448,10 +488,14 @@ class path_canonicalizer {
448488
#if defined(_WIN32)
449489
DWORD attributes = ::GetFileAttributesW(file_path.c_str());
450490
if (attributes == INVALID_FILE_ATTRIBUTES) {
491+
DWORD error = ::GetLastError();
492+
if (error == ERROR_FILE_NOT_FOUND) {
493+
return file_type::does_not_exist;
494+
}
451495
error_ = std::string("failed to canonicalize path ") +
452496
string_for_error_message(original_path_) + ": " +
453497
string_for_error_message(canonical_) + ": " +
454-
windows_error_message(::GetLastError());
498+
windows_error_message(error);
455499
return file_type::error;
456500
}
457501
if (attributes & FILE_ATTRIBUTE_REPARSE_POINT) {
@@ -465,6 +509,9 @@ class path_canonicalizer {
465509
struct stat s;
466510
int lstat_rc = ::lstat(file_path.c_str(), &s);
467511
if (lstat_rc == -1) {
512+
if (errno == ENOENT) {
513+
return file_type::does_not_exist;
514+
}
468515
error_ = std::string("failed to canonicalize path ") +
469516
string_for_error_message(original_path_) + ": " +
470517
string_for_error_message(canonical_) + ": " +
@@ -540,6 +587,13 @@ class path_canonicalizer {
540587
path_string canonical_;
541588
bool need_root_slash_;
542589

590+
// During canonicalization, if existing_path_length_ is 0, then we have not
591+
// found a non-existing path.
592+
//
593+
// During canonicalization, if existing_path_length_ is not 0, then we have
594+
// found a non-existing path. '..' should be preserved.
595+
std::size_t existing_path_length_ = 0;
596+
543597
path_string readlink_buffers_[2];
544598
int used_readlink_buffer_ = 0; // Index into readlink_buffers_.
545599

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,24 @@
1111
#include <string_view>
1212

1313
namespace quick_lint_js {
14+
class canonical_path_result;
15+
1416
// A filesystem path.
1517
//
1618
// * The path is absolute (i.e. not relative to the current working directory or
1719
// current drive). (The path is relative to the current chroot/jail/namespace,
1820
// though.)
19-
// * The path has no '.' or '..' components.
21+
// * The path has no '.' components.
22+
// * The path has no '..' components, unless the exception mentioned below
23+
// applies.
2024
// * The path has no redundant component separators (\ or /, depending on the
2125
// operating system).
2226
// * No subpath refers to a symlink, assuming no changes to the filesystem since
2327
// creation of the canonical_path.
28+
//
29+
// Exception to the above rules: A canonical_path can contain one or more '..'
30+
// components before canonical_path_result::drop_missing_components has been
31+
// called.
2432
class canonical_path {
2533
public:
2634
// Does not check the validity of the path.
@@ -57,12 +65,14 @@ class canonical_path {
5765

5866
private:
5967
std::string path_;
68+
69+
friend canonical_path_result;
6070
};
6171

6272
class canonical_path_result {
6373
public:
64-
explicit canonical_path_result(std::string &&path);
65-
explicit canonical_path_result(const char *path);
74+
explicit canonical_path_result(std::string &&path,
75+
std::size_t existing_path_length);
6676

6777
std::string_view path() const &noexcept;
6878
std::string &&path() && noexcept;
@@ -75,13 +85,17 @@ class canonical_path_result {
7585

7686
bool ok() const noexcept { return this->error_.empty(); }
7787

88+
bool have_missing_components() const noexcept;
89+
void drop_missing_components();
90+
7891
static canonical_path_result failure(std::string &&error);
7992

8093
private:
8194
explicit canonical_path_result();
8295

8396
std::optional<canonical_path> path_;
8497
std::string error_;
98+
std::size_t existing_path_length_;
8599
};
86100

87101
canonical_path_result canonicalize_path(const char *path);

test/quick-lint-js/file-matcher.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,12 @@ inline ::testing::AssertionResult assert_same_file(const char* lhs_expr,
3737
#elif QLJS_HAVE_SYS_STAT_H
3838
{
3939
struct stat lhs_stat = {};
40-
EXPECT_EQ(::lstat(lhs_path, &lhs_stat), 0) << std::strerror(errno);
40+
EXPECT_EQ(::lstat(lhs_path, &lhs_stat), 0)
41+
<< lhs_path << ": " << std::strerror(errno);
4142

4243
struct stat rhs_stat = {};
43-
EXPECT_EQ(::lstat(rhs_path, &rhs_stat), 0) << std::strerror(errno);
44+
EXPECT_EQ(::lstat(rhs_path, &rhs_stat), 0)
45+
<< rhs_path << ": " << std::strerror(errno);
4446

4547
same = lhs_stat.st_dev == rhs_stat.st_dev &&
4648
lhs_stat.st_ino == rhs_stat.st_ino;

test/test-configuration-loader.cpp

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include <quick-lint-js/configuration.h>
1111
#include <quick-lint-js/file-canonical.h>
1212
#include <quick-lint-js/file-matcher.h>
13+
#include <quick-lint-js/file-path.h>
1314
#include <quick-lint-js/file.h>
1415
#include <quick-lint-js/options.h>
1516
#include <quick-lint-js/temporary-directory.h>
@@ -414,7 +415,9 @@ TEST_F(test_configuration_loader, missing_config_file_fails) {
414415
});
415416

416417
EXPECT_FALSE(config);
417-
EXPECT_THAT(loader.error(), HasSubstr(config_file));
418+
EXPECT_THAT(loader.error(),
419+
HasSubstr(temp_dir + QLJS_PREFERRED_PATH_DIRECTORY_SEPARATOR +
420+
"config.json"));
418421
EXPECT_THAT(loader.error(),
419422
AnyOf(HasSubstr("No such file"), HasSubstr("cannot find")));
420423
}
@@ -559,7 +562,8 @@ TEST_F(
559562
}
560563
}
561564

562-
TEST_F(test_configuration_loader, finding_config_fails_if_file_is_missing) {
565+
TEST_F(test_configuration_loader,
566+
finding_config_succeeds_even_if_file_is_missing) {
563567
std::string temp_dir = this->make_temporary_directory();
564568
std::string config_file = temp_dir + "/quick-lint-js.config";
565569
write_file(config_file, u8R"({})"sv);
@@ -571,10 +575,25 @@ TEST_F(test_configuration_loader, finding_config_fails_if_file_is_missing) {
571575
.config_file = nullptr,
572576
});
573577

574-
EXPECT_FALSE(config);
575-
EXPECT_THAT(loader.error(), HasSubstr(js_file));
576-
EXPECT_THAT(loader.error(),
577-
AnyOf(HasSubstr("No such file"), HasSubstr("cannot find")));
578+
EXPECT_TRUE(config);
579+
EXPECT_SAME_FILE(config->config_file_path(), config_file);
580+
}
581+
582+
TEST_F(test_configuration_loader,
583+
finding_config_succeeds_even_if_directory_is_missing) {
584+
std::string temp_dir = this->make_temporary_directory();
585+
std::string config_file = temp_dir + "/quick-lint-js.config";
586+
write_file(config_file, u8R"({})"sv);
587+
588+
std::string js_file = temp_dir + "/dir/hello.js";
589+
configuration_loader loader;
590+
configuration* config = loader.load_for_file(file_to_lint{
591+
.path = js_file.c_str(),
592+
.config_file = nullptr,
593+
});
594+
595+
EXPECT_TRUE(config);
596+
EXPECT_SAME_FILE(config->config_file_path(), config_file);
578597
}
579598
}
580599
}

0 commit comments

Comments
 (0)