Skip to content

Commit bf0541d

Browse files
committed
feat(cli): add --stdin-path option
--stdin-path will make it easy to have quick-lint-js detect the language (JavaScript, TypeScript, etc.) when used from Emacs Flymake without teaching the ELISP code all language variants (.d.ts, .tsx, etc.). This feature is similar to ESLint's --stdin-path option.
1 parent 8e34bd5 commit bf0541d

File tree

7 files changed

+228
-11
lines changed

7 files changed

+228
-11
lines changed

docs/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ Semantic Versioning.
2323
is not allowed on getters or setters"). (Implemented by [koopiehoop][].)
2424
* Emacs: The Debian/Ubuntu package now installs the Emacs plugin. Manual
2525
installation of the .el files is no longer required.
26+
* CLI: The new `--stdin-path` CLI option allows users of the `--stdin` option
27+
(primarily text editors) to have quick-lint-js detect the language
28+
automatically via `--language=default` or `--language=experimental-default`.
2629
* TypeScript support (still experimental):
2730
* CLI: The new `--language=experimental-default` option auto-detects the
2831
language based on the file's extension (`.ts`, `.tsx`, `.d.ts`, or `.js`).

docs/cli.adoc

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,43 @@ _path_ does not need to exist in the filesystem.
101101
Therefore, if multiple input files are given, *--path-for-config-search* can be specified multiple times.
102102
If *--path-for-config-search* is the last option, it has no effect.
103103
+
104+
*--path-for-config-search* overrides *--stdin-path*.
105+
+
104106
Incompatible with *--lsp-server*.
105107
+
106108
Added in quick-lint-js version 0.4.0.
107109

