ruff/src/lib_wasm.rs
messense 1991d618a3
Add proc-macro to derive CheckCodePrefix (#1656)
IMO a derive macro is a natural way to generate new code, and it reduces
the chance of merge conflicts.
2023-01-05 11:39:16 -05:00

244 lines
7.6 KiB
Rust

use std::path::Path;
use rustpython_ast::Location;
use rustpython_parser::lexer::LexResult;
use serde::Serialize;
use wasm_bindgen::prelude::*;
use crate::linter::check_path;
use crate::registry::{CheckCode, CheckCodePrefix};
use crate::rustpython_helpers::tokenize;
use crate::settings::configuration::Configuration;
use crate::settings::options::Options;
use crate::settings::types::PythonVersion;
use crate::settings::{flags, Settings};
use crate::source_code_locator::SourceCodeLocator;
use crate::source_code_style::SourceCodeStyleDetector;
use crate::{
directives, flake8_annotations, flake8_bandit, flake8_bugbear, flake8_errmsg,
flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_tidy_imports,
flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pyupgrade,
};
const VERSION: &str = env!("CARGO_PKG_VERSION");
#[wasm_bindgen(typescript_custom_section)]
const TYPES: &'static str = r#"
export interface Check {
code: string;
message: string;
location: {
row: number;
column: number;
};
end_location: {
row: number;
column: number;
};
fix: {
content: string;
message: string | null;
location: {
row: number;
column: number;
};
end_location: {
row: number;
column: number;
};
} | null;
};
"#;
#[derive(Serialize)]
struct ExpandedMessage {
code: CheckCode,
message: String,
location: Location,
end_location: Location,
fix: Option<ExpandedFix>,
}
#[derive(Serialize)]
struct ExpandedFix {
content: String,
message: Option<String>,
location: Location,
end_location: Location,
}
#[wasm_bindgen(start)]
pub fn run() {
use log::Level;
console_error_panic_hook::set_once();
console_log::init_with_level(Level::Debug).expect("Initializing logger went wrong.");
}
#[wasm_bindgen]
#[allow(non_snake_case)]
pub fn currentVersion() -> JsValue {
JsValue::from(VERSION)
}
#[wasm_bindgen]
#[allow(non_snake_case)]
pub fn defaultSettings() -> Result<JsValue, JsValue> {
Ok(serde_wasm_bindgen::to_value(&Options {
// Propagate defaults.
allowed_confusables: Some(Vec::default()),
dummy_variable_rgx: Some("^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$".to_string()),
extend_ignore: Some(Vec::default()),
extend_select: Some(Vec::default()),
external: Some(Vec::default()),
ignore: Some(Vec::default()),
line_length: Some(88),
select: Some(vec![CheckCodePrefix::E, CheckCodePrefix::F]),
target_version: Some(PythonVersion::default()),
// Ignore a bunch of options that don't make sense in a single-file editor.
cache_dir: None,
exclude: None,
extend: None,
extend_exclude: None,
fix: None,
fix_only: None,
fixable: None,
force_exclude: None,
format: None,
ignore_init_module_imports: None,
per_file_ignores: None,
required_version: None,
respect_gitignore: None,
show_source: None,
src: None,
unfixable: None,
task_tags: None,
update_check: None,
// Use default options for all plugins.
flake8_annotations: Some(flake8_annotations::settings::Settings::default().into()),
flake8_bandit: Some(flake8_bandit::settings::Settings::default().into()),
flake8_bugbear: Some(flake8_bugbear::settings::Settings::default().into()),
flake8_errmsg: Some(flake8_errmsg::settings::Settings::default().into()),
flake8_pytest_style: Some(flake8_pytest_style::settings::Settings::default().into()),
flake8_quotes: Some(flake8_quotes::settings::Settings::default().into()),
flake8_tidy_imports: Some(flake8_tidy_imports::settings::Settings::default().into()),
flake8_import_conventions: Some(
flake8_import_conventions::settings::Settings::default().into(),
),
flake8_unused_arguments: Some(
flake8_unused_arguments::settings::Settings::default().into(),
),
isort: Some(isort::settings::Settings::default().into()),
mccabe: Some(mccabe::settings::Settings::default().into()),
pep8_naming: Some(pep8_naming::settings::Settings::default().into()),
pycodestyle: Some(pycodestyle::settings::Settings::default().into()),
pydocstyle: Some(pydocstyle::settings::Settings::default().into()),
pyupgrade: Some(pyupgrade::settings::Settings::default().into()),
})?)
}
#[wasm_bindgen]
#[allow(non_snake_case)]
pub fn check(contents: &str, options: JsValue) -> Result<JsValue, JsValue> {
let options: Options = serde_wasm_bindgen::from_value(options).map_err(|e| e.to_string())?;
let configuration =
Configuration::from_options(options, Path::new(".")).map_err(|e| e.to_string())?;
let settings =
Settings::from_configuration(configuration, Path::new(".")).map_err(|e| e.to_string())?;
// Tokenize once.
let tokens: Vec<LexResult> = tokenize(contents);
// Map row and column locations to byte slices (lazily).
let locator = SourceCodeLocator::new(contents);
// Detect the current code style (lazily).
let stylist = SourceCodeStyleDetector::from_contents(contents, &locator);
// Extract the `# noqa` and `# isort: skip` directives from the source.
let directives = directives::extract_directives(&tokens, &locator, directives::Flags::empty());
// Generate checks.
let checks = check_path(
Path::new("<filename>"),
None,
contents,
tokens,
&locator,
&stylist,
&directives,
&settings,
flags::Autofix::Enabled,
flags::Noqa::Enabled,
)
.map_err(|e| e.to_string())?;
let messages: Vec<ExpandedMessage> = checks
.into_iter()
.map(|check| ExpandedMessage {
code: check.kind.code().clone(),
message: check.kind.body(),
location: check.location,
end_location: check.end_location,
fix: check.fix.map(|fix| ExpandedFix {
content: fix.content,
message: check.kind.commit(),
location: fix.location,
end_location: fix.end_location,
}),
})
.collect();
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<Message> = 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: CheckCode::F634,
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, []);
}
}