Skip to content

Commit d39dd9e

Browse files
authored
Improve handling of errors in @for loop ranges. (#206)
2 parents 57777b4 + de03072 commit d39dd9e

File tree

13 files changed

+172
-146
lines changed

13 files changed

+172
-146
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ project adheres to
2121
* All numbers are now represented as f64 (#203).
2222
This removes integration with `num-rational`, `num-bigint`,
2323
`num-integer` and `num-traits`.
24+
* Improved `@for` loop evaluation and error handling (#206).
2425
* Msrv is now 1.63.0 for rsass (and 1.74 for rsass-cli).
2526

2627
### Other changes:

rsass/src/error.rs

+22-10
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
use crate::css::InvalidCss;
1+
use crate::css::{is_not, InvalidCss};
22
use crate::input::{LoadError, SourcePos};
3+
use crate::output::{Format, Formatted};
34
use crate::parser::ParseError;
4-
use crate::value::RangeError;
55
use crate::ScopeError;
66
use std::{fmt, io};
77

@@ -29,8 +29,6 @@ pub enum Error {
2929
///
3030
/// The bool is true for a used module and false for an import.
3131
ImportLoop(bool, SourcePos, Option<SourcePos>),
32-
/// A range error
33-
BadRange(RangeError),
3432
/// Error parsing sass data.
3533
ParseError(ParseError),
3634
/// Something bad at a specific position.
@@ -90,7 +88,6 @@ impl fmt::Display for Error {
9088
writeln!(out, "{what}")?;
9189
pos.show(out)
9290
}
93-
Self::BadRange(ref err) => err.fmt(out),
9491
Self::IoError(ref err) => err.fmt(out),
9592
}
9693
}
@@ -112,11 +109,6 @@ impl From<ParseError> for Error {
112109
Self::ParseError(e)
113110
}
114111
}
115-
impl From<RangeError> for Error {
116-
fn from(e: RangeError) -> Self {
117-
Self::BadRange(e)
118-
}
119-
}
120112

121113
impl From<LoadError> for Error {
122114
fn from(err: LoadError) -> Self {
@@ -163,6 +155,26 @@ pub enum Invalid {
163155
}
164156

165157
impl Invalid {
158+
pub(crate) fn not<'a, T>(v: &'a T, what: &str) -> Self
159+
where
160+
Formatted<'a, T>: std::fmt::Display,
161+
{
162+
Self::AtError(is_not(v, what))
163+
}
164+
pub(crate) fn expected_to<'a, T>(value: &'a T, cond: &str) -> Self
165+
where
166+
Formatted<'a, T>: std::fmt::Display,
167+
{
168+
Self::AtError(format!(
169+
"Expected {} to {}.",
170+
Formatted {
171+
value,
172+
format: Format::introspect()
173+
},
174+
cond
175+
))
176+
}
177+
166178
/// Combine this with a position to get an [`Error`].
167179
pub fn at(self, pos: SourcePos) -> Error {
168180
Error::Invalid(self, pos)

rsass/src/output/transform.rs

+2-13
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ use crate::css::{self, AtRule, Import, SelectorCtx, Value};
77
use crate::error::ResultPos;
88
use crate::input::{Context, Loader, Parsed, SourceKind};
99
use crate::sass::{get_global_module, Expose, Item, UseAs};
10-
use crate::value::ValueRange;
1110
use crate::{Error, Invalid, ScopeRef};
1211

1312
pub fn handle_parsed(
@@ -315,18 +314,8 @@ fn handle_item(
315314
}
316315
scope.restore_local_values(pushed);
317316
}
318-
Item::For {
319-
ref name,
320-
ref from,
321-
ref to,
322-
inclusive,
323-
ref body,
324-
} => {
325-
let range = ValueRange::new(
326-
from.evaluate(scope.clone())?,
327-
to.evaluate(scope.clone())?,
328-
*inclusive,
329-
)?;
317+
Item::For(ref name, ref range, ref body) => {
318+
let range = range.evaluate(scope.clone())?;
330319
check_body(body, BodyContext::Control)?;
331320
for value in range {
332321
let scope = ScopeRef::sub(scope.clone());

rsass/src/parser/mod.rs

+17-22
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,12 @@ use self::value::{
3030
value_expression,
3131
};
3232
use crate::input::{SourceFile, SourceName, SourcePos};
33-
use crate::sass::parser::{variable_declaration2, variable_declaration_mod};
34-
use crate::sass::{Callable, FormalArgs, Item, Name, Selectors, Value};
33+
use crate::sass::parser::{
34+
src_range, variable_declaration2, variable_declaration_mod,
35+
};
36+
use crate::sass::{
37+
Callable, FormalArgs, Item, Name, Selectors, SrcValue, Value,
38+
};
3539
use crate::value::ListSeparator;
3640
#[cfg(test)]
3741
use crate::value::{Numeric, Unit};
@@ -387,27 +391,18 @@ fn each_loop2(input: Span) -> PResult<Item> {
387391
/// A for loop after the initial `@for`.
388392
fn for_loop2(input: Span) -> PResult<Item> {
389393
let (input, name) = delimited(tag("$"), name, ignore_comments)(input)?;
390-
let (input, from) = delimited(
391-
terminated(tag("from"), ignore_comments),
392-
single_value,
393-
ignore_comments,
394-
)(input)?;
395-
let (input, inclusive) = terminated(
396-
alt((value(true, tag("through")), value(false, tag("to")))),
397-
ignore_comments,
398-
)(input)?;
399-
let (input, to) = terminated(single_value, ignore_comments)(input)?;
394+
let (input, range) = src_range(input)?;
400395
let (input, body) = body_block(input)?;
401-
Ok((
402-
input,
403-
Item::For {
404-
name: name.into(),
405-
from: Box::new(from),
406-
to: Box::new(to),
407-
inclusive,
408-
body,
409-
},
410-
))
396+
Ok((input, Item::For(name.into(), range, body)))
397+
}
398+
399+
/// A single `SrcValue`.
400+
///
401+
/// That is, a single sass value with source position.
402+
pub fn single_value_p(input: Span) -> PResult<SrcValue> {
403+
let (rest, value) = single_value(input)?;
404+
let pos = input.up_to(&rest).to_owned();
405+
Ok((rest, SrcValue::new(value, pos)))
411406
}
412407

413408
fn while_loop2(input: Span) -> PResult<Item> {

rsass/src/sass/item.rs

+2-13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use super::{
2-
CallArgs, Callable, Name, SassString, Selectors, Value,
2+
CallArgs, Callable, Name, SassString, Selectors, SrcRange, Value,
33
VariableDeclaration,
44
};
55
use crate::input::SourcePos;
@@ -62,18 +62,7 @@ pub enum Item {
6262
/// The value may be or evaluate to a list.
6363
Each(Vec<Name>, Value, Vec<Item>),
6464
/// An `@for` loop directive.
65-
For {
66-
/// The name of the iteration variable.
67-
name: Name,
68-
/// The start value for the iteration.
69-
from: Box<Value>,
70-
/// The end value for the iteration.
71-
to: Box<Value>,
72-
/// True if the end should be included in the range.
73-
inclusive: bool,
74-
/// The body of the loop.
75-
body: Vec<Item>,
76-
},
65+
For(Name, SrcRange, Vec<Item>),
7766
/// An `@while` loop directive.
7867
While(Value, Vec<Item>),
7968

rsass/src/sass/mod.rs

+6
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ mod item;
1818
mod mixin;
1919
mod name;
2020
mod selectors;
21+
mod srcrange;
22+
mod srcvalue;
2123
mod string;
2224
mod value;
2325
mod variabledeclaration;
@@ -36,6 +38,10 @@ pub use self::string::{SassString, StringPart};
3638
pub use self::value::{BinOp, Value};
3739
pub use self::variabledeclaration::VariableDeclaration;
3840

41+
pub(crate) use srcrange::SrcRange;
42+
pub(crate) use srcvalue::SrcValue;
43+
3944
pub(crate) mod parser {
45+
pub(crate) use super::srcrange::parser::*;
4046
pub(crate) use super::variabledeclaration::parser::*;
4147
}

rsass/src/sass/srcrange.rs

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
use super::SrcValue;
2+
use crate::value::{Numeric, ValueRange};
3+
use crate::{Error, Invalid, ScopeRef};
4+
5+
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd)]
6+
pub struct SrcRange {
7+
from: SrcValue,
8+
to: SrcValue,
9+
inclusive: bool,
10+
}
11+
12+
impl SrcRange {
13+
pub fn evaluate(&self, scope: ScopeRef) -> Result<ValueRange, Error> {
14+
let (from, unit) = self.from.eval_map(scope.clone(), |v| {
15+
let v = v
16+
.numeric_value()
17+
.map_err(|v| Invalid::not(&v, "a number"))?;
18+
let unit = v.unit;
19+
let v = v.value.into_integer().map_err(|e| {
20+
Invalid::not(&Numeric::new(e, unit.clone()), "an int")
21+
})?;
22+
23+
Ok((v, unit))
24+
})?;
25+
let to = self.to.eval_map(scope.clone(), |v| {
26+
let v = v
27+
.numeric_value()
28+
.map_err(|v| Invalid::not(&v, "a number"))?;
29+
30+
let v = if unit.is_none() || v.is_no_unit() {
31+
v.value
32+
} else if let Some(scaled) = v.as_unitset(&unit) {
33+
scaled
34+
} else {
35+
return Err(Invalid::expected_to(
36+
&v,
37+
&format!("have unit {unit}"),
38+
));
39+
};
40+
41+
let v = v.into_integer().map_err(|e| {
42+
Invalid::not(&Numeric::new(e, unit.clone()), "an int")
43+
})?;
44+
45+
Ok(v)
46+
})?;
47+
Ok(ValueRange::new(from, to, self.inclusive, unit))
48+
}
49+
}
50+
51+
pub mod parser {
52+
use super::SrcRange;
53+
use crate::parser::util::ignore_comments;
54+
use crate::parser::{single_value_p, PResult, Span};
55+
use nom::branch::alt;
56+
use nom::bytes::complete::tag;
57+
use nom::combinator::value;
58+
use nom::sequence::{delimited, terminated};
59+
60+
pub fn src_range(input: Span) -> PResult<SrcRange> {
61+
let (input, from) = delimited(
62+
terminated(tag("from"), ignore_comments),
63+
single_value_p,
64+
ignore_comments,
65+
)(input)?;
66+
let (input, inclusive) = terminated(
67+
alt((value(true, tag("through")), value(false, tag("to")))),
68+
ignore_comments,
69+
)(input)?;
70+
let (input, to) = terminated(single_value_p, ignore_comments)(input)?;
71+
Ok((
72+
input,
73+
SrcRange {
74+
from,
75+
to,
76+
inclusive,
77+
},
78+
))
79+
}
80+
}

rsass/src/sass/srcvalue.rs

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
use super::Value;
2+
use crate::{css, input::SourcePos, Error, Invalid, ScopeRef};
3+
4+
/// A value with a specific source position
5+
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd)]
6+
pub struct SrcValue {
7+
value: Value,
8+
pos: SourcePos,
9+
}
10+
11+
impl SrcValue {
12+
pub fn new(value: Value, pos: SourcePos) -> Self {
13+
Self { value, pos }
14+
}
15+
pub fn eval_map<T, F>(&self, scope: ScopeRef, f: F) -> Result<T, Error>
16+
where
17+
F: Fn(css::Value) -> Result<T, Invalid>,
18+
{
19+
// TODO: The position should be applied to err from evaluate as well!
20+
self.value
21+
.evaluate(scope)
22+
.and_then(|v| f(v).map_err(|e| e.at(self.pos.clone())))
23+
}
24+
}

rsass/src/value/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@ pub use self::operator::{BadOp, Operator};
1717
pub use self::quotes::Quotes;
1818
pub use self::unit::{CssDimension, Dimension, Unit};
1919
pub use self::unitset::{CssDimensionSet, UnitSet};
20-
pub(crate) use range::{RangeError, ValueRange};
20+
pub(crate) use range::ValueRange;

0 commit comments

Comments
 (0)