diff --git a/Cargo.lock b/Cargo.lock index 2e0b43b..6310b31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -192,6 +192,12 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "bytes" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" + [[package]] name = "camino" version = "1.1.9" @@ -228,6 +234,7 @@ version = "0.1.0" dependencies = [ "anyhow", "beans", + "insta", "log", "napi", "napi-derive", @@ -239,6 +246,7 @@ dependencies = [ "oxc_span", "oxc_syntax", "serde", + "tokio", "utils", ] @@ -927,6 +935,17 @@ dependencies = [ "adler", ] +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + [[package]] name = "module_graph" version = "0.1.0" @@ -1112,15 +1131,15 @@ checksum = "fb37767f6569cd834a413442455e0f066d0d522de8630436e2a1761d9726ba56" [[package]] name = "oxc-browserslist" -version = "1.0.3" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b5ad390a7cf69d5f574ea41eada88a7c0947272647703fe1ec6ce23bfe1714" +checksum = "1b585351ba1b93f0bf9683df23285cc0978ec35a6e3074794815756086770c14" dependencies = [ "nom", "rustc-hash", "serde", "serde_json", - "thiserror", + "thiserror 2.0.9", "time", ] @@ -1134,7 +1153,7 @@ dependencies = [ "owo-colors", "oxc-miette-derive", "textwrap", - "thiserror", + "thiserror 1.0.61", "unicode-width 0.2.0", ] @@ -1376,7 +1395,7 @@ dependencies = [ "serde", "serde_json", "simdutf8", - "thiserror", + "thiserror 1.0.61", "tracing", ] @@ -1799,6 +1818,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "simdutf8" version = "0.1.5" @@ -1829,6 +1857,16 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -1892,7 +1930,16 @@ version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.61", +] + +[[package]] +name = "thiserror" +version = "2.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" +dependencies = [ + "thiserror-impl 2.0.9", ] [[package]] @@ -1906,6 +1953,17 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror-impl" +version = "2.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "time" version = "0.3.36" @@ -1942,8 +2000,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" dependencies = [ "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", "pin-project-lite", + "signal-hook-registry", + "socket2", "tokio-macros", + "windows-sys", ] [[package]] @@ -2102,6 +2167,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "wasm-bindgen" version = "0.2.99" @@ -2167,7 +2238,7 @@ dependencies = [ "nom", "pori", "regex", - "thiserror", + "thiserror 1.0.61", "walkdir", ] diff --git a/__test__/check_browser_supported/__snapshots__/index.spec.ts.snap b/__test__/check_browser_supported/__snapshots__/index.spec.ts.snap index d4ad985..264335b 100644 --- a/__test__/check_browser_supported/__snapshots__/index.spec.ts.snap +++ b/__test__/check_browser_supported/__snapshots__/index.spec.ts.snap @@ -38,7 +38,7 @@ exports[`Multiple occurrences of the same syntax results in multiple calculation "web-features:snapshot:ecmascript-2020", ], }, - "filePath": "", + "filePath": "test.ts", "name": "operators.nullish_coalescing", }, ] @@ -82,7 +82,7 @@ exports[`Multiple occurrences of the same syntax results in multiple calculation "web-features:snapshot:ecmascript-2020", ], }, - "filePath": "", + "filePath": "test.ts", "name": "operators.nullish_coalescing", }, { @@ -121,7 +121,7 @@ exports[`Multiple occurrences of the same syntax results in multiple calculation "web-features:snapshot:ecmascript-2020", ], }, - "filePath": "", + "filePath": "test.ts", "name": "operators.nullish_coalescing", }, ] diff --git a/__test__/check_browser_supported/index.spec.ts b/__test__/check_browser_supported/index.spec.ts index 9223e72..1292f6e 100644 --- a/__test__/check_browser_supported/index.spec.ts +++ b/__test__/check_browser_supported/index.spec.ts @@ -5,28 +5,32 @@ import { checkBrowserSupportedWithSourceCode, checkBrowserSupported } from "../. const __dirname = dirname(fileURLToPath(import.meta.url)); -test("There are 4 syntaxes under normal that are incompatible under chrome 40", () => { +test("There are 4 syntaxes under normal that are incompatible under chrome 40", async () => { const cwd = path.resolve(__dirname, "features", "normal"); - const response = checkBrowserSupported({ chrome: "40" }, { cwd }); + const response = await checkBrowserSupported({ chrome: "40" }, { cwd }); expect(response.length).toBe(4); }); -test("There are 3 syntaxes under normal that are incompatible under chrome 45", () => { +test("There are 3 syntaxes under normal that are incompatible under chrome 45", async () => { const cwd = path.resolve(__dirname, "features", "normal"); - const response = checkBrowserSupported({ chrome: "45" }, { cwd }); + const response = await checkBrowserSupported({ chrome: "45" }, { cwd }); expect(response.length).toBe(3); }); -test("Multiple occurrences of the same syntax results in multiple calculations.", () => { - const response1 = checkBrowserSupportedWithSourceCode({ chrome: "70" }, "const b = cc ?? 3;"); +test("Multiple occurrences of the same syntax results in multiple calculations.", async () => { + const response1 = await checkBrowserSupportedWithSourceCode({ chrome: "70" }, "const b = cc ?? 3;", "test.ts"); expect(response1.length).toBe(1); expect(response1).toMatchSnapshot(); - const response2 = checkBrowserSupportedWithSourceCode({ chrome: "70" }, "const b = cc ?? 3; const c = cc ?? 3;"); + const response2 = await checkBrowserSupportedWithSourceCode( + { chrome: "70" }, + "const b = cc ?? 3; const c = cc ?? 3;", + "test.ts", + ); expect(response2.length).toBe(2); expect(response2).toMatchSnapshot(); }); diff --git a/__test__/check_module_member_usage/index.spec.ts b/__test__/check_module_member_usage/index.spec.ts index fe2593f..412f60b 100644 --- a/__test__/check_module_member_usage/index.spec.ts +++ b/__test__/check_module_member_usage/index.spec.ts @@ -1,82 +1,91 @@ -import { expect, test } from 'vitest' +import { expect, test } from "vitest"; import { fileURLToPath } from "node:url"; import path, { dirname } from "node:path"; -import { checkModuleMemberUsage } from '../../index.js' +import { checkModuleMemberUsage } from "../../index.js"; const __filename = fileURLToPath(import.meta.url); -test('should return empty array when module is only imported but not used', () => { - const cwd = dirname(__filename); - const response = checkModuleMemberUsage(["antd"],{cwd:path.resolve(cwd,'only-imported')}) - expect(response.length).toBe(0) -}) - -test('should return 1 result when module member used in statement', () => { - const cwd = dirname(__filename); - const response = checkModuleMemberUsage(["antd"],{cwd:path.resolve(cwd,'features/use-in-statement')}) - expect(response.length).toBe(1) -}) - - -test('should return 1 result when module member used in tsx', () => { - const cwd = dirname(__filename); - const response = checkModuleMemberUsage(["antd"],{cwd:path.resolve(cwd,'features/use-in-tsx')}) - expect(response.length).toBe(1) -}) - -test('should return 1 result when module member used in tsx with self closed', () => { - const cwd = dirname(__filename); - const response = checkModuleMemberUsage(["antd"],{cwd:path.resolve(cwd,'features/use-in-tsx-with-self-closed')}) - expect(response.length).toBe(1) -}) - -test('should return 1 result when module member used in tsx with static member', () => { - const cwd = dirname(__filename); - const response = checkModuleMemberUsage(["antd"],{cwd:path.resolve(cwd,'features/use-in-tsx-with-static-member')}) - expect(response.length).toBe(1) -}) - -test('should return 1 result when module member used in tsx with static member self closed', () => { - const cwd = dirname(__filename); - const response = checkModuleMemberUsage(["antd"],{cwd:path.resolve(cwd,'features/use-in-tsx-with-static-member-self-closed')}) - expect(response.length).toBe(1) -}) - -test('should return 1 result when module member used in tsx with namespace import', () => { - const cwd = dirname(__filename); - const response = checkModuleMemberUsage(["antd"],{cwd:path.resolve(cwd,'features/use-in-tsx-with-namespace-import')}) - expect(response.length).toBe(1) -}) - -test('should return 1 result when module member used in tsx with multi library usage', () => { - const cwd = dirname(__filename); - const response = checkModuleMemberUsage(["antd","lodash"],{cwd:path.resolve(cwd,'features/multi-library-usage')}) - expect(response.length).toBe(2) -}) - -test('should return 1 result when module member used in tsx with no-impored-but-used', () => { - const cwd = dirname(__filename); - const response = checkModuleMemberUsage(["antd"],{cwd:path.resolve(cwd,'features/no-impored-but-used')}) - expect(response.length).toBe(1) -}) - - -test('should return 1 result when module member used in tsx with use-in-call-expression', () => { - const cwd = dirname(__filename); - const response = checkModuleMemberUsage(["antd"],{cwd:path.resolve(cwd,'features/use-in-call-expression')}) - expect(response.length).toBe(1) -}) - -test('should return 1 result when module member used in tsx with use-in-jsx-alias', () => { - const cwd = dirname(__filename); - const response = checkModuleMemberUsage(["antd"],{cwd:path.resolve(cwd,'features/use-in-jsx-with-alias')}) - expect(response.length).toBe(3) -}) - -test('should return 1 result when module member used in tsx with tree-fold', () => { - const cwd = dirname(__filename); - const response = checkModuleMemberUsage(["antd"],{cwd:path.resolve(cwd,'features/tree-fold')}) - expect(response.length).toBe(4) -}) - - +test("should return empty array when module is only imported but not used", async () => { + const cwd = dirname(__filename); + const response = await checkModuleMemberUsage(["antd"], { cwd: path.resolve(cwd, "only-imported") }); + expect(response.flatMap((item) => item.items).length).toBe(0); +}); + +test("should return 1 result when module member used in statement", async () => { + const cwd = dirname(__filename); + const response = await checkModuleMemberUsage(["antd"], { cwd: path.resolve(cwd, "features/use-in-statement") }); + expect(response.flatMap((item) => item.items).length).toBe(1); +}); + +test("should return 1 result when module member used in tsx", async () => { + const cwd = dirname(__filename); + const response = await checkModuleMemberUsage(["antd"], { cwd: path.resolve(cwd, "features/use-in-tsx") }); + expect(response.flatMap((item) => item.items).length).toBe(1); +}); + +test("should return 1 result when module member used in tsx with self closed", async () => { + const cwd = dirname(__filename); + const response = await checkModuleMemberUsage(["antd"], { + cwd: path.resolve(cwd, "features/use-in-tsx-with-self-closed"), + }); + expect(response.flatMap((item) => item.items).length).toBe(1); +}); + +test("should return 1 result when module member used in tsx with static member", async () => { + const cwd = dirname(__filename); + const response = await checkModuleMemberUsage(["antd"], { + cwd: path.resolve(cwd, "features/use-in-tsx-with-static-member"), + }); + expect(response.flatMap((item) => item.items).length).toBe(1); +}); + +test("should return 1 result when module member used in tsx with static member self closed", async () => { + const cwd = dirname(__filename); + const response = await checkModuleMemberUsage(["antd"], { + cwd: path.resolve(cwd, "features/use-in-tsx-with-static-member-self-closed"), + }); + expect(response.flatMap((item) => item.items).length).toBe(1); +}); + +test("should return 1 result when module member used in tsx with namespace import", async () => { + const cwd = dirname(__filename); + const response = await checkModuleMemberUsage(["antd"], { + cwd: path.resolve(cwd, "features/use-in-tsx-with-namespace-import"), + }); + expect(response.flatMap((item) => item.items).length).toBe(1); +}); + +test("should return 1 result when module member used in tsx with multi library usage", async () => { + const cwd = dirname(__filename); + const response = await checkModuleMemberUsage(["antd", "lodash"], { + cwd: path.resolve(cwd, "features/multi-library-usage"), + }); + expect(response.flatMap((item) => item.items).length).toBe(2); +}); + +test("should return 1 result when module member used in tsx with no-impored-but-used", async () => { + const cwd = dirname(__filename); + const response = await checkModuleMemberUsage(["antd"], { cwd: path.resolve(cwd, "features/no-impored-but-used") }); + expect(response.flatMap((item) => item.items).length).toBe(1); +}); + +test("should return 1 result when module member used in tsx with use-in-call-expression", async () => { + const cwd = dirname(__filename); + const response = await checkModuleMemberUsage(["antd"], { + cwd: path.resolve(cwd, "features/use-in-call-expression"), + }); + expect(response.flatMap((item) => item.items).length).toBe(1); +}); + +test("should return 1 result when module member used in tsx with use-in-jsx-alias", async () => { + const cwd = dirname(__filename); + const response = await checkModuleMemberUsage(["antd"], { cwd: path.resolve(cwd, "features/use-in-jsx-with-alias") }); + expect(response.flatMap((item) => item.items).length).toBe(3); +}); + +test("should return 1 result when module member used in tsx with tree-fold", async () => { + const cwd = dirname(__filename); + const response = await checkModuleMemberUsage(["antd"], { cwd: path.resolve(cwd, "features/tree-fold") }); + + expect(response.flatMap((item) => item.items).length).toBe(4); +}); diff --git a/crates/binding/src/lib.rs b/crates/binding/src/lib.rs index ffb86f5..edc88c1 100644 --- a/crates/binding/src/lib.rs +++ b/crates/binding/src/lib.rs @@ -102,7 +102,7 @@ pub async fn check_danger_strings( } #[napi] -pub fn check_module_member_usage( +pub async fn check_module_member_usage( npm_name_vec: Vec, args: GlobJsArgs, ) -> Result> { @@ -119,7 +119,7 @@ pub async fn check_filename_case( #[napi] pub async fn check_browser_supported( - target: check_browser_supported::Target, + target: check_browser_supported::target::Target, args: utils::GlobJsArgs, ) -> Result> { check_browser_supported::check_browser_supported(target, args.into()) @@ -128,7 +128,7 @@ pub async fn check_browser_supported( #[napi] pub async fn check_browser_supported_with_source_code( - target: check_browser_supported::Target, + target: check_browser_supported::target::Target, source_code: String, file_path: String, ) -> Result> { diff --git a/crates/check_browser_supported/Cargo.toml b/crates/check_browser_supported/Cargo.toml index e6450af..fb5e6ba 100644 --- a/crates/check_browser_supported/Cargo.toml +++ b/crates/check_browser_supported/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -oxc-browserslist = "1.0.3" +oxc-browserslist = "1.1.1" oxc_ast = { workspace = true } oxc_semantic = { workspace = true } oxc_span = { workspace = true } @@ -19,4 +19,8 @@ napi-derive = { workspace = true } log = { workspace = true } beans = { path = "../beans" } utils = { path = "../utils" } -anyhow = { workspace = true } \ No newline at end of file +anyhow = { workspace = true } +insta = { workspace = true } + +[dev-dependencies] +tokio = { workspace = true, features = ["full"] } diff --git a/crates/check_browser_supported/src/browser_versions.rs b/crates/check_browser_supported/src/browser_versions.rs new file mode 100644 index 0000000..f0584be --- /dev/null +++ b/crates/check_browser_supported/src/browser_versions.rs @@ -0,0 +1,67 @@ +use anyhow::{Context, Result}; +use browserslist::{resolve, Distrib, Opts}; +use std::collections::HashMap; + +use crate::target::Target; + +#[derive(Debug, Clone)] +pub struct BrowserVersions { + versions: HashMap>, +} + +impl BrowserVersions { + pub fn new(target: Target) -> Result { + let browser_list = Self::get_queries(target)?; + let mut versions = HashMap::new(); + browser_list.into_iter().for_each(|distrib| { + versions + .entry(distrib.name().to_string()) + .or_insert_with(Vec::new) + .push(distrib.version().to_string()); + }); + Ok(Self { versions }) + } + + fn get_queries(target: Target) -> anyhow::Result> { + let mut queries = vec![format!("chrome > {}", target.chrome)]; + if let Some(firefox) = &target.firefox { + queries.push(format!("firefox > {}", firefox)); + } + + if let Some(safari) = &target.safari { + queries.push(format!("safari > {}", safari)); + } + + if let Some(edge) = &target.edge { + queries.push(format!("edge > {}", edge)); + } + + if let Some(node) = &target.node { + queries.push(format!("node > {}", node)); + } + + // if let Some(deno) = &target.deno { + // queries.push(format!("deno > {}", deno)); + // } + + let browser_list = + resolve(&queries, &Opts::default()).with_context(|| { + format!( + "{}:{} - Failed to resolve browser list\nQueries: {:?}", + file!(), + line!(), + queries + ) + })?; + + Ok(browser_list) + } + + pub fn contains_version(&self, browser: &str, version: &str) -> bool { + self + .versions + .get(browser) + .map(|versions| versions.contains(&version.to_string())) + .unwrap_or(false) + } +} diff --git a/crates/check_browser_supported/src/compat_checker.rs b/crates/check_browser_supported/src/compat_checker.rs new file mode 100644 index 0000000..2ebc65f --- /dev/null +++ b/crates/check_browser_supported/src/compat_checker.rs @@ -0,0 +1,149 @@ +use std::path::Path; + +use oxc_allocator::Allocator; +use oxc_parser::Parser; +use oxc_semantic::SemanticBuilder; +use utils::{ + glob_by_semantic, source_type_from_path, GlobErrorHandler, GlobSuccessHandler, +}; + +use crate::{ + browser_versions::BrowserVersions, classes, functions, grammar, operators, + statements, target::Target, CompatBox, CompatHandler, +}; + +pub struct CompatChecker { + handlers: Vec>, +} + +impl CompatChecker { + pub fn new(target: Target) -> anyhow::Result { + let versions = BrowserVersions::new(target)?; + let handlers = Self::setup_handlers(&versions); + Ok(Self { handlers }) + } + + fn setup_handlers(versions: &BrowserVersions) -> Vec> { + vec![ + classes::setup(), + functions::setup(), + grammar::setup(), + operators::setup(), + statements::setup(), + ] + .into_iter() + .flat_map(|setup| setup.into_iter()) + .filter(|item| Self::is_compatible(versions, item)) + .collect() + } + + fn is_compatible( + versions: &BrowserVersions, + item: &Box, + ) -> bool { + let compat = item.get_compat(); + let support = &compat.support; + versions.contains_version("chrome", &support.chrome) + || versions.contains_version("firefox", &support.firefox) + || versions.contains_version("safari", &support.safari) + || versions.contains_version("edge", &support.edge) + || versions.contains_version("node", &support.node) + } + + pub fn check_source( + &self, + source_code: &str, + file_path: &str, + ) -> anyhow::Result> { + let allocator = Allocator::default(); + + let source_type = source_type_from_path(&Path::new(&file_path)); + + let parser = Parser::new(&allocator, &source_code, source_type); + + let parse = parser.parse(); + + let program = allocator.alloc(&parse.program); + + let semantic_return = SemanticBuilder::new() + .with_check_syntax_error(false) + // TODO 很多场景下是不需要开启的,只有 oxlint 下需要开启,这可能对性能会产生一定的影响 + .with_cfg(true) + .build(program); + + let nodes = semantic_return.semantic.nodes(); + + let nodes = nodes + .iter() + .map(|item| { + return self + .handlers + .iter() + .filter_map(|compat_handler| { + if compat_handler.handle( + semantic_return.semantic.source_text(), + item, + nodes, + ) { + let ast_node = + beans::AstNode::with_source_and_ast_node(&source_code, item); + Some(CompatBox::new( + ast_node, + compat_handler.get_compat().clone(), + file_path.to_string(), + )) + } else { + None + } + }) + .collect::>(); + }) + .flatten() + .collect::>(); + + Ok(nodes) + } + + pub fn check_glob( + &self, + args: utils::GlobArgs, + ) -> anyhow::Result> { + let responses = glob_by_semantic( + |GlobSuccessHandler { + relative_path, + semantic, + .. + }| { + let mut used: Vec = Vec::new(); + for node in semantic.nodes().iter() { + for compat_handler in self.handlers.iter() { + if compat_handler.handle( + semantic.source_text(), + node, + semantic.nodes(), + ) { + let ast_node = beans::AstNode::with_source_and_ast_node( + semantic.source_text(), + node, + ); + + used.push(CompatBox::new( + ast_node, + compat_handler.get_compat().clone(), + relative_path.to_string(), + )); + } + } + } + Some(used) + }, + |GlobErrorHandler { .. }| None, + &args, + )? + .into_iter() + .flatten() + .collect(); + + Ok(responses) + } +} diff --git a/crates/check_browser_supported/src/lib.rs b/crates/check_browser_supported/src/lib.rs index 46b5460..f2e6703 100644 --- a/crates/check_browser_supported/src/lib.rs +++ b/crates/check_browser_supported/src/lib.rs @@ -6,304 +6,28 @@ mod macros; mod operators; mod statements; -use std::{collections::HashMap, path::Path}; - -use browserslist::{resolve, Distrib, Opts}; pub use compat::{CompatBox, CompatHandler}; -use anyhow::Result; -use log::debug; -use napi_derive::napi; - -use oxc_allocator::Allocator; -use oxc_parser::Parser; -use oxc_semantic::SemanticBuilder; -use utils::{ - glob_by_semantic, source_type_from_path, GlobErrorHandler, GlobSuccessHandler, -}; - -macro_rules! enabled_debug { - ($($body:tt)*) => { - if log::log_enabled!(log::Level::Debug) { - $($body)* - } - }; -} - -#[derive(Debug, Clone)] -#[napi[object]] -pub struct Target { - pub chrome: String, - pub firefox: Option, - pub safari: Option, - pub edge: Option, - pub node: Option, - pub deno: Option, -} - -struct BrowserVersions { - versions: HashMap>, -} - -impl BrowserVersions { - fn new(target: Target) -> anyhow::Result { - let browser_list = Self::get_queries(target)?; - let mut versions = HashMap::new(); - browser_list.into_iter().for_each(|distrib| { - versions - .entry(distrib.name().to_string()) - .or_insert_with(Vec::new) - .push(distrib.version().to_string()); - }); - Ok(Self { versions }) - } - - fn get_queries(target: Target) -> anyhow::Result> { - let mut queries = vec![format!("chrome > {}", target.chrome)]; - if let Some(firefox) = &target.firefox { - queries.push(format!("firefox > {}", firefox)); - } - - if let Some(safari) = &target.safari { - queries.push(format!("safari > {}", safari)); - } +use compat_checker::CompatChecker; +use target::Target; - if let Some(edge) = &target.edge { - queries.push(format!("edge > {}", edge)); - } - - if let Some(node) = &target.node { - queries.push(format!("node > {}", node)); - } - - if let Some(deno) = &target.deno { - queries.push(format!("deno > {}", deno)); - } - - let browser_list = resolve(&queries, &Opts::default())?; - - Ok(browser_list) - } - - pub fn contains_version(&self, browser: &str, version: &str) -> bool { - self - .versions - .get(browser) - .map(|versions| versions.contains(&version.to_string())) - .unwrap_or(false) // 如果浏览器不在列表中,默认返回 true - } -} +mod browser_versions; +mod compat_checker; +pub mod target; pub fn check_browser_supported_with_source_code( target: Target, source_code: String, file_path: String, -) -> Result> { - let versions = BrowserVersions::new(target)?; - - let compat_handlers: Vec> = vec![ - classes::setup(), - functions::setup(), - grammar::setup(), - operators::setup(), - statements::setup(), - ] - .into_iter() - .flat_map(|setup| setup.into_iter()) - .filter(|item| { - let compat = item.get_compat(); - let compat_support = &compat.support; - versions.contains_version("chrome", &compat_support.chrome) - || versions.contains_version("firefox", &compat_support.firefox) - || versions.contains_version("safari", &compat_support.safari) - || versions.contains_version("edge", &compat_support.edge) - || versions.contains_version("node", &compat_support.node) - }) - .collect(); - - for compat_handler in compat_handlers.iter() { - println!( - "Compat handler: {:?}", - compat_handler.get_compat().name.clone() - ); - } - - enabled_debug! { - for compat_handler in compat_handlers.iter() { - debug!( - "Compat handler: {:?}", - compat_handler.get_compat().name.clone() - ); - } - } - - let allocator = Allocator::default(); - - let source_type = source_type_from_path(&Path::new(&file_path)); - - let parser = Parser::new(&allocator, &source_code, source_type); - - let parse = parser.parse(); - - let program = allocator.alloc(&parse.program); - - let semantic_return = SemanticBuilder::new() - .with_check_syntax_error(false) - // TODO 很多场景下是不需要开启的,只有 oxlint 下需要开启,这可能对性能会产生一定的影响 - .with_cfg(true) - .build(program); - - let nodes = semantic_return.semantic.nodes(); - - let nodes = nodes - .iter() - .map(|item| { - return compat_handlers - .iter() - .filter_map(|compat_handler| { - if compat_handler.handle( - semantic_return.semantic.source_text(), - item, - nodes, - ) { - let ast_node = - beans::AstNode::with_source_and_ast_node(&source_code, item); - Some(CompatBox::new( - ast_node, - compat_handler.get_compat().clone(), - String::new(), - )) - } else { - None - } - }) - .collect::>(); - }) - .flatten() - .collect::>(); - - Ok(nodes) +) -> anyhow::Result> { + let checker = CompatChecker::new(target)?; + checker.check_source(&source_code, &file_path) } pub fn check_browser_supported( target: Target, args: utils::GlobArgs, -) -> Result> { - let versions = BrowserVersions::new(target)?; - - let compat_handlers: Vec> = vec![ - classes::setup(), - functions::setup(), - grammar::setup(), - operators::setup(), - statements::setup(), - ] - .into_iter() - .flat_map(|setup| setup.into_iter()) - .filter(|item| { - let compat = item.get_compat(); - let compat_support = &compat.support; - versions.contains_version("chrome", &compat_support.chrome) - || versions.contains_version("firefox", &compat_support.firefox) - || versions.contains_version("safari", &compat_support.safari) - || versions.contains_version("edge", &compat_support.edge) - || versions.contains_version("node", &compat_support.node) - }) - .collect(); - - enabled_debug! { - for compat_handler in compat_handlers.iter() { - debug!( - "Compat handler: {:?}", - compat_handler.get_compat().name.clone() - ); - } - } - - let responses = glob_by_semantic( - |GlobSuccessHandler { - relative_path, - semantic, - .. - }| { - let mut used: Vec = Vec::new(); - for node in semantic.nodes().iter() { - for compat_handler in compat_handlers.iter() { - if compat_handler.handle( - semantic.source_text(), - node, - semantic.nodes(), - ) { - let ast_node = beans::AstNode::with_source_and_ast_node( - semantic.source_text(), - node, - ); - - used.push(CompatBox::new( - ast_node, - compat_handler.get_compat().clone(), - relative_path.to_string(), - )); - } - } - } - Some(used) - }, - |GlobErrorHandler { .. }| None, - &args, - )? - .into_iter() - .flatten() - .collect(); - - Ok(responses) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_check_browser_supported_with_source_code() { - // Set up test data - let target = "chrome >= 40".to_string(); - let source_code = r#" - class MyClass { - #privateField = 42; - - constructor() { - console.log(this.#privateField); - } - } - - new MyClass(); - "# - .to_string(); - - // Call the function - let result = check_browser_supported_with_source_code( - Target { - chrome: "40".to_string(), - firefox: None, - safari: None, - edge: None, - node: None, - deno: None, - }, - source_code, - "test.ts".to_string(), - ); - - // Assert the result - assert!(result.is_ok()); - let compat_boxes = result.unwrap(); - - // Check if we have at least one CompatBox (for private class fields) - assert!(!compat_boxes.is_empty()); - - // Check the first CompatBox - let first_compat = &compat_boxes[0]; - assert_eq!(first_compat.compat.name, "statements.classes"); - assert!(first_compat.compat.support.chrome.parse::().unwrap() > 40.0); - } +) -> anyhow::Result> { + let checker = CompatChecker::new(target)?; + checker.check_glob(args) } diff --git a/crates/check_browser_supported/src/target.rs b/crates/check_browser_supported/src/target.rs new file mode 100644 index 0000000..df85e43 --- /dev/null +++ b/crates/check_browser_supported/src/target.rs @@ -0,0 +1,12 @@ +use napi_derive::napi; + +#[derive(Debug, Clone)] +#[napi[object]] +pub struct Target { + pub chrome: String, + pub firefox: Option, + pub safari: Option, + pub edge: Option, + pub node: Option, + // pub deno: Option, +} diff --git a/crates/check_browser_supported/tests/check_browser_supported_with_source_code_test.rs b/crates/check_browser_supported/tests/check_browser_supported_with_source_code_test.rs new file mode 100644 index 0000000..7bbcb5c --- /dev/null +++ b/crates/check_browser_supported/tests/check_browser_supported_with_source_code_test.rs @@ -0,0 +1,35 @@ +use check_browser_supported::{ + check_browser_supported_with_source_code, target::Target, +}; + +#[tokio::test] +async fn test_check_browser_supported_with_source_code() -> anyhow::Result<()> { + let source_code = r#" + class MyClass { + #privateField = 42; + + constructor() { + console.log(this.#privateField); + } + } + + new MyClass(); + "# + .to_string(); + let result = check_browser_supported_with_source_code( + Target { + chrome: "40".to_string(), + firefox: Some("45".to_string()), + safari: Some("9".to_string()), + edge: Some("13".to_string()), + node: Some("6.0.0".to_string()), + // deno: Some("1.0.0".to_string()), + }, + source_code, + "test.ts".to_string(), + )?; + + insta::assert_debug_snapshot!(result); + + Ok(()) +} diff --git a/crates/check_browser_supported/tests/snapshots/check_browser_supported_with_source_code_test__check_browser_supported_with_source_code.snap b/crates/check_browser_supported/tests/snapshots/check_browser_supported_with_source_code_test__check_browser_supported_with_source_code.snap new file mode 100644 index 0000000..9ada126 --- /dev/null +++ b/crates/check_browser_supported/tests/snapshots/check_browser_supported_with_source_code_test__check_browser_supported_with_source_code.snap @@ -0,0 +1,127 @@ +--- +source: crates/check_browser_supported/tests/check_browser_supported_with_source_code_test.rs +expression: result +snapshot_kind: text +--- +[ + CompatBox { + name: "statements.classes", + compat: Compat { + name: "statements.classes", + mdn_url: "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/class", + description: "The class declaration creates a binding of a new class to a given name.", + tags: [ + "web-features:class-syntax", + "web-features:snapshot:ecmascript-2015", + ], + support: Support { + chrome: "49", + chrome_android: "49", + firefox: "45", + firefox_android: "45", + safari: "10.1", + safari_ios: "10.1", + edge: "13", + node: "6.0.0", + deno: "1.0.0", + }, + }, + file_path: "test.ts", + ast_node: AstNode { + span: Span { + start: 13, + end: 198, + }, + loc: Location { + start: Position { + line: 2, + col: 13, + }, + end: Position { + line: 8, + col: 14, + }, + }, + }, + }, + CompatBox { + name: "classes.private_class_fields", + compat: Compat { + name: "classes.private_class_fields", + mdn_url: "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_properties#browser_compatibility", + description: "Private class fields", + tags: [ + "web-features:class-syntax", + "web-features:snapshot:ecmascript-2015", + ], + support: Support { + chrome: "74", + chrome_android: "74", + firefox: "90", + firefox_android: "90", + safari: "14", + safari_ios: "14", + edge: "74", + node: "12.0.0", + deno: "1.0.0", + }, + }, + file_path: "test.ts", + ast_node: AstNode { + span: Span { + start: 45, + end: 64, + }, + loc: Location { + start: Position { + line: 3, + col: 17, + }, + end: Position { + line: 3, + col: 36, + }, + }, + }, + }, + CompatBox { + name: "classes.constructor", + compat: Compat { + name: "classes.constructor", + mdn_url: "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/constructor", + description: "constructor function", + tags: [ + "web-features:class-syntax", + "web-features:snapshot:ecmascript-2015", + ], + support: Support { + chrome: "49", + chrome_android: "49", + firefox: "45", + firefox_android: "45", + safari: "9", + safari_ios: "9", + edge: "13", + node: "6.0.0", + deno: "1.0.0", + }, + }, + file_path: "test.ts", + ast_node: AstNode { + span: Span { + start: 98, + end: 184, + }, + loc: Location { + start: Position { + line: 5, + col: 17, + }, + end: Position { + line: 7, + col: 18, + }, + }, + }, + }, +] diff --git a/crates/module_member_usage/src/response.rs b/crates/module_member_usage/src/response.rs index 889302a..3f305a1 100644 --- a/crates/module_member_usage/src/response.rs +++ b/crates/module_member_usage/src/response.rs @@ -10,7 +10,7 @@ pub struct ModuleMemberUsageResponse { pub errors: Vec, } -#[napi(object)] +#[napi(object, js_name = "ModuleMemberUsageResponseItem")] #[derive(Debug, Serialize, Clone)] pub struct ModuleMemberUsageResponseItem { pub lib_name: String, diff --git a/package.json b/package.json index c395bf6..412ca85 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@shined/source-code-diagnosis", - "version": "0.0.82", + "version": "0.0.83", "main": "index.js", "types": "index.d.ts", "napi": {