Skip to content

Commit 0845021

Browse files
committed
[4/n] Configurable globals: find quick-lint-js.config (CLI)
In the CLI, automatically search for a file called quick-lint-js.config or .quick-lint-js.config. This implements most of the directory search system documented in docs/config.adoc. Not yet implemented: * Searching for and using package.json * Configuring in the LSP server * Configuring in the VS Code plugin Implementation details: * Canonicalize paths before searching. This avoids a bunch of gotchas when symbolic links are involved. * Stop searching for config files at the filesystem root. * Reuse loaded config files if multiple inputs are being linted. * Remember the path to the config file in the configuration class for easier testing. This also might be useful for a --dump-config option in the future.
1 parent db62ab9 commit 0845021

15 files changed

+988
-20
lines changed

docs/config.adoc

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,11 @@ and also quick-lint-js editor plugins, can be configured using a `quick-lint-js.
4343

4444
quick-lint-js uses the following algorithm to find its configuration:
4545

46-
1. Remember the directory containing the input JavaScript file as the _current directory_.
47-
If the input JavaScript file has no path, remember the current working directory as the _current directory_.
46+
1. Compute the absolute canonical path of the input JavaScript file by joining the JavaScript file's given path with the current working directory, following all symbolic links, and resolving all `.` and `..` components.
47+
Remove the last component of the absolute canonical path.
48+
Remember this path as the _current directory_.
49+
+
50+
If the input JavaScript file has no path (e.g. if its contents are given using standard input), remember the current working directory as the _current directory_.
4851
2. Look for a configuration file in the _current directory_:
4952
a. Check if the file `quick-lint-js.config` exists.
5053
If so, use it as the configuration file and stop.
@@ -54,9 +57,7 @@ quick-lint-js uses the following algorithm to find its configuration:
5457
If so, use it as the configuration file and stop.
5558
d. Go to step 3.
5659
3. If the _current directory_ is a filesystem root, assume no configuration file and stop.
57-
4. Remember the parent of the _current directory_ as the _current directory_.
58-
The parent of a directory is determined by suffixing a `..` component to the directory's path.
59-
(This method of determining the parent affects how symbolic links in paths are handled.)
60+
4. Remove the last component of the _current directory_.
6061
5. Go to step 2.
6162

6263
In short, quick-lint-js looks for `quick-lint-js.config`, `.quick-lint-js.config`, or `package.json` in ancestor directories.

docs/man/quick-lint-js.config.5

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,11 @@ quick\-lint\-js uses the following algorithm to find its configuration:
6161
. sp -1
6262
. IP " 1." 4.2
6363
.\}
64-
Remember the directory containing the input JavaScript file as the \fIcurrent directory\fP.
65-
If the input JavaScript file has no path, remember the current working directory as the \fIcurrent directory\fP.
64+
Compute the absolute canonical path of the input JavaScript file by joining the JavaScript file\(cqs given path with the current working directory, following all symbolic links, and resolving all \f(CR.\fP and \f(CR..\fP components.
65+
Remove the last component of the absolute canonical path.
66+
Remember this path as the \fIcurrent directory\fP.
67+
.sp
68+
If the input JavaScript file has no path (e.g. if its contents are given using standard input), remember the current working directory as the \fIcurrent directory\fP.
6669
.RE
6770
.sp
6871
.RS 4
@@ -142,9 +145,7 @@ If the \fIcurrent directory\fP is a filesystem root, assume no configuration fil
142145
. sp -1
143146
. IP " 4." 4.2
144147
.\}
145-
Remember the parent of the \fIcurrent directory\fP as the \fIcurrent directory\fP.
146-
The parent of a directory is determined by suffixing a \f(CR..\fP component to the directory\(cqs path.
147-
(This method of determining the parent affects how symbolic links in paths are handled.)
148+
Remove the last component of the \fIcurrent directory\fP.
148149
.RE
149150
.sp
150151
.RS 4

src/CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ quick_lint_js_add_library(
4141
error-formatter.cpp
4242
error-list.cpp
4343
file-handle.cpp
44+
file-path.cpp
4445
file.cpp
4546
gmo.cpp
4647
integer.cpp
@@ -83,6 +84,7 @@ quick_lint_js_add_library(
8384
quick-lint-js/expression.h
8485
quick-lint-js/feature.h
8586
quick-lint-js/file-handle.h
87+
quick-lint-js/file-path.h
8688
quick-lint-js/file.h
8789
quick-lint-js/force-inline.h
8890
quick-lint-js/gmo.h
@@ -134,6 +136,9 @@ quick_lint_js_add_library(
134136
)
135137
target_include_directories(quick-lint-js-lib PUBLIC .)
136138
target_link_libraries(quick-lint-js-lib PUBLIC boost_container simdjson)
139+
if (WIN32)
140+
target_link_libraries(quick-lint-js-lib PUBLIC Pathcch)
141+
endif ()
137142
if (${CMAKE_VERSION} VERSION_GREATER_EQUAL 3.17.3)
138143
target_precompile_headers(
139144
quick-lint-js-lib

src/configuration-loader.cpp

Lines changed: 99 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,125 @@
11
// Copyright (C) 2020 Matthew Glazar
22
// See end of file for extended copyright information.
33

4+
#include <quick-lint-js/assert.h>
45
#include <quick-lint-js/configuration-loader.h>
56
#include <quick-lint-js/configuration.h>
7+
#include <quick-lint-js/file-path.h>
68
#include <quick-lint-js/file.h>
79
#include <quick-lint-js/options.h>
10+
#include <quick-lint-js/warning.h>
811
#include <string_view>
912
#include <unordered_map>
1013

14+
using namespace std::literals::string_view_literals;
15+
1116
namespace quick_lint_js {
1217
configuration* configuration_loader::load_for_file(const file_to_lint& file) {
13-
if (!file.config_file) {
14-
return &this->default_config_;
18+
if (file.config_file) {
19+
return this->load_config_file(file.config_file);
20+
} else {
21+
return this->find_and_load_config_file(file.path);
1522
}
16-
auto existing_config_it = this->loaded_config_files_.find(file.config_file);
17-
if (existing_config_it != this->loaded_config_files_.end()) {
18-
return &existing_config_it->second;
23+
}
24+
25+
configuration* configuration_loader::load_config_file(const char* config_path) {
26+
canonical_path_result canonical_config_path = canonicalize_path(config_path);
27+
if (!canonical_config_path.ok()) {
28+
this->last_error_ = std::move(canonical_config_path.error);
29+
return nullptr;
1930
}
2031

21-
read_file_result config_json = read_file(file.config_file);
32+
if (configuration* config =
33+
this->get_loaded_config(canonical_config_path.c_str())) {
34+
return config;
35+
}
36+
read_file_result config_json = read_file(canonical_config_path.c_str());
2237
if (!config_json.ok()) {
2338
this->last_error_ = std::move(config_json.error);
2439
return nullptr;
2540
}
26-
configuration* config = &this->loaded_config_files_[file.config_file];
41+
configuration* config =
42+
&this->loaded_config_files_[canonical_config_path.path];
43+
config->set_config_file_path(canonical_config_path.c_str());
2744
config->load_from_json(&config_json.content);
2845
return config;
2946
}
3047

48+
QLJS_WARNING_PUSH
49+
QLJS_WARNING_IGNORE_GCC("-Wuseless-cast")
50+
51+
configuration* configuration_loader::find_and_load_config_file(
52+
const char* input_path) {
53+
canonical_path_result canonical_input_path =
54+
canonicalize_path(input_path ? input_path : ".");
55+
if (!canonical_input_path.ok()) {
56+
this->last_error_ = std::move(canonical_input_path.error);
57+
return nullptr;
58+
}
59+
60+
std::string parent_directory =
61+
input_path ? parent_path(std::move(canonical_input_path.path))
62+
: std::move(canonical_input_path.path);
63+
64+
// TODO(strager): Directory -> config to reduce lookups in cases like the
65+
// following:
66+
//
67+
// input paths: ./a/b/c/d/1.js, ./a/b/c/d/2.js, ./a/b/c/d/3.js
68+
// config path: ./quick-lint-js.config
69+
70+
for (;;) {
71+
std::string config_path = parent_directory;
72+
for (const std::string_view& suffix : {
73+
QLJS_PREFERRED_PATH_DIRECTORY_SEPARATOR "quick-lint-js.config"sv,
74+
QLJS_PREFERRED_PATH_DIRECTORY_SEPARATOR ".quick-lint-js.config"sv,
75+
}) {
76+
config_path.resize(parent_directory.size());
77+
QLJS_ASSERT(config_path == parent_directory);
78+
config_path.append(suffix);
79+
80+
if (configuration* config =
81+
this->get_loaded_config(config_path.c_str())) {
82+
return config;
83+
}
84+
85+
read_file_result config_json = read_file(config_path.c_str());
86+
if (config_json.ok()) {
87+
configuration* config = &this->loaded_config_files_[config_path];
88+
config->set_config_file_path(std::move(config_path));
89+
config->load_from_json(&config_json.content);
90+
return config;
91+
}
92+
if (!config_json.is_not_found_error) {
93+
this->last_error_ = std::move(config_json.error);
94+
return nullptr;
95+
}
96+
97+
// Loop, looking for a different file.
98+
}
99+
100+
// Loop, looking in parent directories.
101+
std::string new_parent_directory =
102+
parent_path(std::string(parent_directory));
103+
if (new_parent_directory == parent_directory) {
104+
// We searched the root directory which has no parent.
105+
break;
106+
}
107+
parent_directory = std::move(new_parent_directory);
108+
}
109+
110+
return &this->default_config_;
111+
}
112+
113+
QLJS_WARNING_POP
114+
115+
configuration* configuration_loader::get_loaded_config(
116+
const char* path) noexcept {
117+
auto existing_config_it = this->loaded_config_files_.find(path);
118+
return existing_config_it == this->loaded_config_files_.end()
119+
? nullptr
120+
: &existing_config_it->second;
121+
}
122+
31123
std::string configuration_loader::error() const { return this->last_error_; }
32124
}
33125

src/configuration.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ const global_declared_variable_set& configuration::globals() noexcept {
136136
return this->globals_;
137137
}
138138

139+
const std::string& configuration::config_file_path() const noexcept {
140+
return this->config_file_path_;
141+
}
142+
139143
void configuration::reset_global_groups() {
140144
this->add_global_group_node_js_ = false;
141145
this->add_global_group_ecmascript_ = false;
@@ -204,6 +208,10 @@ void configuration::load_from_json(padded_string_view json) {
204208
}
205209
}
206210

211+
void configuration::set_config_file_path(std::string&& path) {
212+
this->config_file_path_ = std::move(path);
213+
}
214+
207215
void configuration::load_global_groups_from_json(
208216
::simdjson::ondemand::value& global_groups_value) {
209217
::simdjson::fallback::ondemand::json_type global_groups_value_type;

src/file-path.cpp

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// Copyright (C) 2020 Matthew Glazar
2+
// See end of file for extended copyright information.
3+
4+
#include <quick-lint-js/assert.h>
5+
#include <quick-lint-js/file-path.h>
6+
#include <quick-lint-js/have.h>
7+
#include <quick-lint-js/narrow-cast.h>
8+
#include <quick-lint-js/utf-16.h>
9+
10+
#if QLJS_HAVE_WINDOWS_H
11+
#include <Windows.h>
12+
#include <pathcch.h>
13+
#endif
14+
15+
#if QLJS_HAVE_DIRNAME
16+
#include <libgen.h>
17+
#endif
18+
19+
#if QLJS_HAVE_STD_FILESYSTEM
20+
#include <filesystem>
21+
#endif
22+
23+
using namespace std::literals::string_view_literals;
24+
25+
namespace quick_lint_js {
26+
std::string parent_path(std::string&& path) {
27+
#if QLJS_HAVE_DIRNAME
28+
return ::dirname(path.data());
29+
#elif defined(QLJS_HAVE_WINDOWS_H)
30+
HRESULT result;
31+
32+
if (path == R"(\\?\)"sv || path == R"(\\?)"sv) {
33+
// Invalid path. Leave as-is.
34+
return path;
35+
}
36+
37+
std::optional<std::wstring> wpath = mbstring_to_wstring(path.c_str());
38+
if (!wpath.has_value()) {
39+
QLJS_UNIMPLEMENTED();
40+
}
41+
42+
// The PathCch functions only support '\' as a directory separator. Convert
43+
// all '/'s into '\'s.
44+
for (wchar_t& c : *wpath) {
45+
if (c == L'/') {
46+
c = L'\\';
47+
}
48+
}
49+
50+
remove_backslash:
51+
result = ::PathCchRemoveBackslash(wpath->data(), wpath->size() + 1);
52+
switch (result) {
53+
case S_OK:
54+
// PathCchRemoveBackslash removes only one backslash. Make sure we remove
55+
// them all.
56+
goto remove_backslash;
57+
case S_FALSE:
58+
// Path is a root path, or no backslashes needed removal.
59+
break;
60+
case HRESULT_FROM_WIN32(ERROR_INVALID_PARAMETER):
61+
// Path is invalid.
62+
QLJS_UNIMPLEMENTED();
63+
break;
64+
default:
65+
QLJS_UNIMPLEMENTED();
66+
break;
67+
}
68+
69+
result = ::PathCchRemoveFileSpec(wpath->data(), wpath->size() + 1);
70+
switch (result) {
71+
case S_OK:
72+
break;
73+
case S_FALSE:
74+
// Path is a root path already.
75+
break;
76+
case HRESULT_FROM_WIN32(ERROR_INVALID_PARAMETER):
77+
// Path is invalid.
78+
QLJS_UNIMPLEMENTED();
79+
break;
80+
default:
81+
QLJS_UNIMPLEMENTED();
82+
break;
83+
}
84+
85+
wpath->resize(std::wcslen(wpath->data()));
86+
if (wpath->empty()) {
87+
return ".";
88+
}
89+
90+
std::string result_with_backslashes = std::filesystem::path(*wpath).string();
91+
92+
// Convert '\' back into '/' if necessary.
93+
#if !(defined(NDEBUG) && NDEBUG)
94+
{
95+
std::string path_with_backslashes = path;
96+
for (char& c : path_with_backslashes) {
97+
if (c == '/') {
98+
c = '\\';
99+
}
100+
}
101+
QLJS_ALWAYS_ASSERT(
102+
path_with_backslashes.starts_with(result_with_backslashes));
103+
}
104+
#endif
105+
return path.substr(0, result_with_backslashes.size());
106+
#endif
107+
}
108+
}
109+
110+
// quick-lint-js finds bugs in JavaScript programs.
111+
// Copyright (C) 2020 Matthew Glazar
112+
//
113+
// This file is part of quick-lint-js.
114+
//
115+
// quick-lint-js is free software: you can redistribute it and/or modify
116+
// it under the terms of the GNU General Public License as published by
117+
// the Free Software Foundation, either version 3 of the License, or
118+
// (at your option) any later version.
119+
//
120+
// quick-lint-js is distributed in the hope that it will be useful,
121+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
122+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
123+
// GNU General Public License for more details.
124+
//
125+
// You should have received a copy of the GNU General Public License
126+
// along with quick-lint-js. If not, see <https://www.gnu.org/licenses/>.

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
#define QUICK_LINT_JS_CONFIGURATION_LOADER_H
66

77
#include <quick-lint-js/configuration.h>
8-
#include <string_view>
8+
#include <string>
99
#include <unordered_map>
1010

1111
namespace quick_lint_js {
@@ -18,8 +18,13 @@ class configuration_loader {
1818
std::string error() const;
1919

2020
private:
21+
configuration* load_config_file(const char* config_path);
22+
configuration* find_and_load_config_file(const char* input_path);
23+
24+
configuration* get_loaded_config(const char* path) noexcept;
25+
2126
configuration default_config_;
22-
std::unordered_map<std::string_view, configuration> loaded_config_files_;
27+
std::unordered_map<std::string, configuration> loaded_config_files_;
2328
std::string last_error_;
2429
};
2530
}

0 commit comments

Comments
 (0)