Skip to content

Commit

Permalink
feat: support full recovery when parsing plugins
Browse files Browse the repository at this point in the history
  • Loading branch information
arlyon committed Dec 26, 2022
1 parent 3ddd529 commit 4f78b54
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 40 deletions.
59 changes: 47 additions & 12 deletions crates/tailwind-parse/src/directive.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
use nom::{
character::complete::space0,
combinator::eof,
multi::{many0, many1},
sequence::terminated,
IResult, Parser,
};
use nom::{character::complete::space0, combinator::eof, multi::many1, IResult, Parser};
use nom_locate::LocatedSpan;
use stailwc_swc_utils::merge_literals;
use swc_core::{common::DUMMY_SP, ecma::ast::ObjectLit};
use swc_core::{
common::{Span, DUMMY_SP},
ecma::ast::ObjectLit,
};
use tailwind_config::TailwindConfig;

use crate::{Expression, ExpressionConversionError, NomSpan};
Expand All @@ -18,10 +16,47 @@ pub struct Directive<'a> {

impl<'a> Directive<'a> {
/// Same as parse, but with an added check for an EOF.
pub fn parse(s: NomSpan<'a>) -> IResult<NomSpan<'a>, Self, nom::error::Error<NomSpan<'a>>> {
terminated(many0(Expression::parse).and(space0), eof)
.map(|(exps, _)| Directive { exps })
.parse(s)
pub fn parse(s: NomSpan<'a>) -> (NomSpan<'a>, Self, Vec<LocatedSpan<&str, Span>>) {
let mut exps = vec![];
let mut errs = vec![];

let (mut s, _) = space0::<LocatedSpan<&str, Span>, ()>.parse(s).unwrap();

let mut exp = Expression::parse.and(space0);
let mut fast_forward =
nom::bytes::complete::take_while::<_, LocatedSpan<&str, Span>, ()>(|c: char| c != ' ')
.and(space0);

// todo: we can probably move the expression parser first
loop {
if let Ok((s_next, _)) = eof::<_, ()>.parse(s) {
s = s_next;
break;
}

let _parse_err = match exp.parse(s) {
Ok((s_next, (exp, _))) => {
s = s_next;
exps.push(exp);
continue;
}
Err(e) => e,
};

let (s_next, (mut ffd, _)) = fast_forward
.parse(s)
.expect("fast-forward will always succeed");

ffd.extra = ffd.extra.from_inner_byte_pos(
ffd.location_offset() + 1,
ffd.location_offset() + ffd.len() + 1,
);

errs.push(ffd);
s = s_next;
}

(s, Directive { exps }, errs)
}

pub(crate) fn parse_inner(
Expand Down
32 changes: 26 additions & 6 deletions crates/tailwind-parse/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,34 @@ mod test {

#[test]
fn directive() -> anyhow::Result<()> {
let (rest, _d) = Directive::parse(LocatedSpan::new_extra(
let (rest, _d, _errs) = Directive::parse(LocatedSpan::new_extra(
"-h-4 md:bg-blue text-white! hover:(text-blue bg-white lg:text-black!)",
DUMMY_SP,
))?;
));

assert!(rest.len() == 0);

Ok(())
}

#[test]
fn recovery() {
let (rest, _d, errs) =
Directive::parse(LocatedSpan::new_extra(" fail text-white", DUMMY_SP));

assert!(rest.len() == 0);
assert_eq!(errs.len(), 1);
}

#[test]
fn recovery2() {
let (rest, _d, errs) =
Directive::parse(LocatedSpan::new_extra("sm:max--3xl smpx-6", DUMMY_SP));

assert!(rest.len() == 0);
assert_eq!(errs.len(), 2);
}

#[test_case("flex!", None, None, true ; "important")]
#[test_case("underline!", None, None, true ; "important with transparent command")]
#[test_case("min-w-4!", Some(SubjectValue::Value(Value("4"))), None, true ; "important with rootless command")]
Expand Down Expand Up @@ -106,7 +124,9 @@ mod test {
#[test_case("relative rounded-2xl px-6 py-10 bg-primary-500 overflow-hidden shadow-xl sm:px-12 sm:py-20"; "example")]
#[test_case("text-white/40 bg-white/50" ; "chained transparency")]
fn directive_tests(s: &str) {
Directive::parse(LocatedSpan::new_extra(s, DUMMY_SP)).unwrap();
let (s, _d, e) = Directive::parse(LocatedSpan::new_extra(s, DUMMY_SP));
assert_matches!(*s, "");
assert_eq!(e.len(), 0);
}

#[test_case(&["bg-white", "text-black"] ; "basic case")]
Expand Down Expand Up @@ -135,7 +155,7 @@ mod test {
let lits = inputs
.iter()
.map(|s| {
let (_, d) = Directive::parse(LocatedSpan::new_extra(s, DUMMY_SP)).unwrap();
let (_, d, _e) = Directive::parse(LocatedSpan::new_extra(s, DUMMY_SP));
let (lit, _) = d.to_literal(&config);
(s, sort_recursive(lit))
})
Expand All @@ -149,12 +169,12 @@ mod test {
}
}

#[should_panic]
#[test_case("-mod:sub" ; "when the minus is in the wrong place")]
#[test_case("()" ; "rejects empty group")]
fn parse_failure_tests(s: &str) {
let (rest, _d) = Directive::parse(LocatedSpan::new_extra(s, DUMMY_SP)).unwrap();
let (rest, _d, errs) = Directive::parse(LocatedSpan::new_extra(s, DUMMY_SP));
assert_matches!(*rest, "");
assert_eq!(errs.len(), 1);
}

#[test_case("40" ; "a number")]
Expand Down
35 changes: 13 additions & 22 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,17 +146,14 @@ impl<'a> VisitMut for TransformVisitor<'a> {
expr: JSXExpr::Expr(box Expr::Lit(Lit::Str(Str{span, value, ..}))),
..
})) => {
let d = match Directive::parse(LocatedSpan::new_extra(value, *span)) {
Ok((_, d)) => d,
Err(e) => {
HANDLER.with(|h| {
self.report(h, *span, &e.to_string(), None)
.note("unknown plugin")
.emit()
});
return;
},
};
let (_s, d, errs) = Directive::parse(LocatedSpan::new_extra(value, *span));

for err in errs {
HANDLER.with(|h| {
self.report(h, err.extra, "unknown plugin", None)
.emit()
});
}

let (x, errs) = d.to_literal(&self.config);

Expand Down Expand Up @@ -311,17 +308,11 @@ impl<'a> VisitMut for TransformVisitor<'a> {
}
};

let d = match Directive::parse(LocatedSpan::new_extra(text, *span)) {
Ok((_, d)) => d,
Err(e) => {
HANDLER.with(|h| {
self.report(h, *span, "invalid syntax", None)
.note(&e.to_string())
.emit()
});
return ObjectLit::dummy();
}
};
let (_s, d, errs) = Directive::parse(LocatedSpan::new_extra(text, *span));

for err in errs {
HANDLER.with(|h| self.report(h, err.extra, "unknown plugin", None).emit());
}

let (lit, errs) = d.to_literal(&self.config);

Expand Down

0 comments on commit 4f78b54

Please sign in to comment.