Skip to content

Commit 57d4a13

Browse files
committed
Improve handling of errors in @for loop ranges.
1 parent 57777b4 commit 57d4a13

File tree

12 files changed

+147
-133
lines changed

12 files changed

+147
-133
lines changed

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

+19-9
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ use self::value::{
3131
};
3232
use crate::input::{SourceFile, SourceName, SourcePos};
3333
use crate::sass::parser::{variable_declaration2, variable_declaration_mod};
34-
use crate::sass::{Callable, FormalArgs, Item, Name, Selectors, Value};
34+
use crate::sass::{
35+
Callable, FormalArgs, Item, Name, Selectors, SrcRange, SrcValue, Value,
36+
};
3537
use crate::value::ListSeparator;
3638
#[cfg(test)]
3739
use crate::value::{Numeric, Unit};
@@ -389,27 +391,35 @@ fn for_loop2(input: Span) -> PResult<Item> {
389391
let (input, name) = delimited(tag("$"), name, ignore_comments)(input)?;
390392
let (input, from) = delimited(
391393
terminated(tag("from"), ignore_comments),
392-
single_value,
394+
single_value_p,
393395
ignore_comments,
394396
)(input)?;
395397
let (input, inclusive) = terminated(
396398
alt((value(true, tag("through")), value(false, tag("to")))),
397399
ignore_comments,
398400
)(input)?;
399-
let (input, to) = terminated(single_value, ignore_comments)(input)?;
401+
let (input, to) = terminated(single_value_p, ignore_comments)(input)?;
400402
let (input, body) = body_block(input)?;
401403
Ok((
402404
input,
403-
Item::For {
404-
name: name.into(),
405-
from: Box::new(from),
406-
to: Box::new(to),
407-
inclusive,
405+
Item::For(
406+
name.into(),
407+
SrcRange {
408+
from,
409+
to,
410+
inclusive,
411+
},
408412
body,
409-
},
413+
),
410414
))
411415
}
412416

417+
fn single_value_p(input: Span) -> PResult<SrcValue> {
418+
let (rest, value) = single_value(input)?;
419+
let pos = input.up_to(&rest).to_owned();
420+
Ok((rest, SrcValue::new(value, pos)))
421+
}
422+
413423
fn while_loop2(input: Span) -> PResult<Item> {
414424
let (input, cond) = terminated(value_expression, opt_spacelike)(input)?;
415425
let (input, body) = body_block(input)?;

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

+5
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,9 @@ 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 {
4045
pub(crate) use super::variabledeclaration::parser::*;
4146
}

rsass/src/sass/srcrange.rs

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
use crate::{
2+
value::{Numeric, ValueRange},
3+
Error, Invalid, ScopeRef,
4+
};
5+
6+
use super::SrcValue;
7+
8+
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd)]
9+
pub struct SrcRange {
10+
/// The start value for the iteration.
11+
pub from: SrcValue,
12+
/// The end value for the iteration.
13+
pub to: SrcValue,
14+
/// True if the end should be included in the range.
15+
pub inclusive: bool,
16+
}
17+
18+
impl SrcRange {
19+
pub fn evaluate(&self, scope: ScopeRef) -> Result<ValueRange, Error> {
20+
let (from, unit) = self.from.eval_map(scope.clone(), |v| {
21+
let v = v
22+
.numeric_value()
23+
.map_err(|v| Invalid::not(&v, "a number"))?;
24+
let unit = v.unit;
25+
let v = v.value.into_integer().map_err(|e| {
26+
Invalid::not(&Numeric::new(e, unit.clone()), "an int")
27+
})?;
28+
29+
Ok((v, unit))
30+
})?;
31+
let to = self.to.eval_map(scope.clone(), |v| {
32+
let v = v
33+
.numeric_value()
34+
.map_err(|v| Invalid::not(&v, "a number"))?;
35+
36+
let v = if unit.is_none() || v.is_no_unit() {
37+
v.value
38+
} else if let Some(scaled) = v.as_unitset(&unit) {
39+
scaled
40+
} else {
41+
return Err(Invalid::expected_to(
42+
&v,
43+
&format!("have unit {unit}"),
44+
));
45+
};
46+
47+
let v = v.into_integer().map_err(|e| {
48+
Invalid::not(&Numeric::new(e, unit.clone()), "an int")
49+
})?;
50+
51+
Ok(v)
52+
})?;
53+
Ok(ValueRange::new(from, to, self.inclusive, unit))
54+
}
55+
}

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;

