diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 23aee6c7e0..2668aa1886 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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. diff --git a/docs/cli.adoc b/docs/cli.adoc index 5cb9e873d4..4d90897d58 100644 --- a/docs/cli.adoc +++ b/docs/cli.adoc @@ -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. @@ -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. @@ -162,6 +197,12 @@ See the <> 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. @@ -175,18 +216,20 @@ See the <> 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*. + diff --git a/src/quick-lint-js/cli/main.cpp b/src/quick-lint-js/cli/main.cpp index f4febfdc8d..1d2a94cc16 100644 --- a/src/quick-lint-js/cli/main.cpp +++ b/src/quick-lint-js/cli/main.cpp @@ -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(), diff --git a/src/quick-lint-js/cli/options.cpp b/src/quick-lint-js/cli/options.cpp index adecddb3bc..684a619377 100644 --- a/src/quick-lint-js/cli/options.cpp +++ b/src/quick-lint-js/cli/options.cpp @@ -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; @@ -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) { @@ -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)); @@ -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, diff --git a/src/quick-lint-js/cli/options.h b/src/quick-lint-js/cli/options.h index cdb8fd4803..646c530f30 100644 --- a/src/quick-lint-js/cli/options.h +++ b/src/quick-lint-js/cli/options.h @@ -55,10 +55,6 @@ struct File_To_Lint { std::optional 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; @@ -70,6 +66,7 @@ struct Options { Option_When diagnostic_hyperlinks = Option_When::auto_; std::vector files_to_lint; Compiled_Diag_Code_List exit_fail_on; + const char *path_for_stdin = nullptr; std::vector error_unrecognized_options; std::vector warning_vim_bufnr_without_file; @@ -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); } diff --git a/test/test-cli.cpp b/test/test-cli.cpp index 8b99b971e5..b59ba8d1d0 100644 --- a/test/test-cli.cpp +++ b/test/test-cli.cpp @@ -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(); @@ -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"}, diff --git a/test/test-options.cpp b/test/test-options.cpp index 609f10acb5..7935537c74 100644 --- a/test/test-options.cpp +++ b/test/test-options.cpp @@ -557,6 +557,7 @@ 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); } { @@ -564,6 +565,7 @@ TEST(Test_Options, stdin_file) { 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); } { @@ -571,6 +573,7 @@ TEST(Test_Options, stdin_file) { 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); } } @@ -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"});