Skip to content

Commit d0fe994

Browse files
committed
Improved parse error handling
Many parse errors now match the dart sass error message. Also allow "loud" comments in more places. Use nom "verbose" error handling to build proper parse errors. Fixes #141.
1 parent b66d263 commit d0fe994

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+316
-328
lines changed

Diff for: CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ project adheres to
2424
- Made all color channels f64 instead of Rational (PR #199).
2525
* Fixed a bug where `clamp(..)` was sometimes evaluated to a value
2626
even though units wasn't comparable.
27+
* Improved parse error handling (PR #201, Issue #141).
28+
Many parse errors now match the dart sass error message.
29+
Also allow "loud" comments in more places.
2730
* Updated sass-spec test suite to 2024-09-20.
2831

2932

Diff for: rsass/src/parser/error.rs

+31-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use super::{PResult, Span};
22
use crate::input::SourcePos;
3-
use nom::Finish;
3+
use nom::{character::complete::one_of, error::VerboseErrorKind, Finish};
44
use std::fmt;
55

66
/// An error encountered when parsing sass.
@@ -44,12 +44,36 @@ impl ParseError {
4444
}
4545
}
4646

47-
impl From<nom::error::Error<Span<'_>>> for ParseError {
48-
fn from(err: nom::error::Error<Span>) -> Self {
49-
Self::new(
50-
format!("Parse error: {:?}", err.code),
51-
err.input.up_to(&err.input).to_owned(),
52-
)
47+
impl From<nom::error::VerboseError<Span<'_>>> for ParseError {
48+
fn from(value: nom::error::VerboseError<Span<'_>>) -> Self {
49+
let (msg, pos) = value
50+
.errors
51+
.iter()
52+
.filter_map(|(pos, kind)| {
53+
match kind {
54+
VerboseErrorKind::Context(ctx) => {
55+
Some((ctx.to_string(), pos))
56+
}
57+
VerboseErrorKind::Char(ch) => {
58+
Some((format!("expected {:?}.", ch.to_string()), pos))
59+
}
60+
VerboseErrorKind::Nom(_) => None, // Try the next one!
61+
}
62+
})
63+
.next()
64+
.or_else(|| {
65+
value.errors.first().map(|(pos, kind)| {
66+
if pos.is_at_end() {
67+
("expected more input.".to_string(), pos)
68+
} else if let PResult::Ok((_, b)) = one_of(")}]")(*pos) {
69+
(format!("unmatched \"{b}\"."), pos)
70+
} else {
71+
(format!("Parse error: {kind:?}"), pos)
72+
}
73+
})
74+
})
75+
.unwrap();
76+
Self::new(msg, pos.up_to(pos).to_owned())
5377
}
5478
}
5579

Diff for: rsass/src/parser/formalargs.rs

+18-13
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,22 @@ use super::value::space_list;
44
use super::{PResult, Span};
55
use crate::sass::{CallArgs, FormalArgs, Name};
66
use nom::bytes::complete::tag;
7-
use nom::combinator::{map, map_res, opt};
7+
use nom::character::complete::char;
8+
use nom::combinator::{cut, map, map_res, opt};
9+
use nom::error::context;
810
use nom::multi::separated_list0;
911
use nom::sequence::{delimited, pair, preceded, terminated};
1012

1113
pub fn formal_args(input: Span) -> PResult<FormalArgs> {
12-
let (input, _) = terminated(tag("("), opt_spacelike)(input)?;
14+
let (input, _) = terminated(char('('), opt_spacelike)(input)?;
1315
let (input, v) = separated_list0(
1416
preceded(tag(","), opt_spacelike),
1517
map(
1618
pair(
1719
delimited(tag("$"), name, opt_spacelike),
1820
opt(delimited(
1921
terminated(tag(":"), opt_spacelike),
20-
space_list,
22+
cut(context("Expected expression.", space_list)),
2123
opt_spacelike,
2224
)),
2325
),
@@ -26,7 +28,7 @@ pub fn formal_args(input: Span) -> PResult<FormalArgs> {
2628
)(input)?;
2729
let (input, _) = terminated(opt(tag(",")), opt_spacelike)(input)?;
2830
let (input, va) = terminated(opt(tag("...")), opt_spacelike)(input)?;
29-
let (input, _) = tag(")")(input)?;
31+
let (input, _) = char(')')(input)?;
3032
Ok((
3133
input,
3234
if va.is_none() {
@@ -39,28 +41,31 @@ pub fn formal_args(input: Span) -> PResult<FormalArgs> {
3941

4042
pub fn call_args(input: Span) -> PResult<CallArgs> {
4143
delimited(
42-
terminated(tag("("), opt_spacelike),
44+
terminated(char('('), opt_spacelike),
4345
map_res(
4446
pair(
4547
separated_list0(
4648
terminated(tag(","), opt_spacelike),
4749
pair(
48-
opt(delimited(
49-
tag("$"),
50-
map(name, Name::from),
50+
opt(map(
5151
delimited(
52-
ignore_comments,
53-
tag(":"),
54-
opt_spacelike,
52+
tag("$"),
53+
name,
54+
delimited(
55+
ignore_comments,
56+
char(':'),
57+
opt_spacelike,
58+
),
5559
),
60+
Name::from,
5661
)),
5762
terminated(space_list, opt_spacelike),
5863
),
5964
),
60-
opt(terminated(tag(","), opt_spacelike)),
65+
opt(terminated(char(','), opt_spacelike)),
6166
),
6267
|(args, trail)| CallArgs::new(args, trail.is_some()),
6368
),
64-
tag(")"),
69+
cut(char(')')),
6570
)(input)
6671
}

Diff for: rsass/src/parser/imports.rs

+38-27
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ use super::strings::{
33
name, sass_string, sass_string_dq, sass_string_sq, special_url,
44
};
55
use super::util::{ignore_comments, opt_spacelike, semi_or_end};
6-
use super::value::space_list;
6+
use super::value::{identifier, space_list};
77
use super::{media, position, PResult, Span};
88
use crate::sass::{Expose, Item, Name, SassString, UseAs, Value};
99
use nom::branch::alt;
1010
use nom::bytes::complete::tag;
11-
use nom::combinator::{map, opt, value};
11+
use nom::character::complete::char;
12+
use nom::combinator::{cut, map, opt, value};
13+
use nom::error::context;
1214
use nom::multi::{separated_list0, separated_list1};
1315
use nom::sequence::{delimited, pair, preceded, terminated, tuple};
1416
use std::collections::BTreeSet;
@@ -43,12 +45,15 @@ pub fn use2<'a>(start: Span, input: Span<'a>) -> PResult<'a, Item> {
4345
map(
4446
terminated(
4547
tuple((
46-
terminated(quoted_sass_string, opt_spacelike),
48+
context(
49+
"Expected string.",
50+
terminated(quoted_sass_string, ignore_comments),
51+
),
4752
opt(preceded(
48-
terminated(tag("with"), opt_spacelike),
53+
terminated(tag("with"), ignore_comments),
4954
with_arg,
5055
)),
51-
opt(preceded(terminated(tag("as"), opt_spacelike), as_arg)),
56+
opt(preceded(terminated(tag("as"), ignore_comments), as_arg)),
5257
position,
5358
)),
5459
semi_or_end,
@@ -65,24 +70,28 @@ pub fn use2<'a>(start: Span, input: Span<'a>) -> PResult<'a, Item> {
6570
}
6671

6772
pub fn forward2<'a>(start: Span, input: Span<'a>) -> PResult<'a, Item> {
68-
let (mut end, path) =
69-
terminated(quoted_sass_string, opt_spacelike)(input)?;
73+
let (mut end, path) = context(
74+
"Expected string.",
75+
terminated(quoted_sass_string, opt_spacelike),
76+
)(input)?;
7077
let mut found_as = None;
7178
let mut expose = Expose::All;
7279
let mut found_with = None;
73-
while let Ok((rest, arg)) = terminated(name, opt_spacelike)(end) {
80+
while let Ok((rest, arg)) =
81+
delimited(ignore_comments, name, ignore_comments)(end)
82+
{
7483
end = match arg.as_ref() {
75-
"as" if found_as.is_none() => {
76-
let (i, a) = as_arg(rest)?;
84+
"as" if found_as.is_none() && found_with.is_none() => {
85+
let (i, a) = fwd_as_arg(rest)?;
7786
found_as = Some(a);
7887
i
7988
}
80-
"hide" if expose == Expose::All => {
89+
"hide" if expose == Expose::All && found_with.is_none() => {
8190
let (i, (funs, vars)) = exposed_names(rest)?;
8291
expose = Expose::Hide(funs, vars);
8392
i
8493
}
85-
"show" if expose == Expose::All => {
94+
"show" if expose == Expose::All && found_with.is_none() => {
8695
let (i, (funs, vars)) = exposed_names(rest)?;
8796
expose = Expose::Show(funs, vars);
8897
i
@@ -92,12 +101,7 @@ pub fn forward2<'a>(start: Span, input: Span<'a>) -> PResult<'a, Item> {
92101
found_with = Some(w);
93102
i
94103
}
95-
_ => {
96-
return Err(nom::Err::Error(nom::error::Error::new(
97-
end,
98-
nom::error::ErrorKind::MapRes,
99-
)));
100-
}
104+
_ => break,
101105
};
102106
}
103107
let (rest, ()) = semi_or_end(end)?;
@@ -119,7 +123,10 @@ fn exposed_names(input: Span) -> PResult<(BTreeSet<Name>, BTreeSet<Name>)> {
119123
terminated(tag(","), opt_spacelike),
120124
pair(
121125
map(opt(tag("$")), |v| v.is_some()),
122-
map(terminated(name, opt_spacelike), Name::from),
126+
cut(context(
127+
"Expected variable, mixin, or function name",
128+
map(terminated(name, opt_spacelike), Name::from),
129+
)),
123130
),
124131
),
125132
|items| {
@@ -146,24 +153,28 @@ fn as_arg(input: Span) -> PResult<UseAs> {
146153
)(input)
147154
}
148155

156+
fn fwd_as_arg(input: Span) -> PResult<UseAs> {
157+
map(terminated(identifier, char('*')), UseAs::Prefix)(input)
158+
}
159+
149160
fn with_arg(input: Span) -> PResult<Vec<(Name, Value, bool)>> {
150161
delimited(
151-
terminated(tag("("), opt_spacelike),
152-
separated_list0(
162+
terminated(char('('), ignore_comments),
163+
separated_list1(
153164
comma,
154165
tuple((
155166
delimited(
156-
tag("$"),
157-
map(name, Name::from),
158-
delimited(opt_spacelike, tag(":"), opt_spacelike),
167+
char('$'),
168+
map(identifier, Name::from),
169+
delimited(ignore_comments, char(':'), ignore_comments),
159170
),
160-
terminated(space_list, opt_spacelike),
171+
terminated(space_list, ignore_comments),
161172
map(opt(terminated(tag("!default"), opt_spacelike)), |o| {
162173
o.is_some()
163174
}),
164175
)),
165176
),
166-
delimited(opt(comma), tag(")"), opt_spacelike),
177+
delimited(opt(comma), char(')'), opt_spacelike),
167178
)(input)
168179
}
169180

@@ -172,5 +183,5 @@ fn quoted_sass_string(input: Span) -> PResult<SassString> {
172183
}
173184

174185
fn comma(input: Span) -> PResult<()> {
175-
map(terminated(tag(","), ignore_comments), |_| ())(input)
186+
delimited(ignore_comments, map(tag(","), |_| ()), ignore_comments)(input)
176187
}

Diff for: rsass/src/parser/media.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use super::strings::{sass_string_dq, sass_string_sq};
44
use super::util::{ignore_comments, opt_spacelike, semi_or_end};
55
use super::value::{
66
self, any_additive_expr, any_product, bracket_list, dictionary,
7-
function_call_or_string, variable,
7+
function_call_or_string_rulearg, variable,
88
};
99
use super::{body_block, list_or_single, PResult};
1010
use crate::sass::{BinOp, Item, Value};
@@ -53,7 +53,7 @@ pub fn args(input: Span) -> PResult<Value> {
5353
bracket_list,
5454
into(value::numeric),
5555
variable,
56-
map(function_call_or_string, |s| match s {
56+
map(function_call_or_string_rulearg, |s| match s {
5757
Value::Literal(s) => Value::Literal({
5858
let lower = s
5959
.single_raw()

0 commit comments

Comments
 (0)