mixins and functions can use their module's scope

This commit is contained in:
Connor Skees 2021-07-31 11:42:20 -04:00
parent c86b5f82e9
commit 1f672c4c49
8 changed files with 146 additions and 41 deletions

View File

@ -283,7 +283,7 @@ pub(crate) fn call(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
.into())
}
};
func.call(args.decrement(), parser)
func.call(args.decrement(), None, parser)
}
#[allow(clippy::needless_pass_by_value)]

View File

@ -14,8 +14,16 @@ use crate::{
use super::{common::ContextFlags, Parser, Stmt};
/// Names that functions are not allowed to have
const RESERVED_IDENTIFIERS: [&str; 7] =
["calc", "element", "expression", "url", "and", "or", "not"];
const RESERVED_IDENTIFIERS: [&str; 8] = [
"calc",
"element",
"expression",
"url",
"and",
"or",
"not",
"clamp",
];
impl<'a, 'b> Parser<'a, 'b> {
pub(super) fn parse_function(&mut self) -> SassResult<()> {
@ -56,16 +64,15 @@ impl<'a, 'b> Parser<'a, 'b> {
let name_as_ident = Identifier::from(name);
let sass_function = SassFunction::UserDefined {
function: Box::new(function),
name: name_as_ident,
};
if self.at_root {
self.global_scope.insert_fn(
name_as_ident,
SassFunction::UserDefined(Box::new(function), name_as_ident),
);
self.global_scope.insert_fn(name_as_ident, sass_function);
} else {
self.scopes.insert_fn(
name_as_ident,
SassFunction::UserDefined(Box::new(function), name_as_ident),
);
self.scopes.insert_fn(name_as_ident, sass_function);
}
Ok(())
}
@ -78,7 +85,12 @@ impl<'a, 'b> Parser<'a, 'b> {
Ok(Box::new(v.node))
}
pub fn eval_function(&mut self, function: Function, args: CallArgs) -> SassResult<Value> {
pub fn eval_function(
&mut self,
function: Function,
args: CallArgs,
module: Option<Spanned<Identifier>>,
) -> SassResult<Value> {
let Function {
body,
args: fn_args,
@ -97,6 +109,16 @@ impl<'a, 'b> Parser<'a, 'b> {
self.scopes.enter_scope(scope);
};
if let Some(module) = module {
let module = self.modules.get(module.node, module.span)?;
if declared_at_root {
new_scope.enter_scope(module.scope.clone());
} else {
self.scopes.enter_scope(module.scope.clone());
}
}
let mut return_value = Parser {
toks: &mut Lexer::new(body),
map: self.map,
@ -125,6 +147,10 @@ impl<'a, 'b> Parser<'a, 'b> {
self.scopes.exit_scope();
}
if module.is_some() {
self.scopes.exit_scope();
}
debug_assert!(
return_value.len() <= 1,
"we expect there to be only one return value"

View File

@ -73,15 +73,18 @@ impl<'a, 'b> Parser<'a, 'b> {
self.whitespace_or_comment();
let name = self.parse_identifier()?.map_node(Into::into);
let mixin = if self.consume_char_if_exists('.') {
let (mixin, module) = if self.consume_char_if_exists('.') {
let module = name;
let name = self.parse_identifier()?.map_node(Into::into);
(
self.modules
.get(module.node, module.span)?
.get_mixin(name)?
.get_mixin(name)?,
Some(module),
)
} else {
self.scopes.get_mixin(name, self.global_scope)?
(self.scopes.get_mixin(name, self.global_scope)?, None)
};
self.whitespace_or_comment();
@ -152,6 +155,11 @@ impl<'a, 'b> Parser<'a, 'b> {
self.scopes.enter_scope(scope);
if let Some(module) = module {
let module = self.modules.get(module.node, module.span)?;
self.scopes.enter_scope(module.scope.clone());
}
self.content.push(Content {
content,
content_args,
@ -180,6 +188,11 @@ impl<'a, 'b> Parser<'a, 'b> {
.parse_stmt()?;
self.content.pop();
if module.is_some() {
self.scopes.exit_scope();
}
self.scopes.exit_scope();
if declared_at_root {

View File

@ -7,7 +7,7 @@ use num_traits::Zero;
use crate::{
args::CallArgs,
common::{Op, QuoteKind},
common::{Identifier, Op, QuoteKind},
error::SassResult,
unit::Unit,
value::{SassFunction, Value},
@ -19,7 +19,7 @@ use super::super::Parser;
pub(crate) enum HigherIntermediateValue {
Literal(Value),
/// A function that hasn't yet been evaluated
Function(SassFunction, CallArgs),
Function(SassFunction, CallArgs, Option<Spanned<Identifier>>),
BinaryOp(Box<Self>, Op, Box<Self>),
UnaryOp(Op, Box<Self>),
}
@ -31,8 +31,13 @@ impl HigherIntermediateValue {
}
impl<'a, 'b> Parser<'a, 'b> {
fn call_function(&mut self, function: SassFunction, args: CallArgs) -> SassResult<Value> {
function.call(args, self)
fn call_function(
&mut self,
function: SassFunction,
args: CallArgs,
module: Option<Spanned<Identifier>>,
) -> SassResult<Value> {
function.call(args, module, self)
}
}
@ -54,8 +59,8 @@ impl<'a, 'b: 'a, 'c> ValueVisitor<'a, 'b, 'c> {
HigherIntermediateValue::Literal(v) => Ok(v),
HigherIntermediateValue::BinaryOp(v1, op, v2) => self.bin_op(*v1, op, *v2, in_parens),
HigherIntermediateValue::UnaryOp(op, val) => self.unary_op(op, *val, in_parens),
HigherIntermediateValue::Function(function, args) => {
self.parser.call_function(function, args)
HigherIntermediateValue::Function(function, args, module) => {
self.parser.call_function(function, args, module)
}
}
}
@ -216,8 +221,8 @@ impl<'a, 'b: 'a, 'c> ValueVisitor<'a, 'b, 'c> {
HigherIntermediateValue::UnaryOp(op, val) => {
HigherIntermediateValue::Literal(self.unary_op(op, *val, in_parens)?)
}
HigherIntermediateValue::Function(function, args) => {
HigherIntermediateValue::Literal(self.parser.call_function(function, args)?)
HigherIntermediateValue::Function(function, args, module) => {
HigherIntermediateValue::Literal(self.parser.call_function(function, args, module)?)
}
val => val,
})
@ -301,7 +306,6 @@ impl<'a, 'b: 'a, 'c> ValueVisitor<'a, 'b, 'c> {
.into());
}
if unit == unit2 {
// dbg!(&num, &num2, num.clone() + num2.clone(), unit.clone(), unit2.clone());
Value::Dimension(Some(num + num2), unit, true)
} else if unit == Unit::None {
Value::Dimension(Some(num + num2), unit2, true)

View File

@ -198,8 +198,7 @@ impl<'a, 'b> Parser<'a, 'b> {
#[allow(clippy::eval_order_dependence)]
fn parse_module_item(
&mut self,
module: &str,
mut module_span: Span,
mut module: Spanned<Identifier>,
) -> SassResult<Spanned<IntermediateValue>> {
Ok(
IntermediateValue::Value(if self.consume_char_if_exists('$') {
@ -207,9 +206,9 @@ impl<'a, 'b> Parser<'a, 'b> {
.parse_identifier_no_interpolation(false)?
.map_node(|i| i.into());
module_span = module_span.merge(var.span);
module.span = module.span.merge(var.span);
let value = self.modules.get(module.into(), module_span)?.get_var(var)?;
let value = self.modules.get(module.node, module.span)?.get_var(var)?;
HigherIntermediateValue::Literal(value.clone())
} else {
let fn_name = self
@ -218,7 +217,7 @@ impl<'a, 'b> Parser<'a, 'b> {
let function = self
.modules
.get(module.into(), module_span)?
.get(module.node, module.span)?
.get_fn(fn_name)?
.ok_or(("Undefined function.", fn_name.span))?;
@ -226,9 +225,9 @@ impl<'a, 'b> Parser<'a, 'b> {
let call_args = self.parse_call_args()?;
HigherIntermediateValue::Function(function, call_args)
HigherIntermediateValue::Function(function, call_args, Some(module))
})
.span(module_span),
.span(module.span),
)
}
@ -260,6 +259,7 @@ impl<'a, 'b> Parser<'a, 'b> {
return Ok(IntermediateValue::Value(HigherIntermediateValue::Function(
SassFunction::Builtin(f.clone(), as_ident),
self.parse_call_args()?,
None,
))
.span(self.span_before));
}
@ -297,7 +297,7 @@ impl<'a, 'b> Parser<'a, 'b> {
let call_args = self.parse_call_args()?;
Ok(
IntermediateValue::Value(HigherIntermediateValue::Function(func, call_args))
IntermediateValue::Value(HigherIntermediateValue::Function(func, call_args, None))
.span(self.span_before),
)
}
@ -336,7 +336,10 @@ impl<'a, 'b> Parser<'a, 'b> {
Some(Token { kind: '.', .. }) => {
if !predicate(self) {
self.toks.next();
return self.parse_module_item(&s, span);
return self.parse_module_item(Spanned {
node: s.into(),
span,
});
}
}
_ => {}

View File

@ -13,7 +13,7 @@ use crate::{
/// A singular scope
///
/// Contains variables, functions, and mixins
#[derive(Debug, Default)]
#[derive(Debug, Default, Clone)]
pub(crate) struct Scope {
pub vars: BTreeMap<Identifier, Value>,
pub mixins: BTreeMap<Identifier, Mixin>,

View File

@ -11,6 +11,8 @@
use std::fmt;
use codemap::Spanned;
use crate::{
args::CallArgs, atrule::Function, builtin::Builtin, common::Identifier, error::SassResult,
parse::Parser, value::Value,
@ -25,7 +27,10 @@ use crate::{
#[derive(Clone, Eq, PartialEq)]
pub(crate) enum SassFunction {
Builtin(Builtin, Identifier),
UserDefined(Box<Function>, Identifier),
UserDefined {
function: Box<Function>,
name: Identifier,
},
}
impl SassFunction {
@ -34,7 +39,7 @@ impl SassFunction {
/// Used mainly in debugging and `inspect()`
pub fn name(&self) -> &Identifier {
match self {
Self::Builtin(_, name) | Self::UserDefined(_, name) => name,
Self::Builtin(_, name) | Self::UserDefined { name, .. } => name,
}
}
@ -44,14 +49,19 @@ impl SassFunction {
fn kind(&self) -> &'static str {
match &self {
Self::Builtin(..) => "Builtin",
Self::UserDefined(..) => "UserDefined",
Self::UserDefined { .. } => "UserDefined",
}
}
pub fn call(self, args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
pub fn call(
self,
args: CallArgs,
module: Option<Spanned<Identifier>>,
parser: &mut Parser,
) -> SassResult<Value> {
match self {
Self::Builtin(f, ..) => f.0(args, parser),
Self::UserDefined(f, ..) => parser.eval_function(*f, args),
Self::UserDefined { function, .. } => parser.eval_function(*function, args, module),
}
}
}

View File

@ -369,6 +369,55 @@ fn use_modules_imported_by_other_modules_does_not_cause_conflict() {
);
}
#[test]
fn use_mixin_can_use_scope_from_own_module() {
let input = r#"
@use "use_mixin_can_use_scope_from_own_module__a" as a;
@include a.foo();
"#;
tempfile!(
"use_mixin_can_use_scope_from_own_module__a.scss",
"$a: red;
@mixin foo() {
a {
color: $a;
}
}"
);
assert_eq!(
"a {\n color: red;\n}\n",
&grass::from_string(input.to_string(), &grass::Options::default()).expect(input)
);
}
#[test]
fn use_function_can_use_scope_from_own_module() {
let input = r#"
@use "use_function_can_use_scope_from_own_module__a" as a;
a {
color: a.foo();
}
"#;
tempfile!(
"use_function_can_use_scope_from_own_module__a.scss",
"$a: red;
@function foo() {
@return $a;
}"
);
assert_eq!(
"a {\n color: red;\n}\n",
&grass::from_string(input.to_string(), &grass::Options::default()).expect(input)
);
}
#[test]
fn use_variable_redeclaration_builtin() {
let input = "@use \"sass:math\";\nmath.$e: red;";