From ad943775ab9b86671ebbe1bc735d6af1f0a5f422 Mon Sep 17 00:00:00 2001 From: "Matthew \"strager\" Glazar" Date: Wed, 18 Oct 2023 21:34:21 -0400 Subject: [PATCH] feat(cli): add --language=experimental-default Allow users to more easily opt into TypeScript support by adding --language=experimental-default. This option will eventually become the default, but because TypeScript support is experimental, it needs to be opt-in. --- docs/CHANGELOG.md | 2 ++ docs/cli.adoc | 7 ++++ src/quick-lint-js/cli/options.cpp | 16 ++++++++- src/quick-lint-js/cli/options.h | 7 +++- src/quick-lint-js/io/file-path.h | 4 +++ src/quick-lint-js/util/classify-path.cpp | 24 +++++++++++-- src/quick-lint-js/util/classify-path.h | 9 +++++ test/test-options.cpp | 44 ++++++++++++++++++++++++ 8 files changed, 108 insertions(+), 5 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 8a99571bf0..23aee6c7e0 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -24,6 +24,8 @@ Semantic Versioning. * Emacs: The Debian/Ubuntu package now installs the Emacs plugin. Manual installation of the .el files is no longer required. * 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. * Class method overload signatures are now parsed. * [E0398][] is now reported when using both `abstract` and `static` on a single class property. diff --git a/docs/cli.adoc b/docs/cli.adoc index 46f998c2f9..5cb9e873d4 100644 --- a/docs/cli.adoc +++ b/docs/cli.adoc @@ -138,6 +138,13 @@ Added in quick-lint-js version 0.3.0. ** *.js*: *javascript-jsx* ** *.jsx*: *javascript-jsx* ** (anything else): *javascript-jsx* +* *experimental-default*: infer the _languageid_ from the file's extension (EXPERIMENTAL; subject to change in future versions of quick-lint-js): +** *.js*: *javascript-jsx* +** *.jsx*: *javascript-jsx* +** *.d.ts*: *experimental-typescript-definition* +** *.ts*: *experimental-typescript* +** *.tsx*: *experimental-typescript-jsx* +** (anything else): *javascript-jsx* * *javascript*: the latest ECMAScript standard with proposed features. * *javascript-jsx*: like *javascript* but with JSX (React) extensions. * *experimental-typescript*: the latest TypeScript version. diff --git a/src/quick-lint-js/cli/options.cpp b/src/quick-lint-js/cli/options.cpp index 6dfeba759b..4aed5a6c8d 100644 --- a/src/quick-lint-js/cli/options.cpp +++ b/src/quick-lint-js/cli/options.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -115,6 +116,8 @@ Options parse_options(int argc, char** argv) { unused_language_option = arg_value; if (arg_value == "default"sv) { language = Raw_Input_File_Language::default_; + } else if (arg_value == "experimental-default"sv) { + language = Raw_Input_File_Language::experimental_default; } else if (arg_value == "javascript"sv) { language = Raw_Input_File_Language::javascript; } else if (arg_value == "javascript-jsx"sv) { @@ -264,9 +267,20 @@ Resolved_Input_File_Language File_To_Lint::get_language() const { Resolved_Input_File_Language get_language(const char* file, Raw_Input_File_Language language) { - static_cast(file); // Unused for now. if (language == Raw_Input_File_Language::default_) { return Resolved_Input_File_Language::javascript_jsx; + } else if (language == Raw_Input_File_Language::experimental_default) { + Path_Classification classification = classify_path(file); + if (classification.typescript_jsx) { + return Resolved_Input_File_Language::typescript_jsx; + } else if (classification.typescript) { + if (classification.typescript_definition) { + return Resolved_Input_File_Language::typescript_definition; + } + return Resolved_Input_File_Language::typescript; + } else { + return Resolved_Input_File_Language::javascript_jsx; + } } else { return static_cast(language); } diff --git a/src/quick-lint-js/cli/options.h b/src/quick-lint-js/cli/options.h index e8ca3b6947..968f42901b 100644 --- a/src/quick-lint-js/cli/options.h +++ b/src/quick-lint-js/cli/options.h @@ -18,7 +18,12 @@ enum class Output_Format { }; enum class Raw_Input_File_Language : unsigned char { - default_, // Explicit (--language=default) or implicit (no --language). + // Explicit (--language=default) or implicit (no --language). + default_, + // Explicit --language=experimental-default. + // TODO(#690): Make experimental_default the default and delete default_. + experimental_default, + javascript, javascript_jsx, typescript, diff --git a/src/quick-lint-js/io/file-path.h b/src/quick-lint-js/io/file-path.h index 591f5a6dbc..9747fd716a 100644 --- a/src/quick-lint-js/io/file-path.h +++ b/src/quick-lint-js/io/file-path.h @@ -4,6 +4,7 @@ #pragma once #include +#include #include #if defined(_WIN32) @@ -14,6 +15,9 @@ #define QLJS_ALL_PATH_DIRECTORY_SEPARATORS "/" #endif +#define QLJS_ALL_PATH_DIRECTORY_SEPARATORS_SV \ + QLJS_CPP_CONCAT(QLJS_ALL_PATH_DIRECTORY_SEPARATORS, sv) + namespace quick_lint_js { std::string parent_path(std::string&&); diff --git a/src/quick-lint-js/util/classify-path.cpp b/src/quick-lint-js/util/classify-path.cpp index 1842620bce..766f4ef68b 100644 --- a/src/quick-lint-js/util/classify-path.cpp +++ b/src/quick-lint-js/util/classify-path.cpp @@ -2,18 +2,36 @@ // See end of file for extended copyright information. #include +#include #include +#include #include #include #include +#include namespace quick_lint_js { Path_Classification classify_uri(String8_View uri) { // FIXME(strager): Should this unescape % encoding? - String8_View base_name = uri_base_name(uri); + return classify_file_base_name(uri_base_name(uri)); +} + +Path_Classification classify_path(String8_View path) { + String8_View base_name = + to_string8_view(path_file_name(to_string_view(path))); + return classify_file_base_name(base_name); +} + +Path_Classification classify_path(const char *path) { + return classify_path(to_string8_view(std::string_view(path))); +} + +Path_Classification classify_file_base_name(String8_View name) { + QLJS_SLOW_ASSERT(name.find(u8'/') == name.npos); return Path_Classification{ - .typescript_definition = base_name.find(u8".d."_sv) != base_name.npos, - .typescript_jsx = ends_with(base_name, u8".tsx"_sv), + .typescript_definition = name.find(u8".d."_sv) != name.npos, + .typescript = ends_with(name, u8".ts"_sv), + .typescript_jsx = ends_with(name, u8".tsx"_sv), }; } } diff --git a/src/quick-lint-js/util/classify-path.h b/src/quick-lint-js/util/classify-path.h index 13485ff5d9..d4f51c8f13 100644 --- a/src/quick-lint-js/util/classify-path.h +++ b/src/quick-lint-js/util/classify-path.h @@ -17,11 +17,20 @@ struct Path_Classification { // https://github.com/microsoft/TypeScript/blob/daa7e985f5adc972aa241e5b0761c7dc433e94bf/src/compiler/parser.ts#L10408 bool typescript_definition; + // True if the path's base name ends with '.ts', including '.d.ts'. False + // otherwise, for example if the path's base name ends with '.js' or '.tsx'. + bool typescript; + // True if the path's base name ends with '.tsx'. bool typescript_jsx; }; Path_Classification classify_uri(String8_View uri); +Path_Classification classify_path(String8_View path); +Path_Classification classify_path(const char* path); + +// Precondition: name has no directory components. +Path_Classification classify_file_base_name(String8_View name); } // quick-lint-js finds bugs in JavaScript programs. diff --git a/test/test-options.cpp b/test/test-options.cpp index 20c08b7e9b..633827991c 100644 --- a/test/test-options.cpp +++ b/test/test-options.cpp @@ -6,8 +6,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -365,6 +367,20 @@ TEST(Test_Options, language) { EXPECT_EQ(o.files_to_lint[3].language, default_language) << "--stdin"; } + { + Options o = parse_options_no_errors({"--language=default", "file.js"}); + ASSERT_EQ(o.files_to_lint.size(), 1); + EXPECT_EQ(o.files_to_lint[0].language, Raw_Input_File_Language::default_); + } + + { + Options o = + parse_options_no_errors({"--language=experimental-default", "file.js"}); + ASSERT_EQ(o.files_to_lint.size(), 1); + EXPECT_EQ(o.files_to_lint[0].language, + Raw_Input_File_Language::experimental_default); + } + { Options o = parse_options_no_errors( {"--language=javascript", "one.js", "two.ts", "three.txt"}); @@ -474,10 +490,38 @@ TEST(Test_Options, default_language_is_javascript_jsx_regardless_of_extension) { EXPECT_EQ(get_language("hi.jsx", default_language), javascript_jsx); EXPECT_EQ(get_language("hi.ts", default_language), javascript_jsx); EXPECT_EQ(get_language("hi.d.ts", default_language), javascript_jsx); + EXPECT_EQ(get_language("hi.d.js", default_language), javascript_jsx); EXPECT_EQ(get_language("hi.tsx", default_language), javascript_jsx); EXPECT_EQ(get_language("hi.txt", default_language), javascript_jsx); } +TEST(Test_Options, + experimental_default_language_guesses_language_from_extension) { + constexpr auto default_language = + Raw_Input_File_Language::experimental_default; + constexpr auto javascript_jsx = Resolved_Input_File_Language::javascript_jsx; + EXPECT_EQ(get_language("", default_language), javascript_jsx); + EXPECT_EQ(get_language("hi.js", default_language), javascript_jsx); + EXPECT_EQ(get_language("hi.jsx", default_language), javascript_jsx); + EXPECT_EQ(get_language("hi.ts", default_language), + Resolved_Input_File_Language::typescript); + EXPECT_EQ(get_language("hi.d.ts", default_language), + Resolved_Input_File_Language::typescript_definition); + EXPECT_EQ(get_language("hi.d.js", default_language), javascript_jsx); + EXPECT_EQ(get_language("hi.tsx", default_language), + Resolved_Input_File_Language::typescript_jsx); + EXPECT_EQ(get_language("hi.txt", default_language), javascript_jsx); + + for (char separator : QLJS_ALL_PATH_DIRECTORY_SEPARATORS_SV) { + std::string path = + concat("foo.d.ts"sv, std::string_view(&separator, 1), "bar.ts"sv); + SCOPED_TRACE(path); + EXPECT_EQ(get_language(path.c_str(), default_language), + Resolved_Input_File_Language::typescript) + << ".d.ts in containing folder should be ignored"; + } +} + TEST(Test_Options, get_language_overwritten) { constexpr auto in_javascript = Raw_Input_File_Language::javascript; constexpr auto in_javascript_jsx = Raw_Input_File_Language::javascript_jsx;