more robust handling of empty input after resolving interpolation

This commit is contained in:
Connor Skees 2022-12-28 14:41:36 -05:00
parent bba405392c
commit bb937ae84f
8 changed files with 63 additions and 26 deletions

View File

@ -54,7 +54,7 @@ impl MediaQuery {
} }
pub fn parse_list(list: &str, span: Span) -> SassResult<Vec<Self>> { pub fn parse_list(list: &str, span: Span) -> SassResult<Vec<Self>> {
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() MediaQueryParser::new(toks).parse()
} }

View File

@ -204,7 +204,7 @@ pub(crate) struct AstExtendRule {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub(crate) struct AstAtRootRule { pub(crate) struct AstAtRootRule {
pub children: Vec<AstStmt>, pub children: Vec<AstStmt>,
pub query: Option<Interpolation>, pub query: Option<Spanned<Interpolation>>,
#[allow(unused)] #[allow(unused)]
pub span: Span, pub span: Span,
} }

View File

@ -969,14 +969,14 @@ impl<'a> Visitor<'a> {
fn visit_at_root_rule(&mut self, mut at_root_rule: AstAtRootRule) -> SassResult<Option<Value>> { fn visit_at_root_rule(&mut self, mut at_root_rule: AstAtRootRule) -> SassResult<Option<Value>> {
let query = match at_root_rule.query.clone() { let query = match at_root_rule.query.clone() {
Some(val) => { Some(query) => {
let resolved = self.perform_interpolation(val, true)?; let resolved = self.perform_interpolation(query.node, true)?;
let span = query.span;
let query_toks = Lexer::new( let query_toks = Lexer::new(
resolved resolved.chars().map(|x| Token::new(span, x)).collect(),
.chars() span,
.map(|x| Token::new(self.span_before, x))
.collect(),
); );
AtRootQueryParser::new(query_toks).parse()? AtRootQueryParser::new(query_toks).parse()?
@ -1137,7 +1137,10 @@ impl<'a> Visitor<'a> {
allows_placeholder: bool, allows_placeholder: bool,
span: Span, span: Span,
) -> SassResult<SelectorList> { ) -> SassResult<SelectorList> {
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() 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)?; let selector_text = self.interpolation_to_value(ruleset_selector, true, true)?;
if self.flags.in_keyframes() { if self.flags.in_keyframes() {
let span = ruleset.selector_span;
let sel_toks = Lexer::new( let sel_toks = Lexer::new(
selector_text selector_text.chars().map(|x| Token::new(span, x)).collect(),
.chars() span,
.map(|x| Token::new(self.span_before, x))
.collect(),
); );
let parsed_selector = let parsed_selector =
KeyframesSelectorParser::new(sel_toks).parse_keyframes_selector()?; KeyframesSelectorParser::new(sel_toks).parse_keyframes_selector()?;

View File

@ -7,8 +7,11 @@ use crate::Token;
const FORM_FEED: char = '\x0C'; const FORM_FEED: char = '\x0C';
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
// todo: remove lifetime as Cow is now superfluous
pub(crate) struct Lexer<'a> { pub(crate) struct Lexer<'a> {
buf: Cow<'a, [Token]>, buf: Cow<'a, [Token]>,
/// The span to be used in the case that `buf` is empty
empty_span: Span,
cursor: usize, cursor: usize,
} }
@ -37,19 +40,23 @@ impl<'a> Lexer<'a> {
} }
pub fn prev_span(&self) -> Span { pub fn prev_span(&self) -> Span {
self.buf match self.buf.get(self.cursor.saturating_sub(1)) {
.get(self.cursor.saturating_sub(1)) Some(tok) => tok.pos,
.copied() None => match self.buf.last() {
.unwrap_or_else(|| self.buf.last().copied().unwrap()) Some(tok) => tok.pos,
.pos None => self.empty_span,
},
}
} }
pub fn current_span(&self) -> Span { pub fn current_span(&self) -> Span {
self.buf match self.buf.get(self.cursor) {
.get(self.cursor) Some(tok) => tok.pos,
.copied() None => match self.buf.last() {
.unwrap_or_else(|| self.buf.last().copied().unwrap()) Some(tok) => tok.pos,
.pos None => self.empty_span,
},
}
} }
pub fn peek(&self) -> Option<Token> { pub fn peek(&self) -> Option<Token> {
@ -131,13 +138,14 @@ impl<'a> Lexer<'a> {
} }
.collect(); .collect();
Self::new(buf) Self::new(buf, file.span.subspan(0, 0))
} }
pub fn new(buf: Vec<Token>) -> Self { pub fn new(buf: Vec<Token>, empty_span: Span) -> Self {
Lexer { Lexer {
buf: Cow::Owned(buf), buf: Cow::Owned(buf),
cursor: 0, cursor: 0,
empty_span,
} }
} }
} }

View File

@ -325,12 +325,17 @@ pub(crate) trait StylesheetParser<'a>: BaseParser<'a> + Sized {
fn parse_at_root_rule(&mut self, start: usize) -> SassResult<AstStmt> { fn parse_at_root_rule(&mut self, start: usize) -> SassResult<AstStmt> {
Ok(AstStmt::AtRootRule(if self.toks_mut().next_char_is('(') { 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 = self.parse_at_root_query()?;
let query_span = self.toks_mut().span_from(query_start);
self.whitespace()?; self.whitespace()?;
let children = self.with_children(Self::parse_statement)?.node; let children = self.with_children(Self::parse_statement)?.node;
AstAtRootRule { AstAtRootRule {
query: Some(query), query: Some(Spanned {
node: query,
span: query_span,
}),
children, children,
span: self.toks_mut().span_from(start), span: self.toks_mut().span_from(start),
} }
@ -1521,6 +1526,7 @@ pub(crate) trait StylesheetParser<'a>: BaseParser<'a> + Sized {
.chars() .chars()
.map(|x| Token::new(self.span_before(), x)) .map(|x| Token::new(self.span_before(), x))
.collect(), .collect(),
self.span_before(),
); );
// if namespace is empty, avoid attempting to parse an identifier from // if namespace is empty, avoid attempting to parse an identifier from

View File

@ -279,3 +279,12 @@ error!(
}", }",
"Error: @extend may only be used within style rules." "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"."#
);

View File

@ -327,6 +327,13 @@ error!(
}", }",
r#"Error: Expected digit."# r#"Error: Expected digit."#
); );
error!(
selector_is_empty_after_interpolation_is_resolved,
"@keyframes foo {
#{null} {}
}",
r#"Error: Expected number."#
);
// todo: span for this // todo: span for this
// @keyframes foo { // @keyframes foo {

View File

@ -570,3 +570,8 @@ error!(
}"#, }"#,
"Error: expected no more input." "Error: expected no more input."
); );
error!(
empty_query_after_resolving_interpolation,
"@media #{null} {}",
"Error: expected no more input."
);