Skip to content

Commit

Permalink
feat(cli): add --stdin-path option
Browse files Browse the repository at this point in the history
--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.
  • Loading branch information
strager committed Oct 19, 2023
1 parent 9c5c33f commit 106bfe7
Show file tree
Hide file tree
Showing 7 changed files with 228 additions and 11 deletions.
3 changes: 3 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ Semantic Versioning.
is not allowed on getters or setters"). (Implemented by [koopiehoop][].)
* Emacs: The Debian/Ubuntu package now installs the Emacs plugin. Manual
installation of the .el files is no longer required.
* CLI: The new `--stdin-path` CLI option allows users of the `--stdin` option
(primarily text editors) to have quick-lint-js detect the language
automatically via `--language=default` or `--language=experimental-default`.
* TypeScript support (still experimental):
* CLI: The new `--language=experimental-default` option auto-detects
the language based on `.ts`, `.tsx`, and `.d.ts` in the file path.
Expand Down
51 changes: 47 additions & 4 deletions docs/cli.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,43 @@ _path_ does not need to exist in the filesystem.
Therefore, if multiple input files are given, *--path-for-config-search* can be specified multiple times.
If *--path-for-config-search* is the last option, it has no effect.
+
*--path-for-config-search* overrides *--stdin-path*.
+
Incompatible with *--lsp-server*.
+
Added in quick-lint-js version 0.4.0.

*--stdin-path*=_path_::
Change the behavior of *--stdin*.
*--stdin* still reads a string from standard input, but otherwise it behaves as if the file at _path_ was specified instead:
+
--
- The default language is determined by _path_ (unless overridden by *--language*).
See *--language* for details.
ifdef::backend-manpage[]
- Searching for a configuration file
endif::[]
ifdef::backend-html5[]
- link:../config/[Searching for a configuration file]
endif::[]
is based on _path_ (unless overridden by *--config-file* or *--path-for-config-file*).
ifdef::backend-manpage[]
(See *quick-lint-js.config*(5) for details on configuration file searching.)
endif::[]

*--stdin-path* applies to only *--stdin*, not file paths (even special files such as /dev/stdin).

*--stdin-path* may appear anywhere in the command line (except after *--*).

_path_ must be a syntactically-valid path.
_path_ does not need to exist in the filesystem.
_path_ may be a relative path or an absolute path.

Incompatible with *--lsp-server*.

Added in quick-lint-js version 2.17.0.
--

