diff --git a/src/parse/args.rs b/src/parse/args.rs index e42abe5..e2422c6 100644 --- a/src/parse/args.rs +++ b/src/parse/args.rs @@ -15,7 +15,10 @@ use super::Parser; impl<'a> Parser<'a> { pub(super) fn parse_func_args(&mut self) -> SassResult { let mut args: Vec = Vec::new(); - let mut close_paren_span: Span = self.toks.peek().unwrap().pos(); + let mut close_paren_span: Span = match self.toks.peek() { + Some(Token { pos, .. }) => *pos, + None => return Err(("expected \")\".", self.span_before).into()), + }; self.whitespace(); while let Some(Token { kind, pos }) = self.toks.next() { @@ -130,9 +133,10 @@ impl<'a> Parser<'a> { .ok_or(("expected \")\".", self.span_before))? .pos(); loop { - match self.toks.peek() { - Some(Token { kind: '$', .. }) => { - let Token { pos, .. } = self.toks.next().unwrap(); + match self.toks.peek().cloned() { + Some(Token { kind: '$', pos }) => { + span = span.merge(pos); + self.toks.next(); let v = self.parse_identifier_no_interpolation(false)?; let whitespace = self.whitespace_or_comment(); if let Some(Token { kind: ':', .. }) = self.toks.peek() { diff --git a/src/parse/function.rs b/src/parse/function.rs index 20aed43..f9d54a8 100644 --- a/src/parse/function.rs +++ b/src/parse/function.rs @@ -35,7 +35,10 @@ impl<'a> Parser<'a> { self.whitespace(); let mut body = read_until_closing_curly_brace(self.toks)?; - body.push(self.toks.next().unwrap()); + body.push(match self.toks.next() { + Some(tok) => tok, + None => return Err(("expected \"}\".", self.span_before).into()), + }); self.whitespace(); let function = Function::new(self.scopes.last().clone(), args, body, span); diff --git a/src/parse/mixin.rs b/src/parse/mixin.rs index ff64b0c..e40102d 100644 --- a/src/parse/mixin.rs +++ b/src/parse/mixin.rs @@ -29,7 +29,10 @@ impl<'a> Parser<'a> { self.whitespace(); let mut body = read_until_closing_curly_brace(self.toks)?; - body.push(self.toks.next().unwrap()); + body.push(match self.toks.next() { + Some(tok) => tok, + None => return Err(("expected \"}\".", self.span_before).into()), + }); // todo: `@include` can only give content when `@content` is present within the body // if `@content` is *not* present and `@include` attempts to give a body, we throw an error diff --git a/src/parse/mod.rs b/src/parse/mod.rs index fbfd008..bc5c1ea 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -171,8 +171,9 @@ impl<'a> Parser<'a> { span, } = self.parse_value_from_vec(toks)?; span.merge(kind_string.span); - if let Some(Token { kind: ';', .. }) = self.toks.peek() { - kind_string.span.merge(self.toks.next().unwrap().pos()); + if let Some(Token { kind: ';', pos }) = self.toks.peek() { + kind_string.span.merge(*pos); + self.toks.next(); } self.warn(&Spanned { node: message.to_css_string(span)?, @@ -186,8 +187,9 @@ impl<'a> Parser<'a> { span, } = self.parse_value_from_vec(toks)?; span.merge(kind_string.span); - if let Some(Token { kind: ';', .. }) = self.toks.peek() { - kind_string.span.merge(self.toks.next().unwrap().pos()); + if let Some(Token { kind: ';', pos }) = self.toks.peek() { + kind_string.span.merge(*pos); + self.toks.next(); } self.debug(&Spanned { node: message.inspect(span)?, @@ -529,7 +531,11 @@ impl<'a> Parser<'a> { if let Some(tok) = self.toks.next() { self.whitespace(); match tok.kind.to_ascii_lowercase() { - 'i' if self.toks.next().unwrap().kind.to_ascii_lowercase() == 'f' => { + 'i' if matches!( + self.toks.peek(), + Some(Token { kind: 'f', .. }) | Some(Token { kind: 'F', .. }) + ) => + { self.toks.next(); let cond = read_until_open_curly_brace(self.toks)?; self.toks.next(); @@ -610,10 +616,10 @@ impl<'a> Parser<'a> { _ => return Err(("expected \"$\".", self.span_before).into()), }; self.whitespace(); - if self.toks.peek().is_none() { - return Err(("Expected \"from\".", var.span).into()); - } - self.span_before = self.toks.peek().unwrap().pos; + self.span_before = match self.toks.peek() { + Some(tok) => tok.pos, + None => return Err(("Expected \"from\".", var.span).into()), + }; if self.parse_identifier()?.node.to_ascii_lowercase() != "from" { return Err(("Expected \"from\".", var.span).into()); } @@ -644,7 +650,10 @@ impl<'a> Parser<'a> { '{' => { return Err(("Expected \"to\" or \"through\".", tok.pos()).into()); } - _ => from_toks.push(self.toks.next().unwrap()), + _ => { + from_toks.push(tok); + self.toks.next(); + } } } self.whitespace(); @@ -766,7 +775,10 @@ impl<'a> Parser<'a> { let mut body = read_until_closing_curly_brace(self.toks)?; - body.push(self.toks.next().unwrap()); + body.push(match self.toks.next() { + Some(tok) => tok, + None => return Err(("expected \"}\".", self.span_before).into()), + }); self.whitespace(); @@ -860,7 +872,10 @@ impl<'a> Parser<'a> { self.toks.next(); self.whitespace(); let mut body = read_until_closing_curly_brace(self.toks)?; - body.push(self.toks.next().unwrap()); + body.push(match self.toks.next() { + Some(tok) => tok, + None => return Err(("expected \"}\".", self.span_before).into()), + }); self.whitespace(); let mut stmts = Vec::new(); @@ -1102,7 +1117,10 @@ impl<'a> Parser<'a> { self.whitespace(); let mut body = read_until_closing_curly_brace(self.toks)?; - body.push(self.toks.next().unwrap()); + body.push(match self.toks.next() { + Some(tok) => tok, + None => return Err(("expected \"}\".", self.span_before).into()), + }); self.whitespace(); diff --git a/src/parse/value.rs b/src/parse/value.rs index 827fb02..b861d70 100644 --- a/src/parse/value.rs +++ b/src/parse/value.rs @@ -710,7 +710,7 @@ impl<'a> Parser<'a> { } fn peek_interpolation(&mut self) -> SassResult<(Spanned, usize)> { - let vec = peek_until_closing_curly_brace(self.toks); + let vec = peek_until_closing_curly_brace(self.toks)?; let peek_counter = vec.len(); self.toks.move_forward(1); let val = self.parse_value_from_vec(vec)?; diff --git a/src/utils/number.rs b/src/utils/number.rs index 2d4a57d..704982a 100644 --- a/src/utils/number.rs +++ b/src/utils/number.rs @@ -51,11 +51,7 @@ pub(crate) fn eat_number>( ) -> SassResult> { let mut whole = String::with_capacity(1); // TODO: merge this span with chars - let span = if let Some(tok) = toks.peek() { - tok.pos() - } else { - todo!() - }; + let span = toks.peek().unwrap().pos; eat_whole_number(toks, &mut whole); if toks.peek().is_none() { diff --git a/src/utils/peek_until.rs b/src/utils/peek_until.rs index 36e31fe..faa1039 100644 --- a/src/utils/peek_until.rs +++ b/src/utils/peek_until.rs @@ -8,19 +8,19 @@ use super::{as_hex, hex_char_for, is_name, is_name_start, IsWhitespace}; pub(crate) fn peek_until_closing_curly_brace>( toks: &mut PeekMoreIterator, -) -> Vec { +) -> SassResult> { let mut t = Vec::new(); let mut nesting = 0; - while let Some(tok) = toks.peek() { + while let Some(tok) = toks.peek().cloned() { match tok.kind { q @ '"' | q @ '\'' => { - t.push(*toks.peek().unwrap()); + t.push(tok); toks.move_forward(1); - t.extend(peek_until_closing_quote(toks, q)); + t.extend(peek_until_closing_quote(toks, q)?); } '{' => { nesting += 1; - t.push(*toks.peek().unwrap()); + t.push(tok); toks.move_forward(1); } '}' => { @@ -28,62 +28,71 @@ pub(crate) fn peek_until_closing_curly_brace>( break; } else { nesting -= 1; - t.push(*toks.peek().unwrap()); + t.push(tok); toks.move_forward(1); } } '/' => { - let next = *toks.peek_forward(1).unwrap(); - match toks.peek().unwrap().kind { - '/' => peek_until_newline(toks), + let next = *toks + .peek_forward(1) + .ok_or(("Expected expression.", tok.pos))?; + match toks.peek() { + Some(Token { kind: '/', .. }) => peek_until_newline(toks), _ => t.push(next), }; continue; } _ => { - t.push(*toks.peek().unwrap()); + t.push(tok); toks.move_forward(1); } } } peek_whitespace(toks); - t + Ok(t) } fn peek_until_closing_quote>( toks: &mut PeekMoreIterator, q: char, -) -> Vec { +) -> SassResult> { let mut t = Vec::new(); - while let Some(tok) = toks.peek() { + while let Some(tok) = toks.peek().cloned() { match tok.kind { '"' if q == '"' => { - t.push(*tok); + t.push(tok); toks.move_forward(1); break; } '\'' if q == '\'' => { - t.push(*tok); + t.push(tok); toks.move_forward(1); break; } '\\' => { - t.push(*tok); - t.push(*toks.peek_forward(1).unwrap()); + t.push(tok); + t.push(match toks.peek_forward(1) { + Some(tok) => *tok, + None => return Err((format!("Expected {}.", q), tok.pos).into()), + }); } '#' => { - t.push(*tok); - let next = toks.peek().unwrap(); + t.push(tok); + let next = match toks.peek() { + Some(tok) => tok, + None => return Err((format!("Expected {}.", q), tok.pos).into()), + }; if next.kind == '{' { - t.push(*toks.peek_forward(1).unwrap()); - t.append(&mut peek_until_closing_curly_brace(toks)); + t.push(*next); + toks.peek_forward(1); + t.append(&mut peek_until_closing_curly_brace(toks)?); } } - _ => t.push(*tok), + _ => t.push(tok), } toks.move_forward(1); } - t + Ok(t) } fn peek_until_newline>(toks: &mut PeekMoreIterator) { @@ -166,13 +175,13 @@ pub(crate) fn peek_ident_no_interpolation>( .ok_or(("Expected identifier.", span_before))? .pos(); let mut text = String::new(); - if toks.peek().unwrap().kind == '-' { + if let Some(Token { kind: '-', .. }) = toks.peek() { toks.peek_forward(1); text.push('-'); if toks.peek().is_none() { return Ok(Spanned { node: text, span }); } - if toks.peek().unwrap().kind == '-' { + if let Some(Token { kind: '-', .. }) = toks.peek() { toks.peek_forward(1); text.push('-'); text.push_str(&peek_ident_body_no_interpolation(toks, unit, span)?.node); diff --git a/src/utils/read_until.rs b/src/utils/read_until.rs index 81e49d2..9a18054 100644 --- a/src/utils/read_until.rs +++ b/src/utils/read_until.rs @@ -28,10 +28,10 @@ pub(crate) fn read_until_open_curly_brace>( } '\\' => { t.push(toks.next().unwrap()); - if toks.peek().is_some() { - t.push(toks.next().unwrap()); - } - continue; + t.push(match toks.next() { + Some(tok) => tok, + None => continue, + }); } q @ '"' | q @ '\'' => { t.push(toks.next().unwrap()); @@ -89,9 +89,10 @@ pub(crate) fn read_until_closing_curly_brace>( } '\\' => { t.push(toks.next().unwrap()); - if toks.peek().is_some() { - t.push(toks.next().unwrap()); - } + t.push(match toks.next() { + Some(tok) => tok, + None => continue, + }); } _ => t.push(toks.next().unwrap()), } @@ -120,16 +121,21 @@ pub(crate) fn read_until_closing_quote>( } '\\' => { t.push(tok); - if toks.peek().is_some() { - t.push(toks.next().unwrap()); - } + t.push(match toks.next() { + Some(tok) => tok, + None => return Err((format!("Expected {}.", q), tok.pos).into()), + }); } '#' => { t.push(tok); - let next = toks.peek().unwrap(); - if next.kind == '{' { - t.push(toks.next().unwrap()); - t.append(&mut read_until_closing_curly_brace(toks)?); + match toks.peek() { + Some(tok @ Token { kind: '{', .. }) => { + t.push(*tok); + toks.next(); + t.append(&mut read_until_closing_curly_brace(toks)?); + } + Some(..) => continue, + None => return Err((format!("Expected {}.", q), tok.pos).into()), } } _ => t.push(tok), @@ -156,9 +162,10 @@ pub(crate) fn read_until_semicolon_or_closing_curly_brace { t.push(toks.next().unwrap()); - if toks.peek().is_some() { - t.push(toks.next().unwrap()); - } + t.push(match toks.next() { + Some(tok) => tok, + None => continue, + }); } '"' | '\'' => { let quote = toks.next().unwrap(); @@ -217,11 +224,11 @@ pub(crate) fn read_until_closing_paren>( continue; } '\\' => { - t.push(tok); - if toks.peek().is_some() { - t.push(toks.next().unwrap()); - } - continue; + t.push(toks.next().unwrap()); + t.push(match toks.next() { + Some(tok) => tok, + None => continue, + }); } _ => {} } @@ -253,10 +260,11 @@ pub(crate) fn read_until_closing_square_brace>( continue; } '\\' => { - t.push(tok); - if toks.peek().is_some() { - t.push(toks.next().unwrap()); - } + t.push(toks.next().unwrap()); + t.push(match toks.next() { + Some(tok) => tok, + None => continue, + }); } _ => {} } diff --git a/src/value/mod.rs b/src/value/mod.rs index 716e71a..800de76 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -148,7 +148,7 @@ impl Value { Self::List(vals, sep, brackets) => match brackets { Brackets::None => Cow::owned( vals.iter() - .filter(|x| !x.clone().is_null(span).unwrap_or(false)) + .filter(|x| !x.is_null(span).unwrap_or(false)) .map(|x| x.to_css_string(span)) .collect::>>>()? .join(sep.as_str()), diff --git a/tests/args.rs b/tests/args.rs index 04a7569..21354e9 100644 --- a/tests/args.rs +++ b/tests/args.rs @@ -46,7 +46,11 @@ test!( "a {\n color: 2;\n}\n" ); error!( - #[ignore = "does not fail"] + #[ignore = "expects incorrect char, '{'"] nothing_after_open, "a { color:rgb(; }", "Error: expected \")\"." ); +error!( + nothing_after_open_paren_in_fn_args, + "@function foo(", "Error: expected \")\"." +); diff --git a/tests/at-root.rs b/tests/at-root.rs index f704ecb..c919255 100644 --- a/tests/at-root.rs +++ b/tests/at-root.rs @@ -68,3 +68,7 @@ test!( "a {}\n\n@at-root {\n @-ms-viewport { width: device-width; }\n}\n", "@-ms-viewport {\n width: device-width;\n}\n" ); +error!( + missing_closing_curly_brace, + "@at-root {", "Error: expected \"}\"." +); diff --git a/tests/each.rs b/tests/each.rs index 59220f7..aa406c7 100644 --- a/tests/each.rs +++ b/tests/each.rs @@ -48,3 +48,7 @@ test!( "a {\n @each $i in 1 2 3 {\n color: type-of($i);\n }\n}\n", "a {\n color: number;\n color: number;\n color: number;\n}\n" ); +error!( + missing_closing_curly_brace, + "@each $i in 1 {", "Error: expected \"}\"." +); diff --git a/tests/functions.rs b/tests/functions.rs index d9de492..aa8d96c 100644 --- a/tests/functions.rs +++ b/tests/functions.rs @@ -95,3 +95,7 @@ error!( double_comma_args, "@function foo($a,$b,,) {}", "Error: expected \")\"." ); +error!( + body_missing_closing_curly_brace, + "@function foo() {", "Error: expected \"}\"." +); diff --git a/tests/if.rs b/tests/if.rs index 13a0f43..f8a5fcd 100644 --- a/tests/if.rs +++ b/tests/if.rs @@ -149,3 +149,7 @@ error!(unclosed_sgl_quote, "@if true ' {}", "Error: Expected '."); error!(unclosed_call_args, "@if a({}", "Error: expected \")\"."); error!(nothing_after_div, "@if a/", "Error: Expected expression."); error!(multiline_error, "@if \"\n\"{}", "Error: Expected \"."); +error!( + nothing_after_i_after_else, + "@if true {} @else i", "Error: expected \"{\"." +); diff --git a/tests/mixins.rs b/tests/mixins.rs index 28a2e21..c92e338 100644 --- a/tests/mixins.rs +++ b/tests/mixins.rs @@ -233,6 +233,10 @@ error!( undefined_mixin, "a {@include foo;}", "Error: Undefined mixin." ); +error!( + body_missing_closing_curly_brace, + "@mixin foo() {", "Error: expected \"}\"." +); test!( include_empty_args_no_semicolon, "@mixin c {}\n\na {\n @include c()\n}\n", diff --git a/tests/url.rs b/tests/url.rs index a83b3c3..0373825 100644 --- a/tests/url.rs +++ b/tests/url.rs @@ -134,3 +134,15 @@ test!( "a {\n color: UrL(http://foo);\n}\n", "a {\n color: url(http://foo);\n}\n" ); +error!( + url_nothing_after_forward_slash_in_interpolation, + "a { color: url(#{/", "Error: Expected expression." +); +error!( + url_nothing_after_backslash_in_interpolation_in_quote, + "a { color: url(#{\"\\", "Error: Expected \"." +); +error!( + url_nothing_after_hash_in_interpolation_in_quote, + "a { color: url(#{\"#", "Error: Expected \"." +); diff --git a/tests/while.rs b/tests/while.rs index 4725a72..3af9ac5 100644 --- a/tests/while.rs +++ b/tests/while.rs @@ -107,7 +107,6 @@ test!( }", "result {\n root_default: initial;\n root_implicit: inner;\n root_explicit: inner;\n}\n" ); - test!( if_inside_while, "$continue_outer: true; @@ -126,3 +125,7 @@ test!( }", "a {\n color: red;\n}\n\na {\n color: blue;\n}\n" ); +error!( + missing_closing_curly_brace, + "@while true {", "Error: expected \"}\"." +);