From 438abe52be8a3ae43ce6dfb3c9689a5eaab8ab29 Mon Sep 17 00:00:00 2001 From: Connor Skees Date: Fri, 7 Aug 2020 02:01:04 -0400 Subject: [PATCH] allow redeclaration of module variables --- src/builtin/modules/mod.rs | 88 +++++++++++++++++++++++++++++--------- src/parse/common.rs | 3 +- src/parse/mod.rs | 43 +++++++++++++++++++ src/parse/module.rs | 2 +- src/parse/style.rs | 79 +++++++++++++++++++++------------- src/parse/variable.rs | 10 ++--- src/scope.rs | 2 +- tests/use.rs | 63 +++++++++++++++++++++++++++ 8 files changed, 232 insertions(+), 58 deletions(-) diff --git a/src/builtin/modules/mod.rs b/src/builtin/modules/mod.rs index 1490adc..8a12a9d 100644 --- a/src/builtin/modules/mod.rs +++ b/src/builtin/modules/mod.rs @@ -22,7 +22,13 @@ mod selector; mod string; #[derive(Debug, Default)] -pub(crate) struct Module(pub Scope); +pub(crate) struct Module { + pub scope: Scope, + + /// Whether or not this module is builtin + /// e.g. `"sass:math"` + is_builtin: bool, +} #[derive(Debug, Default)] pub(crate) struct Modules(BTreeMap); @@ -83,9 +89,27 @@ impl Modules { .into()), } } + + pub fn get_mut(&mut self, name: Identifier, span: Span) -> SassResult<&mut Module> { + match self.0.get_mut(&name) { + Some(v) => Ok(v), + None => Err(( + format!( + "There is no module with the namespace \"{}\".", + name.as_str() + ), + span, + ) + .into()), + } + } } impl Module { + pub fn new_builtin() -> Self { + Module { scope: Scope::default(), is_builtin: true } + } + pub fn get_var(&self, name: Spanned) -> SassResult<&Value> { if name.node.as_str().starts_with('-') { return Err(( @@ -95,12 +119,36 @@ impl Module { .into()); } - match self.0.vars.get(&name.node) { + match self.scope.vars.get(&name.node) { Some(v) => Ok(v), None => Err(("Undefined variable.", name.span).into()), } } + pub fn update_var(&mut self, name: Spanned, value: Value) -> SassResult<()> { + if self.is_builtin { + return Err(( + "Cannot modify built-in variable.", + name.span, + ) + .into()); + } + + if name.node.as_str().starts_with('-') { + return Err(( + "Private members can't be accessed from outside their modules.", + name.span, + ) + .into()); + } + + if self.scope.insert_var(name.node, value).is_some() { + Ok(()) + } else { + Err(("Undefined variable.", name.span).into()) + } + } + pub fn get_mixin(&self, name: Spanned) -> SassResult { if name.node.as_str().starts_with('-') { return Err(( @@ -110,18 +158,18 @@ impl Module { .into()); } - match self.0.mixins.get(&name.node) { + match self.scope.mixins.get(&name.node) { Some(v) => Ok(v.clone()), None => Err(("Undefined mixin.", name.span).into()), } } pub fn insert_builtin_mixin(&mut self, name: &'static str, mixin: BuiltinMixin) { - self.0.mixins.insert(name.into(), Mixin::Builtin(mixin)); + self.scope.mixins.insert(name.into(), Mixin::Builtin(mixin)); } pub fn insert_builtin_var(&mut self, name: &'static str, value: Value) { - self.0.vars.insert(name.into(), value); + self.scope.vars.insert(name.into(), value); } pub fn get_fn(&self, name: Spanned) -> SassResult> { @@ -133,15 +181,15 @@ impl Module { .into()); } - Ok(self.0.functions.get(&name.node).cloned()) + Ok(self.scope.functions.get(&name.node).cloned()) } pub fn var_exists(&self, name: Identifier) -> bool { - !name.as_str().starts_with('-') && self.0.var_exists(name) + !name.as_str().starts_with('-') && self.scope.var_exists(name) } pub fn mixin_exists(&self, name: Identifier) -> bool { - !name.as_str().starts_with('-') && self.0.mixin_exists(name) + !name.as_str().starts_with('-') && self.scope.mixin_exists(name) } pub fn insert_builtin( @@ -150,14 +198,14 @@ impl Module { function: fn(CallArgs, &mut Parser<'_>) -> SassResult, ) { let ident = name.into(); - self.0 + self.scope .functions .insert(ident, SassFunction::Builtin(Builtin::new(function), ident)); } pub fn functions(&self) -> SassMap { SassMap::new_with( - self.0 + self.scope .functions .iter() .filter(|(key, _)| !key.as_str().starts_with('-')) @@ -173,7 +221,7 @@ impl Module { pub fn variables(&self) -> SassMap { SassMap::new_with( - self.0 + self.scope .vars .iter() .filter(|(key, _)| !key.as_str().starts_with('-')) @@ -187,49 +235,49 @@ impl Module { ) } - pub const fn new_from_scope(scope: Scope) -> Self { - Module(scope) + pub const fn new_from_scope(scope: Scope, is_builtin: bool) -> Self { + Module { scope, is_builtin } } } pub(crate) fn declare_module_color() -> Module { - let mut module = Module::default(); + let mut module = Module::new_builtin(); color::declare(&mut module); module } pub(crate) fn declare_module_list() -> Module { - let mut module = Module::default(); + let mut module = Module::new_builtin(); list::declare(&mut module); module } pub(crate) fn declare_module_map() -> Module { - let mut module = Module::default(); + let mut module = Module::new_builtin(); map::declare(&mut module); module } pub(crate) fn declare_module_math() -> Module { - let mut module = Module::default(); + let mut module = Module::new_builtin(); math::declare(&mut module); module } pub(crate) fn declare_module_meta() -> Module { - let mut module = Module::default(); + let mut module = Module::new_builtin(); meta::declare(&mut module); module } pub(crate) fn declare_module_selector() -> Module { - let mut module = Module::default(); + let mut module = Module::new_builtin(); selector::declare(&mut module); module } pub(crate) fn declare_module_string() -> Module { - let mut module = Module::default(); + let mut module = Module::new_builtin(); string::declare(&mut module); module } diff --git a/src/parse/common.rs b/src/parse/common.rs index fa724b8..d23514a 100644 --- a/src/parse/common.rs +++ b/src/parse/common.rs @@ -2,7 +2,7 @@ use std::ops::{BitAnd, BitOr}; use codemap::Spanned; -use crate::{interner::InternedString, value::Value}; +use crate::{common::Identifier, interner::InternedString, value::Value}; #[derive(Debug, Clone)] pub(crate) struct NeverEmptyVec { @@ -42,6 +42,7 @@ impl NeverEmptyVec { pub(super) enum SelectorOrStyle { Selector(String), Style(InternedString, Option>>), + ModuleVariableRedeclaration(Identifier), } #[derive(Debug, Copy, Clone)] diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 6b5ece3..6536748 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -11,6 +11,7 @@ use crate::{ AtRuleKind, SupportsRule, UnknownAtRule, }, builtin::modules::{ModuleConfig, Modules}, + common::Identifier, error::SassResult, scope::{Scope, Scopes}, selector::{ @@ -27,6 +28,7 @@ use crate::{ use common::{Comment, ContextFlags, NeverEmptyVec, SelectorOrStyle}; pub(crate) use value::{HigherIntermediateValue, ValueVisitor}; +use variable::VariableValue; mod args; pub mod common; @@ -131,6 +133,41 @@ impl<'a> Parser<'a> { } } + fn parse_module_variable_redeclaration(&mut self, module: Identifier) -> SassResult<()> { + let variable = self + .parse_identifier_no_interpolation(false)? + .map_node(|n| n.into()); + + self.whitespace_or_comment(); + self.expect_char(':')?; + + let VariableValue { + val_toks, + global, + default, + } = self.parse_variable_value()?; + + if global { + return Err(( + "!global isn't allowed for variables in other modules.", + variable.span, + ) + .into()); + } + + if default { + return Ok(()); + } + + let value = self.parse_value_from_vec(val_toks, true)?; + + self.modules + .get_mut(module, variable.span)? + .update_var(variable, value.node)?; + + Ok(()) + } + fn parse_stmt(&mut self) -> SassResult> { let mut stmts = Vec::new(); while let Some(Token { kind, pos }) = self.toks.peek() { @@ -294,6 +331,9 @@ impl<'a> Parser<'a> { } if self.flags.in_keyframes() { match self.is_selector_or_style()? { + SelectorOrStyle::ModuleVariableRedeclaration(module) => { + self.parse_module_variable_redeclaration(module)? + } SelectorOrStyle::Style(property, value) => { if let Some(value) = value { stmts.push(Stmt::Style(Style { property, value })); @@ -321,6 +361,9 @@ impl<'a> Parser<'a> { } match self.is_selector_or_style()? { + SelectorOrStyle::ModuleVariableRedeclaration(module) => { + self.parse_module_variable_redeclaration(module)? + } SelectorOrStyle::Style(property, value) => { if let Some(value) = value { stmts.push(Stmt::Style(Style { property, value })); diff --git a/src/parse/module.rs b/src/parse/module.rs index 0e97b0f..6032026 100644 --- a/src/parse/module.rs +++ b/src/parse/module.rs @@ -143,7 +143,7 @@ impl<'a> Parser<'a> { .into()); } - (Module::new_from_scope(global_scope), stmts) + (Module::new_from_scope(global_scope, false), stmts) } else { return Err(("Can't find stylesheet to import.", self.span_before).into()); } diff --git a/src/parse/style.rs b/src/parse/style.rs index 0c40a66..0e10bd1 100644 --- a/src/parse/style.rs +++ b/src/parse/style.rs @@ -111,43 +111,62 @@ impl<'a> Parser<'a> { let mut property = self.parse_identifier()?.node; let whitespace_after_property = self.whitespace(); - if let Some(Token { kind: ':', .. }) = self.toks.peek() { - self.toks.next(); - if let Some(Token { kind, .. }) = self.toks.peek() { - return Ok(match kind { - ':' => { - if whitespace_after_property { - property.push(' '); - } - property.push(':'); - SelectorOrStyle::Selector(property) - } - c if is_name(*c) => { - if let Some(toks) = self.parse_style_value_when_no_space_after_semicolon() { - let len = toks.len(); - if let Ok(val) = self.parse_value_from_vec(toks, false) { - self.toks.take(len).for_each(drop); - return Ok(SelectorOrStyle::Style( - InternedString::get_or_intern(property), - Some(Box::new(val)), - )); + match self.toks.peek() { + Some(Token { kind: ':', .. }) => { + self.toks.next(); + if let Some(Token { kind, .. }) = self.toks.peek() { + return Ok(match kind { + ':' => { + if whitespace_after_property { + property.push(' '); } + property.push(':'); + SelectorOrStyle::Selector(property) } + c if is_name(*c) => { + if let Some(toks) = + self.parse_style_value_when_no_space_after_semicolon() + { + let len = toks.len(); + if let Ok(val) = self.parse_value_from_vec(toks, false) { + self.toks.take(len).for_each(drop); + return Ok(SelectorOrStyle::Style( + InternedString::get_or_intern(property), + Some(Box::new(val)), + )); + } + } - if whitespace_after_property { - property.push(' '); + if whitespace_after_property { + property.push(' '); + } + property.push(':'); + return Ok(SelectorOrStyle::Selector(property)); } - property.push(':'); - return Ok(SelectorOrStyle::Selector(property)); + _ => SelectorOrStyle::Style(InternedString::get_or_intern(property), None), + }); + } + } + Some(Token { kind: '.', .. }) => { + if matches!(self.toks.peek_next(), Some(Token { kind: '$', .. })) { + self.toks.next(); + self.toks.next(); + return Ok(SelectorOrStyle::ModuleVariableRedeclaration( + property.into(), + )); + } else { + if whitespace_after_property { + property.push(' '); } - _ => SelectorOrStyle::Style(InternedString::get_or_intern(property), None), - }); + return Ok(SelectorOrStyle::Selector(property)); + } } - } else { - if whitespace_after_property { - property.push(' '); + _ => { + if whitespace_after_property { + property.push(' '); + } + return Ok(SelectorOrStyle::Selector(property)); } - return Ok(SelectorOrStyle::Selector(property)); } Err(("expected \"{\".", self.span_before).into()) } diff --git a/src/parse/variable.rs b/src/parse/variable.rs index 6a7429c..a5a77e4 100644 --- a/src/parse/variable.rs +++ b/src/parse/variable.rs @@ -8,10 +8,10 @@ use crate::{ use super::Parser; #[derive(Debug)] -struct VariableValue { - val_toks: Vec, - global: bool, - default: bool, +pub(crate) struct VariableValue { + pub val_toks: Vec, + pub global: bool, + pub default: bool, } impl VariableValue { @@ -88,7 +88,7 @@ impl<'a> Parser<'a> { Ok(()) } - fn parse_variable_value(&mut self) -> SassResult { + pub(super) fn parse_variable_value(&mut self) -> SassResult { let mut default = false; let mut global = false; diff --git a/src/scope.rs b/src/scope.rs index 82f27f0..dc968e2 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -81,7 +81,7 @@ impl Scope { } pub fn merge_module(&mut self, other: Module) { - self.merge(other.0); + self.merge(other.scope); } } diff --git a/tests/use.rs b/tests/use.rs index 035a86d..7c83b2e 100644 --- a/tests/use.rs +++ b/tests/use.rs @@ -253,3 +253,66 @@ fn use_with_same_variable_multiple_times() { input ); } + +#[test] +fn use_variable_redeclaration_var_dne() { + let input = "@use \"use_variable_redeclaration_var_dne\" as mod;\nmod.$a: red;"; + tempfile!("use_variable_redeclaration_var_dne.scss", ""); + + assert_err!("Error: Undefined variable.", input); +} + +#[test] +fn use_variable_redeclaration_global() { + let input = "@use \"use_variable_redeclaration_global\" as mod;\nmod.$a: red !global;"; + tempfile!("use_variable_redeclaration_global.scss", "$a: green;"); + + assert_err!( + "Error: !global isn't allowed for variables in other modules.", + input + ); +} + +#[test] +fn use_variable_redeclaration_simple() { + let input = + "@use \"use_variable_redeclaration_simple\" as mod;\nmod.$a: red; a { color: mod.$a; }"; + tempfile!("use_variable_redeclaration_simple.scss", "$a: green;"); + + assert_eq!( + "a {\n color: red;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default()).expect(input) + ); +} + +#[test] +fn use_variable_redeclaration_default() { + let input = "@use \"use_variable_redeclaration_default\" as mod;\nmod.$a: 1 % red !default; a { color: mod.$a; }"; + tempfile!("use_variable_redeclaration_default.scss", "$a: green;"); + + assert_eq!( + "a {\n color: green;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default()).expect(input) + ); +} + +#[test] +fn use_variable_redeclaration_private() { + let input = "@use \"use_variable_redeclaration_private\" as mod;\nmod.$-a: red;"; + tempfile!("use_variable_redeclaration_private.scss", "$a: green;"); + + assert_err!( + "Error: Private members can't be accessed from outside their modules.", + input + ); +} + +#[test] +fn use_variable_redeclaration_builtin() { + let input = "@use \"sass:math\";\nmath.$e: red;"; + + assert_err!( + "Error: Cannot modify built-in variable.", + input + ); +}