diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c654e23302..d3ca3eeea7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -47,7 +47,7 @@ jobs: rustup component add clippy rustup target add wasm32-unknown-unknown - uses: Swatinem/rust-cache@v1 - - run: cargo clippy -p ruff --target wasm32-unknown-unknown --all-features -- -D warnings + - run: cargo clippy -p ruff_wasm --target wasm32-unknown-unknown --all-features -- -D warnings cargo-test: strategy: @@ -80,6 +80,26 @@ jobs: # Setting RUSTDOCFLAGS because `cargo doc --check` isn't yet implemented (https://github.com/rust-lang/cargo/issues/10025). RUSTDOCFLAGS: "-D warnings" + + cargo-test-wasm: + runs-on: ubuntu-latest + name: "cargo test (wasm)" + steps: + - uses: actions/checkout@v3 + - name: "Install Rust toolchain" + run: rustup target add wasm32-unknown-unknown + - uses: actions/setup-node@v3 + with: + node-version: 18 + cache: "npm" + cache-dependency-path: playground/package-lock.json + - uses: jetli/wasm-pack-action@v0.4.0 + - uses: Swatinem/rust-cache@v1 + - name: "Run wasm-pack" + run: | + cd crates/ruff_wasm + wasm-pack test --node + scripts: name: "test scripts" runs-on: ubuntu-latest diff --git a/.github/workflows/playground.yaml b/.github/workflows/playground.yaml index a5c58fe03c..40060bcbb7 100644 --- a/.github/workflows/playground.yaml +++ b/.github/workflows/playground.yaml @@ -28,7 +28,7 @@ jobs: - uses: jetli/wasm-pack-action@v0.4.0 - uses: jetli/wasm-bindgen-action@v0.2.0 - name: "Run wasm-pack" - run: wasm-pack build --target web --out-dir ../../playground/src/pkg crates/ruff + run: wasm-pack build --target web --out-dir ../../playground/src/pkg crates/ruff_wasm - name: "Install Node dependencies" run: npm ci working-directory: playground diff --git a/Cargo.lock b/Cargo.lock index e11a2d247f..7a08d9e3a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -608,17 +608,6 @@ dependencies = [ "syn", ] -[[package]] -name = "derivative" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "diff" version = "0.1.13" @@ -1969,17 +1958,12 @@ dependencies = [ "anyhow", "bisection", "bitflags", - "cfg-if", "chrono", "clap 4.1.8", "colored", - "console_error_panic_hook", - "console_log", "criterion", - "derivative", "dirs", "fern", - "getrandom", "glob", "globset", "ignore", @@ -1987,7 +1971,6 @@ dependencies = [ "insta", "is-macro", "itertools", - "js-sys", "libcst", "log", "natord", @@ -2009,7 +1992,6 @@ dependencies = [ "schemars", "semver", "serde", - "serde-wasm-bindgen", "shellexpand", "smallvec", "strum", @@ -2018,8 +2000,6 @@ dependencies = [ "textwrap", "thiserror", "toml", - "wasm-bindgen", - "wasm-bindgen-test", ] [[package]] @@ -2190,7 +2170,6 @@ name = "ruff_testing_macros" version = "0.0.0" dependencies = [ "glob", - "proc-macro-error", "proc-macro2", "quote", "syn", @@ -2206,6 +2185,25 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "ruff_wasm" +version = "0.0.0" +dependencies = [ + "console_error_panic_hook", + "console_log", + "getrandom", + "js-sys", + "log", + "ruff", + "ruff_python_ast", + "ruff_rustpython", + "rustpython-parser", + "serde", + "serde-wasm-bindgen", + "wasm-bindgen", + "wasm-bindgen-test", +] + [[package]] name = "rust-stemmers" version = "1.2.0" @@ -2411,9 +2409,9 @@ dependencies = [ [[package]] name = "serde-wasm-bindgen" -version = "0.4.5" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b4c031cd0d9014307d82b8abf653c0290fbdaeb4c02d00c63cf52f728628bf" +checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" dependencies = [ "js-sys", "serde", diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index 8d8f1f3f13..2b570b9825 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -25,11 +25,9 @@ ruff_rustpython = { path = "../ruff_rustpython" } anyhow = { workspace = true } bisection = { version = "0.1.0" } bitflags = { workspace = true } -cfg-if = { version = "1.0.0" } chrono = { workspace = true } clap = { workspace = true, features = ["derive", "env", "string"] } colored = { workspace = true } -derivative = { version = "2.2.0" } dirs = { version = "4.0.0" } fern = { version = "0.6.1" } glob = { workspace = true } @@ -65,23 +63,12 @@ textwrap = { workspace = true } thiserror = { version = "1.0.38" } toml = { workspace = true } -# https://docs.rs/getrandom/0.2.7/getrandom/#webassembly-support -[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies] -getrandom = { version = "0.2.8", features = ["js"] } -console_error_panic_hook = { version = "0.1.7" } -console_log = { version = "0.2.1" } -serde-wasm-bindgen = { version = "0.4.5" } -js-sys = { version = "0.3.61" } -wasm-bindgen = { version = "0.2.84" } - [dev-dependencies] insta = { workspace = true, features = ["yaml", "redactions"] } test-case = { workspace = true } -wasm-bindgen-test = { version = "0.3.34" } - -[target.'cfg(not(target_family = "wasm"))'.dev-dependencies] criterion = { version = "0.4.0" } + [features] default = [] logical_lines = [] diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index b8a819cc25..454372db3e 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -1,5 +1,23 @@ use crate::registry::{Linter, Rule}; +#[derive(PartialEq, Eq, PartialOrd, Ord)] +pub struct NoqaCode(&'static str, &'static str); + +impl std::fmt::Display for NoqaCode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + write!(f, "{}{}", self.0, self.1) + } +} + +impl PartialEq<&str> for NoqaCode { + fn eq(&self, other: &&str) -> bool { + match other.strip_prefix(self.0) { + Some(suffix) => suffix == self.1, + None => false, + } + } +} + #[ruff_macros::map_codes] pub fn code_to_rule(linter: Linter, code: &str) -> Option { #[allow(clippy::enum_glob_use)] diff --git a/crates/ruff/src/lib.rs b/crates/ruff/src/lib.rs index 9b0b529a3e..c792e2f3f6 100644 --- a/crates/ruff/src/lib.rs +++ b/crates/ruff/src/lib.rs @@ -5,7 +5,6 @@ //! //! [Ruff]: https://github.com/charliermarsh/ruff -use cfg_if::cfg_if; pub use ruff_python_ast::source_code::round_trip; pub use ruff_python_ast::types::Range; pub use rule_selector::RuleSelector; @@ -16,7 +15,7 @@ mod autofix; mod checkers; mod codes; mod cst; -mod directives; +pub mod directives; mod doc_lines; mod docstrings; pub mod fix; @@ -27,22 +26,14 @@ pub mod linter; pub mod logging; pub mod message; mod noqa; +pub mod packaging; pub mod registry; pub mod resolver; mod rule_redirects; mod rule_selector; -mod rules; +pub mod rules; pub mod settings; mod violation; -cfg_if! { - if #[cfg(target_family = "wasm")] { - mod lib_wasm; - pub use lib_wasm::check; - } else { - pub mod packaging; - } -} - #[cfg(test)] mod test; diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index 69d59f599b..e99b6e62c0 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -611,6 +611,20 @@ ruff_macros::register_rules!( rules::flake8_django::rules::NonLeadingReceiverDecorator, ); +impl Rule { + pub fn from_code(code: &str) -> Result { + let (linter, code) = Linter::parse_code(code).ok_or(FromCodeError::Unknown)?; + let prefix: RuleCodePrefix = RuleCodePrefix::parse(&linter, code)?; + Ok(prefix.into_iter().next().unwrap()) + } +} + +#[derive(thiserror::Error, Debug)] +pub enum FromCodeError { + #[error("unknown rule code")] + Unknown, +} + #[derive(EnumIter, Debug, PartialEq, Eq, Clone, Hash, RuleNamespace)] pub enum Linter { /// [Pyflakes](https://pypi.org/project/pyflakes/) diff --git a/crates/ruff_macros/src/map_codes.rs b/crates/ruff_macros/src/map_codes.rs index bae55028fb..32036ac1f4 100644 --- a/crates/ruff_macros/src/map_codes.rs +++ b/crates/ruff_macros/src/map_codes.rs @@ -156,27 +156,16 @@ pub fn map_codes(func: &ItemFn) -> syn::Result { out.extend(quote! { impl RuleCodePrefix { - pub fn parse(linter: &Linter, code: &str) -> Result { + pub fn parse(linter: &Linter, code: &str) -> Result { use std::str::FromStr; Ok(match linter { - #(Linter::#linter_idents => RuleCodePrefix::#linter_idents(#linter_idents::from_str(code).map_err(|_| FromCodeError::Unknown)?),)* + #(Linter::#linter_idents => RuleCodePrefix::#linter_idents(#linter_idents::from_str(code).map_err(|_| crate::registry::FromCodeError::Unknown)?),)* }) } } }); - out.extend(quote! { - impl crate::registry::Rule { - pub fn from_code(code: &str) -> Result { - use crate::registry::RuleNamespace; - let (linter, code) = Linter::parse_code(code).ok_or(FromCodeError::Unknown)?; - let prefix: RuleCodePrefix = RuleCodePrefix::parse(&linter, code)?; - Ok(prefix.into_iter().next().unwrap()) - } - } - }); - #[allow(clippy::type_complexity)] let mut rule_to_codes: HashMap<&Path, Vec<(&Ident, &String, &Vec)>> = HashMap::new(); let mut linter_code_for_rule_match_arms = quote!(); @@ -245,25 +234,6 @@ pub fn map_codes(func: &ItemFn) -> syn::Result { } } } - - #[derive(PartialEq, Eq, PartialOrd, Ord)] - pub struct NoqaCode(&'static str, &'static str); - - impl std::fmt::Display for NoqaCode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { - use std::fmt::Write; - write!(f, "{}{}", self.0, self.1) - } - } - - impl PartialEq<&str> for NoqaCode { - fn eq(&self, other: &&str) -> bool { - match other.strip_prefix(self.0) { - Some(suffix) => suffix == self.1, - None => false - } - } - } }); let mut linter_into_iter_match_arms = quote!(); @@ -295,12 +265,6 @@ pub fn map_codes(func: &ItemFn) -> syn::Result { vec![ #(#all_codes,)* ].into_iter() } } - - #[derive(thiserror::Error, Debug)] - pub enum FromCodeError { - #[error("unknown rule code")] - Unknown, - } }); Ok(out) diff --git a/crates/ruff_macros/src/rule_code_prefix.rs b/crates/ruff_macros/src/rule_code_prefix.rs index 8171bf9430..3485cd9734 100644 --- a/crates/ruff_macros/src/rule_code_prefix.rs +++ b/crates/ruff_macros/src/rule_code_prefix.rs @@ -66,12 +66,12 @@ pub fn expand<'a>( } impl std::str::FromStr for #prefix_ident { - type Err = FromCodeError; + type Err = crate::registry::FromCodeError; fn from_str(code: &str) -> Result { match code { #(#attributes #variant_strs => Ok(Self::#variant_idents),)* - _ => Err(FromCodeError::Unknown) + _ => Err(crate::registry::FromCodeError::Unknown) } } } diff --git a/crates/ruff_testing_macros/Cargo.toml b/crates/ruff_testing_macros/Cargo.toml index 88e0f6ae72..aeffa59469 100644 --- a/crates/ruff_testing_macros/Cargo.toml +++ b/crates/ruff_testing_macros/Cargo.toml @@ -11,7 +11,6 @@ proc-macro = true [dependencies] glob = { workspace = true } -proc-macro-error = { version = "1.0.4", default-features = false } proc-macro2 = { workspace = true } quote = { workspace = true } syn = { workspace = true } diff --git a/crates/ruff_wasm/Cargo.toml b/crates/ruff_wasm/Cargo.toml new file mode 100644 index 0000000000..a6fcd0b916 --- /dev/null +++ b/crates/ruff_wasm/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "ruff_wasm" +version = "0.0.0" +publish = false +edition = { workspace = true } +rust-version = { workspace = true } +description = "WebAssembly bindings for Ruff" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = ["console_error_panic_hook"] + +[dependencies] +console_log = { version = "0.2.1" } +getrandom = { version = "0.2.8", features = ["js"] } +log = { workspace = true } +ruff = { path = "../ruff" } +ruff_python_ast = { path = "../ruff_python_ast" } +ruff_rustpython = { path = "../ruff_rustpython" } +rustpython-parser = { workspace = true } +serde = { workspace = true } +serde-wasm-bindgen = { version = "0.5.0" } +wasm-bindgen = { version = "0.2.84" } +console_error_panic_hook = { version = "0.1.7", optional = true } + +[dev-dependencies] +js-sys = { version = "0.3.61" } +wasm-bindgen-test = { version = "0.3.34" } diff --git a/crates/ruff/src/lib_wasm.rs b/crates/ruff_wasm/src/lib.rs similarity index 73% rename from crates/ruff/src/lib_wasm.rs rename to crates/ruff_wasm/src/lib.rs index ae37654563..4d36aa978d 100644 --- a/crates/ruff/src/lib_wasm.rs +++ b/crates/ruff_wasm/src/lib.rs @@ -2,21 +2,20 @@ use std::path::Path; use rustpython_parser::ast::Location; use rustpython_parser::lexer::LexResult; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::*; -use crate::directives; -use crate::linter::{check_path, LinterResult}; -use crate::registry::{AsRule, Rule}; -use crate::rules::{ +use ruff::directives; +use ruff::linter::{check_path, LinterResult}; +use ruff::rules::{ flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_errmsg, flake8_implicit_str_concat, flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pylint, pyupgrade, }; -use crate::settings::configuration::Configuration; -use crate::settings::options::Options; -use crate::settings::{defaults, flags, Settings}; +use ruff::settings::configuration::Configuration; +use ruff::settings::options::Options; +use ruff::settings::{defaults, flags, Settings}; use ruff_python_ast::source_code::{Indexer, Locator, Stylist}; const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -49,34 +48,17 @@ export interface Diagnostic { }; "#; -#[derive(Serialize)] -struct ExpandedMessage<'a> { - code: SerializeRuleAsCode<'a>, - message: String, - location: Location, - end_location: Location, - fix: Option, +#[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] +pub struct ExpandedMessage { + pub code: String, + pub message: String, + pub location: Location, + pub end_location: Location, + pub fix: Option, } -struct SerializeRuleAsCode<'a>(&'a Rule); - -impl Serialize for SerializeRuleAsCode<'_> { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.0.noqa_code().to_string()) - } -} - -impl<'a> From<&'a Rule> for SerializeRuleAsCode<'a> { - fn from(rule: &'a Rule) -> Self { - Self(rule) - } -} - -#[derive(Serialize)] -struct ExpandedFix { +#[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] +pub struct ExpandedFix { content: String, message: Option, location: Location, @@ -86,7 +68,16 @@ struct ExpandedFix { #[wasm_bindgen(start)] pub fn run() { use log::Level; + + // When the `console_error_panic_hook` feature is enabled, we can call the + // `set_panic_hook` function at least once during initialization, and then + // we will get better error messages if our code ever panics. + // + // For more details see + // https://github.com/rustwasm/console_error_panic_hook#readme + #[cfg(feature = "console_error_panic_hook")] console_error_panic_hook::set_once(); + console_log::init_with_level(Level::Debug).expect("Initializing logger went wrong."); } @@ -208,8 +199,8 @@ pub fn check(contents: &str, options: JsValue) -> Result { let messages: Vec = diagnostics .into_iter() .map(|message| ExpandedMessage { - code: message.kind.rule().into(), - message: message.kind.body.clone(), + code: message.kind.name, + message: message.kind.body, location: message.location, end_location: message.end_location, fix: message.fix.map(|fix| ExpandedFix { @@ -223,55 +214,3 @@ pub fn check(contents: &str, options: JsValue) -> Result { Ok(serde_wasm_bindgen::to_value(&messages)?) } - -#[cfg(test)] -mod test { - use js_sys; - use wasm_bindgen_test::*; - - use super::*; - - macro_rules! check { - ($source:expr, $config:expr, $expected:expr) => {{ - let foo = js_sys::JSON::parse($config).unwrap(); - match check($source, foo) { - Ok(output) => { - let result: Vec = serde_wasm_bindgen::from_value(output).unwrap(); - assert_eq!(result, $expected); - } - Err(e) => assert!(false, "{:#?}", e), - } - }}; - } - - #[wasm_bindgen_test] - fn empty_config() { - check!( - "if (1, 2): pass", - r#"{}"#, - [ExpandedMessage { - code: Rule::IfTuple.into(), - message: "If test is a tuple, which is always `True`".to_string(), - location: Location::new(1, 0), - end_location: Location::new(1, 15), - fix: None, - }] - ); - } - - #[wasm_bindgen_test] - fn partial_config() { - check!("if (1, 2): pass", r#"{"ignore": ["F"]}"#, []); - } - - #[wasm_bindgen_test] - fn partial_nested_config() { - let config = r#"{ - "select": ["Q"], - "flake8-quotes": { - "inline-quotes": "single" - } - }"#; - check!(r#"print('hello world')"#, config, []); - } -} diff --git a/crates/ruff_wasm/tests/api.rs b/crates/ruff_wasm/tests/api.rs new file mode 100644 index 0000000000..3c9c4ea55b --- /dev/null +++ b/crates/ruff_wasm/tests/api.rs @@ -0,0 +1,52 @@ +#![cfg(target_arch = "wasm32")] + +use js_sys; +use rustpython_parser::ast::Location; +use wasm_bindgen_test::*; + +use ruff::registry::Rule; +use ruff_wasm::*; + +macro_rules! check { + ($source:expr, $config:expr, $expected:expr) => {{ + let foo = js_sys::JSON::parse($config).unwrap(); + match check($source, foo) { + Ok(output) => { + let result: Vec = serde_wasm_bindgen::from_value(output).unwrap(); + assert_eq!(result, $expected); + } + Err(e) => assert!(false, "{:#?}", e), + } + }}; +} + +#[wasm_bindgen_test] +fn empty_config() { + check!( + "if (1, 2):\n pass", + r#"{}"#, + [ExpandedMessage { + code: Rule::IfTuple.noqa_code().to_string(), + message: "If test is a tuple, which is always `True`".to_string(), + location: Location::new(1, 0), + end_location: Location::new(2, 5), + fix: None, + }] + ); +} + +#[wasm_bindgen_test] +fn partial_config() { + check!("if (1, 2):\n pass", r#"{"ignore": ["F"]}"#, []); +} + +#[wasm_bindgen_test] +fn partial_nested_config() { + let config = r#"{ + "select": ["Q"], + "flake8-quotes": { + "inline-quotes": "single" + } + }"#; + check!(r#"print('hello world')"#, config, []); +} diff --git a/playground/README.md b/playground/README.md index f5bf4e9e0c..befd81a0af 100644 --- a/playground/README.md +++ b/playground/README.md @@ -4,7 +4,7 @@ In-browser playground for Ruff. Available [https://play.ruff.rs/](https://play.r ## Getting started -- To build the WASM module, run `wasm-pack build ../crates/ruff --target web --out-dir ../../playground/src/pkg` +- To build the WASM module, run `wasm-pack build ../crates/ruff_wasm --target web --out-dir ../../playground/src/pkg` from the `./playground` directory. - Install TypeScript dependencies with: `npm install`. - Start the development server with: `npm run dev`.