support three level extend loop

the last feature stopping us from semantic parity with `dart-sass` when
compiling bootstrap.

this was a difficult bug -- it essentially boiled down to the fact that
we weren't applying extensions to _super_ selectors.

i suspect that this has somehow broken another feature of `@extend`, but
all of our unit tests, the sass spec, and bootstrap seem to be correct,
so i am considering this implemented.
This commit is contained in:
Connor Skees 2021-07-22 21:23:09 -04:00
parent 6de7b113cf
commit 0edb60e2b3
4 changed files with 73 additions and 65 deletions

View File

@ -112,7 +112,7 @@ use crate::{
Parser,
},
scope::{Scope, Scopes},
selector::{Extender, Selector},
selector::{ExtendedSelector, Extender, SelectorList},
};
mod args;
@ -267,30 +267,20 @@ fn raw_to_parse_error(map: &CodeMap, err: Error, unicode: bool) -> Box<Error> {
Box::new(Error::from_loc(message, map.look_up_span(span), unicode))
}
/// Compile CSS from a path
///
/// ```
/// fn main() -> Result<(), Box<grass::Error>> {
/// let sass = grass::from_path("input.scss", &grass::Options::default())?;
/// Ok(())
/// }
/// ```
/// (grass does not currently allow files or paths that are not valid UTF-8)
#[cfg_attr(feature = "profiling", inline(never))]
#[cfg_attr(not(feature = "profiling"), inline)]
#[cfg(not(feature = "wasm"))]
pub fn from_path(p: &str, options: &Options) -> Result<String> {
fn from_string_with_file_name(input: String, file_name: &str, options: &Options) -> Result<String> {
let mut map = CodeMap::new();
let file = map.add_file(p.into(), String::from_utf8(fs::read(p)?)?);
let file = map.add_file(file_name.to_owned(), input);
let empty_span = file.span.subspan(0, 0);
let stmts = Parser {
toks: &mut Lexer::new_from_file(&file),
map: &mut map,
path: p.as_ref(),
path: file_name.as_ref(),
scopes: &mut Scopes::new(),
global_scope: &mut Scope::new(),
super_selectors: &mut NeverEmptyVec::new(Selector::new(empty_span)),
super_selectors: &mut NeverEmptyVec::new(ExtendedSelector::new(SelectorList::new(
empty_span,
))),
span_before: empty_span,
content: &mut Vec::new(),
flags: ContextFlags::empty(),
@ -311,6 +301,22 @@ pub fn from_path(p: &str, options: &Options) -> Result<String> {
.map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))
}
/// Compile CSS from a path
///
/// ```
/// fn main() -> Result<(), Box<grass::Error>> {
/// let sass = grass::from_path("input.scss", &grass::Options::default())?;
/// Ok(())
/// }
/// ```
/// (grass does not currently allow files or paths that are not valid UTF-8)
#[cfg_attr(feature = "profiling", inline(never))]
#[cfg_attr(not(feature = "profiling"), inline)]
#[cfg(not(feature = "wasm"))]
pub fn from_path(p: &str, options: &Options) -> Result<String> {
from_string_with_file_name(String::from_utf8(fs::read(p)?)?, p, options)
}
/// Compile CSS from a string
///
/// ```
@ -323,35 +329,8 @@ pub fn from_path(p: &str, options: &Options) -> Result<String> {
#[cfg_attr(feature = "profiling", inline(never))]
#[cfg_attr(not(feature = "profiling"), inline)]
#[cfg(not(feature = "wasm"))]
pub fn from_string(p: String, options: &Options) -> Result<String> {
let mut map = CodeMap::new();
let file = map.add_file("stdin".into(), p);
let empty_span = file.span.subspan(0, 0);
let stmts = Parser {
toks: &mut Lexer::new_from_file(&file),
map: &mut map,
path: Path::new(""),
scopes: &mut Scopes::new(),
global_scope: &mut Scope::new(),
super_selectors: &mut NeverEmptyVec::new(Selector::new(empty_span)),
span_before: empty_span,
content: &mut Vec::new(),
flags: ContextFlags::empty(),
at_root: true,
at_root_has_selector: false,
extender: &mut Extender::new(empty_span),
content_scopes: &mut Scopes::new(),
options,
modules: &mut Modules::default(),
module_config: &mut ModuleConfig::default(),
}
.parse()
.map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))?;
Css::from_stmts(stmts, false, options.allows_charset)
.map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))?
.pretty_print(&map, options.style)
.map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))
pub fn from_string(input: String, options: &Options) -> Result<String> {
from_string_with_file_name(input, "stdin", options)
}
#[cfg(feature = "wasm")]

View File