[#config-file]
*--config-file*=_file_::
Read configuration options from _file_ and apply them to input files which are given later in the command line.
Expand All @@ -125,6 +158,8 @@ ifdef::backend-html5[]
If *--config-file* is not given, *quick-lint-js* link:../config/[searches for a configuration file].
endif::[]
+
*--config-file* overrides *--path-for-config-file* and *--stdin-path*.
+
Incompatible with *--lsp-server*.
+
Added in quick-lint-js version 0.3.0.
Expand Down Expand Up @@ -162,6 +197,12 @@ See the <<Example>> section for an example.

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

If the input file is *--stdin*:

- If *--stdin-path* is specified, its _path_ is used for *--language=default*.
- If *--stdin-path* is not specified, then the path is assumed to be *example.js*.
This means that *--language=default* will behave like *--language=javascript-jsx*.

Incompatible with *--lsp-server*.

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

*--stdin*::
Read standard input as a JavaScript file.
Read standard input as an input file.
+
If neither *--config-file* nor *--path-for-config-search* is specified, an empty configuration file is assumed.
If none of *--config-file*, *--path-for-config-search*, or *--stdin-path* are specified, an empty configuration file is assumed.
If *--config-file* is specified, _file_ is used for linting standard input.
If *--path-for-config-search* is specified and *--config-file* is not specified,
If *--config-file* is not specified and either *--stdin-path* or *--path-for-config-search* is specified,
ifdef::backend-manpage[]
*quick-lint-js* searches for a configuration file according to the rules specified in *quick-lint-js.config*(5)
endif::[]
ifdef::backend-html5[]
*quick-lint-js* link:../config/[searches for a configuration file]
endif::[]
starting from *--path-for-config-search*'s _path_.
starting from *--stdin-path*'s _path_ or *--path-for-config-search*'s _path_.
+
If neither *--stdin-path* nor *--language* are specified, the *javascript-jsx* language is used.
+
Incompatible with *--lsp-server*.
+
Expand Down
2 changes: 1 addition & 1 deletion src/quick-lint-js/cli/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ void run(Options o) {
source.error().print_and_exit();
}
Linter_Options lint_options =
get_linter_options_from_language(get_language(file));
get_linter_options_from_language(get_language(file, o));
lint_options.print_parser_visits = o.print_parser_visits;
reporter->set_source(&*source, file);
parse_and_lint(&*source, *reporter->get(), config->globals(),
Expand Down
33 changes: 31 additions & 2 deletions src/quick-lint-js/cli/options.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ Options parse_options(int argc, char** argv) {
next_path_for_config_search = arg_value;
}

QLJS_OPTION(const char* arg_value, "--stdin-path"sv) {
o.path_for_stdin = arg_value;
}

QLJS_OPTION(const char* arg_value, "--vim-file-bufnr"sv) {
o.has_vim_file_bufnr = true;
int bufnr;
Expand Down Expand Up @@ -177,10 +181,26 @@ Options parse_options(int argc, char** argv) {
if (next_vim_file_bufnr.number != std::nullopt) {
o.warning_vim_bufnr_without_file.emplace_back(next_vim_file_bufnr.arg_var);
}
if (o.path_for_stdin != nullptr) {
for (File_To_Lint& file : o.files_to_lint) {
if (file.path_for_config_search == nullptr) {
file.path_for_config_search = o.path_for_stdin;
}
}
}

return o;
}

bool Options::has_stdin() const {
for (const File_To_Lint& file : this->files_to_lint) {
if (file.is_stdin) {
return true;
}
}
return false;
}

bool Options::dump_errors(Output_Stream& out) const {
bool have_errors = false;
if (this->lsp_server) {
Expand Down Expand Up @@ -235,6 +255,11 @@ bool Options::dump_errors(Output_Stream& out) const {
}
}

if (this->path_for_stdin != nullptr && !this->has_stdin()) {
out.append_copy(
u8"warning: '--stdin-path' has no effect without --stdin\n"_sv);
}

for (const auto& option : this->error_unrecognized_options) {
out.append_copy(u8"error: unrecognized option: "_sv);
out.append_copy(to_string8_view(option));
Expand All @@ -261,8 +286,12 @@ bool Options::dump_errors(Output_Stream& out) const {
return have_errors;
}

Resolved_Input_File_Language get_language(const File_To_Lint& file) {
return get_language(file.path, file.language);
Resolved_Input_File_Language get_language(const File_To_Lint& file,
const Options& options) {
const char* path = file.is_stdin && options.path_for_stdin != nullptr
? options.path_for_stdin
: file.path;
return get_language(path, file.language);
}

Resolved_Input_File_Language get_language(const char* file,
Expand Down
12 changes: 8 additions & 4 deletions src/quick-lint-js/cli/options.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,6 @@ struct File_To_Lint {
std::optional<int> vim_bufnr;
};

Resolved_Input_File_Language get_language(const File_To_Lint &file);
Resolved_Input_File_Language get_language(const char *file,
Raw_Input_File_Language language);

struct Options {
bool help = false;
bool list_debug_apps = false;
Expand All @@ -70,6 +66,7 @@ struct Options {
Option_When diagnostic_hyperlinks = Option_When::auto_;
std::vector<File_To_Lint> files_to_lint;
Compiled_Diag_Code_List exit_fail_on;
const char *path_for_stdin = nullptr;

std::vector<const char *> error_unrecognized_options;
std::vector<const char *> warning_vim_bufnr_without_file;
Expand All @@ -79,9 +76,16 @@ struct Options {
bool has_language = false;
bool has_vim_file_bufnr = false;

bool has_stdin() const;

bool dump_errors(Output_Stream &) const;
};

Resolved_Input_File_Language get_language(const File_To_Lint &file,
const Options &);
Resolved_Input_File_Language get_language(const char *file,
Raw_Input_File_Language language);

Options parse_options(int argc, char **argv);
}

Expand Down
84 changes: 84 additions & 0 deletions test/test-cli.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,46 @@ TEST_F(Test_CLI, automatically_find_config_file_given_path_for_config_search) {
EXPECT_EQ(r.exit_status, 0);
}

TEST_F(Test_CLI, path_for_config_search_affects_stdin_file) {
std::string test_directory = this->make_temporary_directory();
std::string config_file = test_directory + "/quick-lint-js.config";
write_file_or_exit(config_file,
u8R"({"globals":{"myGlobalVariable": true}})"_sv);

Run_Program_Result r = run_program(
{
get_quick_lint_js_executable_path(),
"--path-for-config-search",
test_directory + "/app.js",
"--stdin",
},
Run_Program_Options{
.input = u8"console.log(myGlobalVariable);"_sv,
});
EXPECT_EQ(r.output, u8""_sv);
EXPECT_EQ(r.exit_status, 0);
}

TEST_F(Test_CLI, path_for_stdin_affects_stdin_file_config_search) {
std::string test_directory = this->make_temporary_directory();
std::string config_file = test_directory + "/quick-lint-js.config";
write_file_or_exit(config_file,
u8R"({"globals":{"myGlobalVariable": true}})"_sv);

Run_Program_Result r = run_program(
{
get_quick_lint_js_executable_path(),
"--stdin-path",
test_directory + "/app.js",
"--stdin",
},
Run_Program_Options{
.input = u8"console.log(myGlobalVariable);"_sv,
});
EXPECT_EQ(r.output, u8""_sv);
EXPECT_EQ(r.exit_status, 0);
}

TEST_F(Test_CLI, config_file_parse_error_prevents_lint) {
std::string test_directory = this->make_temporary_directory();

Expand Down Expand Up @@ -273,6 +313,50 @@ TEST_F(Test_CLI, errors_for_all_config_files_are_printed) {
<< r.output;
}

TEST_F(Test_CLI, path_for_stdin_affects_default_language) {
{
Run_Program_Result r = run_program(
{get_quick_lint_js_executable_path(), "--language=experimental-default",
"--stdin", "--stdin-path=hello.js"},
Run_Program_Options{
.input = u8"interface I {}"_sv,
});
EXPECT_EQ(r.exit_status, 1);
EXPECT_THAT(to_string(r.output.string_view()), HasSubstr("E0213"))
<< "expected \"TypeScript's 'interface' feature is not allowed in "
"JavaScript code\"\n"
<< r.output;
}

{
Run_Program_Result r = run_program(
{get_quick_lint_js_executable_path(), "--language=experimental-default",
"--stdin", "--stdin-path=hello.ts"},
Run_Program_Options{
.input = u8"interface I {}"_sv,
});
EXPECT_EQ(r.exit_status, 0);
EXPECT_THAT(to_string(r.output.string_view()), Not(HasSubstr("E0213")))
<< "expected no diagnostics because file should be interpreted as "
"TypeScript\n"
<< r.output;
}
}

TEST_F(Test_CLI, language_overrides_path_for_stdin) {
Run_Program_Result r = run_program({get_quick_lint_js_executable_path(),
"--language=experimental-typescript",
"--stdin", "--stdin-path=hello.js"},
Run_Program_Options{
.input = u8"interface I {}"_sv,
});
EXPECT_EQ(r.exit_status, 0);
EXPECT_THAT(to_string(r.output.string_view()), Not(HasSubstr("E0213")))
<< "expected no diagnostics because file should be interpreted as "
"TypeScript\n"
<< r.output;
}

TEST_F(Test_CLI, language_javascript) {
Run_Program_Result r = run_program(
{get_quick_lint_js_executable_path(), "--language=javascript", "--stdin"},
Expand Down
54 changes: 54 additions & 0 deletions test/test-options.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -557,20 +557,23 @@ TEST(Test_Options, stdin_file) {
ASSERT_EQ(o.files_to_lint.size(), 2);
EXPECT_TRUE(o.files_to_lint[0].is_stdin);
EXPECT_FALSE(o.has_multiple_stdin);
EXPECT_EQ(o.path_for_stdin, nullptr);
}

{
Options o = parse_options_no_errors({"one.js", "--stdin"});
ASSERT_EQ(o.files_to_lint.size(), 2);
EXPECT_TRUE(o.files_to_lint[1].is_stdin);
EXPECT_FALSE(o.has_multiple_stdin);
EXPECT_EQ(o.path_for_stdin, nullptr);
}

{
Options o = parse_options_no_errors({"-"});
ASSERT_EQ(o.files_to_lint.size(), 1);
EXPECT_TRUE(o.files_to_lint[0].is_stdin);
EXPECT_FALSE(o.has_multiple_stdin);
EXPECT_EQ(o.path_for_stdin, nullptr);
}
}

Expand All @@ -587,6 +590,57 @@ TEST(Test_Options, is_stdin_emplaced_only_once) {
}
}

TEST(Test_Options, path_for_stdin) {
{
Options o = parse_options_no_errors({"--stdin-path", "a.js", "--stdin"});
ASSERT_EQ(o.files_to_lint.size(), 1);
EXPECT_STREQ(o.files_to_lint[0].path_for_config_search, "a.js");
EXPECT_STREQ(o.path_for_stdin, "a.js");
}

{
Options o = parse_options_no_errors({"--stdin-path=a.js", "--stdin"});
ASSERT_EQ(o.files_to_lint.size(), 1);
EXPECT_STREQ(o.files_to_lint[0].path_for_config_search, "a.js");
EXPECT_STREQ(o.path_for_stdin, "a.js");
}

// Order does not matter.
{
Options o = parse_options_no_errors({"--stdin", "--stdin-path=a.js"});
ASSERT_EQ(o.files_to_lint.size(), 1);
EXPECT_STREQ(o.files_to_lint[0].path_for_config_search, "a.js");
EXPECT_STREQ(o.path_for_stdin, "a.js");
}

// Last --stdin-path option takes effect.
{
Options o = parse_options_no_errors(
{"--stdin-path=a.js", "--stdin-path=b.js", "--stdin"});
ASSERT_EQ(o.files_to_lint.size(), 1);
EXPECT_STREQ(o.path_for_stdin, "b.js");
}

// --path-for-config-search overrides --stdin-path.
{
Options o = parse_options_no_errors(
{"--path-for-config-search=pfcs.js", "--stdin", "--stdin-path=pfs.js"});
ASSERT_EQ(o.files_to_lint.size(), 1);
EXPECT_STREQ(o.files_to_lint[0].path_for_config_search, "pfcs.js");
EXPECT_STREQ(o.path_for_stdin, "pfs.js");
}

{
Options o = parse_options({"--stdin-path=a.js", "file.js"});
ASSERT_EQ(o.files_to_lint.size(), 1);

Dumped_Errors errors = dump_errors(o);
EXPECT_FALSE(errors.have_errors);
EXPECT_EQ(errors.output,
u8"warning: '--stdin-path' has no effect without --stdin\n"_sv);
}
}

TEST(Test_Options, print_help) {
{
Options o = parse_options_no_errors({"--help"});
Expand Down

0 comments on commit 106bfe7

Please sign in to comment.