feat(rule_generator): generate test options (#150)

This commit is contained in:
yangchenye 2023-03-07 21:02:01 -06:00 committed by GitHub
parent 2687d7868f
commit b76ffb4826
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 74 additions and 17 deletions

2
Cargo.lock generated
View file

@ -1129,10 +1129,12 @@ version = "0.1.0"
dependencies = [
"convert_case",
"handlebars",
"lazy_static",
"oxc_allocator",
"oxc_ast",
"oxc_parser",
"oxc_semantic",
"regex",
"serde",
"ureq",
]

@ -1 +1 @@
Subproject commit a547f8724a5c6b4395b8a8f597e3edd44de74bf3
Subproject commit c38bf12f010520ea7abe8a286f62922b2d1e1f1b

@ -1 +1 @@
Subproject commit c4d8f01d3d4a9c1694fe5737346975c00a23521d
Subproject commit d216cc197269fc41eb6eca14710529c3d6650535

@ -1 +1 @@
Subproject commit 6dbec02a88156121c24a152d1e8e5e82d52fdc1d
Subproject commit 8f40d5633fc36df04b4fd4392e3877558149987f

View file

@ -14,4 +14,6 @@ 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"
ureq = "2.6.2"

View file

@ -1,10 +1,11 @@
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, SourceType,
AstKind, GetSpan, SourceType,
};
use oxc_parser::Parser;
use oxc_semantic::SemanticBuilder;
@ -23,7 +24,7 @@ enum TestCase<'a> {
impl<'a> TestCase<'a> {
fn to_code(test_case: &TestCase) -> String {
match test_case {
TestCase::Valid(code) | TestCase::Invalid(code) => format!(r#"("{code}", None)"#),
TestCase::Valid(code) | TestCase::Invalid(code) => code.clone().into_owned(),
}
}
}
@ -35,25 +36,39 @@ struct Context<'a> {
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())),
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) => return parse_test_code(expr),
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,
}
}
None
(test_code, option_code)
}
Expression::CallExpression(call_expr) => match &call_expr.callee {
Expression::MemberExpression(member_expr) => match &member_expr.object() {
@ -65,14 +80,52 @@ fn parse_test_code<'a>(expr: &'a Expression) -> Option<Cow<'a, str>> {
code.push_str(lit.value.as_str());
code.push('\n');
}
Some(Cow::Owned(code))
(Some(Cow::Owned(code)), None)
}
_ => None,
_ => (None, 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() {
@ -140,14 +193,14 @@ fn main() {
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 };
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(expr) else { continue };
let Some(code) = parse_test_code(&body, expr) else { continue };
fail_cases.push(TestCase::Invalid(code));
}
}