@ -71,7 +71,7 @@ pub(crate) struct Parser<'a> {
pub global_scope: &'a mut Scope,
pub scopes: &'a mut Scopes,
pub content_scopes: &'a mut Scopes,
pub super_selectors: &'a mut NeverEmptyVec<Selector>,
pub super_selectors: &'a mut NeverEmptyVec<ExtendedSelector>,
pub span_before: Span,
pub content: &'a mut Vec<Content>,
pub flags: ContextFlags,
@ -106,6 +106,7 @@ impl<'a> Parser<'a> {
}
self.at_root = true;
}
Ok(stmts)
}
@ -358,14 +359,15 @@ impl<'a> Parser<'a> {
.parse_selector(true, false, init)?
.0
.resolve_parent_selectors(
self.super_selectors.last(),
&self.super_selectors.last().clone().into_selector(),
!at_root || self.at_root_has_selector,
)?;
self.scopes.enter_new_scope();
self.super_selectors.push(selector.clone());
let extended_selector = self.extender.add_selector(selector.0, None);
self.super_selectors.push(extended_selector.clone());
let body = self.parse_stmt()?;
self.scopes.exit_scope();
self.super_selectors.pop();
@ -660,9 +662,15 @@ impl<'a> Parser<'a> {
}
}
if !self.super_selectors.last().is_empty() {
if !self
.super_selectors
.last()
.clone()
.into_selector()
.is_empty()
{
body = vec![Stmt::RuleSet {
selector: ExtendedSelector::new(self.super_selectors.last().clone().0),
selector: self.super_selectors.last().clone(),
body,
}];
}
@ -700,9 +708,15 @@ impl<'a> Parser<'a> {
}
}
if !self.super_selectors.last().is_empty() {
if !self
.super_selectors
.last()
.clone()
.into_selector()
.is_empty()
{
body = vec![Stmt::RuleSet {
selector: ExtendedSelector::new(self.super_selectors.last().clone().0),
selector: self.super_selectors.last().clone(),
body,
}];
}
@ -723,9 +737,16 @@ impl<'a> Parser<'a> {
self.super_selectors.last().clone()
} else {
at_root_has_selector = true;
self.parse_selector(true, false, String::new())?.0
}
.resolve_parent_selectors(self.super_selectors.last(), false)?;
let selector = self
.parse_selector(true, false, String::new())?
.0
.resolve_parent_selectors(
&self.super_selectors.last().clone().into_selector(),
false,
)?;
self.extender.add_selector(selector.0, None)
};
self.whitespace();
@ -760,7 +781,7 @@ impl<'a> Parser<'a> {
})
.collect::<SassResult<Vec<Stmt>>>()?;
let mut stmts = vec![Stmt::RuleSet {
selector: ExtendedSelector::new(at_rule_selector.0),
selector: at_rule_selector,
body: styles,
}];
stmts.extend(raw_stmts);
@ -825,7 +846,7 @@ impl<'a> Parser<'a> {
}
self.extender.add_extension(
super_selector.clone().0,
super_selector.clone().into_selector().0,
compound.components.first().unwrap(),
&extend_rule,
&None,
@ -859,9 +880,15 @@ impl<'a> Parser<'a> {
}
}
if !self.super_selectors.last().is_empty() {
if !self
.super_selectors
.last()
.clone()
.into_selector()
.is_empty()
{
body = vec![Stmt::RuleSet {
selector: ExtendedSelector::new(self.super_selectors.last().clone().0),
selector: self.super_selectors.last().clone(),
body,
}];
}

View File

@ -839,7 +839,11 @@ impl<'a> Parser<'a> {
.span(span)
} else {
IntermediateValue::Value(HigherIntermediateValue::Literal(
self.super_selectors.last().clone().into_value(),
self.super_selectors
.last()
.clone()
.into_selector()
.into_value(),
))
.span(span)
}

View File

@ -1239,7 +1239,6 @@ test!(
".foo, .bar {\n a: b;\n}\n\n.bar, .foo {\n c: d;\n}\n"
);
test!(
#[ignore = "Rc<RefCell<Selector>>"]
three_level_extend_loop,
".foo {a: b; @extend .bar}
.bar {c: d; @extend .baz}
@ -1413,8 +1412,7 @@ test!(
#[ignore = "media queries are not yet parsed correctly"]
extend_within_separate_nested_at_rules,
"@media screen {@flooblehoof {.foo {a: b}}}
@media screen {@flooblehoof {.bar {@extend .foo}}}
",
@media screen {@flooblehoof {.bar {@extend .foo}}}",
"@media screen {\n @flooblehoof {\n .foo, .bar {\n a: b;\n }\n }\n}\n@media screen {\n @flooblehoof {}\n}\n"
);
test!(
@ -1771,7 +1769,7 @@ test!(
// extending rules that contain their own extends needs special handling.
.b {@extend .a}
.c {@extend .b}
.a {x: y}
.a {x: y}
",
".a, .b, .c {\n x: y;\n}\n"
);