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, Parser,
}, },
scope::{Scope, Scopes}, scope::{Scope, Scopes},
selector::{Extender, Selector}, selector::{ExtendedSelector, Extender, SelectorList},
}; };
mod args; 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)) Box::new(Error::from_loc(message, map.look_up_span(span), unicode))
} }
/// Compile CSS from a path fn from_string_with_file_name(input: String, file_name: &str, options: &Options) -> Result<String> {
///
/// ```
/// 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> {
let mut map = CodeMap::new(); 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 empty_span = file.span.subspan(0, 0);
let stmts = Parser { let stmts = Parser {
toks: &mut Lexer::new_from_file(&file), toks: &mut Lexer::new_from_file(&file),
map: &mut map, map: &mut map,
path: p.as_ref(), path: file_name.as_ref(),
scopes: &mut Scopes::new(), scopes: &mut Scopes::new(),
global_scope: &mut Scope::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, span_before: empty_span,
content: &mut Vec::new(), content: &mut Vec::new(),
flags: ContextFlags::empty(), 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)) .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 /// 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(feature = "profiling", inline(never))]
#[cfg_attr(not(feature = "profiling"), inline)] #[cfg_attr(not(feature = "profiling"), inline)]
#[cfg(not(feature = "wasm"))] #[cfg(not(feature = "wasm"))]
pub fn from_string(p: String, options: &Options) -> Result<String> { pub fn from_string(input: String, options: &Options) -> Result<String> {
let mut map = CodeMap::new(); from_string_with_file_name(input, "stdin", options)
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))
} }
#[cfg(feature = "wasm")] #[cfg(feature = "wasm")]

View File

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

View File

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

View File

@ -1239,7 +1239,6 @@ test!(
".foo, .bar {\n a: b;\n}\n\n.bar, .foo {\n c: d;\n}\n" ".foo, .bar {\n a: b;\n}\n\n.bar, .foo {\n c: d;\n}\n"
); );
test!( test!(
#[ignore = "Rc<RefCell<Selector>>"]
three_level_extend_loop, three_level_extend_loop,
".foo {a: b; @extend .bar} ".foo {a: b; @extend .bar}
.bar {c: d; @extend .baz} .bar {c: d; @extend .baz}
@ -1413,8 +1412,7 @@ test!(
#[ignore = "media queries are not yet parsed correctly"] #[ignore = "media queries are not yet parsed correctly"]
extend_within_separate_nested_at_rules, extend_within_separate_nested_at_rules,
"@media screen {@flooblehoof {.foo {a: b}}} "@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" "@media screen {\n @flooblehoof {\n .foo, .bar {\n a: b;\n }\n }\n}\n@media screen {\n @flooblehoof {}\n}\n"
); );
test!( test!(