diff --git a/crates/oxc_ast/src/ast/js.rs b/crates/oxc_ast/src/ast/js.rs index 679190744119a..f7a79165fc6b8 100644 --- a/crates/oxc_ast/src/ast/js.rs +++ b/crates/oxc_ast/src/ast/js.rs @@ -316,6 +316,51 @@ impl<'a> Expression<'a> { _ => false, } } + + pub fn get_span(&self) -> &Span { + match self { + Expression::BooleanLiteral(e) => &e.span, + Expression::NullLiteral(e) => &e.span, + Expression::NumericLiteral(e) => &e.span, + Expression::BigintLiteral(e) => &e.span, + Expression::RegExpLiteral(e) => &e.span, + Expression::StringLiteral(e) => &e.span, + Expression::TemplateLiteral(e) => &e.span, + Expression::Identifier(e) => &e.span, + Expression::MetaProperty(e) => &e.span, + Expression::Super(e) => &e.span, + Expression::ArrayExpression(e) => &e.span, + Expression::ArrowFunctionExpression(e) => &e.span, + Expression::AssignmentExpression(e) => &e.span, + Expression::AwaitExpression(e) => &e.span, + Expression::BinaryExpression(e) => &e.span, + Expression::CallExpression(e) => &e.span, + Expression::ChainExpression(e) => &e.span, + Expression::ClassExpression(e) => &e.span, + Expression::ConditionalExpression(e) => &e.span, + Expression::FunctionExpression(e) => &e.span, + Expression::ImportExpression(e) => &e.span, + Expression::LogicalExpression(e) => &e.span, + Expression::MemberExpression(e) => e.get_span(), + Expression::NewExpression(e) => &e.span, + Expression::ObjectExpression(e) => &e.span, + Expression::ParenthesizedExpression(e) => &e.span, + Expression::SequenceExpression(e) => &e.span, + Expression::TaggedTemplateExpression(e) => &e.span, + Expression::ThisExpression(e) => &e.span, + Expression::UnaryExpression(e) => &e.span, + Expression::UpdateExpression(e) => &e.span, + Expression::YieldExpression(e) => &e.span, + Expression::PrivateInExpression(e) => &e.span, + Expression::JSXElement(e) => &e.span, + Expression::JSXFragment(e) => &e.span, + Expression::TSAsExpression(e) => &e.span, + Expression::TSSatisfiesExpression(e) => &e.span, + Expression::TSTypeAssertion(e) => &e.span, + Expression::TSNonNullExpression(e) => &e.span, + Expression::TSInstantiationExpression(e) => &e.span, + } + } } /// Identifier Name @@ -718,6 +763,14 @@ impl<'a> MemberExpression<'a> { self.object().is_specific_id(object) && self.static_property_name().is_some_and(|p| p == property) } + + pub fn get_span(&self) -> &Span { + match self { + MemberExpression::ComputedMemberExpression(e) => &e.span, + MemberExpression::StaticMemberExpression(e) => &e.span, + MemberExpression::PrivateFieldExpression(e) => &e.span, + } + } } /// `MemberExpression[?Yield, ?Await] [ Expression[+In, ?Yield, ?Await] ]` @@ -856,6 +909,13 @@ impl Argument<'_> { pub fn is_spread(&self) -> bool { matches!(self, Self::SpreadElement(_)) } + + pub fn get_span(&self) -> &Span { + match self { + Argument::SpreadElement(e) => &e.span, + Argument::Expression(e) => e.get_span(), + } + } } /// Update Expression @@ -1179,6 +1239,15 @@ pub enum ChainElement<'a> { MemberExpression(Box<'a, MemberExpression<'a>>), } +impl<'a> ChainElement<'a> { + pub fn get_span(&self) -> &Span { + match self { + ChainElement::CallExpression(e) => &e.span, + ChainElement::MemberExpression(e) => e.get_span(), + } + } +} + /// Parenthesized Expression #[derive(Debug, Hash)] #[cfg_attr(feature = "serialize", derive(Serialize, Tsify))] diff --git a/crates/oxc_cli/src/command/lint.rs b/crates/oxc_cli/src/command/lint.rs index b8affbe4ba1ef..31a6f6e4bc2c1 100644 --- a/crates/oxc_cli/src/command/lint.rs +++ b/crates/oxc_cli/src/command/lint.rs @@ -170,6 +170,10 @@ pub struct EnablePlugins { /// Enable the React performance plugin and detect rendering performance problems #[bpaf(switch, hide_usage)] pub react_perf_plugin: bool, + + /// Enable the TypeCheck plugin and detect type-based problems + #[bpaf(switch, hide_usage)] + pub typecheck_plugin: bool, } #[cfg(test)] diff --git a/crates/oxc_cli/src/lint/mod.rs b/crates/oxc_cli/src/lint/mod.rs index 8b63afe94c2cc..76d4f29972b58 100644 --- a/crates/oxc_cli/src/lint/mod.rs +++ b/crates/oxc_cli/src/lint/mod.rs @@ -98,7 +98,8 @@ impl Runner for LintRunner { .with_jest_plugin(enable_plugins.jest_plugin) .with_jsx_a11y_plugin(enable_plugins.jsx_a11y_plugin) .with_nextjs_plugin(enable_plugins.nextjs_plugin) - .with_react_perf_plugin(enable_plugins.react_perf_plugin); + .with_react_perf_plugin(enable_plugins.react_perf_plugin) + .with_type_info(enable_plugins.typecheck_plugin); let linter = match Linter::from_options(lint_options) { Ok(lint_service) => lint_service, diff --git a/crates/oxc_language_server/src/linter.rs b/crates/oxc_language_server/src/linter.rs index d187a41c332ff..9358a429a2237 100644 --- a/crates/oxc_language_server/src/linter.rs +++ b/crates/oxc_language_server/src/linter.rs @@ -300,8 +300,11 @@ impl IsolatedLintHandler { }; let lint_ctx = LintContext::new( + Path::new("./"), path.to_path_buf().into_boxed_path(), &Rc::new(semantic_ret.semantic), + // TODO: create type checker + None, ); let result = linter.run(lint_ctx); diff --git a/crates/oxc_linter/fixtures/typecheck/file.ts b/crates/oxc_linter/fixtures/typecheck/file.ts new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/crates/oxc_linter/fixtures/typecheck/tsconfig.json b/crates/oxc_linter/fixtures/typecheck/tsconfig.json new file mode 100644 index 0000000000000..3336c115f04a2 --- /dev/null +++ b/crates/oxc_linter/fixtures/typecheck/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "jsx": "preserve", + "target": "es5", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "lib": [ + "es2015", + "es2017", + "esnext" + ], + "experimentalDecorators": true + }, + "include": [ + "file.ts" + ] +} \ No newline at end of file diff --git a/crates/oxc_linter/src/context.rs b/crates/oxc_linter/src/context.rs index 3b815cf0e6d31..9c83ce478d0dc 100644 --- a/crates/oxc_linter/src/context.rs +++ b/crates/oxc_linter/src/context.rs @@ -1,4 +1,9 @@ -use std::{cell::RefCell, path::Path, rc::Rc, sync::Arc}; +use std::{ + cell::RefCell, + path::Path, + rc::Rc, + sync::{Arc, Mutex}, +}; use oxc_codegen::{Codegen, CodegenOptions}; use oxc_diagnostics::Error; @@ -9,6 +14,7 @@ use crate::{ disable_directives::{DisableDirectives, DisableDirectivesBuilder}, fixer::{Fix, Message}, javascript_globals::GLOBALS, + typecheck::TSServerClient, ESLintEnv, ESLintSettings, }; @@ -26,15 +32,25 @@ pub struct LintContext<'a> { file_path: Box, + absolute_path: Box, + settings: Arc, env: Arc, + + type_checker: Option>>, } impl<'a> LintContext<'a> { - pub fn new(file_path: Box, semantic: &Rc>) -> Self { + pub fn new( + cwd: &Path, + file_path: Box, + semantic: &Rc>, + type_checker: Option>>, + ) -> Self { let disable_directives = DisableDirectivesBuilder::new(semantic.source_text(), semantic.trivias()).build(); + let absolute_path = Box::new(cwd.join(file_path.as_ref()).to_string_lossy().into()); Self { semantic: Rc::clone(semantic), diagnostics: RefCell::new(vec![]), @@ -42,8 +58,10 @@ impl<'a> LintContext<'a> { fix: false, current_rule_name: "", file_path, + absolute_path, settings: Arc::new(ESLintSettings::default()), env: Arc::new(ESLintEnv::default()), + type_checker, } } @@ -89,6 +107,10 @@ impl<'a> LintContext<'a> { &self.file_path } + pub fn absolute_path(&self) -> &str { + &self.absolute_path.as_ref() + } + pub fn envs(&self) -> &ESLintEnv { &self.env } @@ -109,6 +131,15 @@ impl<'a> LintContext<'a> { self.current_rule_name = name; } + pub fn use_type_checker(&self, run: F) -> R + where + F: FnOnce(&mut TSServerClient) -> R, + { + // Unwrap is safe here, since rules requiring type checker will not run unless type checker is created + let mut type_checker = self.type_checker.as_ref().unwrap().lock().unwrap(); + run(&mut type_checker) + } + /* Diagnostics */ pub fn into_message(self) -> Vec> { diff --git a/crates/oxc_linter/src/lib.rs b/crates/oxc_linter/src/lib.rs index f216bd59ca64c..7d12a47d34cf9 100644 --- a/crates/oxc_linter/src/lib.rs +++ b/crates/oxc_linter/src/lib.rs @@ -16,6 +16,7 @@ pub mod partial_loader; pub mod rule; mod rules; mod service; +mod typecheck; mod utils; use rustc_hash::FxHashMap; diff --git a/crates/oxc_linter/src/options.rs b/crates/oxc_linter/src/options.rs index 3db83150a5032..e7581df59c727 100644 --- a/crates/oxc_linter/src/options.rs +++ b/crates/oxc_linter/src/options.rs @@ -28,6 +28,7 @@ pub struct LintOptions { pub jsx_a11y_plugin: bool, pub nextjs_plugin: bool, pub react_perf_plugin: bool, + pub type_info: bool, pub env: ESLintEnv, } @@ -43,6 +44,7 @@ impl Default for LintOptions { jsx_a11y_plugin: false, nextjs_plugin: false, react_perf_plugin: false, + type_info: false, env: ESLintEnv::default(), } } @@ -105,6 +107,12 @@ impl LintOptions { self } + #[must_use] + pub fn with_type_info(mut self, yes: bool) -> Self { + self.type_info = yes; + self + } + #[must_use] pub fn with_env(mut self, env: Vec) -> Self { self.env = ESLintEnv::from_vec(env); @@ -251,6 +259,10 @@ impl LintOptions { may_exclude_plugin_rules(self.nextjs_plugin, NEXTJS_PLUGIN_NAME); may_exclude_plugin_rules(self.react_perf_plugin, REACT_PERF_PLUGIN_NAME); + if !self.type_info { + rules.retain(|rule| !rule.requires_type_info()) + } + rules } } diff --git a/crates/oxc_linter/src/rule.rs b/crates/oxc_linter/src/rule.rs index 82d169f7061a7..e381ba99ab401 100644 --- a/crates/oxc_linter/src/rule.rs +++ b/crates/oxc_linter/src/rule.rs @@ -24,6 +24,7 @@ pub trait RuleMeta { const NAME: &'static str; const CATEGORY: RuleCategory; + const REQUIRES_TYPE_INFO: bool; fn documentation() -> Option<&'static str> { None diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 447563367de55..41f2de0a63bfd 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -129,6 +129,7 @@ mod typescript { pub mod no_empty_interface; pub mod no_explicit_any; pub mod no_extra_non_null_assertion; + pub mod no_floating_promises; pub mod no_misused_new; pub mod no_namespace; pub mod no_non_null_asserted_optional_chain; @@ -462,6 +463,7 @@ oxc_macros::declare_all_lint_rules! { typescript::no_empty_interface, typescript::no_explicit_any, typescript::no_extra_non_null_assertion, + typescript::no_floating_promises, typescript::no_misused_new, typescript::no_namespace, typescript::no_non_null_asserted_optional_chain, diff --git a/crates/oxc_linter/src/rules/typescript/no_floating_promises.rs b/crates/oxc_linter/src/rules/typescript/no_floating_promises.rs new file mode 100644 index 0000000000000..9cea7e11e5ef9 --- /dev/null +++ b/crates/oxc_linter/src/rules/typescript/no_floating_promises.rs @@ -0,0 +1,1634 @@ +use oxc_ast::{ + ast::{ + Argument, CallExpression, ChainElement, Expression, ExpressionStatement, MemberExpression, + }, + AstKind, +}; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::Error, +}; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; +use oxc_syntax::operator::UnaryOperator; + +use crate::{context::LintContext, rule::Rule, typecheck::requests::NodeRequest, AstNode}; + +#[derive(Debug, Error, Diagnostic)] +#[error("typescript-eslint(no-floating-promises): {0:?}")] +#[diagnostic(severity(warning), help("{1:?}"))] +struct NoFloatingPromisesDiagnostic(String, String, #[label] pub Span); + +#[derive(Debug, Clone)] +pub struct NoFloatingPromises { + ignore_iife: bool, + ignore_void: bool, +} + +impl Default for NoFloatingPromises { + fn default() -> Self { + Self { ignore_iife: false, ignore_void: true } + } +} + +declare_oxc_lint!( + /// ### What it does + /// + /// Require Promise-like statements to be handled appropriately + /// + /// ### Why is this bad? + /// A "floating" Promise is one that is created without any code set up to handle any errors it might throw. Floating Promises can cause several issues, such as improperly sequenced operations, ignored Promise rejections, and more. + /// + /// ### Example + /// ```javascript + /// const promise = new Promise((resolve, reject) => resolve('value')); + /// promise; + /// + /// async function returnsPromise() { + /// return 'value'; + /// } + /// returnsPromise().then(() => {}); + /// + /// Promise.reject('value').catch(); + /// + /// Promise.reject('value').finally(); + /// + /// [1, 2, 3].map(async x => x + 1); + /// ``` + NoFloatingPromises, + nursery, + true +); + +impl Rule for NoFloatingPromises { + fn from_configuration(value: serde_json::Value) -> Self { + Self { + ignore_iife: value + .get(0) + .and_then(|x| x.get("ignoreIIFE")) + .and_then(serde_json::Value::as_bool) + .unwrap_or(false), + ignore_void: value + .get(0) + .and_then(|x| x.get("ignoreVoid")) + .and_then(serde_json::Value::as_bool) + .unwrap_or(true), + } + } + + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::ExpressionStatement(stmt) = node.kind() else { return }; + if self.ignore_iife && is_async_iife(stmt) { + return; + } + + let result = match &stmt.expression { + Expression::ChainExpression(chain) => { + self.is_unhandled_promise_chain(&chain.expression, ctx) + } + expr => self.is_unhandled_promise(expr, ctx), + }; + + // Handle result + let (msg, help_message) = match result { + PromiseState::Handled | PromiseState::Failed => { + return; + } + PromiseState::UnhandledPromiseArray => { + if self.ignore_void { + ("Consider handling the promises' fulfillment or rejection with Promise.all or similar, or explicitly marking the expression as ignored with the `void` operator.", "An array of Promises may be unintentional.") + } else { + ("Consider handling the promises' fulfillment or rejection with Promise.all or similar.", "An array of Promises may be unintentional.") + } + } + PromiseState::Unhandled => { + if self.ignore_void { + ( + "Promises must be awaited, end with a call to .catch, or end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator.", + "Add void operator to ignore." + ) + } else { + ( + "Promises must be awaited, end with a call to .catch, or end with a call to .then with a rejection handler.", + "Add await operator." + ) + } + } + PromiseState::UnhandledNonFunctionHandler => { + if self.ignore_void { + ( + "Promises must be awaited, end with a call to .catch, or end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator. A rejection handler that is not a function will be ignored.", + "Add void operator to ignore." + ) + } else { + ( + "Promises must be awaited, end with a call to .catch, or end with a call to .then with a rejection handler. A rejection handler that is not a function will be ignored.", + "Add await operator." + ) + } + } + }; + + ctx.diagnostic(NoFloatingPromisesDiagnostic(msg.into(), help_message.into(), stmt.span)); + } +} + +#[derive(Debug)] +enum PromiseState { + Handled, + Unhandled, + UnhandledPromiseArray, + UnhandledNonFunctionHandler, + Failed, +} + +impl PromiseState { + fn is_unhandled(&self) -> bool { + match self { + PromiseState::Handled | PromiseState::Failed => false, + _ => true, + } + } +} + +impl NoFloatingPromises { + fn is_unhandled_promise<'a>( + &self, + node: &Expression<'a>, + ctx: &LintContext<'a>, + ) -> PromiseState { + if let Expression::SequenceExpression(expr) = node { + let unhandled = expr + .expressions + .iter() + .map(|e| self.is_unhandled_promise(e, ctx)) + .find(|r| r.is_unhandled()); + return unhandled.unwrap_or(PromiseState::Handled); + } + + if !self.ignore_void { + if let Expression::UnaryExpression(expr) = node { + if expr.operator == UnaryOperator::Void { + return self.is_unhandled_promise(&expr.argument, ctx); + } + } + } + + let path = ctx.absolute_path(); + let request = NodeRequest { file: path, span: node.get_span().into() }; + // TODO: do something about unwrap + let is_promise_array = + ctx.use_type_checker(|type_checker| type_checker.is_promise_array(&request)); + if let Ok(is_promise_array) = is_promise_array { + if is_promise_array { + return PromiseState::UnhandledPromiseArray; + } + } else { + return PromiseState::Failed; + } + + // TODO: do something about unwrap + let is_promise_like = + ctx.use_type_checker(|type_checker| type_checker.is_promise_like(&request)); + if let Ok(is_promise_like) = is_promise_like { + if !is_promise_like { + return PromiseState::Handled; + } + } else { + return PromiseState::Failed; + } + + match node { + Expression::CallExpression(expr) => self.is_unhandled_call_expression(expr, ctx), + Expression::ConditionalExpression(expr) => { + let alternate_result = self.is_unhandled_promise(&expr.alternate, ctx); + if alternate_result.is_unhandled() { + alternate_result + } else { + self.is_unhandled_promise(&expr.consequent, ctx) + } + } + Expression::MemberExpression(_) + | Expression::Identifier(_) + | Expression::NewExpression(_) => PromiseState::Unhandled, + Expression::LogicalExpression(expr) => { + let left_result = self.is_unhandled_promise(&expr.left, ctx); + if left_result.is_unhandled() { + left_result + } else { + self.is_unhandled_promise(&expr.right, ctx) + } + } + _ => PromiseState::Handled, + } + } + + fn is_unhandled_promise_chain<'a>( + &self, + node: &ChainElement<'a>, + ctx: &LintContext<'a>, + ) -> PromiseState { + let path = ctx.absolute_path(); + let request = NodeRequest { file: path, span: node.get_span().into() }; + // TODO: do something about unwrap + let is_promise_array = + ctx.use_type_checker(|type_checker| type_checker.is_promise_array(&request)); + if let Ok(is_promise_array) = is_promise_array { + if is_promise_array { + return PromiseState::UnhandledPromiseArray; + } + } else { + return PromiseState::Failed; + } + + // TODO: do something about unwrap + let is_promise_like = + ctx.use_type_checker(|type_checker| type_checker.is_promise_like(&request)); + if let Ok(is_promise_like) = is_promise_like { + if !is_promise_like { + return PromiseState::Handled; + } + } else { + return PromiseState::Failed; + } + + match node { + ChainElement::CallExpression(expr) => self.is_unhandled_call_expression(expr, ctx), + ChainElement::MemberExpression(_) => PromiseState::Unhandled, + } + } + + fn is_unhandled_call_expression<'a>( + &self, + node: &CallExpression<'a>, + ctx: &LintContext<'a>, + ) -> PromiseState { + // If the outer expression is a call, a `.catch()` or `.then()` with + // rejection handler handles the promise. + + if let Some(catch) = get_rejection_handler_from_catch_call(node) { + if is_valid_rejection_handler(catch, ctx) { + return PromiseState::Handled; + } + return PromiseState::UnhandledNonFunctionHandler; + } + + if let Some(then) = get_rejection_handler_from_then_call(node) { + if is_valid_rejection_handler(then, ctx) { + return PromiseState::Handled; + } + return PromiseState::UnhandledNonFunctionHandler; + } + + // `x.finally()` is transparent to resolution of the promise, so check `x`. + // ("object" in this context is the `x` in `x.finally()`) + if let Some(finally) = get_object_from_finally_call(node) { + return self.is_unhandled_promise(finally, ctx); + } + + // All other cases are unhandled. + PromiseState::Unhandled + } +} + +fn is_async_iife<'a>(node: &ExpressionStatement<'a>) -> bool { + let Expression::CallExpression(ref expr) = node.expression else { return false }; + let Expression::ParenthesizedExpression(ref callee) = expr.callee else { return false }; + callee.expression.is_function() +} + +fn get_rejection_handler_from_catch_call<'a, 'b>( + node: &'b CallExpression<'a>, +) -> Option<&'b Argument<'a>> { + if let Expression::MemberExpression(callee) = &node.callee { + match &callee.0 { + MemberExpression::ComputedMemberExpression(expr) + if expr.expression.is_specific_string_literal("catch") => + { + node.arguments.first() + } + MemberExpression::StaticMemberExpression(expr) if expr.property.name == "catch" => { + node.arguments.first() + } + _ => None, + } + } else { + None + } +} + +fn get_rejection_handler_from_then_call<'a, 'b>( + node: &'b CallExpression<'a>, +) -> Option<&'b Argument<'a>> { + if let Expression::MemberExpression(callee) = &node.callee { + match &callee.0 { + MemberExpression::ComputedMemberExpression(expr) + if expr.expression.is_specific_string_literal("then") => + { + node.arguments.get(1) + } + MemberExpression::StaticMemberExpression(expr) if expr.property.name == "then" => { + node.arguments.get(1) + } + _ => None, + } + } else { + None + } +} + +fn get_object_from_finally_call<'a, 'b>( + node: &'b CallExpression<'a>, +) -> Option<&'b Expression<'a>> { + if let Expression::MemberExpression(callee) = &node.callee { + match &callee.0 { + MemberExpression::ComputedMemberExpression(expr) + if expr.expression.is_specific_string_literal("finally") => + { + Some(callee.object()) + } + MemberExpression::StaticMemberExpression(expr) if expr.property.name == "finally" => { + Some(callee.object()) + } + _ => None, + } + } else { + None + } +} + +fn is_valid_rejection_handler<'a>(node: &Argument<'a>, ctx: &LintContext<'a>) -> bool { + let path = ctx.absolute_path(); + // TODO: do something about unwrap + ctx.use_type_checker(|type_checker| { + type_checker + .is_valid_rejection_handler(&NodeRequest { file: path, span: node.get_span().into() }) + }) + .unwrap_or(true) +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ( + " + async function test() { + await Promise.resolve('value'); + Promise.resolve('value').then( + () => {}, + () => {}, + ); + Promise.resolve('value') + .then(() => {}) + .catch(() => {}); + Promise.resolve('value') + .then(() => {}) + .catch(() => {}) + .finally(() => {}); + Promise.resolve('value').catch(() => {}); + return Promise.resolve('value'); + } + ", + None, + ), + ( + " + async function test() { + void Promise.resolve('value'); + } + ", + Some(serde_json::json!([{ "ignoreVoid": true }])), + ), + ( + " + async function test() { + await Promise.reject(new Error('message')); + Promise.reject(new Error('message')).then( + () => {}, + () => {}, + ); + Promise.reject(new Error('message')) + .then(() => {}) + .catch(() => {}); + Promise.reject(new Error('message')) + .then(() => {}) + .catch(() => {}) + .finally(() => {}); + Promise.reject(new Error('message')).catch(() => {}); + return Promise.reject(new Error('message')); + } + ", + None, + ), + ( + " + async function test() { + await (async () => true)(); + (async () => true)().then( + () => {}, + () => {}, + ); + (async () => true)() + .then(() => {}) + .catch(() => {}); + (async () => true)() + .then(() => {}) + .catch(() => {}) + .finally(() => {}); + (async () => true)().catch(() => {}); + return (async () => true)(); + } + ", + None, + ), + ( + " + async function test() { + async function returnsPromise() {} + await returnsPromise(); + returnsPromise().then( + () => {}, + () => {}, + ); + returnsPromise() + .then(() => {}) + .catch(() => {}); + returnsPromise() + .then(() => {}) + .catch(() => {}) + .finally(() => {}); + returnsPromise().catch(() => {}); + return returnsPromise(); + } + ", + None, + ), + ( + " + async function test() { + const x = Promise.resolve(); + const y = x.then(() => {}); + y.catch(() => {}); + } + ", + None, + ), + ( + " + async function test() { + Math.random() > 0.5 ? Promise.resolve().catch(() => {}) : null; + } + ", + None, + ), + ( + " + async function test() { + Promise.resolve().catch(() => {}), 123; + 123, + Promise.resolve().then( + () => {}, + () => {}, + ); + 123, + Promise.resolve().then( + () => {}, + () => {}, + ), + 123; + } + ", + None, + ), + ( + " + async function test() { + void Promise.resolve().catch(() => {}); + } + ", + None, + ), + ( + " + async function test() { + Promise.resolve().catch(() => {}) || + Promise.resolve().then( + () => {}, + () => {}, + ); + } + ", + None, + ), + ( + " + async function test() { + declare const promiseValue: Promise; + + await promiseValue; + promiseValue.then( + () => {}, + () => {}, + ); + promiseValue.then(() => {}).catch(() => {}); + promiseValue + .then(() => {}) + .catch(() => {}) + .finally(() => {}); + promiseValue.catch(() => {}); + return promiseValue; + } + ", + None, + ), + ( + " + async function test() { + declare const promiseUnion: Promise | number; + + await promiseUnion; + promiseUnion.then( + () => {}, + () => {}, + ); + promiseUnion.then(() => {}).catch(() => {}); + promiseUnion + .then(() => {}) + .catch(() => {}) + .finally(() => {}); + promiseUnion.catch(() => {}); + promiseValue.finally(() => {}); + return promiseUnion; + } + ", + None, + ), + ( + " + async function test() { + declare const promiseIntersection: Promise & number; + + await promiseIntersection; + promiseIntersection.then( + () => {}, + () => {}, + ); + promiseIntersection.then(() => {}).catch(() => {}); + promiseIntersection.catch(() => {}); + return promiseIntersection; + } + ", + None, + ), + ( + " + async function test() { + class CanThen extends Promise {} + const canThen: CanThen = Foo.resolve(2); + + await canThen; + canThen.then( + () => {}, + () => {}, + ); + canThen.then(() => {}).catch(() => {}); + canThen + .then(() => {}) + .catch(() => {}) + .finally(() => {}); + canThen.catch(() => {}); + return canThen; + } + ", + None, + ), + ( + " + async function test() { + await (Math.random() > 0.5 ? numberPromise : 0); + await (Math.random() > 0.5 ? foo : 0); + await (Math.random() > 0.5 ? bar : 0); + + declare const intersectionPromise: Promise & number; + await intersectionPromise; + } + ", + None, + ), + ( + " + async function test() { + class Thenable { + then(callback: () => void): Thenable { + return new Thenable(); + } + } + const thenable = new Thenable(); + + await thenable; + thenable; + thenable.then(() => {}); + return thenable; + } + ", + None, + ), + ( + " + async function test() { + class NonFunctionParamThenable { + then(param: string, param2: number): NonFunctionParamThenable { + return new NonFunctionParamThenable(); + } + } + const thenable = new NonFunctionParamThenable(); + + await thenable; + thenable; + thenable.then('abc', 'def'); + return thenable; + } + ", + None, + ), + ( + " + async function test() { + class NonFunctionThenable { + then: number; + } + const thenable = new NonFunctionThenable(); + + thenable; + thenable.then; + return thenable; + } + ", + None, + ), + ( + " + async function test() { + class CatchableThenable { + then(resolve: () => void, reject: () => void): CatchableThenable { + return new CatchableThenable(); + } + } + const thenable = new CatchableThenable(); + + await thenable; + return thenable; + } + ", + None, + ), + ( + " + // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/promise-polyfill/index.d.ts + // Type definitions for promise-polyfill 6.0 + // Project: https://github.com/taylorhakes/promise-polyfill + // Definitions by: Steve Jenkins + // Daniel Cassidy + // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + + interface PromisePolyfillConstructor extends PromiseConstructor { + _immediateFn?: (handler: (() => void) | string) => void; + } + + declare const PromisePolyfill: PromisePolyfillConstructor; + + async function test() { + const promise = new PromisePolyfill(() => {}); + + await promise; + promise.then( + () => {}, + () => {}, + ); + promise.then(() => {}).catch(() => {}); + promise + .then(() => {}) + .catch(() => {}) + .finally(() => {}); + promise.catch(() => {}); + return promise; + } + ", + None, + ), + ( + " + async function test() { + declare const returnsPromise: () => Promise | null; + await returnsPromise?.(); + returnsPromise()?.then( + () => {}, + () => {}, + ); + returnsPromise() + ?.then(() => {}) + ?.catch(() => {}); + returnsPromise()?.catch(() => {}); + return returnsPromise(); + } + ", + None, + ), + ( + " + const doSomething = async ( + obj1: { a?: { b?: { c?: () => Promise } } }, + obj2: { a?: { b?: { c: () => Promise } } }, + obj3: { a?: { b: { c?: () => Promise } } }, + obj4: { a: { b: { c?: () => Promise } } }, + obj5: { a?: () => { b?: { c?: () => Promise } } }, + obj6?: { a: { b: { c?: () => Promise } } }, + callback?: () => Promise, + ): Promise => { + await obj1.a?.b?.c?.(); + await obj2.a?.b?.c(); + await obj3.a?.b.c?.(); + await obj4.a.b.c?.(); + await obj5.a?.().b?.c?.(); + await obj6?.a.b.c?.(); + + return callback?.(); + }; + + void doSomething(); + ", + None, + ), + ( + " + (async () => { + await something(); + })(); + ", + Some(serde_json::json!([{ "ignoreIIFE": true }])), + ), + ( + " + (async () => { + something(); + })(); + ", + Some(serde_json::json!([{ "ignoreIIFE": true }])), + ), + ("(async function foo() {})();", Some(serde_json::json!([{ "ignoreIIFE": true }]))), + ( + " + function foo() { + (async function bar() {})(); + } + ", + Some(serde_json::json!([{ "ignoreIIFE": true }])), + ), + // TODO: ignore promises in return statements + ( + " + const foo = () => + new Promise(res => { + (async function () { + await res(1); + })(); + }); + ", + Some(serde_json::json!([{ "ignoreIIFE": true }])), + ), + ( + " + (async function () { + await res(1); + })(); + ", + Some(serde_json::json!([{ "ignoreIIFE": true }])), + ), + ( + " + async function foo() { + const myPromise = async () => void 0; + const condition = true; + void (condition && myPromise()); + } + ", + None, + ), + ( + " + async function foo() { + const myPromise = async () => void 0; + const condition = true; + await (condition && myPromise()); + } + ", + Some(serde_json::json!([{ "ignoreVoid": false }])), + ), + ( + " + async function foo() { + const myPromise = async () => void 0; + const condition = true; + condition && void myPromise(); + } + ", + None, + ), + ( + " + async function foo() { + const myPromise = async () => void 0; + const condition = true; + condition && (await myPromise()); + } + ", + Some(serde_json::json!([{ "ignoreVoid": false }])), + ), + ( + " + async function foo() { + const myPromise = async () => void 0; + let condition = false; + condition && myPromise(); + condition = true; + condition || myPromise(); + condition ?? myPromise(); + } + ", + Some(serde_json::json!([{ "ignoreVoid": false }])), + ), + ( + " + declare const definitelyCallable: () => void; + Promise.reject().catch(definitelyCallable); + ", + Some(serde_json::json!([{ "ignoreVoid": false }])), + ), + ( + " + Promise.reject() + .catch(() => {}) + .finally(() => {}); + ", + None, + ), + ( + " + Promise.reject() + .catch(() => {}) + .finally(() => {}) + .finally(() => {}); + ", + Some(serde_json::json!([{ "ignoreVoid": false }])), + ), + ( + " + Promise.reject() + .catch(() => {}) + .finally(() => {}) + .finally(() => {}) + .finally(() => {}); + ", + None, + ), + ( + " + await Promise.all([Promise.resolve(), Promise.resolve()]); + ", + None, + ), + ( + " + declare const promiseArray: Array>; + void promiseArray; + ", + None, + ), + ( + " + [Promise.reject(), Promise.reject()].then(() => {}); + ", + None, + ), + ( + " + [1, 2, void Promise.reject(), 3]; + ", + Some(serde_json::json!([{ "ignoreVoid": false }])), + ), + ( + " + ['I', 'am', 'just', 'an', 'array']; + ", + None, + ), + ( + " + declare const myTag: (strings: TemplateStringsArray) => Promise; + myTag`abc`.catch(() => {}); + ", + None, + ), + ( + " + declare const myTag: (strings: TemplateStringsArray) => string; + myTag`abc`; + ", + None, + ), + ]; + + let fail = vec![ + ( + " + async function test() { + Promise.resolve('value'); + Promise.resolve('value').then(() => {}); + Promise.resolve('value').catch(); + Promise.resolve('value').finally(); + } + ", + None, + ), + ( + " + const doSomething = async ( + obj1: { a?: { b?: { c?: () => Promise } } }, + obj2: { a?: { b?: { c: () => Promise } } }, + obj3: { a?: { b: { c?: () => Promise } } }, + obj4: { a: { b: { c?: () => Promise } } }, + obj5: { a?: () => { b?: { c?: () => Promise } } }, + obj6?: { a: { b: { c?: () => Promise } } }, + callback?: () => Promise, + ): Promise => { + obj1.a?.b?.c?.(); + obj2.a?.b?.c(); + obj3.a?.b.c?.(); + obj4.a.b.c?.(); + obj5.a?.().b?.c?.(); + obj6?.a.b.c?.(); + + callback?.(); + }; + + doSomething(); + ", + None, + ), + ( + " + declare const myTag: (strings: TemplateStringsArray) => Promise; + myTag`abc`; + ", + None, + ), + ( + " + declare const myTag: (strings: TemplateStringsArray) => Promise; + myTag`abc`.then(() => {}); + ", + None, + ), + ( + " + declare const myTag: (strings: TemplateStringsArray) => Promise; + myTag`abc`.finally(() => {}); + ", + None, + ), + ( + " + async function test() { + Promise.resolve('value'); + } + ", + Some(serde_json::json!([{ "ignoreVoid": true }])), + ), + ( + " + async function test() { + Promise.reject(new Error('message')); + Promise.reject(new Error('message')).then(() => {}); + Promise.reject(new Error('message')).catch(); + Promise.reject(new Error('message')).finally(); + } + ", + None, + ), + ( + " + async function test() { + (async () => true)(); + (async () => true)().then(() => {}); + (async () => true)().catch(); + } + ", + None, + ), + ( + " + async function test() { + async function returnsPromise() {} + + returnsPromise(); + returnsPromise().then(() => {}); + returnsPromise().catch(); + returnsPromise().finally(); + } + ", + None, + ), + ( + " + async function test() { + Math.random() > 0.5 ? Promise.resolve() : null; + Math.random() > 0.5 ? null : Promise.resolve(); + } + ", + None, + ), + ( + " + async function test() { + Promise.resolve(), 123; + 123, Promise.resolve(); + 123, Promise.resolve(), 123; + } + ", + None, + ), + ( + " + async function test() { + void Promise.resolve(); + } + ", + Some(serde_json::json!([{ "ignoreVoid": false }])), + ), + ( + " + async function test() { + const promise = new Promise((resolve, reject) => resolve('value')); + promise; + } + ", + Some(serde_json::json!([{ "ignoreVoid": false }])), + ), + ( + " + async function returnsPromise() { + return 'value'; + } + void returnsPromise(); + ", + Some(serde_json::json!([{ "ignoreVoid": false }])), + ), + ( + " + async function returnsPromise() { + return 'value'; + } + void /* ... */ returnsPromise(); + ", + Some(serde_json::json!([{ "ignoreVoid": false }])), + ), + ( + " + async function returnsPromise() { + return 'value'; + } + 1, returnsPromise(); + ", + Some(serde_json::json!([{ "ignoreVoid": false }])), + ), + ( + " + async function returnsPromise() { + return 'value'; + } + bool ? returnsPromise() : null; + ", + Some(serde_json::json!([{ "ignoreVoid": false }])), + ), + ( + " + async function test() { + const obj = { foo: Promise.resolve() }; + obj.foo; + } + ", + None, + ), + ( + " + async function test() { + new Promise(resolve => resolve()); + } + ", + None, + ), + ( + " + async function test() { + declare const promiseValue: Promise; + + promiseValue; + promiseValue.then(() => {}); + promiseValue.catch(); + promiseValue.finally(); + } + ", + None, + ), + ( + " + async function test() { + declare const promiseUnion: Promise | number; + + promiseUnion; + } + ", + None, + ), + ( + " + async function test() { + declare const promiseIntersection: Promise & number; + + promiseIntersection; + promiseIntersection.then(() => {}); + promiseIntersection.catch(); + } + ", + None, + ), + ( + " + async function test() { + class CanThen extends Promise {} + const canThen: CanThen = Foo.resolve(2); + + canThen; + canThen.then(() => {}); + canThen.catch(); + canThen.finally(); + } + ", + None, + ), + ( + " + async function test() { + class CatchableThenable { + then(callback: () => void, callback: () => void): CatchableThenable { + return new CatchableThenable(); + } + } + const thenable = new CatchableThenable(); + + thenable; + thenable.then(() => {}); + } + ", + None, + ), + ( + " + // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/promise-polyfill/index.d.ts + // Type definitions for promise-polyfill 6.0 + // Project: https://github.com/taylorhakes/promise-polyfill + // Definitions by: Steve Jenkins + // Daniel Cassidy + // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + + interface PromisePolyfillConstructor extends PromiseConstructor { + _immediateFn?: (handler: (() => void) | string) => void; + } + + declare const PromisePolyfill: PromisePolyfillConstructor; + + async function test() { + const promise = new PromisePolyfill(() => {}); + + promise; + promise.then(() => {}); + promise.catch(); + } + ", + None, + ), + ( + " + (async () => { + await something(); + })(); + ", + None, + ), + ( + " + (async () => { + something(); + })(); + ", + None, + ), + ("(async function foo() {})();", None), + ( + " + function foo() { + (async function bar() {})(); + } + ", + None, + ), + ( + " + const foo = () => + new Promise(res => { + (async function () { + await res(1); + })(); + }); + ", + None, + ), + ( + " + (async function () { + await res(1); + })(); + ", + None, + ), + ( + " + (async function () { + Promise.resolve(); + })(); + ", + Some(serde_json::json!([{ "ignoreIIFE": true }])), + ), + ( + " + (async function () { + declare const promiseIntersection: Promise & number; + promiseIntersection; + promiseIntersection.then(() => {}); + promiseIntersection.catch(); + promiseIntersection.finally(); + })(); + ", + Some(serde_json::json!([{ "ignoreIIFE": true }])), + ), + ( + " + async function foo() { + const myPromise = async () => void 0; + const condition = true; + + void condition || myPromise(); + } + ", + None, + ), + ( + " + async function foo() { + const myPromise = async () => void 0; + const condition = true; + + (await condition) && myPromise(); + } + ", + Some(serde_json::json!([{ "ignoreVoid": false }])), + ), + ( + " + async function foo() { + const myPromise = async () => void 0; + const condition = true; + + condition && myPromise(); + } + ", + None, + ), + ( + " + async function foo() { + const myPromise = async () => void 0; + const condition = false; + + condition || myPromise(); + } + ", + None, + ), + ( + " + async function foo() { + const myPromise = async () => void 0; + const condition = null; + + condition ?? myPromise(); + } + ", + None, + ), + ( + " + async function foo() { + const myPromise = Promise.resolve(true); + let condition = true; + condition && myPromise; + } + ", + Some(serde_json::json!([{ "ignoreVoid": false }])), + ), + ( + " + async function foo() { + const myPromise = Promise.resolve(true); + let condition = false; + condition || myPromise; + } + ", + Some(serde_json::json!([{ "ignoreVoid": false }])), + ), + ( + " + async function foo() { + const myPromise = Promise.resolve(true); + let condition = null; + condition ?? myPromise; + } + ", + Some(serde_json::json!([{ "ignoreVoid": false }])), + ), + ( + " + async function foo() { + const myPromise = async () => void 0; + const condition = false; + + condition || condition || myPromise(); + } + ", + None, + ), + ( + " + declare const maybeCallable: string | (() => void); + declare const definitelyCallable: () => void; + Promise.resolve().then(() => {}, undefined); + Promise.resolve().then(() => {}, null); + Promise.resolve().then(() => {}, 3); + Promise.resolve().then(() => {}, maybeCallable); + Promise.resolve().then(() => {}, definitelyCallable); + + Promise.resolve().catch(undefined); + Promise.resolve().catch(null); + Promise.resolve().catch(3); + Promise.resolve().catch(maybeCallable); + Promise.resolve().catch(definitelyCallable); + ", + None, + ), + ( + " + Promise.reject() || 3; + ", + None, + ), + ( + " + void Promise.resolve().then(() => {}, undefined); + ", + Some(serde_json::json!([{ "ignoreVoid": false }])), + ), + ( + " + declare const maybeCallable: string | (() => void); + Promise.resolve().then(() => {}, maybeCallable); + ", + Some(serde_json::json!([{ "ignoreVoid": false }])), + ), + ( + " + declare const maybeCallable: string | (() => void); + declare const definitelyCallable: () => void; + Promise.resolve().then(() => {}, undefined); + Promise.resolve().then(() => {}, null); + Promise.resolve().then(() => {}, 3); + Promise.resolve().then(() => {}, maybeCallable); + Promise.resolve().then(() => {}, definitelyCallable); + + Promise.resolve().catch(undefined); + Promise.resolve().catch(null); + Promise.resolve().catch(3); + Promise.resolve().catch(maybeCallable); + Promise.resolve().catch(definitelyCallable); + ", + Some(serde_json::json!([{ "ignoreVoid": false }])), + ), + ( + " + Promise.reject() || 3; + ", + Some(serde_json::json!([{ "ignoreVoid": false }])), + ), + ( + " + Promise.reject().finally(() => {}); + ", + None, + ), + ( + " + Promise.reject() + .finally(() => {}) + .finally(() => {}); + ", + Some(serde_json::json!([{ "ignoreVoid": false }])), + ), + ( + " + Promise.reject() + .finally(() => {}) + .finally(() => {}) + .finally(() => {}); + ", + None, + ), + ( + " + Promise.reject() + .then(() => {}) + .finally(() => {}); + ", + None, + ), + ( + " + declare const returnsPromise: () => Promise | null; + returnsPromise()?.finally(() => {}); + ", + None, + ), + ( + " + const promiseIntersection: Promise & number; + promiseIntersection.finally(() => {}); + ", + None, + ), + ( + " + Promise.resolve().finally(() => {}), 123; + ", + None, + ), + ( + " + (async () => true)().finally(); + ", + None, + ), + ( + " + Promise.reject(new Error('message')).finally(() => {}); + ", + None, + ), + ( + " + function _>>( + maybePromiseArray: S | undefined, + ): void { + maybePromiseArray?.[0]; + } + ", + None, + ), + ( + " + [1, 2, 3].map(() => Promise.reject()); + ", + None, + ), + ( + " + declare const array: unknown[]; + array.map(() => Promise.reject()); + ", + None, + ), + ( + " + declare const promiseArray: Array>; + void promiseArray; + ", + Some(serde_json::json!([{ "ignoreVoid": false }])), + ), + ( + " + [1, 2, Promise.reject(), 3]; + ", + None, + ), + ( + " + [1, 2, Promise.reject().catch(() => {}), 3]; + ", + None, + ), + ( + " + const data = ['test']; + data.map(async () => { + await new Promise((_res, rej) => setTimeout(rej, 1000)); + }); + ", + None, + ), + ( + " + function _>>>( + maybePromiseArrayArray: S | undefined, + ): void { + maybePromiseArrayArray?.[0]; + } + ", + None, + ), + ( + " + function f>>(a: T): void { + a; + } + ", + None, + ), + ( + " + declare const a: Array> | undefined; + a; + ", + None, + ), + ( + " + function f>>(a: T | undefined): void { + a; + } + ", + None, + ), + ( + " + [Promise.reject()] as const; + ", + None, + ), + ( + " + declare function cursed(): [Promise, Promise]; + cursed(); + ", + None, + ), + ( + " + [ + 'Type Argument number ', + 1, + 'is not', + Promise.resolve(), + 'but it still is flagged', + ] as const; + ", + None, + ), + ( + " + declare const arrayOrPromiseTuple: + | Array + | [number, number, Promise, string]; + arrayOrPromiseTuple; + ", + None, + ), + ( + " + declare const okArrayOrPromiseArray: Array | Array>; + okArrayOrPromiseArray; + ", + None, + ), + ]; + + Tester::new(NoFloatingPromises::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/service.rs b/crates/oxc_linter/src/service.rs index 8203ec2c7de74..b98df8d16bbdb 100644 --- a/crates/oxc_linter/src/service.rs +++ b/crates/oxc_linter/src/service.rs @@ -1,5 +1,6 @@ use std::{ collections::HashMap, + env, ffi::OsStr, fs, path::{Path, PathBuf}, @@ -20,6 +21,10 @@ use oxc_span::{SourceType, VALID_EXTENSIONS}; use crate::{ partial_loader::{JavaScriptSource, PartialLoader, LINT_PARTIAL_LOADER_EXT}, + typecheck::{ + requests::{FileRequest, OpenRequest}, + start_typecheck_server, TSServerClient, + }, Fixer, LintContext, Linter, Message, }; @@ -129,20 +134,23 @@ pub struct Runtime { paths: FxHashSet>, linter: Linter, resolver: Option, + type_checker: Option>>, module_map: ModuleMap, cache_state: CacheState, } impl Runtime { fn new(linter: Linter, options: LintServiceOptions) -> Self { - let resolver = linter.options().import_plugin.then(|| { - Self::get_resolver(options.tsconfig.or_else(|| Some(options.cwd.join("tsconfig.json")))) - }); + let tsconfig = options.tsconfig.or_else(|| Some(options.cwd.join("tsconfig.json"))); + let resolver = linter.options().import_plugin.then(|| Self::get_resolver(tsconfig)); + let type_checker = + linter.options.type_info.then(|| Arc::new(Mutex::new(Self::get_type_checker()))); Self { cwd: options.cwd, paths: options.paths.iter().cloned().collect(), linter, resolver, + type_checker, module_map: ModuleMap::default(), cache_state: CacheState::default(), } @@ -166,6 +174,13 @@ impl Runtime { }) } + fn get_type_checker() -> TSServerClient { + // TODO: get actual path from somewhere. And gracefully handle errors. + let path = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../npm/oxc-typecheck/dist/server.js"); + start_typecheck_server(path.to_string_lossy().as_ref()).unwrap() + } + fn get_source_type_and_text( path: &Path, ext: &str, @@ -344,9 +359,33 @@ impl Runtime { return semantic_ret.errors.into_iter().map(|err| Message::new(err, None)).collect(); }; - let lint_ctx = - LintContext::new(path.to_path_buf().into_boxed_path(), &Rc::new(semantic_ret.semantic)); - self.linter.run(lint_ctx) + let lint_ctx = LintContext::new( + self.cwd.as_ref(), + path.to_path_buf().into_boxed_path(), + &Rc::new(semantic_ret.semantic), + self.type_checker.clone(), + ); + + if let Some(ref type_checker) = self.type_checker { + // TODO: do something about unwrap + let mut type_checker = type_checker.lock().unwrap(); + type_checker + .open(&OpenRequest { + file: lint_ctx.absolute_path(), + file_content: Some(&source_text), + }) + .unwrap(); + } + + let result = self.linter.run(lint_ctx); + + if let Some(ref type_checker) = self.type_checker { + // TODO: do something about unwrap + let mut type_checker = type_checker.lock().unwrap(); + type_checker.close(&FileRequest { file: path.to_string_lossy().as_ref() }).unwrap(); + } + + result } fn init_cache_state(&self, path: &Path) -> bool { diff --git a/crates/oxc_linter/src/tester.rs b/crates/oxc_linter/src/tester.rs index f4c3d84ccd71b..5582b97d35148 100644 --- a/crates/oxc_linter/src/tester.rs +++ b/crates/oxc_linter/src/tester.rs @@ -1,7 +1,4 @@ -use std::{ - env, - path::{Path, PathBuf}, -}; +use std::{env, path::PathBuf}; use oxc_allocator::Allocator; use oxc_diagnostics::miette::NamedSource; @@ -57,12 +54,12 @@ impl From<(&str, Option, Option, Option)> for TestCase { pub struct Tester { rule_name: &'static str, - rule_path: PathBuf, + rule_path: Option, + tsconfig: Option, expect_pass: Vec, expect_fail: Vec, expect_fix: Vec<(String, String, Option)>, snapshot: String, - current_working_directory: Box, import_plugin: bool, jest_plugin: bool, jsx_a11y_plugin: bool, @@ -76,19 +73,17 @@ impl Tester { expect_pass: Vec, expect_fail: Vec, ) -> Self { - let rule_path = PathBuf::from(rule_name.replace('-', "_")).with_extension("tsx"); let expect_pass = expect_pass.into_iter().map(Into::into).collect::>(); let expect_fail = expect_fail.into_iter().map(Into::into).collect::>(); - let current_working_directory = - env::current_dir().unwrap().join("fixtures/import").into_boxed_path(); + Self { rule_name, - rule_path, + rule_path: None, + tsconfig: None, expect_pass, expect_fail, expect_fix: vec![], snapshot: String::new(), - current_working_directory, import_plugin: false, jest_plugin: false, jsx_a11y_plugin: false, @@ -99,7 +94,7 @@ impl Tester { /// Change the path pub fn change_rule_path(mut self, path: &str) -> Self { - self.rule_path = self.current_working_directory.join(path); + self.rule_path = Some(PathBuf::from(path)); self } @@ -189,6 +184,7 @@ impl Tester { ) -> TestResult { let allocator = Allocator::default(); let rule = self.find_rule().read_json(config); + let requires_type_info = rule.requires_type_info(); let lint_settings: ESLintSettings = settings .as_ref() .map_or_else(ESLintSettings::default, |v| ESLintSettings::deserialize(v).unwrap()); @@ -198,23 +194,39 @@ impl Tester { .with_jest_plugin(self.jest_plugin) .with_jsx_a11y_plugin(self.jsx_a11y_plugin) .with_nextjs_plugin(self.nextjs_plugin) - .with_react_perf_plugin(self.react_perf_plugin); + .with_react_perf_plugin(self.react_perf_plugin) + .with_type_info(requires_type_info); let linter = Linter::from_options(options) .unwrap() .with_rules(vec![rule]) .with_settings(lint_settings); + let cwd = env::current_dir() + .unwrap() + .join(if requires_type_info { "fixtures/typecheck" } else { "fixtures/import" }) + .into_boxed_path(); + + let default_rule_file = PathBuf::from(self.rule_name.replace('-', "_") + ".tsx"); let path_to_lint = if self.import_plugin { assert!(path.is_none(), "import plugin does not support path"); - self.current_working_directory.join(&self.rule_path) + cwd.join(self.rule_path.as_ref().unwrap_or(&default_rule_file)) + } else if requires_type_info { + assert!(path.is_none(), "type check rules do not support path"); + let default_path = PathBuf::from("file.ts"); + cwd.join(self.rule_path.as_ref().unwrap_or(&default_path)) } else if let Some(path) = path { - self.current_working_directory.join(path) + cwd.join(path) + } else { + default_rule_file.clone() + }; + let tsconfig = if requires_type_info { + let default_tsconfig = PathBuf::from("tsconfig.json"); + Some(cwd.join(self.tsconfig.as_ref().unwrap_or(&default_tsconfig))) } else { - self.rule_path.clone() + None }; - let cwd = self.current_working_directory.clone(); let paths = vec![path_to_lint.into_boxed_path()]; - let options = LintServiceOptions { cwd, paths, tsconfig: None }; + let options = LintServiceOptions { cwd: cwd.clone(), paths, tsconfig }; let lint_service = LintService::from_linter(linter, options); let diagnostic_service = DiagnosticService::default(); let tx_error = diagnostic_service.sender(); @@ -230,9 +242,9 @@ impl Tester { } let diagnostic_path = if self.import_plugin { - self.rule_path.strip_prefix(&self.current_working_directory).unwrap() + self.rule_path.as_ref().unwrap_or(&default_rule_file) } else { - &self.rule_path + &default_rule_file } .to_string_lossy(); diff --git a/crates/oxc_linter/src/typecheck/client.rs b/crates/oxc_linter/src/typecheck/client.rs new file mode 100644 index 0000000000000..5eef39bf96617 --- /dev/null +++ b/crates/oxc_linter/src/typecheck/client.rs @@ -0,0 +1,161 @@ +use std::{ + process::{Child, ChildStdin, ChildStdout}, + time::{Duration, Instant}, +}; + +use super::{requests::*, response::*, utils::read_message, ProtocolError}; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::Error, +}; + +pub struct TSServerClient { + server: Child, + seq: usize, + command_stream: W, + result_stream: R, + running: bool, + start: Instant, + previous_duration: Duration, +} + +impl TSServerClient { + pub fn status(&mut self) -> Result { + self.send_command("status", None)?; + + let result = read_message(&mut self.result_stream); + + self.update_duration(); + result + } + + pub fn exit(&mut self) -> Result<(), ProtocolError> { + if !self.running { + return Ok(()); + } + + let result = self.send_command("exit", None); + if result.is_ok() { + self.running = false; + self.server.wait()?; + } else { + self.server.kill()?; + } + + Ok(()) + } + + pub fn open(&mut self, opts: &OpenRequest<'_>) -> Result<(), ProtocolError> { + let args = serde_json::to_string(&opts)?; + self.send_command("open", Some(args.as_str()))?; + + let result = wait_done(&mut self.result_stream); + self.update_duration(); + result + } + + pub fn close(&mut self, opts: &FileRequest<'_>) -> Result<(), ProtocolError> { + let args = serde_json::to_string(&opts)?; + self.send_command("close", Some(args.as_str()))?; + + let result = wait_done(&mut self.result_stream); + self.update_duration(); + result + } + + pub fn get_node(&mut self, opts: &NodeRequest<'_>) -> Result { + let args = serde_json::to_string(&opts)?; + self.send_command("getNode", Some(args.as_str()))?; + + let result = read_message(&mut self.result_stream); + self.update_duration(); + result + } + + pub fn is_promise_array(&mut self, opts: &NodeRequest<'_>) -> Result { + let args = serde_json::to_string(&opts)?; + self.send_command("noFloatingPromises::isPromiseArray", Some(args.as_str()))?; + + let response = read_message::(&mut self.result_stream); + self.update_duration(); + Ok(response?.result) + } + + pub fn is_promise_like(&mut self, opts: &NodeRequest<'_>) -> Result { + let args = serde_json::to_string(&opts)?; + self.send_command("noFloatingPromises::isPromiseLike", Some(args.as_str()))?; + + let response = read_message::(&mut self.result_stream); + self.update_duration(); + Ok(response?.result) + } + + pub fn is_valid_rejection_handler( + &mut self, + opts: &NodeRequest<'_>, + ) -> Result { + let args = serde_json::to_string(&opts)?; + self.send_command("noFloatingPromises::isValidRejectionHandler", Some(args.as_str()))?; + + let response = read_message::(&mut self.result_stream); + self.update_duration(); + Ok(response?.result) + } + + fn send_command(&mut self, command: &str, args: Option<&str>) -> Result<(), std::io::Error> { + self.seq += 1; + let seq = self.seq; + let args_str = args.map(|x| format!(r#","arguments":{x}"#)).unwrap_or("".to_string()); + let previous_duration = self.previous_duration.as_nanos(); + let msg = + format!("{{\"seq\":{seq},\"type\":\"request\",\"previousDuration\":{previous_duration},\"command\":\"{command}\"{args_str}}}\n"); + + self.start = Instant::now(); + self.command_stream.write_all(msg.as_bytes()) + } + + fn update_duration(&mut self) { + self.previous_duration = Instant::now() - self.start; + } +} + +fn wait_done(result_stream: impl std::io::Read) -> Result<(), ProtocolError> { + read_message::(result_stream)?; + Ok(()) +} + +#[derive(Debug, Error, Diagnostic)] +#[diagnostic()] +pub enum FromChildError { + #[error("child stdout must be piped")] + MissingStdoutStream, + #[error("child stdin must be piped")] + MissingStdinStream, +} + +impl TryFrom for TSServerClient { + type Error = FromChildError; + + fn try_from(mut value: Child) -> Result { + let command_stream = value.stdin.take().ok_or(FromChildError::MissingStdinStream)?; + let result_stream = value.stdout.take().ok_or(FromChildError::MissingStdoutStream)?; + + Ok(Self { + server: value, + seq: 0, + command_stream, + result_stream, + running: true, + start: Instant::now(), + previous_duration: Duration::new(0, 0), + }) + } +} + +impl Drop for TSServerClient { + fn drop(&mut self) { + if self.running { + let _ = self.exit(); + } + } +} diff --git a/crates/oxc_linter/src/typecheck/mod.rs b/crates/oxc_linter/src/typecheck/mod.rs new file mode 100644 index 0000000000000..b0b8f6a41551d --- /dev/null +++ b/crates/oxc_linter/src/typecheck/mod.rs @@ -0,0 +1,44 @@ +pub(self) mod client; +pub(self) mod protocol_error; +pub mod requests; +pub mod response; +pub(self) mod utils; + +#[cfg(windows)] +pub(self) const EOL_LENGTH: usize = 2; +#[cfg(not(windows))] +pub(self) const EOL_LENGTH: usize = 1; + +use std::{ + error::Error, + process::{Command, Stdio}, +}; + +pub use protocol_error::ProtocolError; + +pub type TSServerClient = + client::TSServerClient; + +pub fn start_typecheck_server(server_path: &str) -> Result> { + // Start the TSServer + let tsserver = Command::new("node") + .args([ + "--max-old-space-size=4096", + server_path, + "--disableAutomaticTypingAcquisition", + "--suppressDiagnosticEvents", + ]) + .env_clear() + .env("NODE_ENV", "production") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + // .env("DEBUG", "true") + // .stderr(Stdio::inherit()) + .spawn()?; + + let mut client = TSServerClient::try_from(tsserver)?; + client.status()?; + + Ok(client) +} diff --git a/crates/oxc_linter/src/typecheck/protocol_error.rs b/crates/oxc_linter/src/typecheck/protocol_error.rs new file mode 100644 index 0000000000000..0e4c5c790a302 --- /dev/null +++ b/crates/oxc_linter/src/typecheck/protocol_error.rs @@ -0,0 +1,24 @@ +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::{self, Error}, +}; +#[derive(Debug, Error, Diagnostic)] +#[diagnostic()] +pub enum ProtocolError { + #[error("unexpected character")] + UnexpectedCharacter, + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + StrUtf8(#[from] std::str::Utf8Error), + #[error(transparent)] + StringUtf8(#[from] std::string::FromUtf8Error), + #[error(transparent)] + ParseInt(#[from] std::num::ParseIntError), + #[error(transparent)] + SerdeJson(#[from] serde_json::Error), + #[error("command failed")] + CommandFailed(String), + #[error("missing result")] + ResultMissing, +} diff --git a/crates/oxc_linter/src/typecheck/requests.rs b/crates/oxc_linter/src/typecheck/requests.rs new file mode 100644 index 0000000000000..e7fa217dc4537 --- /dev/null +++ b/crates/oxc_linter/src/typecheck/requests.rs @@ -0,0 +1,37 @@ +use serde::Serialize; + +/// https://github.com/microsoft/TypeScript/blob/61200368bb440ba8a40641be87c44d875ca31f69/src/server/protocol.ts#L1715 +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct OpenRequest<'a> { + pub file: &'a str, + #[serde(skip_serializing_if = "Option::is_none")] + pub file_content: Option<&'a str>, +} + +/// https://github.com/microsoft/TypeScript/blob/61200368bb440ba8a40641be87c44d875ca31f69/src/server/protocol.ts#L292 +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FileRequest<'a> { + pub file: &'a str, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Span { + pub pos: u32, + pub end: u32, +} + +impl From<&oxc_span::Span> for Span { + fn from(value: &oxc_span::Span) -> Self { + Self { pos: value.start, end: value.end } + } +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NodeRequest<'a> { + pub file: &'a str, + pub span: Span, +} diff --git a/crates/oxc_linter/src/typecheck/response.rs b/crates/oxc_linter/src/typecheck/response.rs new file mode 100644 index 0000000000000..8c03af8e99960 --- /dev/null +++ b/crates/oxc_linter/src/typecheck/response.rs @@ -0,0 +1,49 @@ +use serde::Deserialize; + +use super::ProtocolError; + +#[derive(Debug, Deserialize)] +pub(super) struct Response { + // pub seq: usize, + // #[serde(rename = "type")] + // pub kind: &'a str, + // pub command: &'a str, + // pub request_seq: usize, + pub success: bool, + #[serde(default = "Option::default")] + pub body: Option, + #[serde(default = "Option::default")] + pub message: Option, +} + +impl<'a, T> From> for Result { + fn from(value: Response) -> Self { + if value.success { + value.body.ok_or_else(|| ProtocolError::ResultMissing) + } else { + Self::Err(ProtocolError::CommandFailed(value.message.unwrap_or("unknown error".into()))) + } + } +} + +#[derive(Debug, Deserialize, PartialEq)] +pub struct StatusResponse { + pub version: String, +} + +#[derive(Debug, Deserialize)] +pub struct NodeResponse { + pub kind: String, + pub text: String, + #[serde(rename = "type")] + pub type_text: String, + pub symbol: String, +} + +#[derive(Debug, Deserialize)] +pub struct EmptyResponse {} + +#[derive(Debug, Deserialize)] +pub struct BoolResponse { + pub result: bool, +} diff --git a/crates/oxc_linter/src/typecheck/utils.rs b/crates/oxc_linter/src/typecheck/utils.rs new file mode 100644 index 0000000000000..826c2d62e6846 --- /dev/null +++ b/crates/oxc_linter/src/typecheck/utils.rs @@ -0,0 +1,145 @@ +use serde::Deserialize; + +use crate::typecheck::response::Response; + +use super::{ProtocolError, EOL_LENGTH}; + +pub fn read_message(mut result_stream: impl std::io::Read) -> Result +where + T: for<'de> Deserialize<'de>, +{ + const PREFIX_LENGTH: usize = 16; + + let mut buf = [0u8; 40]; // ["Content-Length: " + usize + "\r\n\r\n"] + let mut offset = 0; + let mut prefix_length = PREFIX_LENGTH; + loop { + let read_bytes = result_stream.read(&mut buf[offset..]); + match read_bytes { + Ok(0) => return Err(std::io::Error::from(std::io::ErrorKind::UnexpectedEof).into()), + Ok(n) => { + offset += n; + + // Message too short we need more bytes + if offset <= PREFIX_LENGTH { + continue; + } + + for i in prefix_length..offset { + match buf[i] { + b'0'..=b'9' => {} + b'\r' => { + let length = + std::str::from_utf8(&buf[PREFIX_LENGTH..i])?.parse::()? - 1; + + let msg_start = i + 4; + let mut msg = vec![0u8; length + EOL_LENGTH]; + + let mut msg_writer = msg.as_mut_slice(); + if offset < msg_start { + result_stream.read_exact(&mut buf[offset..msg_start])?; + } else if msg_start < offset { + std::io::Write::write(&mut msg_writer, &buf[msg_start..offset])?; + } + + result_stream.read_exact(msg_writer)?; + msg.truncate(length); + + let mut deserializer = serde_json::Deserializer::from_slice(&msg); + let response = Response::::deserialize(&mut deserializer)?; + return Result::::from(response); + } + _ => return Err(ProtocolError::UnexpectedCharacter), + } + } + + prefix_length = offset; + } + Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {} + Err(err) => return Err(err.into()), + } + } +} + +#[cfg(test)] +mod test { + use crate::typecheck::response::StatusResponse; + + use super::*; + + const EOL_STR: &'static str = if EOL_LENGTH == 1 { "\n" } else { "\r\n" }; + + struct ChunkedReader<'a, T: Iterator> { + iter: T, + } + + impl<'a, T: Iterator> std::io::Read for ChunkedReader<'a, T> { + fn read(&mut self, mut buf: &mut [u8]) -> std::io::Result { + let chunk = self.iter.next(); + match chunk { + None => Ok(0), + Some(data) => std::io::Write::write(&mut buf, data), + } + } + } + + #[test] + fn given_single_chunk_then_reads_message() { + let msg = r#"{"seq":0,"type":"response","command":"status","request_seq":0,"success":true,"body":{"version":"5.3.3"}}"#; + let str = format!("{}{}", ["Content-Length: 105", "", msg].join("\r\n"), EOL_STR); + let mut buf = str.as_bytes(); + let result: StatusResponse = read_message(&mut buf).unwrap(); + assert_eq!(result, StatusResponse { version: "5.3.3".into() }); + } + + #[test] + fn given_header_in_separate_chunk_then_reads_message() { + let msg = r#"{"seq":0,"type":"response","command":"status","request_seq":0,"success":true,"body":{"version":"5.3.3"}}"#; + let msg_chunk = format!("{}{}", msg, EOL_STR); + let reader = ChunkedReader { + iter: [b"Content-Length: 105\r\n\r\n".as_slice(), msg_chunk.as_bytes()].into_iter(), + }; + + let result: StatusResponse = read_message(reader).unwrap(); + assert_eq!(result, StatusResponse { version: "5.3.3".into() }); + } + + #[test] + fn given_chunked_header_then_reads_message() { + let msg = r#"{"seq":0,"type":"response","command":"status","request_seq":0,"success":true,"body":{"version":"5.3.3"}}"#; + let msg_chunk = format!("{}{}", msg, EOL_STR); + let reader = ChunkedReader { + iter: [b"Content-Length: 1".as_slice(), b"05\r\n\r\n", msg_chunk.as_bytes()] + .into_iter(), + }; + + let result: StatusResponse = read_message(reader).unwrap(); + assert_eq!(result, StatusResponse { version: "5.3.3".into() }); + } + + #[test] + fn given_chunked_delimiter_then_reads_message() { + let msg = r#"{"seq":0,"type":"response","command":"status","request_seq":0,"success":true,"body":{"version":"5.3.3"}}"#; + let msg_chunk = format!("{}{}", msg, EOL_STR); + let reader = ChunkedReader { + iter: [b"Content-Length: 105\r\n\r".as_slice(), b"\n", msg_chunk.as_bytes()] + .into_iter(), + }; + + let result: StatusResponse = read_message(reader).unwrap(); + assert_eq!(result, StatusResponse { version: "5.3.3".into() }); + } + + #[test] + fn given_error_response_then_deserializes_into_error() { + let msg = r#"{"seq":0,"type":"response","command":"status","request_seq":0,"success":false,"message":"Error processing request.\n at bar (foo.js:123:45)"}"#; + let str = format!("{}{}", ["Content-Length: 142", "", msg].join("\r\n"), EOL_STR); + let mut buf = str.as_bytes(); + let result = read_message::(&mut buf).unwrap_err(); + if let ProtocolError::CommandFailed(error) = result { + assert_eq!(error, "Error processing request.\n at bar (foo.js:123:45)"); + } else { + panic!("{:#?}", result) + } + } +} diff --git a/crates/oxc_linter/src/utils/jest.rs b/crates/oxc_linter/src/utils/jest.rs index ffed23df0bdcf..3ae6cedda5c2f 100644 --- a/crates/oxc_linter/src/utils/jest.rs +++ b/crates/oxc_linter/src/utils/jest.rs @@ -314,16 +314,17 @@ mod test { let semantic_ret = SemanticBuilder::new("", source_type).build(program).semantic; let semantic_ret = Rc::new(semantic_ret); + let cwd = Path::new("./"); let path = Path::new("foo.js"); - let ctx = LintContext::new(Box::from(path), &semantic_ret); + let ctx = LintContext::new(&cwd, Box::from(path), &semantic_ret, None); assert!(!super::is_jest_file(&ctx)); let path = Path::new("foo.test.js"); - let ctx = LintContext::new(Box::from(path), &semantic_ret); + let ctx = LintContext::new(&cwd, Box::from(path), &semantic_ret, None); assert!(super::is_jest_file(&ctx)); let path = Path::new("__tests__/foo/test.spec.js"); - let ctx = LintContext::new(Box::from(path), &semantic_ret); + let ctx = LintContext::new(&cwd, Box::from(path), &semantic_ret, None); assert!(super::is_jest_file(&ctx)); } } diff --git a/crates/oxc_macros/src/declare_all_lint_rules/mod.rs b/crates/oxc_macros/src/declare_all_lint_rules/mod.rs index 8d946f43a2463..cc071fd43fea8 100644 --- a/crates/oxc_macros/src/declare_all_lint_rules/mod.rs +++ b/crates/oxc_macros/src/declare_all_lint_rules/mod.rs @@ -86,6 +86,12 @@ pub fn declare_all_lint_rules(metadata: AllLintRulesMeta) -> TokenStream { } } + pub fn requires_type_info(&self) -> bool { + match self { + #(Self::#struct_names(_) => #struct_names::REQUIRES_TYPE_INFO),* + } + } + pub fn documentation(&self) -> Option<&'static str> { match self { #(Self::#struct_names(_) => #struct_names::documentation()),* diff --git a/crates/oxc_macros/src/declare_oxc_lint.rs b/crates/oxc_macros/src/declare_oxc_lint.rs index ea8345f139ba8..dd374d7bc148c 100644 --- a/crates/oxc_macros/src/declare_oxc_lint.rs +++ b/crates/oxc_macros/src/declare_oxc_lint.rs @@ -3,13 +3,14 @@ use proc_macro::TokenStream; use quote::quote; use syn::{ parse::{Parse, ParseStream}, - Attribute, Error, Expr, Ident, Lit, LitStr, Meta, Result, Token, + Attribute, Error, Expr, Ident, Lit, LitBool, LitStr, Meta, Result, Token, }; pub struct LintRuleMeta { name: Ident, category: Ident, documentation: String, + requires_type_info: bool, pub used_in_test: bool, } @@ -31,16 +32,29 @@ impl Parse for LintRuleMeta { let struct_name = input.parse()?; input.parse::()?; let category = input.parse()?; + let requires_type_info = if input.peek(Token!(,)) { + input.parse::()?; + let token = input.parse::(); + token.map_or(false, |t| t.value) + } else { + false + }; // Ignore the rest input.parse::()?; - Ok(Self { name: struct_name, category, documentation, used_in_test: false }) + Ok(Self { + name: struct_name, + category, + documentation, + requires_type_info, + used_in_test: false, + }) } } pub fn declare_oxc_lint(metadata: LintRuleMeta) -> TokenStream { - let LintRuleMeta { name, category, documentation, used_in_test } = metadata; + let LintRuleMeta { name, category, documentation, used_in_test, requires_type_info } = metadata; let canonical_name = name.to_string().to_case(Case::Kebab); let category = match category.to_string().as_str() { "correctness" => quote! { RuleCategory::Correctness }, @@ -66,6 +80,7 @@ pub fn declare_oxc_lint(metadata: LintRuleMeta) -> TokenStream { const NAME: &'static str = #canonical_name; const CATEGORY: RuleCategory = #category; + const REQUIRES_TYPE_INFO: bool = #requires_type_info; fn documentation() -> Option<&'static str> { Some(#documentation) diff --git a/crates/oxc_wasm/src/lib.rs b/crates/oxc_wasm/src/lib.rs index ffd57b3672ada..f598f0f856ab8 100644 --- a/crates/oxc_wasm/src/lib.rs +++ b/crates/oxc_wasm/src/lib.rs @@ -3,7 +3,11 @@ mod options; -use std::{cell::RefCell, path::PathBuf, rc::Rc}; +use std::{ + cell::RefCell, + path::{Path, PathBuf}, + rc::Rc, +}; use oxc::{ allocator::Allocator, @@ -190,7 +194,9 @@ impl Oxc { // Only lint if there are not syntax errors if run_options.lint() && self.diagnostics.borrow().is_empty() { let semantic = Rc::new(semantic_ret.semantic); - let lint_ctx = LintContext::new(path.into_boxed_path(), &semantic); + // TODO: create type checker + let lint_ctx = + LintContext::new(Path::new("./"), path.into_boxed_path(), &semantic, None); let linter_ret = Linter::default().run(lint_ctx); let diagnostics = linter_ret.into_iter().map(|e| e.error).collect(); self.save_diagnostics(diagnostics); diff --git a/justfile b/justfile index cbbac91749c51..f2b2a2c8194ee 100755 --- a/justfile +++ b/justfile @@ -13,6 +13,9 @@ alias c := coverage init: cargo binstall cargo-watch cargo-insta cargo-edit typos-cli taplo-cli wasm-pack cargo-llvm-cov -y +init-js: + corepack enable pnpm + # When ready, run the same CI commands ready: git diff --exit-code --quiet @@ -23,6 +26,14 @@ ready: just lint git status +# Ready, but for JS +ready-js: + git diff --exit-code --quiet + just fmt-js + just check-js + just build-js + git status + # Clone or update submodules submodules: just clone-submodule tasks/coverage/test262 git@github.com:tc39/test262.git 17ba9aea47e496f5b2bc6ce7405b3f32e3cfbf7a @@ -45,10 +56,22 @@ fmt: cargo fmt taplo format +# Format JS files +fmt-js: + pnpm --dir ./npm/oxc-typecheck format + # Run cargo check check: cargo ck +# Install JS dependencies +check-js: + pnpm --dir ./npm/oxc-typecheck i + +# Build JS projects +build-js: + pnpm --dir ./npm/oxc-typecheck build + # Run all the tests test: cargo test @@ -57,6 +80,10 @@ test: lint: cargo lint -- --deny warnings +# Lint JS projects +lint-js: + pnpm --dir ./npm/oxc-typecheck typecheck + # Run all the conformance tests. See `tasks/coverage`, `tasks/transform_conformance`, `tasks/minsize` coverage: cargo coverage @@ -132,6 +159,11 @@ new-n-rule name: upgrade: cargo upgrade --incompatible +# Upgrade all JS dependencies +upgrade-js: + cd ./npm/oxc-typecheck && corepack up + pnpm --dir ./npm/oxc-typecheck update --latest + clone-submodule dir url sha: git clone --depth=1 {{url}} {{dir}} || true cd {{dir}} && git fetch origin {{sha}} && git reset --hard {{sha}} diff --git a/npm/oxc-typecheck/.gitignore b/npm/oxc-typecheck/.gitignore new file mode 100644 index 0000000000000..8225baa4a77d8 --- /dev/null +++ b/npm/oxc-typecheck/.gitignore @@ -0,0 +1,2 @@ +/node_modules +/dist diff --git a/npm/oxc-typecheck/.swcrc b/npm/oxc-typecheck/.swcrc new file mode 100644 index 0000000000000..474b3aab9c454 --- /dev/null +++ b/npm/oxc-typecheck/.swcrc @@ -0,0 +1,19 @@ +{ + "$schema": "https://json.schemastore.org/swcrc", + "jsc": { + "parser": { + "syntax": "typescript", + "dynamicImport": true + }, + "baseUrl": "./src/", + "target": "es2022", + "keepClassNames": true + }, + "isModule": true, + "minify": false, + "module": { + "type": "es6", + "strict": true, + "noInterop": true + } +} diff --git a/npm/oxc-typecheck/README.md b/npm/oxc-typecheck/README.md new file mode 100644 index 0000000000000..4a1e79cd8b038 --- /dev/null +++ b/npm/oxc-typecheck/README.md @@ -0,0 +1,11 @@ +# Type Check Server for the JavaScript Oxidation Compiler + +https://github.com/oxc-project/oxc/issues/2218 + +Proof-of-concept implementation of Rust <--> TSServer communication for `no-floating-promises` ESLint rule. +Type checker is only needed as the last step to check the type of `CallExpression`. + +The way the POC works, is it copies typecheck helper implementation for `isPromiseLike` from ESLint, and exposes that as a command in `tsserver` style protocol. +To actually implement the rule, we would traverse the Rust AST until we reach expression we need to check. +And then pass the location and type of the AST node to `isPromiseLike` command to do the type check on the JS side. +This node mapping can probably be optimized to just child index access on the JS side. diff --git a/npm/oxc-typecheck/biome.json b/npm/oxc-typecheck/biome.json new file mode 100644 index 0000000000000..b4def50222ac2 --- /dev/null +++ b/npm/oxc-typecheck/biome.json @@ -0,0 +1,22 @@ +{ + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", + "files": { + "include": ["src/*.ts", "*.json", ".swcrc"] + }, + "formatter": { + "indentStyle": "space" + }, + "linter": { + "enabled": false + }, + "javascript": { + "formatter": { + "quoteStyle": "single" + } + }, + "json": { + "parser": { + "allowComments": true + } + } +} diff --git a/npm/oxc-typecheck/package.json b/npm/oxc-typecheck/package.json new file mode 100644 index 0000000000000..9a7f4e25d2cfd --- /dev/null +++ b/npm/oxc-typecheck/package.json @@ -0,0 +1,42 @@ +{ + "name": "oxc-typecheck", + "version": "0.1.0", + "type": "module", + "description": "Type Check Server for the JavaScript Oxidation Compiler", + "keywords": [], + "author": "Boshen and oxc contributors", + "license": "MIT", + "homepage": "https://oxc-project.github.io", + "bugs": "https://github.com/oxc-project/oxc/issues", + "repository": { + "type": "git", + "url": "https://github.com/oxc-project/oxc", + "directory": "npm/oxc-typecheck" + }, + "bin": "./dist/server.js", + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "files": ["./dist"], + "scripts": { + "start": "node ./dist/server.js", + "build": "swc --delete-dir-on-start --copy-files --out-dir dist --strip-leading-paths ./src", + "typecheck": "tsc -p tsconfig.json --noEmit", + "format": "biome format --write ." + }, + "dependencies": { + "ts-api-utils": "^1.3.0", + "typescript": "^5.4.4" + }, + "devDependencies": { + "@biomejs/biome": "^1.6.4", + "@swc/cli": "^0.3.12", + "@swc/core": "^1.4.13", + "@types/node": "^20.12.6" + }, + "packageManager": "pnpm@8.15.6+sha256.01c01eeb990e379b31ef19c03e9d06a14afa5250b82e81303f88721c99ff2e6f", + "engines": { + "node": ">=20.*" + }, + "os": ["darwin", "win32", "linux"] +} diff --git a/npm/oxc-typecheck/pnpm-lock.yaml b/npm/oxc-typecheck/pnpm-lock.yaml new file mode 100644 index 0000000000000..6eadd7e40e94f --- /dev/null +++ b/npm/oxc-typecheck/pnpm-lock.yaml @@ -0,0 +1,1085 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + ts-api-utils: + specifier: ^1.3.0 + version: 1.3.0(typescript@5.4.4) + typescript: + specifier: ^5.4.4 + version: 5.4.4 + +devDependencies: + '@biomejs/biome': + specifier: ^1.6.4 + version: 1.6.4 + '@swc/cli': + specifier: ^0.3.12 + version: 0.3.12(@swc/core@1.4.13) + '@swc/core': + specifier: ^1.4.13 + version: 1.4.13 + '@types/node': + specifier: ^20.12.6 + version: 20.12.6 + +packages: + + /@biomejs/biome@1.6.4: + resolution: {integrity: sha512-3groVd2oWsLC0ZU+XXgHSNbq31lUcOCBkCcA7sAQGBopHcmL+jmmdoWlY3S61zIh+f2mqQTQte1g6PZKb3JJjA==} + engines: {node: '>=14.21.3'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@biomejs/cli-darwin-arm64': 1.6.4 + '@biomejs/cli-darwin-x64': 1.6.4 + '@biomejs/cli-linux-arm64': 1.6.4 + '@biomejs/cli-linux-arm64-musl': 1.6.4 + '@biomejs/cli-linux-x64': 1.6.4 + '@biomejs/cli-linux-x64-musl': 1.6.4 + '@biomejs/cli-win32-arm64': 1.6.4 + '@biomejs/cli-win32-x64': 1.6.4 + dev: true + + /@biomejs/cli-darwin-arm64@1.6.4: + resolution: {integrity: sha512-2WZef8byI9NRzGajGj5RTrroW9BxtfbP9etigW1QGAtwu/6+cLkdPOWRAs7uFtaxBNiKFYA8j/BxV5zeAo5QOQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@biomejs/cli-darwin-x64@1.6.4: + resolution: {integrity: sha512-uo1zgM7jvzcoDpF6dbGizejDLCqNpUIRkCj/oEK0PB0NUw8re/cn1EnxuOLZqDpn+8G75COLQTOx8UQIBBN/Kg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@biomejs/cli-linux-arm64-musl@1.6.4: + resolution: {integrity: sha512-Hp8Jwt6rjj0wCcYAEN6/cfwrrPLLlGOXZ56Lei4Pt4jy39+UuPeAVFPeclrrCfxyL1wQ2xPrhd/saTHSL6DoJg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@biomejs/cli-linux-arm64@1.6.4: + resolution: {integrity: sha512-wAOieaMNIpLrxGc2/xNvM//CIZg7ueWy3V5A4T7gDZ3OL/Go27EKE59a+vMKsBCYmTt7jFl4yHz0TUkUbodA/w==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@biomejs/cli-linux-x64-musl@1.6.4: + resolution: {integrity: sha512-wqi0hr8KAx5kBO0B+m5u8QqiYFFBJOSJVSuRqTeGWW+GYLVUtXNidykNqf1JsW6jJDpbkSp2xHKE/bTlVaG2Kg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@biomejs/cli-linux-x64@1.6.4: + resolution: {integrity: sha512-qTWhuIw+/ePvOkjE9Zxf5OqSCYxtAvcTJtVmZT8YQnmY2I62JKNV2m7tf6O5ViKZUOP0mOQ6NgqHKcHH1eT8jw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@biomejs/cli-win32-arm64@1.6.4: + resolution: {integrity: sha512-Wp3FiEeF6v6C5qMfLkHwf4YsoNHr/n0efvoC8jCKO/kX05OXaVExj+1uVQ1eGT7Pvx0XVm/TLprRO0vq/V6UzA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@biomejs/cli-win32-x64@1.6.4: + resolution: {integrity: sha512-mz183Di5hTSGP7KjNWEhivcP1wnHLGmOxEROvoFsIxMYtDhzJDad4k5gI/1JbmA0xe4n52vsgqo09tBhrMT/Zg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@mole-inc/bin-wrapper@8.0.1: + resolution: {integrity: sha512-sTGoeZnjI8N4KS+sW2AN95gDBErhAguvkw/tWdCjeM8bvxpz5lqrnd0vOJABA1A+Ic3zED7PYoLP/RANLgVotA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + bin-check: 4.1.0 + bin-version-check: 5.1.0 + content-disposition: 0.5.4 + ext-name: 5.0.0 + file-type: 17.1.6 + filenamify: 5.1.1 + got: 11.8.6 + os-filter-obj: 2.0.0 + dev: true + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + dev: true + + /@sindresorhus/is@4.6.0: + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + dev: true + + /@swc/cli@0.3.12(@swc/core@1.4.13): + resolution: {integrity: sha512-h7bvxT+4+UDrLWJLFHt6V+vNAcUNii2G4aGSSotKz1ECEk4MyEh5CWxmeSscwuz5K3i+4DWTgm4+4EyMCQKn+g==} + engines: {node: '>= 16.14.0'} + hasBin: true + peerDependencies: + '@swc/core': ^1.2.66 + chokidar: ^3.5.1 + peerDependenciesMeta: + chokidar: + optional: true + dependencies: + '@mole-inc/bin-wrapper': 8.0.1 + '@swc/core': 1.4.13 + '@swc/counter': 0.1.3 + commander: 8.3.0 + fast-glob: 3.3.2 + minimatch: 9.0.4 + piscina: 4.4.0 + semver: 7.6.0 + slash: 3.0.0 + source-map: 0.7.4 + dev: true + + /@swc/core-darwin-arm64@1.4.13: + resolution: {integrity: sha512-36P72FLpm5iq85IvoEjBvi22DiqkkEIanJ1M0E8bkxcFHUbjBrYfPY9T6cpPyK5oQqkaTBvNAc3j1BlVD6IH6w==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@swc/core-darwin-x64@1.4.13: + resolution: {integrity: sha512-ye7OgKpDdyA8AMIVVdmD1ICDaFXgoEXORnVO8bBHyul0WN71yUBZMX+YxEx2lpWtiftA2vY/1MAuOR80vHkBCw==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-arm-gnueabihf@1.4.13: + resolution: {integrity: sha512-+x593Jlmu4c3lJtZUKRejWpV2MAij1Js5nmQLLdjo6ChR2D4B2rzj3iMiKn5gITew7fraF9t3fvXALdWh7HmUg==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-arm64-gnu@1.4.13: + resolution: {integrity: sha512-0x8OVw4dfyNerrs/9eZX9wNnmvwbwXSMCi+LbE6Xt1pXOIwvoLtFIXcV3NsrlkFboO3sr5UAQIwDxKqbIZA9pQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-arm64-musl@1.4.13: + resolution: {integrity: sha512-Z9c4JiequtZvngPcxbCuAOkmWBxi2vInZbjjhD5I+Q9oiJdXUz1t2USGwsGPS41Xvk1BOA3ecK2Sn1ilY3titg==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-x64-gnu@1.4.13: + resolution: {integrity: sha512-ChatHtk+vX0Ke5QG+jO+rIapw/KwZsi9MedCBHFXHH6iWF4z8d51cJeN68ykcn+vAXzjNeFNdlNy5Vbkd1zAqg==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-x64-musl@1.4.13: + resolution: {integrity: sha512-0Pz39YR530mXpsztwQkmEKdkkZy4fY4Smdh4pkm6Ly8Nndyo0te/l4bcAGqN24Jp7aVwF/QSy14SAtw4HRjU9g==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-win32-arm64-msvc@1.4.13: + resolution: {integrity: sha512-LVZfhlD+jHcAbz5NN+gAJ1BEasB0WpcvUzcsJt0nQSRsojgzPzFjJ+fzEBnvT7SMtqKkrnVJ0OmDYeh88bDRpw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@swc/core-win32-ia32-msvc@1.4.13: + resolution: {integrity: sha512-78hxHWUvUZtWsnhcf8DKwhBcNFJw+j4y4fN2B9ioXmBWX2tIyw+BqUHOrismOtjPihaZmwe/Ok2e4qmkawE2fw==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@swc/core-win32-x64-msvc@1.4.13: + resolution: {integrity: sha512-WSfy1u2Xde6jU7UpHIInCUMW98Zw9iZglddKUAvmr1obkZji5U6EX0Oca3asEJdZPFb+2lMLjt0Mh5a1YisROg==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@swc/core@1.4.13: + resolution: {integrity: sha512-rOtusBE+2gaeRkAJn5E4zp5yzZekZOypzSOz5ZG6P1hFbd+Cc26fWEdK6sUSnrkkvTd0Oj33KXLB/4UkbK/UHA==} + engines: {node: '>=10'} + requiresBuild: true + peerDependencies: + '@swc/helpers': ^0.5.0 + peerDependenciesMeta: + '@swc/helpers': + optional: true + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.6 + optionalDependencies: + '@swc/core-darwin-arm64': 1.4.13 + '@swc/core-darwin-x64': 1.4.13 + '@swc/core-linux-arm-gnueabihf': 1.4.13 + '@swc/core-linux-arm64-gnu': 1.4.13 + '@swc/core-linux-arm64-musl': 1.4.13 + '@swc/core-linux-x64-gnu': 1.4.13 + '@swc/core-linux-x64-musl': 1.4.13 + '@swc/core-win32-arm64-msvc': 1.4.13 + '@swc/core-win32-ia32-msvc': 1.4.13 + '@swc/core-win32-x64-msvc': 1.4.13 + dev: true + + /@swc/counter@0.1.3: + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + dev: true + + /@swc/types@0.1.6: + resolution: {integrity: sha512-/JLo/l2JsT/LRd80C3HfbmVpxOAJ11FO2RCEslFrgzLltoP9j8XIbsyDcfCt2WWyX+CM96rBoNM+IToAkFOugg==} + dependencies: + '@swc/counter': 0.1.3 + dev: true + + /@szmarczak/http-timer@4.0.6: + resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} + engines: {node: '>=10'} + dependencies: + defer-to-connect: 2.0.1 + dev: true + + /@tokenizer/token@0.3.0: + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + dev: true + + /@types/cacheable-request@6.0.3: + resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + dependencies: + '@types/http-cache-semantics': 4.0.4 + '@types/keyv': 3.1.4 + '@types/node': 20.12.6 + '@types/responselike': 1.0.3 + dev: true + + /@types/http-cache-semantics@4.0.4: + resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + dev: true + + /@types/keyv@3.1.4: + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + dependencies: + '@types/node': 20.12.6 + dev: true + + /@types/node@20.12.6: + resolution: {integrity: sha512-3KurE8taB8GCvZBPngVbp0lk5CKi8M9f9k1rsADh0Evdz5SzJ+Q+Hx9uHoFGsLnLnd1xmkDQr2hVhlA0Mn0lKQ==} + dependencies: + undici-types: 5.26.5 + dev: true + + /@types/responselike@1.0.3: + resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + dependencies: + '@types/node': 20.12.6 + dev: true + + /arch@2.2.0: + resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==} + dev: true + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /bin-check@4.1.0: + resolution: {integrity: sha512-b6weQyEUKsDGFlACWSIOfveEnImkJyK/FGW6FAG42loyoquvjdtOIqO6yBFzHyqyVVhNgNkQxxx09SFLK28YnA==} + engines: {node: '>=4'} + dependencies: + execa: 0.7.0 + executable: 4.1.1 + dev: true + + /bin-version-check@5.1.0: + resolution: {integrity: sha512-bYsvMqJ8yNGILLz1KP9zKLzQ6YpljV3ln1gqhuLkUtyfGi3qXKGuK2p+U4NAvjVFzDFiBBtOpCOSFNuYYEGZ5g==} + engines: {node: '>=12'} + dependencies: + bin-version: 6.0.0 + semver: 7.6.0 + semver-truncate: 3.0.0 + dev: true + + /bin-version@6.0.0: + resolution: {integrity: sha512-nk5wEsP4RiKjG+vF+uG8lFsEn4d7Y6FVDamzzftSunXOoOcOOkzcWdKVlGgFFwlUQCj63SgnUkLLGF8v7lufhw==} + engines: {node: '>=12'} + dependencies: + execa: 5.1.1 + find-versions: 5.1.0 + dev: true + + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + dev: true + + /braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: true + + /cacheable-lookup@5.0.4: + resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} + engines: {node: '>=10.6.0'} + dev: true + + /cacheable-request@7.0.4: + resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} + engines: {node: '>=8'} + dependencies: + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.1.1 + keyv: 4.5.4 + lowercase-keys: 2.0.0 + normalize-url: 6.1.0 + responselike: 2.0.1 + dev: true + + /clone-response@1.0.3: + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + dependencies: + mimic-response: 1.0.1 + dev: true + + /commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + dev: true + + /content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /cross-spawn@5.1.0: + resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} + dependencies: + lru-cache: 4.1.5 + shebang-command: 1.2.0 + which: 1.3.1 + dev: true + + /cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + dev: true + + /decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + dependencies: + mimic-response: 3.1.0 + dev: true + + /defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + dev: true + + /end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + dependencies: + once: 1.4.0 + dev: true + + /escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + dev: true + + /execa@0.7.0: + resolution: {integrity: sha512-RztN09XglpYI7aBBrJCPW95jEH7YF1UEPOoX9yDhUTPdp7mK+CQvnLTuD10BNXZ3byLTu2uehZ8EcKT/4CGiFw==} + engines: {node: '>=4'} + dependencies: + cross-spawn: 5.1.0 + get-stream: 3.0.0 + is-stream: 1.1.0 + npm-run-path: 2.0.2 + p-finally: 1.0.0 + signal-exit: 3.0.7 + strip-eof: 1.0.0 + dev: true + + /execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + dev: true + + /executable@4.1.1: + resolution: {integrity: sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==} + engines: {node: '>=4'} + dependencies: + pify: 2.3.0 + dev: true + + /ext-list@2.2.2: + resolution: {integrity: sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==} + engines: {node: '>=0.10.0'} + dependencies: + mime-db: 1.52.0 + dev: true + + /ext-name@5.0.0: + resolution: {integrity: sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==} + engines: {node: '>=4'} + dependencies: + ext-list: 2.2.2 + sort-keys-length: 1.0.1 + dev: true + + /fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: true + + /fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + dependencies: + reusify: 1.0.4 + dev: true + + /file-type@17.1.6: + resolution: {integrity: sha512-hlDw5Ev+9e883s0pwUsuuYNu4tD7GgpUnOvykjv1Gya0ZIjuKumthDRua90VUn6/nlRKAjcxLUnHNTIUWwWIiw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + readable-web-to-node-stream: 3.0.2 + strtok3: 7.0.0 + token-types: 5.0.1 + dev: true + + /filename-reserved-regex@3.0.0: + resolution: {integrity: sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + + /filenamify@5.1.1: + resolution: {integrity: sha512-M45CbrJLGACfrPOkrTp3j2EcO9OBkKUYME0eiqOCa7i2poaklU0jhlIaMlr8ijLorT0uLAzrn3qXOp5684CkfA==} + engines: {node: '>=12.20'} + dependencies: + filename-reserved-regex: 3.0.0 + strip-outer: 2.0.0 + trim-repeated: 2.0.0 + dev: true + + /fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /find-versions@5.1.0: + resolution: {integrity: sha512-+iwzCJ7C5v5KgcBuueqVoNiHVoQpwiUK5XFLjf0affFTep+Wcw93tPvmb8tqujDNmzhBDPddnWV/qgWSXgq+Hg==} + engines: {node: '>=12'} + dependencies: + semver-regex: 4.0.5 + dev: true + + /get-stream@3.0.0: + resolution: {integrity: sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==} + engines: {node: '>=4'} + dev: true + + /get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + dependencies: + pump: 3.0.0 + dev: true + + /get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + dev: true + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /got@11.8.6: + resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} + engines: {node: '>=10.19.0'} + dependencies: + '@sindresorhus/is': 4.6.0 + '@szmarczak/http-timer': 4.0.6 + '@types/cacheable-request': 6.0.3 + '@types/responselike': 1.0.3 + cacheable-lookup: 5.0.4 + cacheable-request: 7.0.4 + decompress-response: 6.0.0 + http2-wrapper: 1.0.3 + lowercase-keys: 2.0.0 + p-cancelable: 2.1.1 + responselike: 2.0.1 + dev: true + + /http-cache-semantics@4.1.1: + resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + dev: true + + /http2-wrapper@1.0.3: + resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} + engines: {node: '>=10.19.0'} + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + dev: true + + /human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + dev: true + + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: true + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: true + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /is-plain-obj@1.1.0: + resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} + engines: {node: '>=0.10.0'} + dev: true + + /is-stream@1.1.0: + resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + dev: true + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: true + + /json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + dev: true + + /keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + dependencies: + json-buffer: 3.0.1 + dev: true + + /lowercase-keys@2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + dev: true + + /lru-cache@4.1.5: + resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} + dependencies: + pseudomap: 1.0.2 + yallist: 2.1.2 + dev: true + + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + dev: true + + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: true + + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + + /micromatch@4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + dev: true + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: true + + /mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + dev: true + + /mimic-response@1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + dev: true + + /mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + dev: true + + /minimatch@9.0.4: + resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /nice-napi@1.0.2: + resolution: {integrity: sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==} + os: ['!win32'] + requiresBuild: true + dependencies: + node-addon-api: 3.2.1 + node-gyp-build: 4.8.0 + dev: true + optional: true + + /node-addon-api@3.2.1: + resolution: {integrity: sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==} + requiresBuild: true + dev: true + optional: true + + /node-gyp-build@4.8.0: + resolution: {integrity: sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==} + hasBin: true + requiresBuild: true + dev: true + optional: true + + /normalize-url@6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + dev: true + + /npm-run-path@2.0.2: + resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==} + engines: {node: '>=4'} + dependencies: + path-key: 2.0.1 + dev: true + + /npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + dependencies: + path-key: 3.1.1 + dev: true + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: true + + /onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + dependencies: + mimic-fn: 2.1.0 + dev: true + + /os-filter-obj@2.0.0: + resolution: {integrity: sha512-uksVLsqG3pVdzzPvmAHpBK0wKxYItuzZr7SziusRPoz67tGV8rL1szZ6IdeUrbqLjGDwApBtN29eEE3IqGHOjg==} + engines: {node: '>=4'} + dependencies: + arch: 2.2.0 + dev: true + + /p-cancelable@2.1.1: + resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} + engines: {node: '>=8'} + dev: true + + /p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + dev: true + + /path-key@2.0.1: + resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} + engines: {node: '>=4'} + dev: true + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + dev: true + + /peek-readable@5.0.0: + resolution: {integrity: sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==} + engines: {node: '>=14.16'} + dev: true + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + dev: true + + /piscina@4.4.0: + resolution: {integrity: sha512-+AQduEJefrOApE4bV7KRmp3N2JnnyErlVqq4P/jmko4FPz9Z877BCccl/iB3FdrWSUkvbGV9Kan/KllJgat3Vg==} + optionalDependencies: + nice-napi: 1.0.2 + dev: true + + /pseudomap@1.0.2: + resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} + dev: true + + /pump@3.0.0: + resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + dev: true + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + dev: true + + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: true + + /readable-web-to-node-stream@3.0.2: + resolution: {integrity: sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==} + engines: {node: '>=8'} + dependencies: + readable-stream: 3.6.2 + dev: true + + /resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + dev: true + + /responselike@2.0.1: + resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + dependencies: + lowercase-keys: 2.0.0 + dev: true + + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: true + + /semver-regex@4.0.5: + resolution: {integrity: sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw==} + engines: {node: '>=12'} + dev: true + + /semver-truncate@3.0.0: + resolution: {integrity: sha512-LJWA9kSvMolR51oDE6PN3kALBNaUdkxzAGcexw8gjMA8xr5zUqK0JiR3CgARSqanYF3Z1YHvsErb1KDgh+v7Rg==} + engines: {node: '>=12'} + dependencies: + semver: 7.6.0 + dev: true + + /semver@7.6.0: + resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: true + + /shebang-command@1.2.0: + resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} + engines: {node: '>=0.10.0'} + dependencies: + shebang-regex: 1.0.0 + dev: true + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + dev: true + + /shebang-regex@1.0.0: + resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} + engines: {node: '>=0.10.0'} + dev: true + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + dev: true + + /signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + dev: true + + /slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + dev: true + + /sort-keys-length@1.0.1: + resolution: {integrity: sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==} + engines: {node: '>=0.10.0'} + dependencies: + sort-keys: 1.1.2 + dev: true + + /sort-keys@1.1.2: + resolution: {integrity: sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==} + engines: {node: '>=0.10.0'} + dependencies: + is-plain-obj: 1.1.0 + dev: true + + /source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + dev: true + + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /strip-eof@1.0.0: + resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==} + engines: {node: '>=0.10.0'} + dev: true + + /strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + dev: true + + /strip-outer@2.0.0: + resolution: {integrity: sha512-A21Xsm1XzUkK0qK1ZrytDUvqsQWict2Cykhvi0fBQntGG5JSprESasEyV1EZ/4CiR5WB5KjzLTrP/bO37B0wPg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + + /strtok3@7.0.0: + resolution: {integrity: sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==} + engines: {node: '>=14.16'} + dependencies: + '@tokenizer/token': 0.3.0 + peek-readable: 5.0.0 + dev: true + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /token-types@5.0.1: + resolution: {integrity: sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==} + engines: {node: '>=14.16'} + dependencies: + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + dev: true + + /trim-repeated@2.0.0: + resolution: {integrity: sha512-QUHBFTJGdOwmp0tbOG505xAgOp/YliZP/6UgafFXYZ26WT1bvQmSMJUvkeVSASuJJHbqsFbynTvkd5W8RBTipg==} + engines: {node: '>=12'} + dependencies: + escape-string-regexp: 5.0.0 + dev: true + + /ts-api-utils@1.3.0(typescript@5.4.4): + resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + dependencies: + typescript: 5.4.4 + dev: false + + /typescript@5.4.4: + resolution: {integrity: sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==} + engines: {node: '>=14.17'} + hasBin: true + dev: false + + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + dev: true + + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: true + + /which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: true + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: true + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true + + /yallist@2.1.2: + resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==} + dev: true + + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + dev: true diff --git a/npm/oxc-typecheck/src/handlers.ts b/npm/oxc-typecheck/src/handlers.ts new file mode 100644 index 0000000000000..8c8fcb87330a3 --- /dev/null +++ b/npm/oxc-typecheck/src/handlers.ts @@ -0,0 +1,143 @@ +import * as noFloatingPromises from './rules/no-floating-promises.js'; +import { FileRequest, NodeRequest, OpenRequest } from './protocol.js'; +import { service } from './typecheck/createProjectService.js'; +import { useProgramFromProjectService } from './typecheck/useProgramFromProjectService.js'; +import { + deleteNodeCache, + getNodeAtPosition, +} from './typecheck/getNodeAtPosition.js'; +import ts from 'typescript'; +import { stats } from './stats.js'; + +export const handlers: Record Result> = { + status: () => { + const response = { version: '0.1.0' }; + return requiredResponse(response); + }, + exit: () => { + process.exit(0); + }, + open: ({ arguments: { file, fileContent } }: OpenRequest) => { + measure(() => service.openClientFile(file, fileContent, undefined), 'open'); + return notRequired(); + }, + close: ({ arguments: { file } }: FileRequest) => { + measure(() => { + service.closeClientFile(file); + deleteNodeCache(file); + }, 'close'); + return notRequired(); + }, + getNode: ({ arguments: { file, span } }: NodeRequest) => { + const program = useProgramFromProjectService(service, file); + if (!program) { + throw new Error('failed to create TS program'); + } + + const node = getNodeAtPosition(program.ast, span); + + const checker = program.program.getTypeChecker(); + const type = checker.getTypeAtLocation(node); + + return requiredResponse({ + kind: ts.SyntaxKind[node.kind], + text: node.getText(), + type: checker.typeToString(type), + symbol: type.symbol?.name, + }); + }, + 'noFloatingPromises::isPromiseArray': ({ + arguments: { file, span }, + }: NodeRequest) => { + const program = measure( + () => useProgramFromProjectService(service, file), + 'getProgram', + ); + if (!program) { + throw new Error('failed to create TS program'); + } + + const node = measure(() => getNodeAtPosition(program.ast, span), 'getNode'); + const checker = measure( + () => program.program.getTypeChecker(), + 'getTypechecker', + ); + + const result = measure( + () => noFloatingPromises.isPromiseArray(checker, node), + 'isPromiseArray', + ); + return requiredResponse({ result }); + }, + 'noFloatingPromises::isPromiseLike': ({ + arguments: { file, span }, + }: NodeRequest) => { + const program = measure( + () => useProgramFromProjectService(service, file), + 'getProgram', + ); + if (!program) { + throw new Error('failed to create TS program'); + } + + const node = measure(() => getNodeAtPosition(program.ast, span), 'getNode'); + const checker = measure( + () => program.program.getTypeChecker(), + 'getTypechecker', + ); + + const result = measure( + () => noFloatingPromises.isPromiseLike(checker, node), + 'isPromiseLike', + ); + return requiredResponse({ result }); + }, + 'noFloatingPromises::isValidRejectionHandler': ({ + arguments: { file, span }, + }: NodeRequest) => { + const program = measure( + () => useProgramFromProjectService(service, file), + 'getProgram', + ); + if (!program) { + throw new Error('failed to create TS program'); + } + + const node = measure(() => getNodeAtPosition(program.ast, span), 'getNode'); + const checker = measure( + () => program.program.getTypeChecker(), + 'getTypechecker', + ); + + const result = measure( + () => noFloatingPromises.isValidRejectionHandler(checker, node), + 'isValidRejectionHandler', + ); + return requiredResponse({ result }); + }, +}; + +function measure(f: () => R, key: keyof typeof stats): R { + const start = process.hrtime.bigint(); + + const result = f(); + + const duration = process.hrtime.bigint() - start; + stats[key].total += Number(duration); + stats[key].count += 1; + + return result; +} + +export interface Result { + response?: {}; + responseRequired: boolean; +} + +function requiredResponse(response: {}): Result { + return { response, responseRequired: true }; +} + +function notRequired(): Result { + return { responseRequired: false }; +} diff --git a/npm/oxc-typecheck/src/protocol.ts b/npm/oxc-typecheck/src/protocol.ts new file mode 100644 index 0000000000000..9a02ec1c67dc2 --- /dev/null +++ b/npm/oxc-typecheck/src/protocol.ts @@ -0,0 +1,54 @@ +// Types matching tsserver: https://github.com/microsoft/TypeScript/blob/25a708cf633c6c8a66c86ca9e664c31bd8d145d0/src/server/protocol.ts#L182-L276 + +import type ts from 'typescript'; + +export interface Request { + command: string; + seq: number; + previousDuration: number; + arguments?: {}; +} + +export interface Message { + seq: number; + type: 'response' | 'event'; +} + +export interface Response extends Message { + seq: number; + type: 'response'; + command: string; + request_seq: number; + success: boolean; + body?: {}; + message?: string; +} + +export interface Event extends Message { + seq: number; + type: 'event'; + event: string; + body: {}; +} + +export interface OpenRequest extends Request { + command: 'open'; + arguments: { + file: string; + fileContent?: string; + }; +} + +export interface FileRequest extends Request { + arguments: { + file: string; + }; +} + +export interface NodeRequest extends Request { + command: 'node'; + arguments: { + file: string; + span: ts.ReadonlyTextRange; + }; +} diff --git a/npm/oxc-typecheck/src/queue.ts b/npm/oxc-typecheck/src/queue.ts new file mode 100644 index 0000000000000..ef5c3e2d12475 --- /dev/null +++ b/npm/oxc-typecheck/src/queue.ts @@ -0,0 +1,33 @@ +// Source: https://github.com/microsoft/TypeScript/blob/25a708cf633c6c8a66c86ca9e664c31bd8d145d0/src/compiler/core.ts#L1651-L1690 + +export class Queue { + readonly #elements: (T | undefined)[] = []; + #headIndex: number = 0; + + isEmpty() { + return this.#headIndex === this.#elements.length; + } + + enqueue(...items: T[]) { + this.#elements.push(...items); + } + + dequeue(): T { + if (this.isEmpty()) { + throw new Error('Queue is empty'); + } + + const result = this.#elements[this.#headIndex] as T; + this.#elements[this.#headIndex] = undefined; + this.#headIndex++; + + if (this.#headIndex > 100 && this.#headIndex > this.#elements.length >> 1) { + const newLength = (this.#elements.length = this.#headIndex); + this.#elements.copyWithin(0, this.#headIndex); + this.#elements.length = newLength; + this.#headIndex = 0; + } + + return result; + } +} diff --git a/npm/oxc-typecheck/src/rules/no-floating-promises.ts b/npm/oxc-typecheck/src/rules/no-floating-promises.ts new file mode 100644 index 0000000000000..f74b1304a49ab --- /dev/null +++ b/npm/oxc-typecheck/src/rules/no-floating-promises.ts @@ -0,0 +1,100 @@ +// Source: https://github.com/typescript-eslint/typescript-eslint/blob/a41ad155b5fee9177651439adb1c5131e7e6254f/packages/eslint-plugin/src/rules/no-floating-promises.ts + +import * as tsutils from 'ts-api-utils'; +import type * as ts from 'typescript'; + +export function isPromiseArray( + checker: ts.TypeChecker, + node: ts.Node, +): boolean { + const type = checker.getTypeAtLocation(node); + for (const ty of tsutils + .unionTypeParts(type) + .map((t) => checker.getApparentType(t))) { + if (checker.isArrayType(ty)) { + const arrayType = checker.getTypeArguments(ty)[0]; + if (isPromiseLike(checker, node, arrayType)) { + return true; + } + } + + if (checker.isTupleType(ty)) { + for (const tupleElementType of checker.getTypeArguments(ty)) { + if (isPromiseLike(checker, node, tupleElementType)) { + return true; + } + } + } + } + return false; +} + +// Modified from tsutils.isThenable() to only consider thenables which can be +// rejected/caught via a second parameter. Original source (MIT licensed): +// +// https://github.com/ajafff/tsutils/blob/49d0d31050b44b81e918eae4fbaf1dfe7b7286af/util/type.ts#L95-L125 +export function isPromiseLike( + checker: ts.TypeChecker, + node: ts.Node, + type?: ts.Type, +): boolean { + type ??= checker.getTypeAtLocation(node); + for (const ty of tsutils.unionTypeParts(checker.getApparentType(type))) { + const then = ty.getProperty('then'); + if (then === undefined) { + continue; + } + + const thenType = checker.getTypeOfSymbolAtLocation(then, node); + if ( + hasMatchingSignature( + thenType, + (signature) => + signature.parameters.length >= 2 && + isFunctionParam(checker, signature.parameters[0], node) && + isFunctionParam(checker, signature.parameters[1], node), + ) + ) { + return true; + } + } + return false; +} + +export function isValidRejectionHandler( + checker: ts.TypeChecker, + rejectionHandler: ts.Node, +): boolean { + return ( + checker.getTypeAtLocation(rejectionHandler).getCallSignatures().length > 0 + ); +} + +function hasMatchingSignature( + type: ts.Type, + matcher: (signature: ts.Signature) => boolean, +): boolean { + for (const t of tsutils.unionTypeParts(type)) { + if (t.getCallSignatures().some(matcher)) { + return true; + } + } + + return false; +} + +function isFunctionParam( + checker: ts.TypeChecker, + param: ts.Symbol, + node: ts.Node, +): boolean { + const type: ts.Type | undefined = checker.getApparentType( + checker.getTypeOfSymbolAtLocation(param, node), + ); + for (const t of tsutils.unionTypeParts(type)) { + if (t.getCallSignatures().length !== 0) { + return true; + } + } + return false; +} diff --git a/npm/oxc-typecheck/src/server.ts b/npm/oxc-typecheck/src/server.ts new file mode 100644 index 0000000000000..5690497e2f98e --- /dev/null +++ b/npm/oxc-typecheck/src/server.ts @@ -0,0 +1,159 @@ +// Closely mimics tsserver: https://github.com/microsoft/TypeScript/blob/e2bf8b437d063392264ef20c55076cf0922ea2b6/src/server/session.ts#L3631 + +import { createInterface } from 'node:readline'; +import { EOL } from 'node:os'; +import type { Message, Request, Response } from './protocol.js'; +import { Queue } from './queue.js'; +import { Result, handlers } from './handlers.js'; +import { stats } from './stats.js'; + +const writeQueue = new Queue(); +let canWrite = true; +let previousDuration: number = 0; +let idleStart = 0n; + +export function startServer() { + const rl = createInterface({ + input: process.stdin, + output: process.stdout, + terminal: false, + }); + + rl.on('line', (input: string) => { + const start = process.hrtime.bigint(); + + stats.idle.count++; + stats.idle.total += Number(start - idleStart); + + const message = input.trim(); + onMessage(message, start); + idleStart = process.hrtime.bigint(); + }); + rl.on('close', () => { + process.exit(0); + }); + idleStart = process.hrtime.bigint(); +} + +function onMessage(message: string, start: bigint): void { + let request: Request | undefined; + try { + request = JSON.parse(message) as Request; + if (previousDuration) { + stats.channelOverhead.count++; + stats.channelOverhead.total += + request.previousDuration - previousDuration; + } + + const { response, responseRequired } = executeCommand(request, start); + const strStart = process.hrtime.bigint(); + if (response) { + doOutput(response, request.command, request.seq, true, start, strStart); + } else if (responseRequired) { + doOutput( + undefined, + request.command, + request.seq, + false, + start, + strStart, + 'No content available.', + ); + } else { + doOutput({}, request.command, request.seq, true, start, strStart); + } + } catch (err) { + const strStart = process.hrtime.bigint(); + doOutput( + undefined, + request ? request.command : 'unknown', + request ? request.seq : 0, + false, + start, + strStart, + 'Error processing request. ' + + (err as Error).message + + '\n' + + (err as Error).stack, + ); + } +} + +function executeCommand(request: Request, start: bigint): Result { + const handler = handlers[request.command]; + if (handler) { + stats.parse.total += Number(process.hrtime.bigint() - start); + stats.parse.count++; + const response = handler(request); + return response; + } else { + const strStart = process.hrtime.bigint(); + doOutput( + undefined, + 'unknown', + request.seq, + false, + start, + strStart, + `Unrecognized JSON command: ${request.command}`, + ); + return { responseRequired: false }; + } +} + +function doOutput( + response: {} | undefined, + command: string, + seq: number, + success: boolean, + start: bigint, + strStart: bigint, + message?: string, +): void { + const res: Response = { + seq: 0, + type: 'response', + command, + request_seq: seq, + success, + }; + + if (success) { + res.body = response; + } + + if (message) { + res.message = message; + } + + send(res, start, strStart); +} + +function send(msg: Message, start: bigint, strStart: bigint): void { + const json = JSON.stringify(msg); + const len = Buffer.byteLength(json, 'utf8'); + const msgString = `Content-Length: ${1 + len}\r\n\r\n${json}${EOL}`; + writeMessage(Buffer.from(msgString, 'utf8')); + const end = process.hrtime.bigint(); + stats.stringify.count++; + stats.stringify.total += Number(end - strStart); + previousDuration = Number(end - start); +} + +function writeMessage(buf: Buffer): void { + if (!canWrite) { + writeQueue.enqueue(buf); + } else { + canWrite = false; + process.stdout.write(buf, writeMessageCallback); + } +} + +function writeMessageCallback() { + canWrite = true; + if (!writeQueue.isEmpty()) { + writeMessage(writeQueue.dequeue()); + } +} + +startServer(); diff --git a/npm/oxc-typecheck/src/stats.ts b/npm/oxc-typecheck/src/stats.ts new file mode 100644 index 0000000000000..36b5c6f2884c1 --- /dev/null +++ b/npm/oxc-typecheck/src/stats.ts @@ -0,0 +1,46 @@ +import { appendFile, appendFileSync, writeFileSync } from 'node:fs'; + +export const stats = { + parse: { total: 0, count: 0 }, + stringify: { total: 0, count: 0 }, + open: { total: 0, count: 0 }, + close: { total: 0, count: 0 }, + isPromiseArray: { total: 0, count: 0 }, + isPromiseLike: { total: 0, count: 0 }, + isValidRejectionHandler: { total: 0, count: 0 }, + getProgram: { total: 0, count: 0 }, + getTypechecker: { total: 0, count: 0 }, + getNode: { total: 0, count: 0 }, + channelOverhead: { total: 0, count: 0 }, + idle: { total: 0, count: 0 }, +}; + +const statEntries = Object.entries(stats); + +writeFileSync('stats.csv', statEntries.map(([k]) => k).join(';') + '\n'); + +function formatDuration(x: number, scale: number) { + return (x / scale).toFixed(3).padStart(6, ' '); +} + +function formatStats() { + const result: string[] = []; + for (const [k, { total, count }] of statEntries) { + result.push( + `${formatDuration(count && total / count, 1e6)}ms / ${formatDuration( + total, + 1e9, + )}s`, + ); + } + + return result.join(' ; ') + '\n'; +} + +setInterval(() => { + appendFile('stats.csv', formatStats(), () => {}); +}, 1000).unref(); + +process.on('exit', () => { + appendFileSync('stats.csv', formatStats()); +}); diff --git a/npm/oxc-typecheck/src/typecheck/createProjectService.ts b/npm/oxc-typecheck/src/typecheck/createProjectService.ts new file mode 100644 index 0000000000000..c0ca087655831 --- /dev/null +++ b/npm/oxc-typecheck/src/typecheck/createProjectService.ts @@ -0,0 +1,56 @@ +// Source: https://github.com/typescript-eslint/typescript-eslint/blob/a41ad155b5fee9177651439adb1c5131e7e6254f/packages/typescript-estree/src/create-program/createProjectService.ts + +import ts from 'typescript'; + +const noop = (): void => {}; + +const createStubFileWatcher = (): ts.FileWatcher => ({ + close: noop, +}); + +export type TypeScriptProjectService = ts.server.ProjectService; + +export function createProjectService(): TypeScriptProjectService { + const system: ts.server.ServerHost = { + ...ts.sys, + clearImmediate, + clearTimeout, + setImmediate, + setTimeout, + watchDirectory: createStubFileWatcher, + watchFile: createStubFileWatcher, + }; + + const service = new ts.server.ProjectService({ + host: system, + cancellationToken: { isCancellationRequested: (): boolean => false }, + useSingleInferredProject: false, + useInferredProjectPerProjectRoot: false, + logger: { + close: noop, + endGroup: noop, + getLogFileName: () => undefined, + ...(process.env.DEBUG === 'true' + ? { + hasLevel: () => true, + info: (...args) => console.error(...args), + loggingEnabled: () => true, + msg: (...args) => console.error(...args), + } + : { + hasLevel: () => false, + info: noop, + loggingEnabled: () => false, + msg: noop, + }), + perftrc: noop, + startGroup: noop, + }, + session: undefined, + jsDocParsingMode: ts.JSDocParsingMode.ParseForTypeInfo, + }); + + return service; +} + +export const service = createProjectService(); diff --git a/npm/oxc-typecheck/src/typecheck/getNodeAtPosition.ts b/npm/oxc-typecheck/src/typecheck/getNodeAtPosition.ts new file mode 100644 index 0000000000000..6b1dfbc43badb --- /dev/null +++ b/npm/oxc-typecheck/src/typecheck/getNodeAtPosition.ts @@ -0,0 +1,59 @@ +// Source: https://github.com/microsoft/TypeScript/blob/25a708cf633c6c8a66c86ca9e664c31bd8d145d0/src/compiler/program.ts#L3448-L3462 + +import ts from 'typescript'; +import { forEach, hasJSDocNodes } from './utils.js'; + +const cache = new Map(); + +// TODO: consider obtaining array of child indexes directly from AST in Rust and just doing getChildAt(idx) instead +function searchNodeAtPosition( + sourceFile: ts.SourceFile, + pos: number, + end: number, +): ts.Node { + const getContainingChild = (child: ts.Node): ts.Node | undefined => { + if (child.pos <= pos && end <= child.end) { + return child; + } + + return; + }; + + let current: ts.Node = sourceFile; + while (true) { + const child = + (sourceFile.fileName.endsWith('.js') && + hasJSDocNodes(current) && + forEach(current.jsDoc, getContainingChild)) || + ts.forEachChild(current, getContainingChild); + if (!child) { + return current; + } + current = child; + } +} + +export function getNodeAtPosition( + sourceFile: ts.SourceFile, + { pos, end }: ts.ReadonlyTextRange, +): ts.Node { + const cachedNode = cache.get(sourceFile.fileName); + if (cachedNode && cachedNode.pos === pos && cachedNode.end === end) { + return cachedNode.node; + } + + const node = searchNodeAtPosition(sourceFile, pos, end); + if (cachedNode) { + cachedNode.pos = pos; + cachedNode.end = end; + cachedNode.node = node; + } else { + cache.set(sourceFile.fileName, { pos, end, node }); + } + + return node; +} + +export function deleteNodeCache(fileName: string) { + cache.delete(fileName); +} diff --git a/npm/oxc-typecheck/src/typecheck/useProgramFromProjectService.ts b/npm/oxc-typecheck/src/typecheck/useProgramFromProjectService.ts new file mode 100644 index 0000000000000..0c0b6af4fc848 --- /dev/null +++ b/npm/oxc-typecheck/src/typecheck/useProgramFromProjectService.ts @@ -0,0 +1,27 @@ +// Source: https://github.com/typescript-eslint/typescript-eslint/blob/a41ad155b5fee9177651439adb1c5131e7e6254f/packages/typescript-estree/src/useProgramFromProjectService.ts + +import type ts from 'typescript'; +import { TypeScriptProjectService } from './createProjectService.js'; + +export interface ASTAndDefiniteProgram { + ast: ts.SourceFile; + program: ts.Program; +} + +export function useProgramFromProjectService( + service: TypeScriptProjectService, + filePath: string, +): ASTAndDefiniteProgram | undefined { + const scriptInfo = service.getScriptInfo(filePath); + const program = service + .getDefaultProjectForFile(scriptInfo!.fileName, true)! + .getLanguageService(/*ensureSynchronized*/ true) + .getProgram(); + + if (!program) { + return undefined; + } + + const ast = program.getSourceFile(filePath); + return ast && { ast, program }; +} diff --git a/npm/oxc-typecheck/src/typecheck/utils.ts b/npm/oxc-typecheck/src/typecheck/utils.ts new file mode 100644 index 0000000000000..15f70575f9ffb --- /dev/null +++ b/npm/oxc-typecheck/src/typecheck/utils.ts @@ -0,0 +1,27 @@ +import type ts from 'typescript'; + +export function forEach( + array: readonly T[] | undefined, + callback: (element: T, index: number) => U | undefined, +): U | undefined { + if (array) { + for (let i = 0; i < array.length; i++) { + const result = callback(array[i], i); + if (result) { + return result; + } + } + } + return undefined; +} + +export function hasJSDocNodes( + node: ts.Node, +): node is ts.HasJSDoc & { jsDoc: ts.Node[] } { + if (!('jsDoc' in node)) { + return false; + } + + const { jsDoc } = node as { jsDoc?: ts.Node[] }; + return !!jsDoc && jsDoc.length > 0; +} diff --git a/npm/oxc-typecheck/tsconfig.json b/npm/oxc-typecheck/tsconfig.json new file mode 100644 index 0000000000000..3ceb233f7ac39 --- /dev/null +++ b/npm/oxc-typecheck/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "esModuleInterop": true, + "lib": ["ES2022"], + "module": "Node16", + "moduleResolution": "Node16", + "noImplicitReturns": true, + "paths": {}, + "pretty": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "ES2022", + // specifically disable declarations for the plugin + // see reasoning in packages/eslint-plugin/rules.d.ts + "declaration": false, + "declarationMap": false, + "outDir": "./dist", + "composite": false, + "rootDir": "." + }, + "include": ["src", "typings"] +} diff --git a/npm/oxc-typecheck/typings/typescript.d.ts b/npm/oxc-typecheck/typings/typescript.d.ts new file mode 100644 index 0000000000000..dc4d796af1759 --- /dev/null +++ b/npm/oxc-typecheck/typings/typescript.d.ts @@ -0,0 +1,26 @@ +// Source: https://github.com/typescript-eslint/typescript-eslint/blob/5a1e85da65cb83bc4e02965f8eb8f1f51347e004/packages/eslint-plugin/typings/typescript.d.ts + +import 'typescript'; + +declare module 'typescript' { + interface TypeChecker { + // internal TS APIs + + getContextualTypeForArgumentAtIndex(node: Node, argIndex: number): Type; + + /** + * @returns `true` if the given type is an array type: + * - `Array` + * - `ReadonlyArray` + * - `foo[]` + * - `readonly foo[]` + */ + isArrayType(type: Type): type is TypeReference; + /** + * @returns `true` if the given type is a tuple type: + * - `[foo]` + * - `readonly [foo]` + */ + isTupleType(type: Type): type is TupleTypeReference; + } +} diff --git a/tasks/benchmark/benches/linter.rs b/tasks/benchmark/benches/linter.rs index 429095c8d5b1f..a8eb30efa7dcd 100644 --- a/tasks/benchmark/benches/linter.rs +++ b/tasks/benchmark/benches/linter.rs @@ -1,4 +1,8 @@ -use std::{env, path::PathBuf, rc::Rc}; +use std::{ + env, + path::{Path, PathBuf}, + rc::Rc, +}; use oxc_allocator::Allocator; use oxc_benchmark::{criterion_group, criterion_main, BenchmarkId, Criterion}; @@ -39,7 +43,12 @@ fn bench_linter(criterion: &mut Criterion) { let linter = Linter::from_options(lint_options).unwrap(); let semantic = Rc::new(semantic_ret.semantic); b.iter(|| { - linter.run(LintContext::new(PathBuf::from("").into_boxed_path(), &semantic)) + linter.run(LintContext::new( + Path::new("./"), + PathBuf::from("").into_boxed_path(), + &semantic, + None, + )) }); }, );