diff --git a/.cargo/config.toml b/.cargo/config.toml index f32f96df5..cf02899d4 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -2,6 +2,7 @@ lint = "clippy --workspace --all-targets --all-features" coverage = "run -p oxc_coverage --release --" benchmark = "run -p oxc_benchmark --release --" +rule = "run -p rule_generator" [build] rustflags = ["-C", "target-cpu=native"] diff --git a/Cargo.lock b/Cargo.lock index 318fc8fd2..2e25d73d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,6 +63,15 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "block-buffer" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +dependencies = [ + "generic-array", +] + [[package]] name = "bstr" version = "1.3.0" @@ -222,6 +231,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cpufeatures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.3.2" @@ -308,6 +326,26 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "either" version = "1.8.1" @@ -384,6 +422,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "generic-array" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.8" @@ -420,6 +468,20 @@ version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" +[[package]] +name = "handlebars" +version = "4.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "035ef95d03713f2c347a72547b7cd38cbc9af7cd51e6099fb62d586d4a6dee3a" +dependencies = [ + "log", + "pest", + "pest_derive", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -939,6 +1001,50 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +[[package]] +name = "pest" +version = "2.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cbd939b234e95d72bc393d51788aec68aeeb5d51e748ca08ff3aad58cb722f7" +dependencies = [ + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a81186863f3d0a27340815be8f2078dd8050b14cd71913db9fbda795e5f707d7" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a1ef20bf3193c15ac345acb32e26b3dc3223aff4d77ae4fc5359567683796b" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e3b284b1f13a20dc5ebc90aff59a51b8d7137c221131b52a7260c08cbc1cc80" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + [[package]] name = "pico-args" version = "0.5.0" @@ -1017,6 +1123,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "rule_generator" +version = "0.1.0" +dependencies = [ + "convert_case", + "handlebars", + "oxc_allocator", + "oxc_ast", + "oxc_parser", + "oxc_semantic", + "serde", + "ureq", +] + [[package]] name = "rustc-hash" version = "1.1.0" @@ -1142,6 +1262,17 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "sha2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "similar" version = "2.2.1" @@ -1302,6 +1433,18 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "ucd-trie" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" + [[package]] name = "unicode-bidi" version = "0.3.10" diff --git a/tasks/rule_generator/Cargo.toml b/tasks/rule_generator/Cargo.toml new file mode 100644 index 000000000..7a9f9e70a --- /dev/null +++ b/tasks/rule_generator/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "rule_generator" +version = "0.1.0" +edition = "2021" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +convert_case = "0.6.0" +handlebars = "4.3.6" +oxc_allocator = { path = "../../crates/oxc_allocator" } +oxc_ast = { path = "../../crates/oxc_ast" } +oxc_parser = { path = "../../crates/oxc_parser" } +oxc_semantic = { path = "../../crates/oxc_semantic" } +serde = { workspace = true, features = ["derive"] } +ureq = "2.6.2" diff --git a/tasks/rule_generator/src/main.rs b/tasks/rule_generator/src/main.rs new file mode 100644 index 000000000..dd9c9dced --- /dev/null +++ b/tasks/rule_generator/src/main.rs @@ -0,0 +1,178 @@ +use std::{borrow::Cow, fs::File, io::Write, path::Path, process::Command, rc::Rc}; + +use convert_case::{Case, Casing}; +use oxc_allocator::Allocator; +use oxc_ast::{ + ast::{Argument, Expression, ObjectProperty, PropertyKey, PropertyValue}, + AstKind, SourceType, +}; +use oxc_parser::Parser; +use oxc_semantic::SemanticBuilder; +use serde::Serialize; + +const RULE_TEMPLATE: &str = include_str!("../template.txt"); +const ESLINT_TEST_PATH: &str = + "https://raw.githubusercontent.com/eslint/eslint/main/tests/lib/rules"; + +#[derive(Debug)] +enum TestCase<'a> { + Invalid(Cow<'a, str>), + Valid(Cow<'a, str>), +} + +impl<'a> TestCase<'a> { + fn to_code(test_case: &TestCase) -> String { + match test_case { + TestCase::Valid(code) | TestCase::Invalid(code) => format!(r#"("{code}", None)"#), + } + } +} + +#[derive(Serialize)] +struct Context<'a> { + rule: &'a str, + pass_cases: &'a str, + fail_cases: &'a str, +} + +fn parse_test_code<'a>(expr: &'a Expression) -> Option> { + match expr { + Expression::StringLiteral(lit) => Some(Cow::Borrowed(lit.value.as_str())), + Expression::TemplateLiteral(lit) => Some(Cow::Borrowed(lit.quasi().unwrap().as_str())), + Expression::ObjectExpression(obj_expr) => { + for obj_prop in &obj_expr.properties { + match obj_prop { + ObjectProperty::Property(prop) => match &prop.key { + PropertyKey::Identifier(ident) if ident.name == "code" => match &prop.value + { + PropertyValue::Expression(expr) => return parse_test_code(expr), + PropertyValue::Pattern(_) => continue, + }, + _ => continue, + }, + ObjectProperty::SpreadProperty(_) => continue, + } + } + None + } + Expression::CallExpression(call_expr) => match &call_expr.callee { + Expression::MemberExpression(member_expr) => match &member_expr.object() { + // ['class A {', '}'].join('\n') + Expression::ArrayExpression(array_expr) => { + let mut code = String::new(); + for arg in &array_expr.elements { + let Some(Argument::Expression(Expression::StringLiteral(lit))) = arg else { continue }; + code.push_str(lit.value.as_str()); + code.push('\n'); + } + Some(Cow::Owned(code)) + } + _ => None, + }, + _ => None, + }, + _ => None, + } +} + +fn main() { + let mut args = std::env::args(); + let _ = args.next(); + let rule_name = args.next().expect("expected rule name"); + let rule_test_path = format!("{ESLINT_TEST_PATH}/{rule_name}.js"); + + let body = ureq::get(&rule_test_path) + .call() + .expect("failed to fetch source") + .into_string() + .expect("failed to read response as string"); + let allocator = Allocator::default(); + let source_type = SourceType::from_path(rule_test_path).expect("incorrect {path:?}"); + let ret = Parser::new(&allocator, &body, source_type).parse(); + + let program = allocator.alloc(ret.program); + let trivias = Rc::new(ret.trivias); + let semantic = SemanticBuilder::new(source_type).build(program, trivias); + + let tests_object = semantic.nodes().iter().find_map(|node| match node.get().kind() { + AstKind::ExpressionStatement(stmt) => match &stmt.expression { + Expression::CallExpression(call_expr) => { + for arg in &call_expr.arguments { + match arg { + Argument::Expression(Expression::ObjectExpression(obj_expr)) => { + let mut tests = (None, None); + for obj_prop in &obj_expr.properties { + let ObjectProperty::Property(prop) = obj_prop else { return None }; + let PropertyKey::Identifier(ident) = &prop.key else { return None }; + match ident.name.as_str() { + "valid" => match &prop.value { + PropertyValue::Expression(Expression::ArrayExpression( + array_expr, + )) => tests.0 = Some(array_expr), + _ => continue, + }, + "invalid" => match &prop.value { + PropertyValue::Expression(Expression::ArrayExpression( + array_expr, + )) => tests.1 = Some(array_expr), + _ => continue, + }, + _ => continue, + } + } + if tests.0.is_some() && tests.1.is_some() { + return Some(tests); + } + } + _ => continue, + } + } + + None + } + _ => None, + }, + _ => None, + }); + + let mut pass_cases = vec![]; + let mut fail_cases = vec![]; + if let Some((Some(valid), Some(invalid))) = tests_object { + for arg in (&valid.elements).into_iter().flatten() { + if let Argument::Expression(expr) = arg { + let Some(code) = parse_test_code(expr) else { continue }; + pass_cases.push(TestCase::Valid(code)); + } + } + + for arg in (&invalid.elements).into_iter().flatten() { + if let Argument::Expression(expr) = arg { + let Some(code) = parse_test_code(expr) else { continue }; + fail_cases.push(TestCase::Invalid(code)); + } + } + } + + let mut eng = handlebars::Handlebars::new(); + eng.register_escape_fn(handlebars::no_escape); + + let rule = rule_name.to_case(Case::UpperCamel); + let context = Context { + rule: &rule, + pass_cases: &pass_cases.iter().map(TestCase::to_code).collect::>().join(",\n"), + fail_cases: &fail_cases.iter().map(TestCase::to_code).collect::>().join(",\n"), + }; + let rendered = eng.render_template(RULE_TEMPLATE, &handlebars::to_json(context)).unwrap(); + + let out_path = Path::new("crates/oxc_linter/src/rules") + .join(format!("{}.rs", rule_name.to_case(Case::Snake))); + let mut out_file = File::create(out_path.clone()).expect("failed to create output file"); + out_file.write_all(rendered.as_bytes()).expect("failed to write output"); + + Command::new("cargo") + .arg("fmt") + .arg("--") + .arg(out_path) + .spawn() + .expect("failed to format output"); +} diff --git a/tasks/rule_generator/template.txt b/tasks/rule_generator/template.txt new file mode 100644 index 000000000..580aeb6ac --- /dev/null +++ b/tasks/rule_generator/template.txt @@ -0,0 +1,51 @@ +use oxc_ast::Span; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::Error, +}; +use oxc_macros::declare_oxc_lint; + +use crate::{context::LintContext, rule::Rule, AstNode}; + +#[derive(Debug, Error, Diagnostic)] +#[error("")] +#[diagnostic(severity(warning), help(""))] +struct {{rule}}Diagnostic(#[label] pub Span); + +#[derive(Debug, Default, Clone)] +pub struct {{rule}}; + +declare_oxc_lint!( + /// ### What it does + /// + /// + /// ### Why is this bad? + /// + /// + /// ### Example + /// ```javascript + /// ``` + {{rule}}, + correctness +); + +impl Rule for {{rule}} { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + {{pass_cases}} + ]; + + let fail = vec![ + {{fail_cases}} + ]; + + Tester::new({{rule}}::NAME, pass, fail).test_and_snapshot(); +}