rsass/src/value/range.rs

+14-68
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,35 @@
1-
use super::{Number, Numeric, UnitSet};
1+
use super::{Numeric, UnitSet};
22
use crate::css::Value;
3-
use std::fmt;
43

54
pub struct ValueRange {
6-
from: Number,
7-
to: Number,
8-
step: Number,
5+
from: i64,
6+
to: i64,
7+
step: i64,
98
unit: UnitSet,
109
}
1110

1211
impl ValueRange {
13-
pub fn new(
14-
from: Value,
15-
to: Value,
16-
inclusive: bool,
17-
) -> Result<Self, RangeError> {
18-
let from =
19-
from.numeric_value().map_err(RangeError::FromNotNumeric)?;
20-
let to = to.numeric_value().map_err(RangeError::ToNotNumeric)?;
21-
22-
let to = if from.is_no_unit() || to.is_no_unit() {
23-
to.value
24-
} else if let Some(scaled) = to.as_unitset(&from.unit) {
25-
scaled
26-
} else {
27-
return Err(RangeError::IncompatibleUnits(from.unit, to.unit));
28-
};
29-
let step = if to >= from.value {
30-
Number::from(1)
31-
} else {
32-
Number::from(-1)
33-
};
34-
let to = if inclusive { to + step.clone() } else { to };
35-
Ok(Self {
36-
from: from.value,
12+
pub fn new(from: i64, to: i64, inclusive: bool, unit: UnitSet) -> Self {
13+
let step = if to >= from { 1 } else { -1 };
14+
let to = if inclusive { to + step } else { to };
15+
Self {
16+
from,
3717
to,
3818
step,
39-
unit: from.unit,
40-
})
19+
unit,
20+
}
4121
}
4222
}
4323

4424
impl Iterator for ValueRange {
4525
type Item = Value;
4626
fn next(&mut self) -> Option<Value> {
47-
if self.from.partial_cmp(&self.to)
48-
== Number::from(0).partial_cmp(&self.step)
49-
{
50-
let result =
51-
Numeric::new(self.from.clone(), self.unit.clone()).into();
52-
self.from = self.from.clone() + self.step.clone();
27+
if self.from.partial_cmp(&self.to) == 0.partial_cmp(&self.step) {
28+
let result = Numeric::new(self.from, self.unit.clone()).into();
29+
self.from += self.step;
5330
Some(result)
5431
} else {
5532
None
5633
}
5734
}
5835
}
59-
60-
#[derive(Debug)]
61-
pub enum RangeError {
62-
FromNotNumeric(Value),
63-
ToNotNumeric(Value),
64-
IncompatibleUnits(UnitSet, UnitSet),
65-
}
66-
67-
impl fmt::Display for RangeError {
68-
fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result {
69-
match self {
70-
Self::FromNotNumeric(v) => {
71-
write!(
72-
out,
73-
"Bad Range: from needs to be numeric, got {}",
74-
v.format(Default::default()),
75-
)
76-
}
77-
Self::ToNotNumeric(v) => {
78-
write!(
79-
out,
80-
"Bad Range: to needs to be numeric, got {}",
81-
v.format(Default::default())
82-
)
83-
}
84-
Self::IncompatibleUnits(a, b) => {
85-
write!(out, "for loop needs compatible units, got {a}..{b}")
86-
}
87-
}
88-
}
89-
}

rsass/src/value/unitset.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ impl UnitSet {
137137
}
138138
}
139139
self.units.retain(|(_u, p)| *p != 0);
140-
factor.into()
140+
factor
141141
}
142142
}
143143

0 commit comments

Comments
 (0)