diff --git a/src/ast/media.rs b/src/ast/media.rs index 62875f9..03c3449 100644 --- a/src/ast/media.rs +++ b/src/ast/media.rs @@ -54,7 +54,7 @@ impl MediaQuery { } pub fn parse_list(list: &str, span: Span) -> SassResult> { - let toks = Lexer::new(list.chars().map(|x| Token::new(span, x)).collect()); + let toks = Lexer::new(list.chars().map(|x| Token::new(span, x)).collect(), span); MediaQueryParser::new(toks).parse() } diff --git a/src/ast/stmt.rs b/src/ast/stmt.rs index cbcad48..4953fc7 100644 --- a/src/ast/stmt.rs +++ b/src/ast/stmt.rs @@ -204,7 +204,7 @@ pub(crate) struct AstExtendRule { #[derive(Debug, Clone)] pub(crate) struct AstAtRootRule { pub children: Vec, - pub query: Option, + pub query: Option>, #[allow(unused)] pub span: Span, } diff --git a/src/evaluate/visitor.rs b/src/evaluate/visitor.rs index e614fdb..7c6de04 100644 --- a/src/evaluate/visitor.rs +++ b/src/evaluate/visitor.rs @@ -969,14 +969,14 @@ impl<'a> Visitor<'a> { fn visit_at_root_rule(&mut self, mut at_root_rule: AstAtRootRule) -> SassResult> { let query = match at_root_rule.query.clone() { - Some(val) => { - let resolved = self.perform_interpolation(val, true)?; + Some(query) => { + let resolved = self.perform_interpolation(query.node, true)?; + + let span = query.span; let query_toks = Lexer::new( - resolved - .chars() - .map(|x| Token::new(self.span_before, x)) - .collect(), + resolved.chars().map(|x| Token::new(span, x)).collect(), + span, ); AtRootQueryParser::new(query_toks).parse()? @@ -1137,7 +1137,10 @@ impl<'a> Visitor<'a> { allows_placeholder: bool, span: Span, ) -> SassResult { - let sel_toks = Lexer::new(selector_text.chars().map(|x| Token::new(span, x)).collect()); + let sel_toks = Lexer::new( + selector_text.chars().map(|x| Token::new(span, x)).collect(), + span, + ); SelectorParser::new(sel_toks, allows_parent, allows_placeholder, span).parse() } @@ -2742,11 +2745,10 @@ impl<'a> Visitor<'a> { let selector_text = self.interpolation_to_value(ruleset_selector, true, true)?; if self.flags.in_keyframes() { + let span = ruleset.selector_span; let sel_toks = Lexer::new( - selector_text - .chars() - .map(|x| Token::new(self.span_before, x)) - .collect(), + selector_text.chars().map(|x| Token::new(span, x)).collect(), + span, ); let parsed_selector = KeyframesSelectorParser::new(sel_toks).parse_keyframes_selector()?; diff --git a/src/lexer.rs b/src/lexer.rs index a4cc361..067dec0 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -7,8 +7,11 @@ use crate::Token; const FORM_FEED: char = '\x0C'; #[derive(Debug, Clone)] +// todo: remove lifetime as Cow is now superfluous pub(crate) struct Lexer<'a> { buf: Cow<'a, [Token]>, + /// The span to be used in the case that `buf` is empty + empty_span: Span, cursor: usize, } @@ -37,19 +40,23 @@ impl<'a> Lexer<'a> { } pub fn prev_span(&self) -> Span { - self.buf - .get(self.cursor.saturating_sub(1)) - .copied() - .unwrap_or_else(|| self.buf.last().copied().unwrap()) - .pos + match self.buf.get(self.cursor.saturating_sub(1)) { + Some(tok) => tok.pos, + None => match self.buf.last() { + Some(tok) => tok.pos, + None => self.empty_span, + }, + } } pub fn current_span(&self) -> Span { - self.buf - .get(self.cursor) - .copied() - .unwrap_or_else(|| self.buf.last().copied().unwrap()) - .pos + match self.buf.get(self.cursor) { + Some(tok) => tok.pos, + None => match self.buf.last() { + Some(tok) => tok.pos, + None => self.empty_span, + }, + } } pub fn peek(&self) -> Option { @@ -131,13 +138,14 @@ impl<'a> Lexer<'a> { } .collect(); - Self::new(buf) + Self::new(buf, file.span.subspan(0, 0)) } - pub fn new(buf: Vec) -> Self { + pub fn new(buf: Vec, empty_span: Span) -> Self { Lexer { buf: Cow::Owned(buf), cursor: 0, + empty_span, } } } diff --git a/src/parse/stylesheet.rs b/src/parse/stylesheet.rs index 25eea1f..a96922c 100644 --- a/src/parse/stylesheet.rs +++ b/src/parse/stylesheet.rs @@ -325,12 +325,17 @@ pub(crate) trait StylesheetParser<'a>: BaseParser<'a> + Sized { fn parse_at_root_rule(&mut self, start: usize) -> SassResult { Ok(AstStmt::AtRootRule(if self.toks_mut().next_char_is('(') { + let query_start = self.toks().cursor(); let query = self.parse_at_root_query()?; + let query_span = self.toks_mut().span_from(query_start); self.whitespace()?; let children = self.with_children(Self::parse_statement)?.node; AstAtRootRule { - query: Some(query), + query: Some(Spanned { + node: query, + span: query_span, + }), children, span: self.toks_mut().span_from(start), } @@ -1521,6 +1526,7 @@ pub(crate) trait StylesheetParser<'a>: BaseParser<'a> + Sized { .chars() .map(|x| Token::new(self.span_before(), x)) .collect(), + self.span_before(), ); // if namespace is empty, avoid attempting to parse an identifier from diff --git a/tests/at-root.rs b/tests/at-root.rs index 7fce4af..9379b2f 100644 --- a/tests/at-root.rs +++ b/tests/at-root.rs @@ -279,3 +279,12 @@ error!( }", "Error: @extend may only be used within style rules." ); +error!( + selector_is_empty_after_interpolation_is_resolved, + "@at-root #{null} {}", "Error: expected selector." +); +error!( + // todo: dart-sass gives error r#"Error: Expected "with" or "without"."# + query_is_empty_parens_after_interpolation_is_resolved, + "@at-root (#{null}) {}", r#"Error: Expected "without"."# +); diff --git a/tests/keyframes.rs b/tests/keyframes.rs index 150f59e..1550f3d 100644 --- a/tests/keyframes.rs +++ b/tests/keyframes.rs @@ -327,6 +327,13 @@ error!( }", r#"Error: Expected digit."# ); +error!( + selector_is_empty_after_interpolation_is_resolved, + "@keyframes foo { + #{null} {} + }", + r#"Error: Expected number."# +); // todo: span for this // @keyframes foo { diff --git a/tests/media.rs b/tests/media.rs index c7d6935..fdff61b 100644 --- a/tests/media.rs +++ b/tests/media.rs @@ -570,3 +570,8 @@ error!( }"#, "Error: expected no more input." ); +error!( + empty_query_after_resolving_interpolation, + "@media #{null} {}", + "Error: expected no more input." +);