From d7c00d10691ed53b8d24766b522f3d9db1adc036 Mon Sep 17 00:00:00 2001 From: Valentinas Janeiko Date: Sun, 7 Apr 2024 15:59:01 +0100 Subject: [PATCH 01/24] no-floating-promises boilerplate --- crates/oxc_linter/src/rules.rs | 2 + .../rules/typescript/no_floating_promises.rs | 1331 +++++++++++++++++ 2 files changed, 1333 insertions(+) create mode 100644 crates/oxc_linter/src/rules/typescript/no_floating_promises.rs 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..059e156f0f89c --- /dev/null +++ b/crates/oxc_linter/src/rules/typescript/no_floating_promises.rs @@ -0,0 +1,1331 @@ +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::{self, Error}, +}; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; + +use crate::{context::LintContext, rule::Rule, AstNode}; + +#[derive(Debug, Error, Diagnostic)] +#[error("typescript-eslint(no-floating-promises): Promises must be awaited, end with a call to .catch, or end with a call to .then with a rejection handler.")] +#[diagnostic(severity(warning), help("Add `await` or `return`, call `.then()` with two arguments or `.catch()` with one argument."))] +struct NoFloatingPromisesDiagnostic(#[label] pub Span); + +#[derive(Debug, Default, Clone)] +pub struct NoFloatingPromises { + ignore_iife: bool, + ignore_void: bool, +} + +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, + correctness +); + +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>) {} +} + +#[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 }])), + ), + ( + " + 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(); +} From 9e8ba4d519650803d47817ed276b019dff7c5236 Mon Sep 17 00:00:00 2001 From: Valentinas Janeiko Date: Sun, 7 Apr 2024 18:58:38 +0100 Subject: [PATCH 02/24] Option for rules requiring type info --- crates/oxc_cli/src/command/lint.rs | 4 ++ crates/oxc_cli/src/lint/mod.rs | 3 +- crates/oxc_linter/fixtures/typecheck/file.ts | 0 .../fixtures/typecheck/tsconfig.json | 18 +++++++ crates/oxc_linter/src/options.rs | 12 +++++ crates/oxc_linter/src/rule.rs | 1 + .../rules/typescript/no_floating_promises.rs | 3 +- crates/oxc_linter/src/service.rs | 9 ++-- crates/oxc_linter/src/tester.rs | 52 ++++++++++++------- .../src/declare_all_lint_rules/mod.rs | 6 +++ crates/oxc_macros/src/declare_oxc_lint.rs | 21 ++++++-- 11 files changed, 101 insertions(+), 28 deletions(-) create mode 100644 crates/oxc_linter/fixtures/typecheck/file.ts create mode 100644 crates/oxc_linter/fixtures/typecheck/tsconfig.json 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_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/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/typescript/no_floating_promises.rs b/crates/oxc_linter/src/rules/typescript/no_floating_promises.rs index 059e156f0f89c..ae2ceb7c6b75f 100644 --- a/crates/oxc_linter/src/rules/typescript/no_floating_promises.rs +++ b/crates/oxc_linter/src/rules/typescript/no_floating_promises.rs @@ -43,7 +43,8 @@ declare_oxc_lint!( /// [1, 2, 3].map(async x => x + 1); /// ``` NoFloatingPromises, - correctness + nursery, + true ); impl Rule for NoFloatingPromises { diff --git a/crates/oxc_linter/src/service.rs b/crates/oxc_linter/src/service.rs index 8203ec2c7de74..251c4d415e54f 100644 --- a/crates/oxc_linter/src/service.rs +++ b/crates/oxc_linter/src/service.rs @@ -129,20 +129,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)); + // TODO: create type-checker + let type_checker = linter.options.type_info.then(|| ()); Self { cwd: options.cwd, paths: options.paths.iter().cloned().collect(), linter, resolver, + type_checker, module_map: ModuleMap::default(), cache_state: CacheState::default(), } 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_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) From 01f156ddea5a64ec32534c9d5e6075a27723d0fc Mon Sep 17 00:00:00 2001 From: Valentinas Janeiko Date: Wed, 10 Apr 2024 21:44:56 +0100 Subject: [PATCH 03/24] Copy typecheck server POC into oxc --- crates/oxc_linter/src/lib.rs | 1 + crates/oxc_linter/src/typecheck/client.rs | 111 ++ crates/oxc_linter/src/typecheck/mod.rs | 43 + .../src/typecheck/protocol_error.rs | 20 + crates/oxc_linter/src/typecheck/requests.rs | 34 + crates/oxc_linter/src/typecheck/utils.rs | 122 ++ npm/oxc-typecheck/.gitignore | 2 + npm/oxc-typecheck/.swcrc | 19 + npm/oxc-typecheck/README.md | 11 + npm/oxc-typecheck/biome.json | 22 + npm/oxc-typecheck/package.json | 42 + npm/oxc-typecheck/pnpm-lock.yaml | 1085 +++++++++++++++++ npm/oxc-typecheck/src/handlers.ts | 99 ++ npm/oxc-typecheck/src/protocol.ts | 61 + npm/oxc-typecheck/src/queue.ts | 33 + .../src/rules/no-floating-promises.ts | 91 ++ npm/oxc-typecheck/src/server.ts | 135 ++ .../src/typecheck/createProjectService.ts | 56 + .../src/typecheck/getNodeAtPosition.ts | 48 + .../typecheck/useProgramFromProjectService.ts | 27 + npm/oxc-typecheck/src/typecheck/utils.ts | 27 + npm/oxc-typecheck/tsconfig.json | 27 + npm/oxc-typecheck/typings/typescript.d.ts | 26 + 23 files changed, 2142 insertions(+) create mode 100644 crates/oxc_linter/src/typecheck/client.rs create mode 100644 crates/oxc_linter/src/typecheck/mod.rs create mode 100644 crates/oxc_linter/src/typecheck/protocol_error.rs create mode 100644 crates/oxc_linter/src/typecheck/requests.rs create mode 100644 crates/oxc_linter/src/typecheck/utils.rs create mode 100644 npm/oxc-typecheck/.gitignore create mode 100644 npm/oxc-typecheck/.swcrc create mode 100644 npm/oxc-typecheck/README.md create mode 100644 npm/oxc-typecheck/biome.json create mode 100644 npm/oxc-typecheck/package.json create mode 100644 npm/oxc-typecheck/pnpm-lock.yaml create mode 100644 npm/oxc-typecheck/src/handlers.ts create mode 100644 npm/oxc-typecheck/src/protocol.ts create mode 100644 npm/oxc-typecheck/src/queue.ts create mode 100644 npm/oxc-typecheck/src/rules/no-floating-promises.ts create mode 100644 npm/oxc-typecheck/src/server.ts create mode 100644 npm/oxc-typecheck/src/typecheck/createProjectService.ts create mode 100644 npm/oxc-typecheck/src/typecheck/getNodeAtPosition.ts create mode 100644 npm/oxc-typecheck/src/typecheck/useProgramFromProjectService.ts create mode 100644 npm/oxc-typecheck/src/typecheck/utils.ts create mode 100644 npm/oxc-typecheck/tsconfig.json create mode 100644 npm/oxc-typecheck/typings/typescript.d.ts 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/typecheck/client.rs b/crates/oxc_linter/src/typecheck/client.rs new file mode 100644 index 0000000000000..8559ca1453ffa --- /dev/null +++ b/crates/oxc_linter/src/typecheck/client.rs @@ -0,0 +1,111 @@ +use std::process::{Child, ChildStdin, ChildStdout}; + +use super::{requests::*, 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, +} + +impl TSServerClient { + pub fn status(&mut self) -> Result { + self.send_command("status", None)?; + + let response = read_message(&mut self.result_stream)?; + Ok(response) + } + + pub fn exit(&mut self) -> Result<(), ProtocolError> { + if !self.running { + return Ok(()); + } + + let _ = self.send_command("exit", None); + + self.running = false; + self.server.wait()?; + + 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()))?; + Ok(()) + } + + pub fn close(&mut self, opts: FileRequest<'_>) -> Result<(), ProtocolError> { + let args = serde_json::to_string(&opts)?; + self.send_command("close", Some(args.as_str()))?; + Ok(()) + } + + 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 response = read_message(&mut self.result_stream)?; + Ok(response) + } + + pub fn is_promise_array(&mut self, opts: LocationRequest<'_>) -> 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)?; + Ok(response) + } + + pub fn is_promise_like(&mut self, opts: LocationRequest<'_>) -> 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)?; + Ok(response) + } + + 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 msg = + format!("{{\"seq\":{seq},\"type\":\"request\",\"command\":\"{command}\"{args_str}}}\n"); + + self.command_stream.write_all(msg.as_bytes()) + } +} + +#[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 }) + } +} + +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..929792dddaa21 --- /dev/null +++ b/crates/oxc_linter/src/typecheck/mod.rs @@ -0,0 +1,43 @@ +pub(self) mod client; +pub(self) mod protocol_error; +pub mod requests; +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..19c2a681b8195 --- /dev/null +++ b/crates/oxc_linter/src/typecheck/protocol_error.rs @@ -0,0 +1,20 @@ +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), +} diff --git a/crates/oxc_linter/src/typecheck/requests.rs b/crates/oxc_linter/src/typecheck/requests.rs new file mode 100644 index 0000000000000..226626ffea494 --- /dev/null +++ b/crates/oxc_linter/src/typecheck/requests.rs @@ -0,0 +1,34 @@ +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 NodeRequest<'a> { + pub file: &'a str, + pub line: usize, + pub col: usize, + pub kind: &'a str, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LocationRequest<'a> { + pub file: &'a str, + pub line: usize, + pub col: usize, +} diff --git a/crates/oxc_linter/src/typecheck/utils.rs b/crates/oxc_linter/src/typecheck/utils.rs new file mode 100644 index 0000000000000..1098c72d93e42 --- /dev/null +++ b/crates/oxc_linter/src/typecheck/utils.rs @@ -0,0 +1,122 @@ +use super::{ProtocolError, EOL_LENGTH}; + +pub fn read_message(mut result_stream: impl std::io::Read) -> Result { + 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 msg_str = String::from_utf8(msg)?; + return Ok(msg_str); + } + _ => 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 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 = read_message(&mut buf).unwrap(); + assert_eq!(result, msg); + } + + #[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 = read_message(reader).unwrap(); + assert_eq!(result, msg); + } + + #[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 = read_message(reader).unwrap(); + assert_eq!(result, msg); + } + + #[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 = read_message(reader).unwrap(); + assert_eq!(result, msg); + } +} 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..f9fc26a1fe64d --- /dev/null +++ b/npm/oxc-typecheck/src/handlers.ts @@ -0,0 +1,99 @@ +import * as noFloatingPromises from './rules/no-floating-promises.js'; +import { + FileRequest, + LocationRequest, + NodeRequest, + OpenRequest, +} from './protocol.js'; +import { service } from './typecheck/createProjectService.js'; +import { useProgramFromProjectService } from './typecheck/useProgramFromProjectService.js'; +import { + getNodeAtPosition, + getParentOfKind, +} from './typecheck/getNodeAtPosition.js'; +import ts from 'typescript'; + +export const handlers: Record Result> = { + status: () => { + const response = { version: '0.1.0' }; + return requiredResponse(response); + }, + exit: () => { + process.exit(0); + }, + open: ({ arguments: { file, fileContent } }: OpenRequest) => { + service.openClientFile(file, fileContent, undefined); + return notRequired(); + }, + close: ({ arguments: { file } }: FileRequest) => { + service.closeClientFile(file); + return notRequired(); + }, + getNode: ({ arguments: { file, line, col, kind } }: NodeRequest) => { + const program = useProgramFromProjectService(service, file); + if (!program) { + throw new Error('failed to create TS program'); + } + + const innerNode = getNodeAtPosition(program.ast, line, col); + const node = getParentOfKind( + innerNode, + ts.SyntaxKind[kind as keyof typeof ts.SyntaxKind], + ); + + 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, line, col }, + }: LocationRequest) => { + const program = useProgramFromProjectService(service, file); + if (!program) { + throw new Error('failed to create TS program'); + } + + const innerNode = getNodeAtPosition(program.ast, line, col); + const node = getParentOfKind(innerNode, ts.SyntaxKind.CallExpression); + const checker = program.program.getTypeChecker(); + + const result = noFloatingPromises.isPromiseArray(checker, node); + return requiredResponse({ result }); + }, + 'noFloatingPromises::isPromiseLike': ({ + arguments: { file, line, col }, + }: LocationRequest) => { + const program = useProgramFromProjectService(service, file); + if (!program) { + throw new Error('failed to create TS program'); + } + + const innerNode = getNodeAtPosition(program.ast, line, col); + const node = getParentOfKind(innerNode, ts.SyntaxKind.CallExpression); + const checker = program.program.getTypeChecker(); + + const result = noFloatingPromises.isPromiseLike(checker, node); + return requiredResponse({ + 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..66c4da8101ca9 --- /dev/null +++ b/npm/oxc-typecheck/src/protocol.ts @@ -0,0 +1,61 @@ +// Types matching tsserver: https://github.com/microsoft/TypeScript/blob/25a708cf633c6c8a66c86ca9e664c31bd8d145d0/src/server/protocol.ts#L182-L276 + +export interface Request { + command: string; + seq: 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; + line: number; + col: number; + kind: string; + }; +} + +export interface LocationRequest extends Request { + arguments: { + file: string; + line: number; + col: number; + }; +} 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..1137a31ebc035 --- /dev/null +++ b/npm/oxc-typecheck/src/rules/no-floating-promises.ts @@ -0,0 +1,91 @@ +// 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; +} + +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..e05b306c9673c --- /dev/null +++ b/npm/oxc-typecheck/src/server.ts @@ -0,0 +1,135 @@ +// 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, Event } from './protocol.js'; +import { Queue } from './queue.js'; +import { Result, handlers } from './handlers.js'; + +const writeQueue = new Queue(); +let canWrite = true; + +export function startServer() { + const rl = createInterface({ + input: process.stdin, + output: process.stdout, + terminal: false, + }); + + rl.on('line', (input: string) => { + const message = input.trim(); + onMessage(message); + }); + rl.on('close', () => { + process.exit(0); + }); +} + +function onMessage(message: string): void { + let request: Request | undefined; + try { + request = JSON.parse(message) as Request; + const { response, responseRequired } = executeCommand(request); + if (response) { + doOutput(response, request.command, request.seq, true); + } else if (responseRequired) { + doOutput( + undefined, + request.command, + request.seq, + false, + 'No content available.', + ); + } + } catch (err) { + doOutput( + undefined, + request ? request.command : 'unknown', + request ? request.seq : 0, + false, + 'Error processing request. ' + + (err as Error).message + + '\n' + + (err as Error).stack, + ); + } +} + +function executeCommand(request: Request): Result { + const handler = handlers[request.command]; + if (handler) { + const response = handler(request); + return response; + } else { + doOutput( + undefined, + 'unknown', + request.seq, + false, + `Unrecognized JSON command: ${request.command}`, + ); + return { responseRequired: false }; + } +} + +function doOutput( + response: {} | undefined, + command: string, + seq: number, + success: boolean, + 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); +} + +function emitEvent(eventName: string, body: {}): void { + const event: Event = { + seq: 0, + type: 'event', + event: eventName, + body, + }; + + send(event); +} + +function send(msg: Message): 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')); +} + +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/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..d8697aa32a929 --- /dev/null +++ b/npm/oxc-typecheck/src/typecheck/getNodeAtPosition.ts @@ -0,0 +1,48 @@ +// Source: https://github.com/microsoft/TypeScript/blob/25a708cf633c6c8a66c86ca9e664c31bd8d145d0/src/compiler/program.ts#L3448-L3462 + +import ts from 'typescript'; +import { forEach, hasJSDocNodes } from './utils.js'; + +// TODO: consider obtaining array of child indexes directly from AST in Rust and just doing getChildAt(idx) instead +export function getNodeAtPosition( + sourceFile: ts.SourceFile, + line: number, + character: number, +): ts.Node { + // TODO: mapping line to position can be done in Rust + const position = sourceFile.getPositionOfLineAndCharacter(line, character); + + const getContainingChild = (child: ts.Node): ts.Node | undefined => { + if ( + child.pos <= position && + (position < child.end || + (position === child.end && child.kind === ts.SyntaxKind.EndOfFileToken)) + ) { + 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 getParentOfKind(node: ts.Node, kind: ts.SyntaxKind): ts.Node { + let current = node; + while (current.kind !== kind && current.parent) { + current = current.parent; + } + + return current; +} 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; + } +} From 962ce3ae870f7ed4625c136719f7e938d78c69a2 Mon Sep 17 00:00:00 2001 From: Valentinas Janeiko Date: Wed, 10 Apr 2024 21:45:32 +0100 Subject: [PATCH 04/24] Add typecheck server scripts to justfile --- justfile | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) 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}} From 1d4839dbc7bf3c9735aad7720372a94470060930 Mon Sep 17 00:00:00 2001 From: Valentinas Janeiko Date: Wed, 10 Apr 2024 21:46:09 +0100 Subject: [PATCH 05/24] Construct type checker --- crates/oxc_linter/src/service.rs | 36 ++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/crates/oxc_linter/src/service.rs b/crates/oxc_linter/src/service.rs index 251c4d415e54f..2af06cb7ef8eb 100644 --- a/crates/oxc_linter/src/service.rs +++ b/crates/oxc_linter/src/service.rs @@ -20,6 +20,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,7 +133,7 @@ pub struct Runtime { paths: FxHashSet>, linter: Linter, resolver: Option, - type_checker: Option<()>, + type_checker: Option>, module_map: ModuleMap, cache_state: CacheState, } @@ -138,8 +142,7 @@ impl Runtime { fn new(linter: Linter, options: LintServiceOptions) -> Self { let tsconfig = options.tsconfig.or_else(|| Some(options.cwd.join("tsconfig.json"))); let resolver = linter.options().import_plugin.then(|| Self::get_resolver(tsconfig)); - // TODO: create type-checker - let type_checker = linter.options.type_info.then(|| ()); + let type_checker = linter.options.type_info.then(|| Mutex::new(Self::get_type_checker())); Self { cwd: options.cwd, paths: options.paths.iter().cloned().collect(), @@ -169,6 +172,11 @@ impl Runtime { }) } + fn get_type_checker() -> TSServerClient { + // TODO: get actual path from somewhere. And gracefully handle errors. + start_typecheck_server("./npm/oxc-typecheck/dist/server.js").unwrap() + } + fn get_source_type_and_text( path: &Path, ext: &str, @@ -347,9 +355,29 @@ impl Runtime { return semantic_ret.errors.into_iter().map(|err| Message::new(err, None)).collect(); }; + 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: path.to_string_lossy().as_ref(), + file_content: Some(&source_text), + }) + .unwrap(); + } + + // TODO: Make type_checker part of lint context let lint_ctx = LintContext::new(path.to_path_buf().into_boxed_path(), &Rc::new(semantic_ret.semantic)); - self.linter.run(lint_ctx) + 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 { From 1c033380ec08ac3159bdf08eaea3664e17669b00 Mon Sep 17 00:00:00 2001 From: Valentinas Janeiko Date: Wed, 10 Apr 2024 23:35:20 +0100 Subject: [PATCH 06/24] Start implementing the rule --- .../rules/typescript/no_floating_promises.rs | 90 ++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/crates/oxc_linter/src/rules/typescript/no_floating_promises.rs b/crates/oxc_linter/src/rules/typescript/no_floating_promises.rs index ae2ceb7c6b75f..e7a8e7bd89990 100644 --- a/crates/oxc_linter/src/rules/typescript/no_floating_promises.rs +++ b/crates/oxc_linter/src/rules/typescript/no_floating_promises.rs @@ -1,9 +1,14 @@ +use oxc_ast::{ + ast::{CallExpression, ChainElement, Expression, ExpressionStatement}, + AstKind, +}; use oxc_diagnostics::{ miette::{self, Diagnostic}, thiserror::{self, Error}, }; use oxc_macros::declare_oxc_lint; use oxc_span::Span; +use oxc_syntax::operator::UnaryOperator; use crate::{context::LintContext, rule::Rule, AstNode}; @@ -63,7 +68,90 @@ impl Rule for NoFloatingPromises { } } - fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {} + 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 + todo!() + } +} + +impl NoFloatingPromises { + fn is_unhandled_promise<'a>(&self, node: &Expression<'a>, ctx: &LintContext<'a>) -> bool { + if let Expression::SequenceExpression(expr) = node { + // TODO: needs to return first unhandled + return expr.expressions.iter().all(|e| !self.is_unhandled_promise(e, ctx)); + } + + if !self.ignore_void { + if let Expression::UnaryExpression(expr) = node { + if expr.operator == UnaryOperator::Void { + return self.is_unhandled_promise(&expr.argument, ctx); + } + } + } + + // TODO + // if isPromiseArray(node, ctx) { + // return true; // { promiseArray: true }; + // } + + // TODO + // if !isPromiseLike(node, ctx) { + // return false; + // } + + match node { + Expression::CallExpression(expr) => is_unhandled_call_expression(expr), + Expression::ConditionalExpression(_) => todo!(), + Expression::MemberExpression(_) + | Expression::Identifier(_) + | Expression::NewExpression(_) => todo!(), + Expression::LogicalExpression(_) => todo!(), + + _ => false, + } + } + + fn is_unhandled_promise_chain<'a>( + &self, + node: &ChainElement<'a>, + ctx: &LintContext<'a>, + ) -> bool { + // TODO + // if isPromiseArray(node, ctx) { + // return true; // { promiseArray: true }; + // } + + // TODO + // if !isPromiseLike(node, ctx) { + // return false; + // } + + match node { + ChainElement::CallExpression(expr) => is_unhandled_call_expression(expr), + ChainElement::MemberExpression(_) => todo!(), + } + } +} + +fn is_async_iife<'a>(node: &ExpressionStatement<'a>) -> bool { + let Expression::CallExpression(ref expr) = node.expression else { return false }; + expr.callee.is_function() +} + +fn is_unhandled_call_expression<'a>(node: &CallExpression<'a>) -> bool { + todo!() } #[test] From 70dba38461d6b936c9c8408abe21fbcfd5e3ced8 Mon Sep 17 00:00:00 2001 From: Valentinas Janeiko Date: Sun, 14 Apr 2024 14:38:34 +0100 Subject: [PATCH 07/24] Update typecheck server to use Span instead of line+col --- crates/oxc_linter/src/typecheck/client.rs | 4 +-- crates/oxc_linter/src/typecheck/requests.rs | 19 ++++++----- npm/oxc-typecheck/src/handlers.ts | 34 ++++++------------- npm/oxc-typecheck/src/protocol.ts | 14 ++------ .../src/typecheck/getNodeAtPosition.ts | 21 ++---------- 5 files changed, 28 insertions(+), 64 deletions(-) diff --git a/crates/oxc_linter/src/typecheck/client.rs b/crates/oxc_linter/src/typecheck/client.rs index 8559ca1453ffa..6308e75c791ac 100644 --- a/crates/oxc_linter/src/typecheck/client.rs +++ b/crates/oxc_linter/src/typecheck/client.rs @@ -55,7 +55,7 @@ impl TSServerClient { Ok(response) } - pub fn is_promise_array(&mut self, opts: LocationRequest<'_>) -> 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()))?; @@ -63,7 +63,7 @@ impl TSServerClient { Ok(response) } - pub fn is_promise_like(&mut self, opts: LocationRequest<'_>) -> 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()))?; diff --git a/crates/oxc_linter/src/typecheck/requests.rs b/crates/oxc_linter/src/typecheck/requests.rs index 226626ffea494..e7fa217dc4537 100644 --- a/crates/oxc_linter/src/typecheck/requests.rs +++ b/crates/oxc_linter/src/typecheck/requests.rs @@ -18,17 +18,20 @@ pub struct FileRequest<'a> { #[derive(Serialize)] #[serde(rename_all = "camelCase")] -pub struct NodeRequest<'a> { - pub file: &'a str, - pub line: usize, - pub col: usize, - pub kind: &'a str, +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 LocationRequest<'a> { +pub struct NodeRequest<'a> { pub file: &'a str, - pub line: usize, - pub col: usize, + pub span: Span, } diff --git a/npm/oxc-typecheck/src/handlers.ts b/npm/oxc-typecheck/src/handlers.ts index f9fc26a1fe64d..e6d2d5b9c0d9d 100644 --- a/npm/oxc-typecheck/src/handlers.ts +++ b/npm/oxc-typecheck/src/handlers.ts @@ -1,16 +1,8 @@ import * as noFloatingPromises from './rules/no-floating-promises.js'; -import { - FileRequest, - LocationRequest, - NodeRequest, - OpenRequest, -} from './protocol.js'; +import { FileRequest, NodeRequest, OpenRequest } from './protocol.js'; import { service } from './typecheck/createProjectService.js'; import { useProgramFromProjectService } from './typecheck/useProgramFromProjectService.js'; -import { - getNodeAtPosition, - getParentOfKind, -} from './typecheck/getNodeAtPosition.js'; +import { getNodeAtPosition } from './typecheck/getNodeAtPosition.js'; import ts from 'typescript'; export const handlers: Record Result> = { @@ -29,17 +21,13 @@ export const handlers: Record Result> = { service.closeClientFile(file); return notRequired(); }, - getNode: ({ arguments: { file, line, col, kind } }: NodeRequest) => { + getNode: ({ arguments: { file, span } }: NodeRequest) => { const program = useProgramFromProjectService(service, file); if (!program) { throw new Error('failed to create TS program'); } - const innerNode = getNodeAtPosition(program.ast, line, col); - const node = getParentOfKind( - innerNode, - ts.SyntaxKind[kind as keyof typeof ts.SyntaxKind], - ); + const node = getNodeAtPosition(program.ast, span); const checker = program.program.getTypeChecker(); const type = checker.getTypeAtLocation(node); @@ -52,30 +40,28 @@ export const handlers: Record Result> = { }); }, 'noFloatingPromises::isPromiseArray': ({ - arguments: { file, line, col }, - }: LocationRequest) => { + arguments: { file, span }, + }: NodeRequest) => { const program = useProgramFromProjectService(service, file); if (!program) { throw new Error('failed to create TS program'); } - const innerNode = getNodeAtPosition(program.ast, line, col); - const node = getParentOfKind(innerNode, ts.SyntaxKind.CallExpression); + const node = getNodeAtPosition(program.ast, span); const checker = program.program.getTypeChecker(); const result = noFloatingPromises.isPromiseArray(checker, node); return requiredResponse({ result }); }, 'noFloatingPromises::isPromiseLike': ({ - arguments: { file, line, col }, - }: LocationRequest) => { + arguments: { file, span }, + }: NodeRequest) => { const program = useProgramFromProjectService(service, file); if (!program) { throw new Error('failed to create TS program'); } - const innerNode = getNodeAtPosition(program.ast, line, col); - const node = getParentOfKind(innerNode, ts.SyntaxKind.CallExpression); + const node = getNodeAtPosition(program.ast, span); const checker = program.program.getTypeChecker(); const result = noFloatingPromises.isPromiseLike(checker, node); diff --git a/npm/oxc-typecheck/src/protocol.ts b/npm/oxc-typecheck/src/protocol.ts index 66c4da8101ca9..4f39899ec4cf3 100644 --- a/npm/oxc-typecheck/src/protocol.ts +++ b/npm/oxc-typecheck/src/protocol.ts @@ -1,5 +1,7 @@ // 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; @@ -46,16 +48,6 @@ export interface NodeRequest extends Request { command: 'node'; arguments: { file: string; - line: number; - col: number; - kind: string; - }; -} - -export interface LocationRequest extends Request { - arguments: { - file: string; - line: number; - col: number; + span: ts.ReadonlyTextRange; }; } diff --git a/npm/oxc-typecheck/src/typecheck/getNodeAtPosition.ts b/npm/oxc-typecheck/src/typecheck/getNodeAtPosition.ts index d8697aa32a929..bce8a24c4251b 100644 --- a/npm/oxc-typecheck/src/typecheck/getNodeAtPosition.ts +++ b/npm/oxc-typecheck/src/typecheck/getNodeAtPosition.ts @@ -6,18 +6,10 @@ import { forEach, hasJSDocNodes } from './utils.js'; // TODO: consider obtaining array of child indexes directly from AST in Rust and just doing getChildAt(idx) instead export function getNodeAtPosition( sourceFile: ts.SourceFile, - line: number, - character: number, + { pos, end }: ts.ReadonlyTextRange, ): ts.Node { - // TODO: mapping line to position can be done in Rust - const position = sourceFile.getPositionOfLineAndCharacter(line, character); - const getContainingChild = (child: ts.Node): ts.Node | undefined => { - if ( - child.pos <= position && - (position < child.end || - (position === child.end && child.kind === ts.SyntaxKind.EndOfFileToken)) - ) { + if (child.pos <= pos && end <= child.end) { return child; } @@ -37,12 +29,3 @@ export function getNodeAtPosition( current = child; } } - -export function getParentOfKind(node: ts.Node, kind: ts.SyntaxKind): ts.Node { - let current = node; - while (current.kind !== kind && current.parent) { - current = current.parent; - } - - return current; -} From 15002e0924098dda17f28a28b2b679be11c6c7d6 Mon Sep 17 00:00:00 2001 From: Valentinas Janeiko Date: Sun, 14 Apr 2024 22:08:31 +0100 Subject: [PATCH 08/24] Add type checker to LintContext --- crates/oxc_language_server/src/linter.rs | 2 ++ crates/oxc_linter/src/context.rs | 26 ++++++++++++++++++++++-- crates/oxc_linter/src/service.rs | 13 +++++++----- crates/oxc_linter/src/utils/jest.rs | 6 +++--- crates/oxc_wasm/src/lib.rs | 3 ++- 5 files changed, 39 insertions(+), 11 deletions(-) diff --git a/crates/oxc_language_server/src/linter.rs b/crates/oxc_language_server/src/linter.rs index d187a41c332ff..6daf9f9b23042 100644 --- a/crates/oxc_language_server/src/linter.rs +++ b/crates/oxc_language_server/src/linter.rs @@ -302,6 +302,8 @@ impl IsolatedLintHandler { let lint_ctx = LintContext::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/src/context.rs b/crates/oxc_linter/src/context.rs index 3b815cf0e6d31..ed1fdedc29f11 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, }; @@ -29,10 +35,16 @@ pub struct LintContext<'a> { settings: Arc, env: Arc, + + type_checker: Option>>, } impl<'a> LintContext<'a> { - pub fn new(file_path: Box, semantic: &Rc>) -> Self { + pub fn new( + file_path: Box, + semantic: &Rc>, + type_checker: Option>>, + ) -> Self { let disable_directives = DisableDirectivesBuilder::new(semantic.source_text(), semantic.trivias()).build(); Self { @@ -44,6 +56,7 @@ impl<'a> LintContext<'a> { file_path, settings: Arc::new(ESLintSettings::default()), env: Arc::new(ESLintEnv::default()), + type_checker, } } @@ -109,6 +122,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/service.rs b/crates/oxc_linter/src/service.rs index 2af06cb7ef8eb..ec1cc41f7dd29 100644 --- a/crates/oxc_linter/src/service.rs +++ b/crates/oxc_linter/src/service.rs @@ -133,7 +133,7 @@ pub struct Runtime { paths: FxHashSet>, linter: Linter, resolver: Option, - type_checker: Option>, + type_checker: Option>>, module_map: ModuleMap, cache_state: CacheState, } @@ -142,7 +142,8 @@ impl Runtime { fn new(linter: Linter, options: LintServiceOptions) -> Self { 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(|| Mutex::new(Self::get_type_checker())); + 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(), @@ -366,9 +367,11 @@ impl Runtime { .unwrap(); } - // TODO: Make type_checker part of lint context - let lint_ctx = - LintContext::new(path.to_path_buf().into_boxed_path(), &Rc::new(semantic_ret.semantic)); + let lint_ctx = LintContext::new( + path.to_path_buf().into_boxed_path(), + &Rc::new(semantic_ret.semantic), + self.type_checker.clone(), + ); let result = self.linter.run(lint_ctx); if let Some(ref type_checker) = self.type_checker { diff --git a/crates/oxc_linter/src/utils/jest.rs b/crates/oxc_linter/src/utils/jest.rs index ffed23df0bdcf..df150caa44854 100644 --- a/crates/oxc_linter/src/utils/jest.rs +++ b/crates/oxc_linter/src/utils/jest.rs @@ -315,15 +315,15 @@ mod test { let semantic_ret = Rc::new(semantic_ret); let path = Path::new("foo.js"); - let ctx = LintContext::new(Box::from(path), &semantic_ret); + let ctx = LintContext::new(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(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(Box::from(path), &semantic_ret, None); assert!(super::is_jest_file(&ctx)); } } diff --git a/crates/oxc_wasm/src/lib.rs b/crates/oxc_wasm/src/lib.rs index ffd57b3672ada..9e477fe6790f3 100644 --- a/crates/oxc_wasm/src/lib.rs +++ b/crates/oxc_wasm/src/lib.rs @@ -190,7 +190,8 @@ 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.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); From 9586057bdbd49b29921a11239edf138d3ad4c3e1 Mon Sep 17 00:00:00 2001 From: Valentinas Janeiko Date: Sun, 14 Apr 2024 22:13:47 +0100 Subject: [PATCH 09/24] Deserialize Typecheck Server responses --- crates/oxc_linter/src/service.rs | 4 +- crates/oxc_linter/src/typecheck/client.rs | 45 +++++++++--------- crates/oxc_linter/src/typecheck/mod.rs | 1 + .../src/typecheck/protocol_error.rs | 4 ++ crates/oxc_linter/src/typecheck/response.rs | 46 +++++++++++++++++++ crates/oxc_linter/src/typecheck/utils.rs | 32 ++++++++----- 6 files changed, 98 insertions(+), 34 deletions(-) create mode 100644 crates/oxc_linter/src/typecheck/response.rs diff --git a/crates/oxc_linter/src/service.rs b/crates/oxc_linter/src/service.rs index ec1cc41f7dd29..237653d35cf33 100644 --- a/crates/oxc_linter/src/service.rs +++ b/crates/oxc_linter/src/service.rs @@ -360,7 +360,7 @@ impl Runtime { // TODO: do something about unwrap let mut type_checker = type_checker.lock().unwrap(); type_checker - .open(OpenRequest { + .open(&OpenRequest { file: path.to_string_lossy().as_ref(), file_content: Some(&source_text), }) @@ -377,7 +377,7 @@ impl Runtime { 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(); + type_checker.close(&FileRequest { file: path.to_string_lossy().as_ref() }).unwrap(); } result diff --git a/crates/oxc_linter/src/typecheck/client.rs b/crates/oxc_linter/src/typecheck/client.rs index 6308e75c791ac..42825083017fc 100644 --- a/crates/oxc_linter/src/typecheck/client.rs +++ b/crates/oxc_linter/src/typecheck/client.rs @@ -1,6 +1,6 @@ use std::process::{Child, ChildStdin, ChildStdout}; -use super::{requests::*, utils::read_message, ProtocolError}; +use super::{requests::*, response::*, utils::read_message, ProtocolError}; use oxc_diagnostics::{ miette::{self, Diagnostic}, thiserror::Error, @@ -15,11 +15,10 @@ pub struct TSServerClient { } impl TSServerClient { - pub fn status(&mut self) -> Result { + pub fn status(&mut self) -> Result { self.send_command("status", None)?; - let response = read_message(&mut self.result_stream)?; - Ok(response) + read_message(&mut self.result_stream) } pub fn exit(&mut self) -> Result<(), ProtocolError> { @@ -27,48 +26,52 @@ impl TSServerClient { return Ok(()); } - let _ = self.send_command("exit", None); - - self.running = false; - self.server.wait()?; + 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> { + pub fn open(&mut self, opts: &OpenRequest<'_>) -> Result<(), ProtocolError> { let args = serde_json::to_string(&opts)?; self.send_command("open", Some(args.as_str()))?; - Ok(()) + + read_message(&mut self.result_stream) } - pub fn close(&mut self, opts: FileRequest<'_>) -> Result<(), ProtocolError> { + pub fn close(&mut self, opts: &FileRequest<'_>) -> Result<(), ProtocolError> { let args = serde_json::to_string(&opts)?; self.send_command("close", Some(args.as_str()))?; - Ok(()) + + read_message(&mut self.result_stream) } - pub fn get_node(&mut self, opts: NodeRequest<'_>) -> 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 response = read_message(&mut self.result_stream)?; - Ok(response) + read_message(&mut self.result_stream) } - pub fn is_promise_array(&mut self, opts: NodeRequest<'_>) -> 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)?; - Ok(response) + let response = read_message::(&mut self.result_stream)?; + Ok(response.result) } - pub fn is_promise_like(&mut self, opts: NodeRequest<'_>) -> 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)?; - Ok(response) + let response = read_message::(&mut self.result_stream)?; + Ok(response.result) } fn send_command(&mut self, command: &str, args: Option<&str>) -> Result<(), std::io::Error> { diff --git a/crates/oxc_linter/src/typecheck/mod.rs b/crates/oxc_linter/src/typecheck/mod.rs index 929792dddaa21..b0b8f6a41551d 100644 --- a/crates/oxc_linter/src/typecheck/mod.rs +++ b/crates/oxc_linter/src/typecheck/mod.rs @@ -1,6 +1,7 @@ pub(self) mod client; pub(self) mod protocol_error; pub mod requests; +pub mod response; pub(self) mod utils; #[cfg(windows)] diff --git a/crates/oxc_linter/src/typecheck/protocol_error.rs b/crates/oxc_linter/src/typecheck/protocol_error.rs index 19c2a681b8195..0e4c5c790a302 100644 --- a/crates/oxc_linter/src/typecheck/protocol_error.rs +++ b/crates/oxc_linter/src/typecheck/protocol_error.rs @@ -17,4 +17,8 @@ pub enum ProtocolError { 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/response.rs b/crates/oxc_linter/src/typecheck/response.rs new file mode 100644 index 0000000000000..2b2426cf069a1 --- /dev/null +++ b/crates/oxc_linter/src/typecheck/response.rs @@ -0,0 +1,46 @@ +use serde::Deserialize; + +use super::ProtocolError; + +#[derive(Debug, Deserialize)] +pub(super) struct Response<'a, T> { + // pub seq: usize, + // #[serde(rename = "type")] + // pub kind: &'a str, + // pub command: &'a str, + // pub request_seq: usize, + pub success: bool, + pub body: Option, + pub message: Option<&'a str>, +} + +impl<'a, T> From> for Result { + fn from(value: Response<'a, T>) -> Self { + if value.success { + value.body.ok_or_else(|| ProtocolError::ResultMissing) + } else { + Self::Err(ProtocolError::CommandFailed( + value.message.unwrap_or("unknown error").to_owned(), + )) + } + } +} + +#[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 BoolResponse { + pub result: bool, +} diff --git a/crates/oxc_linter/src/typecheck/utils.rs b/crates/oxc_linter/src/typecheck/utils.rs index 1098c72d93e42..654b1a23b1ac6 100644 --- a/crates/oxc_linter/src/typecheck/utils.rs +++ b/crates/oxc_linter/src/typecheck/utils.rs @@ -1,6 +1,13 @@ +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 { +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"] @@ -38,8 +45,9 @@ pub fn read_message(mut result_stream: impl std::io::Read) -> Result::deserialize(&mut deserializer)?; + return Result::::from(response); } _ => return Err(ProtocolError::UnexpectedCharacter), } @@ -55,6 +63,8 @@ pub fn read_message(mut result_stream: impl std::io::Read) -> Result Date: Sun, 14 Apr 2024 22:14:57 +0100 Subject: [PATCH 10/24] Add isValidRejectionHandler command to typecheck server --- crates/oxc_linter/src/typecheck/client.rs | 11 +++++++++++ npm/oxc-typecheck/src/handlers.ts | 16 ++++++++++++++++ .../src/rules/no-floating-promises.ts | 9 +++++++++ .../src/typecheck/getNodeAtPosition.ts | 1 + 4 files changed, 37 insertions(+) diff --git a/crates/oxc_linter/src/typecheck/client.rs b/crates/oxc_linter/src/typecheck/client.rs index 42825083017fc..074fbaceef4b5 100644 --- a/crates/oxc_linter/src/typecheck/client.rs +++ b/crates/oxc_linter/src/typecheck/client.rs @@ -74,6 +74,17 @@ impl TSServerClient { 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)?; + Ok(response.result) + } + fn send_command(&mut self, command: &str, args: Option<&str>) -> Result<(), std::io::Error> { self.seq += 1; let seq = self.seq; diff --git a/npm/oxc-typecheck/src/handlers.ts b/npm/oxc-typecheck/src/handlers.ts index e6d2d5b9c0d9d..d8dede35e4890 100644 --- a/npm/oxc-typecheck/src/handlers.ts +++ b/npm/oxc-typecheck/src/handlers.ts @@ -69,6 +69,22 @@ export const handlers: Record Result> = { result, }); }, + 'noFloatingPromises::isValidRejectionHandler': ({ + 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 result = noFloatingPromises.isValidRejectionHandler(checker, node); + return requiredResponse({ + result, + }); + }, }; export interface Result { diff --git a/npm/oxc-typecheck/src/rules/no-floating-promises.ts b/npm/oxc-typecheck/src/rules/no-floating-promises.ts index 1137a31ebc035..f74b1304a49ab 100644 --- a/npm/oxc-typecheck/src/rules/no-floating-promises.ts +++ b/npm/oxc-typecheck/src/rules/no-floating-promises.ts @@ -61,6 +61,15 @@ export function isPromiseLike( 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, diff --git a/npm/oxc-typecheck/src/typecheck/getNodeAtPosition.ts b/npm/oxc-typecheck/src/typecheck/getNodeAtPosition.ts index bce8a24c4251b..01eb436a64075 100644 --- a/npm/oxc-typecheck/src/typecheck/getNodeAtPosition.ts +++ b/npm/oxc-typecheck/src/typecheck/getNodeAtPosition.ts @@ -4,6 +4,7 @@ import ts from 'typescript'; import { forEach, hasJSDocNodes } from './utils.js'; // TODO: consider obtaining array of child indexes directly from AST in Rust and just doing getChildAt(idx) instead +// TODO: cache the result, since it is very likely we need to evaluate the same node multiple times export function getNodeAtPosition( sourceFile: ts.SourceFile, { pos, end }: ts.ReadonlyTextRange, From 6a3b125e2ef5c35011127edcdb5b8fe8211b9623 Mon Sep 17 00:00:00 2001 From: Valentinas Janeiko Date: Sun, 14 Apr 2024 22:16:50 +0100 Subject: [PATCH 11/24] Add get_span helper to some AST enums --- crates/oxc_ast/src/ast/js.rs | 69 ++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) 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))] From 07c0b96792f044f312b9b0b0ac3cd75dd94b54c3 Mon Sep 17 00:00:00 2001 From: Valentinas Janeiko Date: Sun, 14 Apr 2024 22:17:16 +0100 Subject: [PATCH 12/24] More lint rule implementation --- .../rules/typescript/no_floating_promises.rs | 207 +++++++++++++++--- 1 file changed, 180 insertions(+), 27 deletions(-) diff --git a/crates/oxc_linter/src/rules/typescript/no_floating_promises.rs b/crates/oxc_linter/src/rules/typescript/no_floating_promises.rs index e7a8e7bd89990..469e1424b7a39 100644 --- a/crates/oxc_linter/src/rules/typescript/no_floating_promises.rs +++ b/crates/oxc_linter/src/rules/typescript/no_floating_promises.rs @@ -1,5 +1,7 @@ use oxc_ast::{ - ast::{CallExpression, ChainElement, Expression, ExpressionStatement}, + ast::{ + Argument, CallExpression, ChainElement, Expression, ExpressionStatement, MemberExpression, + }, AstKind, }; use oxc_diagnostics::{ @@ -10,7 +12,7 @@ use oxc_macros::declare_oxc_lint; use oxc_span::Span; use oxc_syntax::operator::UnaryOperator; -use crate::{context::LintContext, rule::Rule, AstNode}; +use crate::{context::LintContext, rule::Rule, typecheck::requests::NodeRequest, AstNode}; #[derive(Debug, Error, Diagnostic)] #[error("typescript-eslint(no-floating-promises): Promises must be awaited, end with a call to .catch, or end with a call to .then with a rejection handler.")] @@ -86,11 +88,50 @@ impl Rule for NoFloatingPromises { } } +#[derive(Debug)] +enum PromiseState { + Handled, + Unhandled, + UnhandledPromiseArray, + UnhandledNonFunctionHandler, +} + +impl PromiseState { + fn is_unhandled(&self) -> bool { + match self { + PromiseState::Handled => false, + _ => true, + } + } + + fn promise_array(&self) -> bool { + match self { + PromiseState::UnhandledPromiseArray => true, + _ => false, + } + } + + fn non_function_handler(&self) -> bool { + match self { + PromiseState::UnhandledNonFunctionHandler => true, + _ => false, + } + } +} + impl NoFloatingPromises { - fn is_unhandled_promise<'a>(&self, node: &Expression<'a>, ctx: &LintContext<'a>) -> bool { + fn is_unhandled_promise<'a>( + &self, + node: &Expression<'a>, + ctx: &LintContext<'a>, + ) -> PromiseState { if let Expression::SequenceExpression(expr) = node { - // TODO: needs to return first unhandled - return expr.expressions.iter().all(|e| !self.is_unhandled_promise(e, ctx)); + 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 { @@ -101,25 +142,31 @@ impl NoFloatingPromises { } } - // TODO - // if isPromiseArray(node, ctx) { - // return true; // { promiseArray: true }; - // } + let path = ctx.file_path().to_string_lossy(); + let request = NodeRequest { file: path.as_ref(), 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)).unwrap(); + if is_promise_array { + return PromiseState::UnhandledPromiseArray; + } - // TODO - // if !isPromiseLike(node, ctx) { - // return false; - // } + // TODO: do something about unwrap + let is_promise_like = + ctx.use_type_checker(|type_checker| type_checker.is_promise_like(&request)).unwrap(); + if !is_promise_like { + return PromiseState::Handled; + } match node { - Expression::CallExpression(expr) => is_unhandled_call_expression(expr), + Expression::CallExpression(expr) => self.is_unhandled_call_expression(expr, ctx), Expression::ConditionalExpression(_) => todo!(), Expression::MemberExpression(_) | Expression::Identifier(_) | Expression::NewExpression(_) => todo!(), Expression::LogicalExpression(_) => todo!(), - _ => false, + _ => PromiseState::Handled, } } @@ -127,22 +174,60 @@ impl NoFloatingPromises { &self, node: &ChainElement<'a>, ctx: &LintContext<'a>, - ) -> bool { - // TODO - // if isPromiseArray(node, ctx) { - // return true; // { promiseArray: true }; - // } + ) -> PromiseState { + let path = ctx.file_path().to_string_lossy(); + let request = NodeRequest { file: path.as_ref(), 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)).unwrap(); + if is_promise_array { + return PromiseState::UnhandledPromiseArray; + } - // TODO - // if !isPromiseLike(node, ctx) { - // return false; - // } + // TODO: do something about unwrap + let is_promise_like = + ctx.use_type_checker(|type_checker| type_checker.is_promise_like(&request)).unwrap(); + if !is_promise_like { + return PromiseState::Handled; + } match node { - ChainElement::CallExpression(expr) => is_unhandled_call_expression(expr), + ChainElement::CallExpression(expr) => self.is_unhandled_call_expression(expr, ctx), ChainElement::MemberExpression(_) => todo!(), } } + + 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 { @@ -150,8 +235,76 @@ fn is_async_iife<'a>(node: &ExpressionStatement<'a>) -> bool { expr.callee.is_function() } -fn is_unhandled_call_expression<'a>(node: &CallExpression<'a>) -> bool { - todo!() +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(2) + } + MemberExpression::StaticMemberExpression(expr) if expr.property.name == "then" => { + node.arguments.get(2) + } + _ => 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.file_path().to_string_lossy(); + // TODO: do something about unwrap + ctx.use_type_checker(|type_checker| { + type_checker.is_valid_rejection_handler(&NodeRequest { + file: path.as_ref(), + span: node.get_span().into(), + }) + }) + .unwrap() } #[test] From 16a983395cf37e83e7d3b972208feafff6888c11 Mon Sep 17 00:00:00 2001 From: Valentinas Janeiko Date: Sun, 14 Apr 2024 22:36:32 +0100 Subject: [PATCH 13/24] Deserialize errors into owned string --- crates/oxc_linter/src/typecheck/response.rs | 14 +++++++------- crates/oxc_linter/src/typecheck/utils.rs | 15 ++++++++++++++- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/crates/oxc_linter/src/typecheck/response.rs b/crates/oxc_linter/src/typecheck/response.rs index 2b2426cf069a1..ed1a953a527ad 100644 --- a/crates/oxc_linter/src/typecheck/response.rs +++ b/crates/oxc_linter/src/typecheck/response.rs @@ -3,25 +3,25 @@ use serde::Deserialize; use super::ProtocolError; #[derive(Debug, Deserialize)] -pub(super) struct Response<'a, T> { +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, - pub message: Option<&'a str>, + #[serde(default = "Option::default")] + pub message: Option, } -impl<'a, T> From> for Result { - fn from(value: Response<'a, T>) -> Self { +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").to_owned(), - )) + Self::Err(ProtocolError::CommandFailed(value.message.unwrap_or("unknown error".into()))) } } } diff --git a/crates/oxc_linter/src/typecheck/utils.rs b/crates/oxc_linter/src/typecheck/utils.rs index 654b1a23b1ac6..826c2d62e6846 100644 --- a/crates/oxc_linter/src/typecheck/utils.rs +++ b/crates/oxc_linter/src/typecheck/utils.rs @@ -46,7 +46,7 @@ where msg.truncate(length); let mut deserializer = serde_json::Deserializer::from_slice(&msg); - let response = Response::<'_, T>::deserialize(&mut deserializer)?; + let response = Response::::deserialize(&mut deserializer)?; return Result::::from(response); } _ => return Err(ProtocolError::UnexpectedCharacter), @@ -129,4 +129,17 @@ mod test { 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) + } + } } From 08427c46f94e9d037b4e0e1eed3e5ae6283cd1e1 Mon Sep 17 00:00:00 2001 From: Valentinas Janeiko Date: Wed, 24 Apr 2024 22:42:50 +0100 Subject: [PATCH 14/24] Resolve full path before sending to typecheck server --- crates/oxc_linter/src/service.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/oxc_linter/src/service.rs b/crates/oxc_linter/src/service.rs index 237653d35cf33..f068716a44048 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}, @@ -361,7 +362,7 @@ impl Runtime { let mut type_checker = type_checker.lock().unwrap(); type_checker .open(&OpenRequest { - file: path.to_string_lossy().as_ref(), + file: env::current_dir().unwrap().join(path).to_string_lossy().as_ref(), file_content: Some(&source_text), }) .unwrap(); From 71b481ff47bf7f205e86fc031669afb695fa4308 Mon Sep 17 00:00:00 2001 From: Valentinas Janeiko Date: Wed, 24 Apr 2024 22:55:24 +0100 Subject: [PATCH 15/24] Add responses to all commands --- crates/oxc_linter/src/typecheck/client.rs | 9 +++++++-- crates/oxc_linter/src/typecheck/response.rs | 3 +++ npm/oxc-typecheck/src/server.ts | 8 +++++--- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/crates/oxc_linter/src/typecheck/client.rs b/crates/oxc_linter/src/typecheck/client.rs index 074fbaceef4b5..95a6cf2287c67 100644 --- a/crates/oxc_linter/src/typecheck/client.rs +++ b/crates/oxc_linter/src/typecheck/client.rs @@ -41,14 +41,14 @@ impl TSServerClient { let args = serde_json::to_string(&opts)?; self.send_command("open", Some(args.as_str()))?; - read_message(&mut self.result_stream) + wait_done(&mut self.result_stream) } pub fn close(&mut self, opts: &FileRequest<'_>) -> Result<(), ProtocolError> { let args = serde_json::to_string(&opts)?; self.send_command("close", Some(args.as_str()))?; - read_message(&mut self.result_stream) + wait_done(&mut self.result_stream) } pub fn get_node(&mut self, opts: &NodeRequest<'_>) -> Result { @@ -96,6 +96,11 @@ impl TSServerClient { } } +fn wait_done(result_stream: impl std::io::Read) -> Result<(), ProtocolError> { + read_message::(result_stream)?; + Ok(()) +} + #[derive(Debug, Error, Diagnostic)] #[diagnostic()] pub enum FromChildError { diff --git a/crates/oxc_linter/src/typecheck/response.rs b/crates/oxc_linter/src/typecheck/response.rs index ed1a953a527ad..8c03af8e99960 100644 --- a/crates/oxc_linter/src/typecheck/response.rs +++ b/crates/oxc_linter/src/typecheck/response.rs @@ -40,6 +40,9 @@ pub struct NodeResponse { pub symbol: String, } +#[derive(Debug, Deserialize)] +pub struct EmptyResponse {} + #[derive(Debug, Deserialize)] pub struct BoolResponse { pub result: bool, diff --git a/npm/oxc-typecheck/src/server.ts b/npm/oxc-typecheck/src/server.ts index e05b306c9673c..1b95aa5609cec 100644 --- a/npm/oxc-typecheck/src/server.ts +++ b/npm/oxc-typecheck/src/server.ts @@ -40,6 +40,8 @@ function onMessage(message: string): void { false, 'No content available.', ); + } else { + doOutput({}, request.command, request.seq, true); } } catch (err) { doOutput( @@ -48,9 +50,9 @@ function onMessage(message: string): void { request ? request.seq : 0, false, 'Error processing request. ' + - (err as Error).message + - '\n' + - (err as Error).stack, + (err as Error).message + + '\n' + + (err as Error).stack, ); } } From 09014ffc402fd90c19d327ac044baadd013f71f2 Mon Sep 17 00:00:00 2001 From: Valentinas Janeiko Date: Thu, 25 Apr 2024 21:42:37 +0100 Subject: [PATCH 16/24] Use absolute paths for all typecheck calls --- crates/oxc_language_server/src/linter.rs | 1 + crates/oxc_linter/src/context.rs | 9 +++++++++ .../src/rules/typescript/no_floating_promises.rs | 16 +++++++--------- crates/oxc_linter/src/service.rs | 15 ++++++++------- crates/oxc_linter/src/utils/jest.rs | 7 ++++--- crates/oxc_wasm/src/lib.rs | 9 +++++++-- tasks/benchmark/benches/linter.rs | 13 +++++++++++-- 7 files changed, 47 insertions(+), 23 deletions(-) diff --git a/crates/oxc_language_server/src/linter.rs b/crates/oxc_language_server/src/linter.rs index 6daf9f9b23042..9358a429a2237 100644 --- a/crates/oxc_language_server/src/linter.rs +++ b/crates/oxc_language_server/src/linter.rs @@ -300,6 +300,7 @@ 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 diff --git a/crates/oxc_linter/src/context.rs b/crates/oxc_linter/src/context.rs index ed1fdedc29f11..9c83ce478d0dc 100644 --- a/crates/oxc_linter/src/context.rs +++ b/crates/oxc_linter/src/context.rs @@ -32,6 +32,8 @@ pub struct LintContext<'a> { file_path: Box, + absolute_path: Box, + settings: Arc, env: Arc, @@ -41,12 +43,14 @@ pub struct LintContext<'a> { impl<'a> LintContext<'a> { 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![]), @@ -54,6 +58,7 @@ 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, @@ -102,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 } diff --git a/crates/oxc_linter/src/rules/typescript/no_floating_promises.rs b/crates/oxc_linter/src/rules/typescript/no_floating_promises.rs index 469e1424b7a39..d08af6aaf4da6 100644 --- a/crates/oxc_linter/src/rules/typescript/no_floating_promises.rs +++ b/crates/oxc_linter/src/rules/typescript/no_floating_promises.rs @@ -142,8 +142,8 @@ impl NoFloatingPromises { } } - let path = ctx.file_path().to_string_lossy(); - let request = NodeRequest { file: path.as_ref(), span: node.get_span().into() }; + 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)).unwrap(); @@ -175,8 +175,8 @@ impl NoFloatingPromises { node: &ChainElement<'a>, ctx: &LintContext<'a>, ) -> PromiseState { - let path = ctx.file_path().to_string_lossy(); - let request = NodeRequest { file: path.as_ref(), span: node.get_span().into() }; + 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)).unwrap(); @@ -296,13 +296,11 @@ fn get_object_from_finally_call<'a, 'b>( } fn is_valid_rejection_handler<'a>(node: &Argument<'a>, ctx: &LintContext<'a>) -> bool { - let path = ctx.file_path().to_string_lossy(); + 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.as_ref(), - span: node.get_span().into(), - }) + type_checker + .is_valid_rejection_handler(&NodeRequest { file: path, span: node.get_span().into() }) }) .unwrap() } diff --git a/crates/oxc_linter/src/service.rs b/crates/oxc_linter/src/service.rs index f068716a44048..df27c14fff068 100644 --- a/crates/oxc_linter/src/service.rs +++ b/crates/oxc_linter/src/service.rs @@ -1,6 +1,5 @@ use std::{ collections::HashMap, - env, ffi::OsStr, fs, path::{Path, PathBuf}, @@ -357,22 +356,24 @@ impl Runtime { return semantic_ret.errors.into_iter().map(|err| Message::new(err, None)).collect(); }; + 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: env::current_dir().unwrap().join(path).to_string_lossy().as_ref(), + file: lint_ctx.absolute_path(), file_content: Some(&source_text), }) .unwrap(); } - let lint_ctx = LintContext::new( - path.to_path_buf().into_boxed_path(), - &Rc::new(semantic_ret.semantic), - self.type_checker.clone(), - ); let result = self.linter.run(lint_ctx); if let Some(ref type_checker) = self.type_checker { diff --git a/crates/oxc_linter/src/utils/jest.rs b/crates/oxc_linter/src/utils/jest.rs index df150caa44854..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, None); + 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, None); + 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, None); + let ctx = LintContext::new(&cwd, Box::from(path), &semantic_ret, None); assert!(super::is_jest_file(&ctx)); } } diff --git a/crates/oxc_wasm/src/lib.rs b/crates/oxc_wasm/src/lib.rs index 9e477fe6790f3..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, @@ -191,7 +195,8 @@ impl Oxc { if run_options.lint() && self.diagnostics.borrow().is_empty() { let semantic = Rc::new(semantic_ret.semantic); // TODO: create type checker - let lint_ctx = LintContext::new(path.into_boxed_path(), &semantic, None); + 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/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, + )) }); }, ); From 2c15fb02ef0ddf93e05d7f21ce0ea8c64945da2d Mon Sep 17 00:00:00 2001 From: Valentinas Janeiko Date: Thu, 25 Apr 2024 22:34:44 +0100 Subject: [PATCH 17/24] Implement result handler for no-floating-promises --- .../rules/typescript/no_floating_promises.rs | 47 +++++++++++++++++-- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/crates/oxc_linter/src/rules/typescript/no_floating_promises.rs b/crates/oxc_linter/src/rules/typescript/no_floating_promises.rs index d08af6aaf4da6..ffbf54db2ba61 100644 --- a/crates/oxc_linter/src/rules/typescript/no_floating_promises.rs +++ b/crates/oxc_linter/src/rules/typescript/no_floating_promises.rs @@ -15,9 +15,9 @@ 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): Promises must be awaited, end with a call to .catch, or end with a call to .then with a rejection handler.")] -#[diagnostic(severity(warning), help("Add `await` or `return`, call `.then()` with two arguments or `.catch()` with one argument."))] -struct NoFloatingPromisesDiagnostic(#[label] pub Span); +#[error("typescript-eslint(no-floating-promises): {0:?}")] +#[diagnostic(severity(warning), help("{1:?}"))] +struct NoFloatingPromisesDiagnostic(String, String, #[label] pub Span); #[derive(Debug, Default, Clone)] pub struct NoFloatingPromises { @@ -84,7 +84,46 @@ impl Rule for NoFloatingPromises { }; // Handle result - todo!() + let (msg, help_message) = match result { + PromiseState::Handled => { + 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)); } } From 5ed0d3cc3b50098fcd904261b3bf3be44e6db0a5 Mon Sep 17 00:00:00 2001 From: Valentinas Janeiko Date: Fri, 26 Apr 2024 21:44:03 +0100 Subject: [PATCH 18/24] Finish implementing the rule --- .../rules/typescript/no_floating_promises.rs | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/crates/oxc_linter/src/rules/typescript/no_floating_promises.rs b/crates/oxc_linter/src/rules/typescript/no_floating_promises.rs index ffbf54db2ba61..99b0deeff01bc 100644 --- a/crates/oxc_linter/src/rules/typescript/no_floating_promises.rs +++ b/crates/oxc_linter/src/rules/typescript/no_floating_promises.rs @@ -6,7 +6,7 @@ use oxc_ast::{ }; use oxc_diagnostics::{ miette::{self, Diagnostic}, - thiserror::{self, Error}, + thiserror::Error, }; use oxc_macros::declare_oxc_lint; use oxc_span::Span; @@ -142,20 +142,6 @@ impl PromiseState { _ => true, } } - - fn promise_array(&self) -> bool { - match self { - PromiseState::UnhandledPromiseArray => true, - _ => false, - } - } - - fn non_function_handler(&self) -> bool { - match self { - PromiseState::UnhandledNonFunctionHandler => true, - _ => false, - } - } } impl NoFloatingPromises { @@ -199,12 +185,25 @@ impl NoFloatingPromises { match node { Expression::CallExpression(expr) => self.is_unhandled_call_expression(expr, ctx), - Expression::ConditionalExpression(_) => todo!(), + 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(_) => todo!(), - Expression::LogicalExpression(_) => todo!(), - + | 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, } } @@ -232,7 +231,7 @@ impl NoFloatingPromises { match node { ChainElement::CallExpression(expr) => self.is_unhandled_call_expression(expr, ctx), - ChainElement::MemberExpression(_) => todo!(), + ChainElement::MemberExpression(_) => PromiseState::Unhandled, } } From 3e570f2070baa858e756151445347d909ba90d65 Mon Sep 17 00:00:00 2001 From: Valentinas Janeiko Date: Sat, 27 Apr 2024 00:01:44 +0100 Subject: [PATCH 19/24] Cache node resolution JS side --- npm/oxc-typecheck/src/handlers.ts | 6 +++- .../src/typecheck/getNodeAtPosition.ts | 33 +++++++++++++++++-- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/npm/oxc-typecheck/src/handlers.ts b/npm/oxc-typecheck/src/handlers.ts index d8dede35e4890..f727af94f83ca 100644 --- a/npm/oxc-typecheck/src/handlers.ts +++ b/npm/oxc-typecheck/src/handlers.ts @@ -2,7 +2,10 @@ 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 { getNodeAtPosition } from './typecheck/getNodeAtPosition.js'; +import { + deleteNodeCache, + getNodeAtPosition, +} from './typecheck/getNodeAtPosition.js'; import ts from 'typescript'; export const handlers: Record Result> = { @@ -19,6 +22,7 @@ export const handlers: Record Result> = { }, close: ({ arguments: { file } }: FileRequest) => { service.closeClientFile(file); + deleteNodeCache(file); return notRequired(); }, getNode: ({ arguments: { file, span } }: NodeRequest) => { diff --git a/npm/oxc-typecheck/src/typecheck/getNodeAtPosition.ts b/npm/oxc-typecheck/src/typecheck/getNodeAtPosition.ts index 01eb436a64075..6b1dfbc43badb 100644 --- a/npm/oxc-typecheck/src/typecheck/getNodeAtPosition.ts +++ b/npm/oxc-typecheck/src/typecheck/getNodeAtPosition.ts @@ -3,11 +3,13 @@ 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 -// TODO: cache the result, since it is very likely we need to evaluate the same node multiple times -export function getNodeAtPosition( +function searchNodeAtPosition( sourceFile: ts.SourceFile, - { pos, end }: ts.ReadonlyTextRange, + pos: number, + end: number, ): ts.Node { const getContainingChild = (child: ts.Node): ts.Node | undefined => { if (child.pos <= pos && end <= child.end) { @@ -30,3 +32,28 @@ export function getNodeAtPosition( 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); +} From 18eb675df9328bba2d9766ca879a1610b8b1c067 Mon Sep 17 00:00:00 2001 From: Valentinas Janeiko Date: Sat, 27 Apr 2024 00:02:28 +0100 Subject: [PATCH 20/24] Resolve typecheck server path relative to oxlint executable --- crates/oxc_linter/src/service.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/oxc_linter/src/service.rs b/crates/oxc_linter/src/service.rs index df27c14fff068..f666498fdb5d4 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}, @@ -175,7 +176,12 @@ impl Runtime { fn get_type_checker() -> TSServerClient { // TODO: get actual path from somewhere. And gracefully handle errors. - start_typecheck_server("./npm/oxc-typecheck/dist/server.js").unwrap() + let path = env::current_exe() + .unwrap() + .parent() + .unwrap() + .join("../../npm/oxc-typecheck/dist/server.js"); + start_typecheck_server(path.to_string_lossy().as_ref()).unwrap() } fn get_source_type_and_text( From 2b11d74cbd8539922648c8d639f0fcf8475af5d8 Mon Sep 17 00:00:00 2001 From: Valentinas Janeiko Date: Sat, 27 Apr 2024 16:30:51 +0100 Subject: [PATCH 21/24] Update typecheck server path to make tests runnable --- crates/oxc_linter/src/service.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/oxc_linter/src/service.rs b/crates/oxc_linter/src/service.rs index f666498fdb5d4..b98df8d16bbdb 100644 --- a/crates/oxc_linter/src/service.rs +++ b/crates/oxc_linter/src/service.rs @@ -176,11 +176,8 @@ impl Runtime { fn get_type_checker() -> TSServerClient { // TODO: get actual path from somewhere. And gracefully handle errors. - let path = env::current_exe() - .unwrap() - .parent() - .unwrap() - .join("../../npm/oxc-typecheck/dist/server.js"); + 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() } From 43ece84e236b83e22dc1dcadf754e0e4f9fd2a1d Mon Sep 17 00:00:00 2001 From: Valentinas Janeiko Date: Sat, 27 Apr 2024 16:32:38 +0100 Subject: [PATCH 22/24] Fix a few test failures --- .../rules/typescript/no_floating_promises.rs | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/crates/oxc_linter/src/rules/typescript/no_floating_promises.rs b/crates/oxc_linter/src/rules/typescript/no_floating_promises.rs index 99b0deeff01bc..57d659bf07aa9 100644 --- a/crates/oxc_linter/src/rules/typescript/no_floating_promises.rs +++ b/crates/oxc_linter/src/rules/typescript/no_floating_promises.rs @@ -19,12 +19,18 @@ use crate::{context::LintContext, rule::Rule, typecheck::requests::NodeRequest, #[diagnostic(severity(warning), help("{1:?}"))] struct NoFloatingPromisesDiagnostic(String, String, #[label] pub Span); -#[derive(Debug, Default, Clone)] +#[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 /// @@ -98,7 +104,7 @@ impl Rule for NoFloatingPromises { 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.", + "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 { @@ -111,7 +117,7 @@ impl Rule for NoFloatingPromises { 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.", + "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 { @@ -270,7 +276,8 @@ impl NoFloatingPromises { fn is_async_iife<'a>(node: &ExpressionStatement<'a>) -> bool { let Expression::CallExpression(ref expr) = node.expression else { return false }; - expr.callee.is_function() + let Expression::ParenthesizedExpression(ref callee) = expr.callee else { return false }; + callee.expression.is_function() } fn get_rejection_handler_from_catch_call<'a, 'b>( @@ -301,10 +308,10 @@ fn get_rejection_handler_from_then_call<'a, 'b>( MemberExpression::ComputedMemberExpression(expr) if expr.expression.is_specific_string_literal("then") => { - node.arguments.get(2) + node.arguments.get(1) } MemberExpression::StaticMemberExpression(expr) if expr.property.name == "then" => { - node.arguments.get(2) + node.arguments.get(1) } _ => None, } @@ -762,6 +769,7 @@ fn test() { ", Some(serde_json::json!([{ "ignoreIIFE": true }])), ), + // TODO: ignore promises in return statements ( " const foo = () => From fae30c7c0d82c031081d3931c42cdb39990be928 Mon Sep 17 00:00:00 2001 From: Valentinas Janeiko Date: Sat, 27 Apr 2024 18:07:36 +0100 Subject: [PATCH 23/24] Ignore errors --- .../rules/typescript/no_floating_promises.rs | 47 +++++++++++++------ 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/crates/oxc_linter/src/rules/typescript/no_floating_promises.rs b/crates/oxc_linter/src/rules/typescript/no_floating_promises.rs index 57d659bf07aa9..9cea7e11e5ef9 100644 --- a/crates/oxc_linter/src/rules/typescript/no_floating_promises.rs +++ b/crates/oxc_linter/src/rules/typescript/no_floating_promises.rs @@ -91,7 +91,7 @@ impl Rule for NoFloatingPromises { // Handle result let (msg, help_message) = match result { - PromiseState::Handled => { + PromiseState::Handled | PromiseState::Failed => { return; } PromiseState::UnhandledPromiseArray => { @@ -139,12 +139,13 @@ enum PromiseState { Unhandled, UnhandledPromiseArray, UnhandledNonFunctionHandler, + Failed, } impl PromiseState { fn is_unhandled(&self) -> bool { match self { - PromiseState::Handled => false, + PromiseState::Handled | PromiseState::Failed => false, _ => true, } } @@ -177,16 +178,24 @@ impl NoFloatingPromises { 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)).unwrap(); - if is_promise_array { - return PromiseState::UnhandledPromiseArray; + 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)).unwrap(); - if !is_promise_like { - return PromiseState::Handled; + 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 { @@ -223,16 +232,24 @@ impl NoFloatingPromises { 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)).unwrap(); - if is_promise_array { - return PromiseState::UnhandledPromiseArray; + 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)).unwrap(); - if !is_promise_like { - return PromiseState::Handled; + 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 { @@ -347,7 +364,7 @@ fn is_valid_rejection_handler<'a>(node: &Argument<'a>, ctx: &LintContext<'a>) -> type_checker .is_valid_rejection_handler(&NodeRequest { file: path, span: node.get_span().into() }) }) - .unwrap() + .unwrap_or(true) } #[test] From 600d6b811cfae038c0b338f50b097644f0b23b67 Mon Sep 17 00:00:00 2001 From: Valentinas Janeiko Date: Sun, 28 Apr 2024 14:34:17 +0100 Subject: [PATCH 24/24] Collect typecheck stats --- crates/oxc_linter/src/typecheck/client.rs | 57 ++++++++++++---- npm/oxc-typecheck/src/handlers.ts | 80 +++++++++++++++++------ npm/oxc-typecheck/src/protocol.ts | 1 + npm/oxc-typecheck/src/server.ts | 68 ++++++++++++------- npm/oxc-typecheck/src/stats.ts | 46 +++++++++++++ 5 files changed, 195 insertions(+), 57 deletions(-) create mode 100644 npm/oxc-typecheck/src/stats.ts diff --git a/crates/oxc_linter/src/typecheck/client.rs b/crates/oxc_linter/src/typecheck/client.rs index 95a6cf2287c67..5eef39bf96617 100644 --- a/crates/oxc_linter/src/typecheck/client.rs +++ b/crates/oxc_linter/src/typecheck/client.rs @@ -1,4 +1,7 @@ -use std::process::{Child, ChildStdin, ChildStdout}; +use std::{ + process::{Child, ChildStdin, ChildStdout}, + time::{Duration, Instant}, +}; use super::{requests::*, response::*, utils::read_message, ProtocolError}; use oxc_diagnostics::{ @@ -12,13 +15,18 @@ pub struct TSServerClient { 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)?; - read_message(&mut self.result_stream) + let result = read_message(&mut self.result_stream); + + self.update_duration(); + result } pub fn exit(&mut self) -> Result<(), ProtocolError> { @@ -41,37 +49,45 @@ impl TSServerClient { let args = serde_json::to_string(&opts)?; self.send_command("open", Some(args.as_str()))?; - wait_done(&mut self.result_stream) + 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()))?; - wait_done(&mut self.result_stream) + 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()))?; - read_message(&mut self.result_stream) + 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)?; - Ok(response.result) + 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)?; - Ok(response.result) + let response = read_message::(&mut self.result_stream); + self.update_duration(); + Ok(response?.result) } pub fn is_valid_rejection_handler( @@ -81,19 +97,26 @@ impl TSServerClient { let args = serde_json::to_string(&opts)?; self.send_command("noFloatingPromises::isValidRejectionHandler", Some(args.as_str()))?; - let response = read_message::(&mut self.result_stream)?; - Ok(response.result) + 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\",\"command\":\"{command}\"{args_str}}}\n"); + 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> { @@ -117,7 +140,15 @@ impl TryFrom for TSServerClient { 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 }) + Ok(Self { + server: value, + seq: 0, + command_stream, + result_stream, + running: true, + start: Instant::now(), + previous_duration: Duration::new(0, 0), + }) } } diff --git a/npm/oxc-typecheck/src/handlers.ts b/npm/oxc-typecheck/src/handlers.ts index f727af94f83ca..8c8fcb87330a3 100644 --- a/npm/oxc-typecheck/src/handlers.ts +++ b/npm/oxc-typecheck/src/handlers.ts @@ -7,6 +7,7 @@ import { getNodeAtPosition, } from './typecheck/getNodeAtPosition.js'; import ts from 'typescript'; +import { stats } from './stats.js'; export const handlers: Record Result> = { status: () => { @@ -17,12 +18,14 @@ export const handlers: Record Result> = { process.exit(0); }, open: ({ arguments: { file, fileContent } }: OpenRequest) => { - service.openClientFile(file, fileContent, undefined); + measure(() => service.openClientFile(file, fileContent, undefined), 'open'); return notRequired(); }, close: ({ arguments: { file } }: FileRequest) => { - service.closeClientFile(file); - deleteNodeCache(file); + measure(() => { + service.closeClientFile(file); + deleteNodeCache(file); + }, 'close'); return notRequired(); }, getNode: ({ arguments: { file, span } }: NodeRequest) => { @@ -46,51 +49,86 @@ export const handlers: Record Result> = { 'noFloatingPromises::isPromiseArray': ({ arguments: { file, span }, }: NodeRequest) => { - const program = useProgramFromProjectService(service, file); + const program = measure( + () => useProgramFromProjectService(service, file), + 'getProgram', + ); if (!program) { throw new Error('failed to create TS program'); } - const node = getNodeAtPosition(program.ast, span); - const checker = program.program.getTypeChecker(); + const node = measure(() => getNodeAtPosition(program.ast, span), 'getNode'); + const checker = measure( + () => program.program.getTypeChecker(), + 'getTypechecker', + ); - const result = noFloatingPromises.isPromiseArray(checker, node); + const result = measure( + () => noFloatingPromises.isPromiseArray(checker, node), + 'isPromiseArray', + ); return requiredResponse({ result }); }, 'noFloatingPromises::isPromiseLike': ({ arguments: { file, span }, }: NodeRequest) => { - const program = useProgramFromProjectService(service, file); + const program = measure( + () => useProgramFromProjectService(service, file), + 'getProgram', + ); if (!program) { throw new Error('failed to create TS program'); } - const node = getNodeAtPosition(program.ast, span); - const checker = program.program.getTypeChecker(); + const node = measure(() => getNodeAtPosition(program.ast, span), 'getNode'); + const checker = measure( + () => program.program.getTypeChecker(), + 'getTypechecker', + ); - const result = noFloatingPromises.isPromiseLike(checker, node); - return requiredResponse({ - result, - }); + const result = measure( + () => noFloatingPromises.isPromiseLike(checker, node), + 'isPromiseLike', + ); + return requiredResponse({ result }); }, 'noFloatingPromises::isValidRejectionHandler': ({ arguments: { file, span }, }: NodeRequest) => { - const program = useProgramFromProjectService(service, file); + const program = measure( + () => useProgramFromProjectService(service, file), + 'getProgram', + ); if (!program) { throw new Error('failed to create TS program'); } - const node = getNodeAtPosition(program.ast, span); - const checker = program.program.getTypeChecker(); + const node = measure(() => getNodeAtPosition(program.ast, span), 'getNode'); + const checker = measure( + () => program.program.getTypeChecker(), + 'getTypechecker', + ); - const result = noFloatingPromises.isValidRejectionHandler(checker, node); - return requiredResponse({ - result, - }); + 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; diff --git a/npm/oxc-typecheck/src/protocol.ts b/npm/oxc-typecheck/src/protocol.ts index 4f39899ec4cf3..9a02ec1c67dc2 100644 --- a/npm/oxc-typecheck/src/protocol.ts +++ b/npm/oxc-typecheck/src/protocol.ts @@ -5,6 +5,7 @@ import type ts from 'typescript'; export interface Request { command: string; seq: number; + previousDuration: number; arguments?: {}; } diff --git a/npm/oxc-typecheck/src/server.ts b/npm/oxc-typecheck/src/server.ts index 1b95aa5609cec..5690497e2f98e 100644 --- a/npm/oxc-typecheck/src/server.ts +++ b/npm/oxc-typecheck/src/server.ts @@ -2,12 +2,15 @@ import { createInterface } from 'node:readline'; import { EOL } from 'node:os'; -import type { Message, Request, Response, Event } from './protocol.js'; +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({ @@ -17,57 +20,81 @@ export function startServer() { }); 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); + onMessage(message, start); + idleStart = process.hrtime.bigint(); }); rl.on('close', () => { process.exit(0); }); + idleStart = process.hrtime.bigint(); } -function onMessage(message: string): void { +function onMessage(message: string, start: bigint): void { let request: Request | undefined; try { request = JSON.parse(message) as Request; - const { response, responseRequired } = executeCommand(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); + 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); + 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, + (err as Error).message + + '\n' + + (err as Error).stack, ); } } -function executeCommand(request: Request): Result { +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 }; @@ -79,6 +106,8 @@ function doOutput( command: string, seq: number, success: boolean, + start: bigint, + strStart: bigint, message?: string, ): void { const res: Response = { @@ -97,25 +126,18 @@ function doOutput( res.message = message; } - send(res); -} - -function emitEvent(eventName: string, body: {}): void { - const event: Event = { - seq: 0, - type: 'event', - event: eventName, - body, - }; - - send(event); + send(res, start, strStart); } -function send(msg: Message): void { +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 { 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()); +});