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:
Shannon Rothe 2023-03-09 20:33:30 +11:00 committed by GitHub
parent 185acc49bd
commit da8355f418
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 334 additions and 236 deletions

View file

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

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

View file

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

View file

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

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