Skip to content

Commit 09d80f2

Browse files
authored
Proper errors from the parser. (#201)
Use nom "verbose" error handling to build proper parse errors. This should fix #141 .
2 parents b66d263 + d0fe994 commit 09d80f2

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)