mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
feat(rule_generator): automatically generate rules and tests (#142)
This commit is contained in:
parent
6b9bbaa091
commit
0f7edfb003
5 changed files with 390 additions and 0 deletions
|
|
@ -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"]
|
||||
|
|
|
|||
143
Cargo.lock
generated
143
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
17
tasks/rule_generator/Cargo.toml
Normal file
17
tasks/rule_generator/Cargo.toml
Normal file
|
|
@ -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"
|
||||
178
tasks/rule_generator/src/main.rs
Normal file
178
tasks/rule_generator/src/main.rs
Normal file
|
|
@ -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<Cow<'a, str>> {
|
||||
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::<Vec<_>>().join(",\n"),
|
||||
fail_cases: &fail_cases.iter().map(TestCase::to_code).collect::<Vec<_>>().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");
|
||||
}
|
||||
51
tasks/rule_generator/template.txt
Normal file
51
tasks/rule_generator/template.txt
Normal file
|
|
@ -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();
|
||||
}
|
||||
Loading…
Reference in a new issue