diff --git a/src/builtin/functions/meta.rs b/src/builtin/functions/meta.rs index 5ae44a2..1bdc1b9 100644 --- a/src/builtin/functions/meta.rs +++ b/src/builtin/functions/meta.rs @@ -1,5 +1,7 @@ use super::{Builtin, GlobalFunctionMap, GLOBAL_FUNCTIONS}; +use codemap::Spanned; + use crate::{ args::CallArgs, common::{Identifier, QuoteKind}, @@ -220,7 +222,10 @@ pub(crate) fn get_function(mut args: CallArgs, parser: &mut Parser<'_>) -> SassR parser .modules .get(module_name.into(), args.span())? - .get_fn(name) + .get_fn(Spanned { + node: name, + span: args.span(), + })? } else { parser.scopes.get_fn(name, parser.global_scope) } { diff --git a/src/builtin/modules/mod.rs b/src/builtin/modules/mod.rs index 7ec2240..aa47e54 100644 --- a/src/builtin/modules/mod.rs +++ b/src/builtin/modules/mod.rs @@ -51,6 +51,14 @@ impl Modules { impl Module { pub fn get_var(&self, name: Spanned) -> SassResult<&Value> { + if name.node.as_str().starts_with('-') { + return Err(( + "Private members can't be accessed from outside their modules.", + name.span, + ) + .into()); + } + match self.0.vars.get(&name.node) { Some(v) => Ok(v), None => Err(("Undefined variable.", name.span).into()), @@ -61,16 +69,24 @@ impl Module { self.0.vars.insert(name.into(), value); } - pub fn get_fn(&self, name: Identifier) -> Option { - self.0.functions.get(&name).cloned() + pub fn get_fn(&self, name: Spanned) -> SassResult> { + if name.node.as_str().starts_with('-') { + return Err(( + "Private members can't be accessed from outside their modules.", + name.span, + ) + .into()); + } + + Ok(self.0.functions.get(&name.node).cloned()) } pub fn var_exists(&self, name: Identifier) -> bool { - self.0.var_exists(name) + !name.as_str().starts_with('-') && self.0.var_exists(name) } pub fn mixin_exists(&self, name: Identifier) -> bool { - self.0.mixin_exists(name) + !name.as_str().starts_with('-') && self.0.mixin_exists(name) } pub fn insert_builtin( @@ -89,6 +105,7 @@ impl Module { self.0 .functions .iter() + .filter(|(key, _)| !key.as_str().starts_with('-')) .map(|(key, value)| { ( Value::String(key.to_string(), QuoteKind::Quoted), @@ -104,6 +121,7 @@ impl Module { self.0 .vars .iter() + .filter(|(key, _)| !key.as_str().starts_with('-')) .map(|(key, value)| { ( Value::String(key.to_string(), QuoteKind::Quoted), diff --git a/src/parse/value/parse.rs b/src/parse/value/parse.rs index b70f66d..d58d37a 100644 --- a/src/parse/value/parse.rs +++ b/src/parse/value/parse.rs @@ -271,7 +271,7 @@ impl<'a> Parser<'a> { let function = self .modules .get(module.into(), module_span)? - .get_fn(fn_name.node) + .get_fn(fn_name)? .ok_or(("Undefined function.", fn_name.span))?; if !matches!(self.toks.next(), Some(Token { kind: '(', .. })) { diff --git a/tests/imports.rs b/tests/imports.rs index c0888d0..9210585 100644 --- a/tests/imports.rs +++ b/tests/imports.rs @@ -26,17 +26,8 @@ fn import_no_semicolon() { fn import_no_quotes() { let input = "@import import_no_quotes"; tempfile!("import_no_quotes", "$a: red;"); - match grass::from_string(input.to_string(), &grass::Options::default()) { - Ok(..) => panic!("did not fail"), - Err(e) => assert_eq!( - "Error: Expected string.", - e.to_string() - .chars() - .take_while(|c| *c != '\n') - .collect::() - .as_str() - ), - } + + assert_err!("Error: Expected string.", input); } #[test] diff --git a/tests/macros.rs b/tests/macros.rs index da4aa99..9e4d3c2 100644 --- a/tests/macros.rs +++ b/tests/macros.rs @@ -85,3 +85,20 @@ macro_rules! tempfile { write!(f, "{}", $content).unwrap(); }; } + +#[macro_export] +macro_rules! assert_err { + ($err:literal, $input:expr) => { + match grass::from_string($input.to_string(), &grass::Options::default()) { + Ok(..) => panic!("did not fail"), + Err(e) => assert_eq!( + $err, + e.to_string() + .chars() + .take_while(|c| *c != '\n') + .collect::() + .as_str() + ), + } + }; +} diff --git a/tests/use.rs b/tests/use.rs index 5998c09..f5e1b3f 100644 --- a/tests/use.rs +++ b/tests/use.rs @@ -74,6 +74,68 @@ fn use_user_defined_same_directory() { ); } +#[test] +fn private_variable_begins_with_underscore() { + let input = "@use \"private_variable_begins_with_underscore\" as module;\na {\n color: module.$_foo;\n}"; + tempfile!( + "private_variable_begins_with_underscore.scss", + "$_foo: red; a { color: $_foo; }" + ); + + assert_err!( + "Error: Private members can't be accessed from outside their modules.", + input + ); +} + +#[test] +fn private_variable_begins_with_hyphen() { + let input = + "@use \"private_variable_begins_with_hyphen\" as module;\na {\n color: module.$-foo;\n}"; + tempfile!( + "private_variable_begins_with_hyphen.scss", + "$-foo: red; a { color: $-foo; }" + ); + + assert_err!( + "Error: Private members can't be accessed from outside their modules.", + input + ); +} + +#[test] +fn private_function() { + let input = "@use \"private_function\" as module;\na {\n color: module._foo(green);\n}"; + tempfile!( + "private_function.scss", + "@function _foo($a) { @return $a; } a { color: _foo(red); }" + ); + + assert_err!( + "Error: Private members can't be accessed from outside their modules.", + input + ); +} + +#[test] +fn global_variable_exists_private() { + let input = r#" + @use "global_variable_exists_private" as module; + a { + color: global-variable-exists($name: foo, $module: module); + color: global-variable-exists($name: _foo, $module: module); + }"#; + tempfile!( + "global_variable_exists_private.scss", + "$foo: red;\n$_foo: red;\n" + ); + + assert_eq!( + "a {\n color: true;\n color: false;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default()).expect(input) + ); +} + #[test] fn use_user_defined_as() { let input = "@use \"use_user_defined_as\" as module;\na {\n color: module.$a;\n}";