feat(rule_generator): automatically generate rules and tests (#142)

This commit is contained in:
Shannon Rothe 2023-03-07 21:21:58 +11:00 committed by GitHub
parent 6b9bbaa091
commit 0f7edfb003
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 390 additions and 0 deletions

View file

@ -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
View file

@ -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"

View 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"

View 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");
}

View 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();
}