From 3cb5e66fda6650db66da189aeab560fb85ddae18 Mon Sep 17 00:00:00 2001 From: connorskees Date: Wed, 11 Jan 2023 01:10:36 +0000 Subject: [PATCH] resolve edge case when interpolated parent selector expands to be larger than source span _and_ it contains an escaped character --- crates/compiler/src/ast/media.rs | 9 ++------ crates/compiler/src/evaluate/visitor.rs | 15 ++++--------- crates/compiler/src/lexer.rs | 19 ++++++++++++++-- crates/compiler/src/parse/base.rs | 2 +- crates/compiler/src/parse/stylesheet.rs | 7 ++---- crates/compiler/src/selector/attribute.rs | 6 ++--- crates/lib/tests/selectors.rs | 27 +++++++++++++++++++++++ 7 files changed, 56 insertions(+), 29 deletions(-) diff --git a/crates/compiler/src/ast/media.rs b/crates/compiler/src/ast/media.rs index f96709b..713fc5e 100644 --- a/crates/compiler/src/ast/media.rs +++ b/crates/compiler/src/ast/media.rs @@ -2,12 +2,7 @@ use std::fmt::{self, Write}; use codemap::Span; -use crate::{ - ast::CssStmt, - error::SassResult, - lexer::{Lexer, TokenLexer}, - parse::MediaQueryParser, -}; +use crate::{ast::CssStmt, error::SassResult, lexer::Lexer, parse::MediaQueryParser}; #[derive(Debug, Clone)] pub(crate) struct MediaRule { @@ -59,7 +54,7 @@ impl MediaQuery { } pub fn parse_list(list: &str, span: Span) -> SassResult> { - let toks = Lexer::new(TokenLexer::new(list.chars().peekable()).collect(), span); + let toks = Lexer::new_from_string(list, span); MediaQueryParser::new(toks).parse() } diff --git a/crates/compiler/src/evaluate/visitor.rs b/crates/compiler/src/evaluate/visitor.rs index 457e3f6..1844680 100644 --- a/crates/compiler/src/evaluate/visitor.rs +++ b/crates/compiler/src/evaluate/visitor.rs @@ -25,7 +25,7 @@ use crate::{ common::{unvendor, BinaryOp, Identifier, ListSeparator, QuoteKind, UnaryOp}, error::{SassError, SassResult}, interner::InternedString, - lexer::{Lexer, TokenLexer}, + lexer::Lexer, parse::{ AtRootQueryParser, CssParser, KeyframesSelectorParser, SassParser, ScssParser, StylesheetParser, @@ -977,8 +977,7 @@ impl<'a> Visitor<'a> { let span = query.span; - let query_toks = - Lexer::new(TokenLexer::new(resolved.chars().peekable()).collect(), span); + let query_toks = Lexer::new_from_string(&resolved, span); AtRootQueryParser::new(query_toks).parse()? } @@ -1138,10 +1137,7 @@ impl<'a> Visitor<'a> { allows_placeholder: bool, span: Span, ) -> SassResult { - let sel_toks = Lexer::new( - TokenLexer::new(selector_text.chars().peekable()).collect(), - span, - ); + let sel_toks = Lexer::new_from_string(&selector_text, span); SelectorParser::new(sel_toks, allows_parent, allows_placeholder, span).parse() } @@ -2783,10 +2779,7 @@ impl<'a> Visitor<'a> { if self.flags.in_keyframes() { let span = ruleset.selector_span; - let sel_toks = Lexer::new( - TokenLexer::new(selector_text.chars().peekable()).collect(), - span, - ); + let sel_toks = Lexer::new_from_string(&selector_text, span); let parsed_selector = KeyframesSelectorParser::new(sel_toks).parse_keyframes_selector()?; diff --git a/crates/compiler/src/lexer.rs b/crates/compiler/src/lexer.rs index a19ddbb..6d8fbda 100644 --- a/crates/compiler/src/lexer.rs +++ b/crates/compiler/src/lexer.rs @@ -16,6 +16,9 @@ pub(crate) struct Lexer<'a> { buf: Cow<'a, [Token]>, entire_span: Span, cursor: usize, + /// If the input this lexer is spanned over is larger than the original span. + /// This is possible due to interpolation. + is_expanded: bool, } impl<'a> Lexer<'a> { @@ -34,6 +37,10 @@ impl<'a> Lexer<'a> { /// bounds, it returns the span of the last character. If the input is empty, /// it returns an empty span fn span_at_index(&self, idx: usize) -> Span { + if self.is_expanded { + return self.entire_span; + } + let (start, len) = match self.buf.get(idx) { Some(tok) => (tok.pos, tok.kind.len_utf8()), None => match self.buf.last() { @@ -146,14 +153,22 @@ impl<'a> Iterator for TokenLexer<'a> { impl<'a> Lexer<'a> { pub fn new_from_file(file: &Arc) -> Self { let buf = TokenLexer::new(file.source().chars().peekable()).collect(); - Self::new(buf, file.span) + Self::new(buf, file.span, false) } - pub fn new(buf: Vec, entire_span: Span) -> Self { + pub fn new_from_string(s: &str, entire_span: Span) -> Self { + let is_expanded = s.len() as u64 > entire_span.len(); + let buf = TokenLexer::new(s.chars().peekable()).collect(); + + Self::new(buf, entire_span, is_expanded) + } + + fn new(buf: Vec, entire_span: Span, is_expanded: bool) -> Self { Lexer { buf: Cow::Owned(buf), cursor: 0, entire_span, + is_expanded, } } } diff --git a/crates/compiler/src/parse/base.rs b/crates/compiler/src/parse/base.rs index 186e7e6..8bd7b9d 100644 --- a/crates/compiler/src/parse/base.rs +++ b/crates/compiler/src/parse/base.rs @@ -247,7 +247,7 @@ pub(crate) trait BaseParser<'a> { } let c = std::char::from_u32(value) - .ok_or(("Invalid Unicode code point.", self.toks().span_from(start)))?; + .ok_or_else(|| ("Invalid Unicode code point.", self.toks().span_from(start)))?; if (identifier_start && is_name_start(c) && !c.is_ascii_digit()) || (!identifier_start && is_name(c)) { diff --git a/crates/compiler/src/parse/stylesheet.rs b/crates/compiler/src/parse/stylesheet.rs index 2ed9494..9a64ada 100644 --- a/crates/compiler/src/parse/stylesheet.rs +++ b/crates/compiler/src/parse/stylesheet.rs @@ -13,7 +13,7 @@ use crate::{ ast::*, common::{unvendor, Identifier, QuoteKind}, error::SassResult, - lexer::{Lexer, TokenLexer}, + lexer::Lexer, utils::{is_name, is_name_start, is_plain_css_import, opposite_bracket}, ContextFlags, Options, Token, }; @@ -1534,10 +1534,7 @@ pub(crate) trait StylesheetParser<'a>: BaseParser<'a> + Sized { &base_name[start..end] }; - let mut toks = Lexer::new( - TokenLexer::new(namespace.chars().peekable()).collect(), - url_span, - ); + let mut toks = Lexer::new_from_string(&namespace, url_span); // if namespace is empty, avoid attempting to parse an identifier from // an empty string, as there will be no span to emit diff --git a/crates/compiler/src/selector/attribute.rs b/crates/compiler/src/selector/attribute.rs index 32e657f..c5f0b1c 100644 --- a/crates/compiler/src/selector/attribute.rs +++ b/crates/compiler/src/selector/attribute.rs @@ -45,7 +45,7 @@ fn attribute_name(parser: &mut SelectorParser) -> SassResult { let next = parser .toks .peek() - .ok_or(("Expected identifier.", parser.toks.current_span()))?; + .ok_or_else(|| ("Expected identifier.", parser.toks.current_span()))?; if next.kind == '*' { parser.toks.next(); parser.expect_char('|')?; @@ -110,7 +110,7 @@ impl Attribute { if parser .toks .peek() - .ok_or(("expected more input.", parser.toks.current_span()))? + .ok_or_else(|| ("expected more input.", parser.toks.current_span()))? .kind == ']' { @@ -130,7 +130,7 @@ impl Attribute { let peek = parser .toks .peek() - .ok_or(("expected more input.", parser.toks.current_span()))?; + .ok_or_else(|| ("expected more input.", parser.toks.current_span()))?; let value = match peek.kind { '\'' | '"' => parser.parse_string()?, diff --git a/crates/lib/tests/selectors.rs b/crates/lib/tests/selectors.rs index dfeed8e..40fcdbf 100644 --- a/crates/lib/tests/selectors.rs +++ b/crates/lib/tests/selectors.rs @@ -913,6 +913,33 @@ test!( }"#, "::foo(/a/b/) {\n color: ::foo(/a/b/);\n}\n" ); +test!( + interpolated_parent_selector_as_child_to_selector_with_escape_and_length_greater_than_child, + r#"abcde \a { + #{&} { + color: red; + } + }"#, + "abcde \\a abcde \\a {\n color: red;\n}\n" +); +error!( + interpolated_parent_selector_as_child_to_selector_with_escape_and_invalid_escape_and_length_greater_than_child, + r#"abcde \a { + #{&} \1111111 { + color: red; + } + }"#, + "Error: Invalid Unicode code point." +); +test!( + interpolated_parent_selector_as_child_to_selector_with_attribute_selector_and_length_greater_than_child, + r#"abcde [a] { + #{&} { + color: red; + } + }"#, + "abcde [a] abcde [a] {\n color: red;\n}\n" +); error!( pseudo_element_interpolated_semicolon_no_brackets, r#"::foo(#{";"}) {