mirror of
https://github.com/danbulant/oxc
synced 2026-05-25 12:51:57 +00:00
feat(rulegen): refactor to visitor pattern (#162)
* feat(rulegen): refactor to visitor pattern * convert `parse_test_code` to visitor pattern
This commit is contained in:
parent
185acc49bd
commit
da8355f418
8 changed files with 334 additions and 236 deletions
|
|
@ -2,7 +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"
|
||||
rule = "run -p rulegen"
|
||||
|
||||
[build]
|
||||
rustflags = ["-C", "target-cpu=native"]
|
||||
|
|
|
|||
3
Cargo.lock
generated
3
Cargo.lock
generated
|
|
@ -1185,7 +1185,7 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "rule_generator"
|
||||
name = "rulegen"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
|
|
@ -1194,7 +1194,6 @@ dependencies = [
|
|||
"oxc_allocator",
|
||||
"oxc_ast",
|
||||
"oxc_parser",
|
||||
"oxc_semantic",
|
||||
"regex",
|
||||
"serde",
|
||||
"ureq",
|
||||
|
|
|
|||
|
|
@ -1,231 +0,0 @@
|
|||
use std::{borrow::Cow, fs::File, io::Write, path::Path, process::Command, rc::Rc};
|
||||
|
||||
use convert_case::{Case, Casing};
|
||||
use lazy_static::lazy_static;
|
||||
use oxc_allocator::Allocator;
|
||||
use oxc_ast::{
|
||||
ast::{Argument, Expression, ObjectProperty, PropertyKey, PropertyValue},
|
||||
AstKind, GetSpan, 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) => code.clone().into_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Context<'a> {
|
||||
rule: &'a str,
|
||||
pass_cases: &'a str,
|
||||
fail_cases: &'a str,
|
||||
}
|
||||
|
||||
fn parse_test_code<'a>(source_text: &'a str, expr: &'a Expression) -> Option<Cow<'a, str>> {
|
||||
let (test_code, option_code) = match expr {
|
||||
Expression::StringLiteral(lit) => (Some(Cow::Borrowed(lit.value.as_str())), None),
|
||||
Expression::TemplateLiteral(lit) => {
|
||||
(Some(Cow::Borrowed(lit.quasi().unwrap().as_str())), None)
|
||||
}
|
||||
Expression::ObjectExpression(obj_expr) => {
|
||||
let mut test_code = None;
|
||||
let mut option_code: Option<Cow<'_, str>> = None;
|
||||
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) => {
|
||||
let Expression::StringLiteral(s) = expr else {
|
||||
return None;
|
||||
};
|
||||
test_code = Some(Cow::Borrowed(s.value.as_str()));
|
||||
}
|
||||
PropertyValue::Pattern(_) => continue,
|
||||
},
|
||||
PropertyKey::Identifier(ident) if ident.name == "options" => {
|
||||
let span = prop.value.span();
|
||||
let option_text = &source_text[span.start as usize..span.end as usize];
|
||||
option_code = Some(Cow::Owned(wrap_property_in_quotes(option_text)));
|
||||
}
|
||||
_ => continue,
|
||||
},
|
||||
ObjectProperty::SpreadProperty(_) => continue,
|
||||
}
|
||||
}
|
||||
(test_code, option_code)
|
||||
}
|
||||
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),
|
||||
},
|
||||
_ => (None, None),
|
||||
},
|
||||
_ => (None, None),
|
||||
};
|
||||
|
||||
test_code.map(|test_code| {
|
||||
let option_code = option_code.map_or(Cow::Borrowed("None"), |option_code| {
|
||||
Cow::Owned(format!("Some(serde_json::json!({option_code}))"))
|
||||
});
|
||||
Cow::Owned(format!(r#"({test_code:?}, {option_code})"#))
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert a javascript object literal to JSON by wrapping the property keys in double quote
|
||||
fn wrap_property_in_quotes(object: &str) -> String {
|
||||
use regex::{Captures, Regex};
|
||||
|
||||
lazy_static! {
|
||||
static ref IDENT_MATCHER: Regex = Regex::new(r"(?P<ident>[[:alpha:]]\w*)").unwrap();
|
||||
static ref DUP_QUOTE_MATCHER: Regex =
|
||||
Regex::new(r#"(?P<outer>"(?P<inner>"\w+")")"#).unwrap();
|
||||
}
|
||||
|
||||
let add_quote = IDENT_MATCHER
|
||||
.replace_all(object, |capture: &Captures| {
|
||||
// don't replace true and false, which are json boolean values
|
||||
let ident = &capture["ident"];
|
||||
if ident == "true" || ident == "false" {
|
||||
Cow::Owned(ident.to_string())
|
||||
} else {
|
||||
Cow::Owned(format!(r#""{ident}""#))
|
||||
}
|
||||
})
|
||||
.into_owned();
|
||||
|
||||
// After the above step, valid json strings will have duplicate quotes now
|
||||
// This step removes duplicate quotes.
|
||||
let remove_dup_quote = DUP_QUOTE_MATCHER
|
||||
.replace_all(&add_quote, |capture: &Captures| Cow::Owned(capture["inner"].to_string()))
|
||||
.into_owned();
|
||||
|
||||
remove_dup_quote
|
||||
}
|
||||
|
||||
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(&body, 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(&body, 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");
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
[package]
|
||||
name = "rule_generator"
|
||||
name = "rulegen"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
|
@ -12,7 +12,6 @@ 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"] }
|
||||
regex = "1.7.1"
|
||||
lazy_static = "1.4.0"
|
||||
34
tasks/rulegen/src/json.rs
Normal file
34
tasks/rulegen/src/json.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
/// Convert a javascript object literal to JSON by wrapping the property keys in double quote
|
||||
pub fn wrap_property_in_quotes(object: &str) -> String {
|
||||
use regex::{Captures, Regex};
|
||||
|
||||
lazy_static! {
|
||||
static ref IDENT_MATCHER: Regex = Regex::new(r"(?P<ident>[[:alpha:]]\w*)").unwrap();
|
||||
static ref DUP_QUOTE_MATCHER: Regex =
|
||||
Regex::new(r#"(?P<outer>"(?P<inner>"\w+")")"#).unwrap();
|
||||
}
|
||||
|
||||
let add_quote = IDENT_MATCHER
|
||||
.replace_all(object, |capture: &Captures| {
|
||||
// don't replace true and false, which are json boolean values
|
||||
let ident = &capture["ident"];
|
||||
if ident == "true" || ident == "false" {
|
||||
Cow::Owned(ident.to_string())
|
||||
} else {
|
||||
Cow::Owned(format!(r#""{ident}""#))
|
||||
}
|
||||
})
|
||||
.into_owned();
|
||||
|
||||
// After the above step, valid json strings will have duplicate quotes now
|
||||
// This step removes duplicate quotes.
|
||||
let remove_dup_quote = DUP_QUOTE_MATCHER
|
||||
.replace_all(&add_quote, |capture: &Captures| Cow::Owned(capture["inner"].to_string()))
|
||||
.into_owned();
|
||||
|
||||
remove_dup_quote
|
||||
}
|
||||
253
tasks/rulegen/src/main.rs
Normal file
253
tasks/rulegen/src/main.rs
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use convert_case::{Case, Casing};
|
||||
use oxc_allocator::Allocator;
|
||||
use oxc_ast::ast::{
|
||||
ArrayExpression, CallExpression, ExpressionStatement, ObjectExpression, Program, Property,
|
||||
Statement, StringLiteral, TemplateLiteral,
|
||||
};
|
||||
use oxc_ast::visit::Visit;
|
||||
use oxc_ast::{
|
||||
ast::{Argument, Expression, ObjectProperty, PropertyKey, PropertyValue},
|
||||
GetSpan, SourceType,
|
||||
};
|
||||
use oxc_parser::Parser;
|
||||
use serde::Serialize;
|
||||
|
||||
mod json;
|
||||
mod template;
|
||||
|
||||
const ESLINT_TEST_PATH: &str =
|
||||
"https://raw.githubusercontent.com/eslint/eslint/main/tests/lib/rules";
|
||||
|
||||
struct TestCase<'a> {
|
||||
source_text: &'a str,
|
||||
code: Option<Cow<'a, str>>,
|
||||
test_code: Option<Cow<'a, str>>,
|
||||
}
|
||||
|
||||
impl<'a> TestCase<'a> {
|
||||
fn new(source_text: &'a str, arg: &'a Argument<'a>) -> Option<Self> {
|
||||
let mut test_case = TestCase { source_text, code: None, test_code: None };
|
||||
if let Argument::Expression(expr) = arg {
|
||||
test_case.visit_expression(expr);
|
||||
return Some(test_case);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn code(&self) -> Option<Cow<'a, str>> {
|
||||
self.code.as_ref().map(|test_code| {
|
||||
let option_code =
|
||||
self.test_code.as_ref().map_or(Cow::Borrowed("None"), |option_code| {
|
||||
Cow::Owned(format!("Some(serde_json::json!({option_code}))"))
|
||||
});
|
||||
Cow::Owned(format!(r#"({test_code:?}, {option_code})"#))
|
||||
})
|
||||
}
|
||||
|
||||
fn to_code(test_case: &TestCase) -> String {
|
||||
test_case.code().map_or_else(String::new, |code| code.clone().into_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Visit<'a> for TestCase<'a> {
|
||||
fn visit_expression(&mut self, expr: &'a Expression<'a>) {
|
||||
match expr {
|
||||
Expression::StringLiteral(lit) => self.visit_string_literal(lit),
|
||||
Expression::TemplateLiteral(lit) => self.visit_template_literal(lit),
|
||||
Expression::ObjectExpression(obj_expr) => self.visit_object_expression(obj_expr),
|
||||
Expression::CallExpression(call_expr) => self.visit_call_expression(call_expr),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_call_expression(&mut self, expr: &'a CallExpression<'a>) {
|
||||
if let Expression::MemberExpression(member_expr) = &expr.callee {
|
||||
if let Expression::ArrayExpression(array_expr) = member_expr.object() {
|
||||
// ['class A {', '}'].join('\n')
|
||||
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');
|
||||
}
|
||||
self.code = Some(Cow::Owned(code));
|
||||
self.test_code = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_object_expression(&mut self, expr: &'a ObjectExpression<'a>) {
|
||||
for obj_prop in &expr.properties {
|
||||
match obj_prop {
|
||||
ObjectProperty::Property(prop) => match &prop.key {
|
||||
PropertyKey::Identifier(ident) if ident.name == "code" => match &prop.value {
|
||||
PropertyValue::Expression(expr) => {
|
||||
let Expression::StringLiteral(s) = expr else {
|
||||
continue;
|
||||
};
|
||||
self.code = Some(Cow::Borrowed(s.value.as_str()));
|
||||
}
|
||||
PropertyValue::Pattern(_) => continue,
|
||||
},
|
||||
PropertyKey::Identifier(ident) if ident.name == "options" => {
|
||||
let span = prop.value.span();
|
||||
let option_text = &self.source_text[span.start as usize..span.end as usize];
|
||||
self.test_code =
|
||||
Some(Cow::Owned(json::wrap_property_in_quotes(option_text)));
|
||||
}
|
||||
_ => continue,
|
||||
},
|
||||
ObjectProperty::SpreadProperty(_) => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_template_literal(&mut self, lit: &'a TemplateLiteral<'a>) {
|
||||
self.code = Some(Cow::Borrowed(lit.quasi().unwrap().as_str()));
|
||||
self.test_code = None;
|
||||
}
|
||||
|
||||
fn visit_string_literal(&mut self, lit: &'a StringLiteral) {
|
||||
self.code = Some(Cow::Borrowed(lit.value.as_str()));
|
||||
self.test_code = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Context<'a> {
|
||||
rule: &'a str,
|
||||
rule_name: &'a str,
|
||||
pass_cases: &'a str,
|
||||
fail_cases: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> Context<'a> {
|
||||
fn new(
|
||||
upper_rule_name: &'a str,
|
||||
rule_name: &'a str,
|
||||
pass_cases: &'a str,
|
||||
fail_cases: &'a str,
|
||||
) -> Self {
|
||||
Context { rule: upper_rule_name, rule_name, pass_cases, fail_cases }
|
||||
}
|
||||
}
|
||||
|
||||
struct State<'a> {
|
||||
source_text: &'a str,
|
||||
valid_tests: Vec<&'a ArrayExpression<'a>>,
|
||||
invalid_tests: Vec<&'a ArrayExpression<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> State<'a> {
|
||||
fn new(source_text: &'a str) -> Self {
|
||||
Self { source_text, valid_tests: vec![], invalid_tests: vec![] }
|
||||
}
|
||||
|
||||
fn pass_cases(&self) -> Vec<TestCase> {
|
||||
self.valid_tests
|
||||
.iter()
|
||||
.flat_map(|array_expr| (&array_expr.elements).into_iter().flatten())
|
||||
.filter_map(|arg| TestCase::new(self.source_text, arg))
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
fn fail_cases(&self) -> Vec<TestCase> {
|
||||
self.invalid_tests
|
||||
.iter()
|
||||
.flat_map(|array_expr| (&array_expr.elements).into_iter().flatten())
|
||||
.filter_map(|arg| TestCase::new(self.source_text, arg))
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Visit<'a> for State<'a> {
|
||||
fn visit_program(&mut self, program: &'a Program<'a>) {
|
||||
for stmt in &program.body {
|
||||
self.visit_statement(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_statement(&mut self, stmt: &'a Statement<'a>) {
|
||||
if let Statement::ExpressionStatement(expr_stmt) = stmt {
|
||||
self.visit_expression_statement(expr_stmt);
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_expression_statement(&mut self, stmt: &'a ExpressionStatement<'a>) {
|
||||
self.visit_expression(&stmt.expression);
|
||||
}
|
||||
|
||||
fn visit_expression(&mut self, expr: &'a Expression<'a>) {
|
||||
if let Expression::CallExpression(call_expr) = expr {
|
||||
for arg in &call_expr.arguments {
|
||||
self.visit_argument(arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_argument(&mut self, arg: &'a Argument<'a>) {
|
||||
if let Argument::Expression(Expression::ObjectExpression(obj_expr)) = arg {
|
||||
for obj_prop in &obj_expr.properties {
|
||||
let ObjectProperty::Property(prop) = obj_prop else { return };
|
||||
self.visit_property(prop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_property(&mut self, prop: &'a Property<'a>) {
|
||||
let PropertyKey::Identifier(ident) = &prop.key else { return };
|
||||
match ident.name.as_str() {
|
||||
"valid" => {
|
||||
if let PropertyValue::Expression(Expression::ArrayExpression(array_expr)) =
|
||||
&prop.value
|
||||
{
|
||||
self.valid_tests.push(array_expr);
|
||||
}
|
||||
}
|
||||
"invalid" => {
|
||||
if let PropertyValue::Expression(Expression::ArrayExpression(array_expr)) =
|
||||
&prop.value
|
||||
{
|
||||
self.invalid_tests.push(array_expr);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut args = std::env::args();
|
||||
let _ = args.next();
|
||||
|
||||
let rule_name = args.next().expect("expected rule name");
|
||||
let upper_rule_name = rule_name.to_case(Case::UpperCamel);
|
||||
|
||||
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 mut state = State::new(&body);
|
||||
state.visit_program(program);
|
||||
|
||||
let pass_cases =
|
||||
state.pass_cases().iter().map(TestCase::to_code).collect::<Vec<_>>().join(",\n");
|
||||
let fail_cases =
|
||||
state.fail_cases().iter().map(TestCase::to_code).collect::<Vec<_>>().join(",\n");
|
||||
|
||||
let context = Context::new(&upper_rule_name, &rule_name, &pass_cases, &fail_cases);
|
||||
let template = template::Template::with_context(&context);
|
||||
if template.render().is_err() {
|
||||
eprintln!("failed to render {} rule template", context.rule);
|
||||
}
|
||||
}
|
||||
44
tasks/rulegen/src/template.rs
Normal file
44
tasks/rulegen/src/template.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
use std::{
|
||||
fs::File,
|
||||
io::{Error, Write},
|
||||
path::{Path, PathBuf},
|
||||
process::{Child, Command},
|
||||
};
|
||||
|
||||
use handlebars::Handlebars;
|
||||
|
||||
use crate::Context;
|
||||
|
||||
const RULE_TEMPLATE: &str = include_str!("../template.txt");
|
||||
|
||||
pub struct Template<'a> {
|
||||
context: &'a Context<'a>,
|
||||
registry: Handlebars<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Template<'a> {
|
||||
pub fn with_context(context: &'a Context) -> Self {
|
||||
let mut registry = handlebars::Handlebars::new();
|
||||
registry.register_escape_fn(handlebars::no_escape);
|
||||
Self { context, registry }
|
||||
}
|
||||
|
||||
pub fn render(&self) -> Result<(), Error> {
|
||||
let rendered = self
|
||||
.registry
|
||||
.render_template(RULE_TEMPLATE, &handlebars::to_json(self.context))
|
||||
.unwrap();
|
||||
|
||||
let out_path =
|
||||
Path::new("crates/oxc_linter/src/rules").join(format!("{}.rs", self.context.rule_name));
|
||||
|
||||
File::create(out_path.clone())?.write_all(rendered.as_bytes())?;
|
||||
format_rule_output(out_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn format_rule_output(path: PathBuf) -> Result<Child, Error> {
|
||||
Command::new("cargo").arg("fmt").arg("--").arg(path).spawn()
|
||||
}
|
||||
Loading…
Reference in a new issue