110+
*--stdin-path*=_path_::
111+
Change the behavior of *--stdin*.
112+
*--stdin* still reads a string from standard input, but otherwise it behaves as if the file at _path_ was specified instead:
113+
+
114+
--
115+
- The default language is determined by _path_ (unless overridden by *--language*).
116+
See *--language* for details.
117+
ifdef::backend-manpage[]
118+
- Searching for a configuration file
119+
endif::[]
120+
ifdef::backend-html5[]
121+
- link:../config/[Searching for a configuration file]
122+
endif::[]
123+
is based on _path_ (unless overridden by *--config-file* or *--path-for-config-file*).
124+
ifdef::backend-manpage[]
125+
(See *quick-lint-js.config*(5) for details on configuration file searching.)
126+
endif::[]
127+
128+
*--stdin-path* applies to only *--stdin*, not file paths (even special files such as /dev/stdin).
129+
130+
*--stdin-path* may appear anywhere in the command line (except after *--*).
131+
132+
_path_ must be a syntactically-valid path.
133+
_path_ does not need to exist in the filesystem.
134+
_path_ may be a relative path or an absolute path.
135+
136+
Incompatible with *--lsp-server*.
137+
138+
Added in quick-lint-js version 2.17.0.
139+
--
140+
108141
[#config-file]
109142
*--config-file*=_file_::
110143
Read configuration options from _file_ and apply them to input files which are given later in the command line.
@@ -125,6 +158,8 @@ ifdef::backend-html5[]
125158
If *--config-file* is not given, *quick-lint-js* link:../config/[searches for a configuration file].
126159
endif::[]
127160
+
161+
*--config-file* overrides *--path-for-config-file* and *--stdin-path*.
162+
+
128163
Incompatible with *--lsp-server*.
129164
+
130165
Added in quick-lint-js version 0.3.0.
@@ -162,6 +197,12 @@ See the <<Example>> section for an example.
162197

163198
If *--language* is the last option, it has no effect.
164199

200+
If the input file is *--stdin*:
201+
202+
- If *--stdin-path* is specified, its _path_ is used for *--language=default*.
203+
- If *--stdin-path* is not specified, then the path is assumed to be *example.js*.
204+
This means that *--language=default* will behave like *--language=javascript-jsx*.
205+
165206
Incompatible with *--lsp-server*.
166207

167208
Added in quick-lint-js version 2.10.0.
@@ -175,18 +216,20 @@ See the <<Error lists>> section for a description of the format for _errors_.
175216
Incompatible with *--lsp-server*.
176217

177218
*--stdin*::
178-
Read standard input as a JavaScript file.
219+
Read standard input as an input file.
179220
+
180-
If neither *--config-file* nor *--path-for-config-search* is specified, an empty configuration file is assumed.
221+
If none of *--config-file*, *--path-for-config-search*, or *--stdin-path* are specified, an empty configuration file is assumed.
181222
If *--config-file* is specified, _file_ is used for linting standard input.
182-
If *--path-for-config-search* is specified and *--config-file* is not specified,
223+
If *--config-file* is not specified and either *--stdin-path* or *--path-for-config-search* is specified,
183224
ifdef::backend-manpage[]
184225
*quick-lint-js* searches for a configuration file according to the rules specified in *quick-lint-js.config*(5)
185226
endif::[]
186227
ifdef::backend-html5[]
187228
*quick-lint-js* link:../config/[searches for a configuration file]
188229
endif::[]
189-
starting from *--path-for-config-search*'s _path_.
230+
starting from *--stdin-path*'s _path_ or *--path-for-config-search*'s _path_.
231+
+
232+
If neither *--stdin-path* nor *--language* are specified, the *javascript-jsx* language is used.
190233
+
191234
Incompatible with *--lsp-server*.
192235
+

src/quick-lint-js/cli/main.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ void run(Options o) {
301301
source.error().print_and_exit();
302302
}
303303
Linter_Options lint_options =
304-
get_linter_options_from_language(get_language(file));
304+
get_linter_options_from_language(get_language(file, o));
305305
lint_options.print_parser_visits = o.print_parser_visits;
306306
reporter->set_source(&*source, file);
307307
parse_and_lint(&*source, *reporter->get(), config->globals(),

src/quick-lint-js/cli/options.cpp

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ Options parse_options(int argc, char** argv) {
137137
next_path_for_config_search = arg_value;
138138
}
139139

140+
QLJS_OPTION(const char* arg_value, "--stdin-path"sv) {
141+
o.path_for_stdin = arg_value;
142+
}
143+
140144
QLJS_OPTION(const char* arg_value, "--vim-file-bufnr"sv) {
141145
o.has_vim_file_bufnr = true;
142146
int bufnr;
@@ -177,10 +181,26 @@ Options parse_options(int argc, char** argv) {
177181
if (next_vim_file_bufnr.number != std::nullopt) {
178182
o.warning_vim_bufnr_without_file.emplace_back(next_vim_file_bufnr.arg_var);
179183
}
184+
if (o.path_for_stdin != nullptr) {
185+
for (File_To_Lint& file : o.files_to_lint) {
186+
if (file.path_for_config_search == nullptr) {
187+
file.path_for_config_search = o.path_for_stdin;
188+
}
189+
}
190+
}
180191

181192
return o;
182193
}
183194

195+
bool Options::has_stdin() const {
196+
for (const File_To_Lint& file : this->files_to_lint) {
197+
if (file.is_stdin) {
198+
return true;
199+
}
200+
}
201+
return false;
202+
}
203+
184204
bool Options::dump_errors(Output_Stream& out) const {
185205
bool have_errors = false;
186206
if (this->lsp_server) {
@@ -235,6 +255,11 @@ bool Options::dump_errors(Output_Stream& out) const {
235255
}
236256
}
237257

258+
if (this->path_for_stdin != nullptr && !this->has_stdin()) {
259+
out.append_copy(
260+
u8"warning: '--stdin-path' has no effect without --stdin\n"_sv);
261+
}
262+
238263
for (const auto& option : this->error_unrecognized_options) {
239264
out.append_copy(u8"error: unrecognized option: "_sv);
240265
out.append_copy(to_string8_view(option));
@@ -261,8 +286,12 @@ bool Options::dump_errors(Output_Stream& out) const {
261286
return have_errors;
262287
}
263288

264-
Resolved_Input_File_Language get_language(const File_To_Lint& file) {
265-
return get_language(file.path, file.language);
289+
Resolved_Input_File_Language get_language(const File_To_Lint& file,
290+
const Options& options) {
291+
const char* path = file.is_stdin && options.path_for_stdin != nullptr
292+
? options.path_for_stdin
293+
: file.path;
294+
return get_language(path, file.language);
266295
}
267296

268297
Resolved_Input_File_Language get_language(const char* file,

src/quick-lint-js/cli/options.h

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,6 @@ struct File_To_Lint {
5555
std::optional<int> vim_bufnr;
5656
};
5757

58-
Resolved_Input_File_Language get_language(const File_To_Lint &file);
59-
Resolved_Input_File_Language get_language(const char *file,
60-
Raw_Input_File_Language language);
61-
6258
struct Options {
6359
bool help = false;
6460
bool list_debug_apps = false;
@@ -70,6 +66,7 @@ struct Options {
7066
Option_When diagnostic_hyperlinks = Option_When::auto_;
7167
std::vector<File_To_Lint> files_to_lint;
7268
Compiled_Diag_Code_List exit_fail_on;
69+
const char *path_for_stdin = nullptr;
7370

7471
std::vector<const char *> error_unrecognized_options;
7572
std::vector<const char *> warning_vim_bufnr_without_file;
@@ -79,9 +76,16 @@ struct Options {
7976
bool has_language = false;
8077
bool has_vim_file_bufnr = false;
8178

79+
bool has_stdin() const;
80+
8281
bool dump_errors(Output_Stream &) const;
8382
};
8483

84+
Resolved_Input_File_Language get_language(const File_To_Lint &file,
85+
const Options &);
86+
Resolved_Input_File_Language get_language(const char *file,
87+
Raw_Input_File_Language language);
88+
8589
Options parse_options(int argc, char **argv);
8690
}
8791

test/test-cli.cpp

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,46 @@ TEST_F(Test_CLI, automatically_find_config_file_given_path_for_config_search) {
201201
EXPECT_EQ(r.exit_status, 0);
202202
}
203203

204+
TEST_F(Test_CLI, path_for_config_search_affects_stdin_file) {
205+
std::string test_directory = this->make_temporary_directory();
206+
std::string config_file = test_directory + "/quick-lint-js.config";
207+
write_file_or_exit(config_file,
208+
u8R"({"globals":{"myGlobalVariable": true}})"_sv);
209+
210+
Run_Program_Result r = run_program(
211+
{
212+
get_quick_lint_js_executable_path(),
213+
"--path-for-config-search",
214+
test_directory + "/app.js",
215+
"--stdin",
216+
},
217+
Run_Program_Options{
218+
.input = u8"console.log(myGlobalVariable);"_sv,
219+
});
220+
EXPECT_EQ(r.output, u8""_sv);
221+
EXPECT_EQ(r.exit_status, 0);
222+
}
223+
224+
TEST_F(Test_CLI, path_for_stdin_affects_stdin_file_config_search) {
225+
std::string test_directory = this->make_temporary_directory();
226+
std::string config_file = test_directory + "/quick-lint-js.config";
227+
write_file_or_exit(config_file,
228+
u8R"({"globals":{"myGlobalVariable": true}})"_sv);
229+
230+
Run_Program_Result r = run_program(
231+
{
232+
get_quick_lint_js_executable_path(),
233+
"--stdin-path",
234+
test_directory + "/app.js",
235+
"--stdin",
236+
},
237+
Run_Program_Options{
238+
.input = u8"console.log(myGlobalVariable);"_sv,
239+
});
240+
EXPECT_EQ(r.output, u8""_sv);
241+
EXPECT_EQ(r.exit_status, 0);
242+
}
243+
204244
TEST_F(Test_CLI, config_file_parse_error_prevents_lint) {
205245
std::string test_directory = this->make_temporary_directory();
206246

@@ -273,6 +313,50 @@ TEST_F(Test_CLI, errors_for_all_config_files_are_printed) {
273313
<< r.output;
274314
}
275315

316+
TEST_F(Test_CLI, path_for_stdin_affects_default_language) {
317+
{
318+
Run_Program_Result r = run_program(
319+
{get_quick_lint_js_executable_path(), "--language=experimental-default",
320+
"--stdin", "--stdin-path=hello.js"},
321+
Run_Program_Options{
322+
.input = u8"interface I {}"_sv,
323+
});
324+
EXPECT_EQ(r.exit_status, 1);
325+
EXPECT_THAT(to_string(r.output.string_view()), HasSubstr("E0213"))
326+
<< "expected \"TypeScript's 'interface' feature is not allowed in "
327+
"JavaScript code\"\n"
328+
<< r.output;
329+
}
330+
331+
{
332+
Run_Program_Result r = run_program(
333+
{get_quick_lint_js_executable_path(), "--language=experimental-default",
334+
"--stdin", "--stdin-path=hello.ts"},
335+
Run_Program_Options{
336+
.input = u8"interface I {}"_sv,
337+
});
338+
EXPECT_EQ(r.exit_status, 0);
339+
EXPECT_THAT(to_string(r.output.string_view()), Not(HasSubstr("E0213")))
340+
<< "expected no diagnostics because file should be interpreted as "
341+
"TypeScript\n"
342+
<< r.output;
343+
}
344+
}
345+
346+
TEST_F(Test_CLI, language_overrides_path_for_stdin) {
347+
Run_Program_Result r = run_program({get_quick_lint_js_executable_path(),
348+
"--language=experimental-typescript",
349+
"--stdin", "--stdin-path=hello.js"},
350+
Run_Program_Options{
351+
.input = u8"interface I {}"_sv,
352+
});
353+
EXPECT_EQ(r.exit_status, 0);
354+
EXPECT_THAT(to_string(r.output.string_view()), Not(HasSubstr("E0213")))
355+
<< "expected no diagnostics because file should be interpreted as "
356+
"TypeScript\n"
357+
<< r.output;
358+
}
359+
276360
TEST_F(Test_CLI, language_javascript) {
277361
Run_Program_Result r = run_program(
278362
{get_quick_lint_js_executable_path(), "--language=javascript", "--stdin"},

test/test-options.cpp

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,20 +557,23 @@ TEST(Test_Options, stdin_file) {
557557
ASSERT_EQ(o.files_to_lint.size(), 2);
558558
EXPECT_TRUE(o.files_to_lint[0].is_stdin);
559559
EXPECT_FALSE(o.has_multiple_stdin);
560+
EXPECT_EQ(o.path_for_stdin, nullptr);
560561
}
561562

562563
{
563564
Options o = parse_options_no_errors({"one.js", "--stdin"});
564565
ASSERT_EQ(o.files_to_lint.size(), 2);
565566
EXPECT_TRUE(o.files_to_lint[1].is_stdin);
566567
EXPECT_FALSE(o.has_multiple_stdin);
568+
EXPECT_EQ(o.path_for_stdin, nullptr);
567569
}
568570

569571
{
570572
Options o = parse_options_no_errors({"-"});
571573
ASSERT_EQ(o.files_to_lint.size(), 1);
572574
EXPECT_TRUE(o.files_to_lint[0].is_stdin);
573575
EXPECT_FALSE(o.has_multiple_stdin);
576+
EXPECT_EQ(o.path_for_stdin, nullptr);
574577
}
575578
}
576579

@@ -587,6 +590,57 @@ TEST(Test_Options, is_stdin_emplaced_only_once) {
587590
}
588591
}
589592

593+
TEST(Test_Options, path_for_stdin) {
594+
{
595+
Options o = parse_options_no_errors({"--stdin-path", "a.js", "--stdin"});
596+
ASSERT_EQ(o.files_to_lint.size(), 1);
597+
EXPECT_STREQ(o.files_to_lint[0].path_for_config_search, "a.js");
598+
EXPECT_STREQ(o.path_for_stdin, "a.js");
599+
}
600+
601+
{
602+
Options o = parse_options_no_errors({"--stdin-path=a.js", "--stdin"});
603+
ASSERT_EQ(o.files_to_lint.size(), 1);
604+
EXPECT_STREQ(o.files_to_lint[0].path_for_config_search, "a.js");
605+
EXPECT_STREQ(o.path_for_stdin, "a.js");
606+
}
607+
608+
// Order does not matter.
609+
{
610+
Options o = parse_options_no_errors({"--stdin", "--stdin-path=a.js"});
611+
ASSERT_EQ(o.files_to_lint.size(), 1);
612+
EXPECT_STREQ(o.files_to_lint[0].path_for_config_search, "a.js");
613+
EXPECT_STREQ(o.path_for_stdin, "a.js");
614+
}
615+
616+
// Last --stdin-path option takes effect.
617+
{
618+
Options o = parse_options_no_errors(
619+
{"--stdin-path=a.js", "--stdin-path=b.js", "--stdin"});
620+
ASSERT_EQ(o.files_to_lint.size(), 1);
621+
EXPECT_STREQ(o.path_for_stdin, "b.js");
622+
}
623+
624+
// --path-for-config-search overrides --stdin-path.
625+
{
626+
Options o = parse_options_no_errors(
627+
{"--path-for-config-search=pfcs.js", "--stdin", "--stdin-path=pfs.js"});
628+
ASSERT_EQ(o.files_to_lint.size(), 1);
629+
EXPECT_STREQ(o.files_to_lint[0].path_for_config_search, "pfcs.js");
630+
EXPECT_STREQ(o.path_for_stdin, "pfs.js");
631+
}
632+
633+
{
634+
Options o = parse_options({"--stdin-path=a.js", "file.js"});
635+
ASSERT_EQ(o.files_to_lint.size(), 1);
636+
637+
Dumped_Errors errors = dump_errors(o);
638+
EXPECT_FALSE(errors.have_errors);
639+
EXPECT_EQ(errors.output,
640+
u8"warning: '--stdin-path' has no effect without --stdin\n"_sv);
641+
}
642+
}
643+
590644
TEST(Test_Options, print_help) {
591645
{
592646
Options o = parse_options_no_errors({"--help"});

0 commit comments

Comments
 (0)