use std::path::Path;
use std::process::Command;
use sasso::{compile, Options};
const MATCHING: &[&str] = &[
"debug-string",
"debug-values",
"warn-plain",
"warn-interpolated",
"warn-in-mixin",
"error-plain",
"error-interpolated",
"error-in-mixin",
"error-stack-nested",
"error-in-function",
"error-cross-file",
"compile-undefined-variable",
"compile-undefined-variable-stack",
"compile-tab-expansion",
"compile-gutter-alignment",
"deprecation-import",
"deprecation-global-builtin",
"deprecation-feature-exists",
"deprecation-call-string",
"deprecation-color-functions",
"deprecation-darken",
"deprecation-if-function",
];
fn fixtures_dir() -> std::path::PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/diagnostics")
}
fn run_sasso_stderr(name: &str) -> String {
let bin = env!("CARGO_BIN_EXE_sasso");
let out = Command::new(bin)
.current_dir(fixtures_dir())
.arg("--no-unicode")
.arg(format!("{name}.scss"))
.output()
.expect("run sasso");
String::from_utf8_lossy(&out.stderr).into_owned()
}
#[test]
fn diagnostics_match_dart_ascii_fixtures() {
let dir = fixtures_dir();
for name in MATCHING {
let expected = std::fs::read_to_string(dir.join(format!("{name}.ascii.stderr")))
.unwrap_or_else(|e| panic!("read {name}.ascii.stderr: {e}"));
let got = run_sasso_stderr(name);
assert_eq!(got, expected, "stderr mismatch for fixture {name}");
}
}
#[test]
fn diagnostics_exit_codes() {
let bin = env!("CARGO_BIN_EXE_sasso");
let dir = fixtures_dir();
let error_code = Command::new(bin)
.current_dir(&dir)
.arg("--no-unicode")
.arg("error-plain.scss")
.output()
.expect("run")
.status
.code();
assert_eq!(error_code, Some(65), "@error must exit 65");
let warn_code = Command::new(bin)
.current_dir(&dir)
.arg("--no-unicode")
.arg("warn-plain.scss")
.output()
.expect("run")
.status
.code();
assert_eq!(warn_code, Some(0), "@warn must exit 0");
}
#[test]
fn invalid_utf8_input_exits_65() {
let bin = env!("CARGO_BIN_EXE_sasso");
let dir = std::env::temp_dir().join(format!("sasso_invalid_utf8_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("create temp dir");
let input = dir.join("input.scss");
std::fs::write(&input, b"foo{;\xF6\xFC").expect("write invalid utf8");
let out = Command::new(bin).arg(&input).output().expect("run");
let _ = std::fs::remove_dir_all(&dir);
assert_eq!(out.status.code(), Some(65));
assert_eq!(String::from_utf8_lossy(&out.stderr), "Error: Invalid UTF-8.\n");
}
fn err_block(src: &str, url: &str) -> String {
compile(src, &Options::default().with_url(url))
.expect_err("expected a compile error")
.to_string()
}
fn caret_line(block: &str) -> String {
block
.lines()
.find(|l| l.contains('^'))
.unwrap_or_else(|| panic!("no caret line in:\n{block}"))
.rsplit('\u{2502}')
.next()
.unwrap_or_default()
.trim()
.to_string()
}
#[test]
fn a_module_diagnostic_carets_the_construct_it_is_about() {
let cases = [
("@use \"nope\";\n", "1 \u{2502} @use \"nope\";\n", "^^^^^^^^^^^\n"),
(
"@forward \"nope\";\n",
"1 \u{2502} @forward \"nope\";\n",
"^^^^^^^^^^^^^^^\n",
),
(
".a { b: c }\n@use \"nope\" ;\n",
"2 \u{2502} @use \"nope\" ;\n",
"^^^^^^^^^^^^^^\n",
),
(
".a { @include nope; }\n",
"1 \u{2502} .a { @include nope; }\n",
"^^^^^^^^^^^^^\n",
),
(
".a { @include nope(1); }\n",
"1 \u{2502} .a { @include nope(1); }\n",
"^^^^^^^^^^^^^^^^\n",
),
(
".a { @include nope using ($x) { c: $x; } }\n",
"1 \u{2502} .a { @include nope using ($x) { c: $x; } }\n",
"^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
),
(
".a { @include nope { c: d; } }\n",
"1 \u{2502} .a { @include nope { c: d; } }\n",
"^^^^^^^^^^^^^^^^^^^^^^^\n",
),
(
"@use \"nope\" with ($x: 1) ;\n",
"1 \u{2502} @use \"nope\" with ($x: 1) ;\n",
"^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
),
(
"@forward \"nope\" with ($x: 1) ;\n",
"1 \u{2502} @forward \"nope\" with ($x: 1) ;\n",
"^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
),
(
"@use \"sass:math\";\n.a { b: math.nope(1); }\n",
"2 \u{2502} .a { b: math.nope(1); }\n",
"^^^^^^^^^^^^\n",
),
(
".a { b: nope.foo(1); }\n",
"1 \u{2502} .a { b: nope.foo(1); }\n",
"^^^^^^^^^^^\n",
),
(
".a { @include nope.pub; }\n",
"1 \u{2502} .a { @include nope.pub; }\n",
"^^^^^^^^^^^^^^^^^\n",
),
(
"@use \"sass:meta\";\n.a { @include meta.load-css(\"nope\"); }\n",
"2 \u{2502} .a { @include meta.load-css(\"nope\"); }\n",
"^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
),
];
for (src, line, caret) in cases {
let block = err_block(src, "in.scss");
assert!(block.contains(line), "{src:?}\n{block}");
assert_eq!(caret_line(&block), caret.trim(), "{src:?}\n{block}");
}
assert!(
err_block(".a { @include nope; }\n", "in.scss").starts_with("Error: Undefined mixin.\n"),
"{}",
err_block(".a { @include nope; }\n", "in.scss")
);
assert!(
err_block("@import \"nope\";\n", "in.scss").contains("Error: Can't find stylesheet to import.\n"),
"{}",
err_block("@import \"nope\";\n", "in.scss")
);
}
#[test]
fn a_namespaced_member_diagnostic_points_at_the_reference() {
let dir = std::env::temp_dir().join(format!("sasso_ns_diag_{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("mkdir");
std::fs::write(
dir.join("_lib.scss"),
"@mixin -priv { a: b; }\n$pub: 1;\n$-pv: 1;\n@mixin -pr\\69 v { a: b; }\n",
)
.expect("write");
let entry = dir.join("in.scss");
let url = entry.to_string_lossy().into_owned();
let imp = sasso::FsImporter::new(Vec::new());
let run = |src: &str| {
std::fs::write(&entry, src).unwrap();
compile(src, &Options::default().with_importer(&imp).with_url(&url))
.expect_err("expected a compile error")
.to_string()
};
let block = run("@use \"lib\";\n.a { b: lib.$nope; }\n");
assert!(block.contains("2 \u{2502} .a { b: lib.$nope; }\n"), "{block}");
assert_eq!(caret_line(&block), "^^^^^^^^^", "{block}");
assert!(block.contains("in.scss 2:9"), "{block}");
let block = run("@use \"lib\";\n.a { b: lib.nope(1); }\n");
assert_eq!(caret_line(&block), "^^^^^^^^^^^", "{block}");
let block = run("@use \"lib\";\n.a { @include lib.-priv; }\n");
assert_eq!(caret_line(&block), "^^^^^", "{block}");
assert!(block.contains("in.scss 2:19"), "{block}");
let block = run("@use \"lib\";\n.a { @include lib.-pr\\69 v; }\n");
assert_eq!(caret_line(&block), "^^^^^^^^", "{block}");
let block = run("@use \"lib\";\n.a { b: lib.$-pv; }\n");
assert_eq!(caret_line(&block), "^^^^^^^^", "{block}");
assert!(block.contains("in.scss 2:9"), "{block}");
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn a_span_that_crosses_lines_ends_where_dart_ends_it() {
let block = err_block(
".a {\r\n @include nope {\r\n c: d;\r\n }\r\n}\r\n",
"in.scss",
);
assert!(block.contains("4 \u{2502} \u{2514} }\n"), "{block}");
assert!(!block.contains("5 \u{2502}"), "{block}");
let lf = err_block(".a {\n @include nope {\n c: d;\n }\n}\n", "in.scss");
assert_eq!(block.replace("\r", ""), lf);
}
#[test]
fn an_indented_include_carets_its_own_line() {
let block = compile(
".a\n @include nope\n c: d\n",
&Options::default()
.with_syntax(sasso::Syntax::Sass)
.with_url("in.sass"),
)
.expect_err("expected a compile error")
.to_string();
assert_eq!(caret_line(&block), "^^^^^^^^^^^^^", "{block}");
assert!(block.contains("in.sass 2:3"), "{block}");
}
#[test]
fn an_escaped_member_name_is_not_private() {
let dir = std::env::temp_dir().join(format!("sasso_esc_priv_{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("mkdir");
std::fs::write(
dir.join("_lib.scss"),
"@mixin -priv { a: b; }\n@function -pf() { @return 1; }\n$-pv: 1;\n",
)
.expect("write");
let entry = dir.join("in.scss");
let url = entry.to_string_lossy().into_owned();
let imp = sasso::FsImporter::new(Vec::new());
let run = |src: &str| {
std::fs::write(&entry, src).unwrap();
compile(src, &Options::default().with_importer(&imp).with_url(&url))
.expect_err("expected a compile error")
.to_string()
};
let block = run("@use \"lib\";\n.a { @include lib.\\2d priv; }\n");
assert!(block.starts_with("Error: Undefined mixin.\n"), "{block}");
assert_eq!(caret_line(&block), "^^^^^^^^^^^^^^^^^^^^^", "{block}");
let block = run("@use \"lib\";\n.a { b: lib.$\\2d pv; }\n");
assert!(block.starts_with("Error: Undefined variable.\n"), "{block}");
assert_eq!(caret_line(&block), "^^^^^^^^^^^", "{block}");
let block = run("@use \"lib\";\n.a { b: lib.\\2d pf(); }\n");
assert!(block.starts_with("Error: Undefined function.\n"), "{block}");
let block = run("@use \"lib\";\n.a { @include lib.-priv; }\n");
assert!(
block.starts_with("Error: Private members can't be accessed from outside their modules.\n"),
"{block}"
);
let block = run(
"@use \"sass:meta\";\n@use \"lib\";\n.a { b: meta.call(meta.get-function(\"-pf\", $module: \"lib\")); }\n",
);
assert!(
block.starts_with("Error: Function not found: \"-pf\"\n"),
"{block}"
);
assert_eq!(caret_line(&block), "^".repeat(40), "{block}");
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn a_missing_argument_points_at_the_invocation() {
let dir = std::env::temp_dir().join(format!("sasso_missing_arg_{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("mkdir");
std::fs::write(
dir.join("_lib.scss"),
"@function f($x) { @return $x; }\n@mixin m($x) { a: $x; }\n",
)
.expect("write");
let entry = dir.join("in.scss");
let url = entry.to_string_lossy().into_owned();
let imp = sasso::FsImporter::new(Vec::new());
let run = |src: &str| {
std::fs::write(&entry, src).unwrap();
compile(src, &Options::default().with_importer(&imp).with_url(&url))
.expect_err("expected a compile error")
.to_string()
};
let block = run("@function f($x) { @return $x; }\n.a { b: f(); }\n");
assert!(block.starts_with("Error: Missing argument $x.\n"), "{block}");
assert!(block.contains("2 \u{2502} .a { b: f(); }\n"), "{block}");
assert_eq!(caret_line(&block), "^^^ invocation", "{block}");
assert!(
block.contains("1 \u{2502} @function f($x) { @return $x; }\n"),
"{block}"
);
assert!(
block.contains("\u{2501}\u{2501}\u{2501}\u{2501}\u{2501} declaration"),
"{block}"
);
let block = run("@use \"lib\";\n.a { b: lib.f(); }\n");
assert!(block.contains("2 \u{2502} .a { b: lib.f(); }\n"), "{block}");
assert_eq!(caret_line(&block), "^^^^^^^ invocation", "{block}");
assert!(block.contains("in.scss 2:9 f()"), "{block}");
let block = run("@use \"lib\";\n.a { @include lib.m; }\n");
assert!(block.contains("2 \u{2502} .a { @include lib.m; }\n"), "{block}");
assert_eq!(caret_line(&block), "^^^^^^^^^^^^^^ invocation", "{block}");
let block = run(
"@use \"sass:meta\";\n@function f($x) { @return $x; }\n.a { b: meta.call(meta.get-function(\"f\")); }\n",
);
assert_eq!(
caret_line(&block),
format!("{} invocation", "^".repeat(33)),
"{block}"
);
std::fs::write(dir.join("_fwd.scss"), "@forward \"sass:math\";\n").expect("write");
let block = run("@use \"fwd\";\n.a { b: fwd.div(1); }\n");
assert_eq!(caret_line(&block), "^^^^^^^^^^", "{block}");
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn a_multi_line_span_uses_the_gutter_at_a_line_edge() {
let both = err_block(".a {\n @include nope {\n c: d;\n }\n}\n", "in.scss");
assert!(
both.contains(
"2 \u{2502} \u{250c} @include nope {\n3 \u{2502} \u{2502} c: d;\n4 \u{2502} \u{2514} }\n"
),
"{both}"
);
assert!(!both.contains('^'), "{both}");
let open = err_block(".a { @include nope {\n c: d;\n }\n}\n", "in.scss");
assert!(
open.contains("\u{2502} \u{250c}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}^\n"),
"{open}"
);
assert!(open.contains("3 \u{2502} \u{2514} }\n"), "{open}");
let close = err_block(".a {\n @include nope {\n c: d;\n } x: y;\n}\n", "in.scss");
assert!(
close.contains("2 \u{2502} \u{250c} @include nope {\n"),
"{close}"
);
assert!(
close.contains("\u{2502} \u{2514}\u{2500}\u{2500}\u{2500}^\n"),
"{close}"
);
}
#[test]
fn an_error_at_the_end_of_a_file_points_at_the_last_line_with_content() {
for (src, line, caret_col) in [
(".a { b: c\n", 1, 10),
(".a {\n", 1, 5),
("@media screen {\n .a { b: c }\n", 2, 14),
(".a { b: c; \n\n\n", 1, 12),
] {
let block = err_block(src, "in.scss");
assert!(block.starts_with("Error: expected \"}\".\n"), "{src:?}\n{block}");
assert!(
block.contains(&format!("in.scss {line}:{caret_col}")),
"{src:?}\n{block}"
);
assert!(block.contains(&format!("{line} \u{2502} ")), "{src:?}\n{block}");
}
}
#[test]
fn a_value_that_cannot_start_reports_what_was_expected() {
for src in [
".a { b: ; }\n",
".a { b: 1 + ; }\n",
".a { b: ) }\n",
"@if { a: b; }\n",
".a { b: \n",
".a { b: 1 +\n",
] {
let block = err_block(src, "in.scss");
assert!(
block.starts_with("Error: Expected expression.\n"),
"{src:?}\n{block}"
);
}
}
fn warnings(src: &str, url: &str) -> Vec<String> {
use std::cell::RefCell;
use std::rc::Rc;
let seen: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
let sink = Rc::clone(&seen);
let opts =
Options::default()
.with_url(url)
.with_warn_handler(Rc::new(move |ev: &sasso::WarnEvent<'_>| {
sink.borrow_mut().push(ev.formatted.to_string());
}));
let _ = compile(src, &opts);
let out = seen.borrow().clone();
out
}
#[test]
fn a_global_builtin_with_a_module_form_is_deprecated() {
for (src, replacement, caret) in [
(".a { b: map-get((x: 1), x); }\n", "map.get", "^^^^^^^^^^^^^^^^^^"),
(".a { b: nth(1 2 3, 1); }\n", "list.nth", "^^^^^^^^^^^^^"),
(
".a { b: percentage(0.5); }\n",
"math.percentage",
"^^^^^^^^^^^^^^^",
),
(
".a { b: lighten(#fff, 10%); }\n",
"color.adjust",
"^^^^^^^^^^^^^^^^^^",
),
(".a { b: unitless(1); }\n", "math.is-unitless", "^^^^^^^^^^^"),
(
".a { b: comparable(1px, 2px); }\n",
"math.compatible",
"^^^^^^^^^^^^^^^^^^^^",
),
(
".a { b: list-separator(1 2); }\n",
"list.separator",
"^^^^^^^^^^^^^^^^^^^",
),
(
".a { b: str-length(\"abc\"); }\n",
"string.length",
"^^^^^^^^^^^^^^^^^",
),
(".a { b: type-of(1); }\n", "meta.type-of", "^^^^^^^^^^"),
(
".a { b: selector-parse(\"a\"); }\n",
"selector.parse",
"^^^^^^^^^^^^^^^^^^",
),
] {
let w: Vec<String> = warnings(src, "in.scss")
.into_iter()
.filter(|x| x.contains("[global-builtin]"))
.collect();
assert_eq!(w.len(), 1, "{src:?} -> {w:?}");
assert!(
w[0].starts_with(&format!(
"DEPRECATION WARNING [global-builtin]: Global built-in functions are deprecated and will be removed in Dart Sass 3.0.0.\nUse {replacement} instead.\n\nMore info and automated migrator: https://sass-lang.com/d/import\n"
)),
"{src:?}\n{}",
w[0]
);
assert!(w[0].contains(caret), "{src:?}\n{}", w[0]);
}
for src in [
".a { b: abs(-1); }\n",
".a { b: round(1.5); }\n",
".a { b: min(1, 2); }\n",
".a { b: rgba(0, 0, 0, 0.5); }\n",
".a { b: ie-hex-str(#fff); }\n",
] {
assert!(warnings(src, "in.scss").is_empty(), "{src:?}");
}
assert!(warnings(
"@use \"sass:math\";\n.a { b: math.percentage(0.5); }\n",
"in.scss"
)
.is_empty());
for (src, replacement) in [
(
"$x: 1;\n.a { b: variable-exists(\"x\"); }\n",
"meta.variable-exists",
),
(
"$x: 1;\n.a { b: global-variable-exists(\"x\"); }\n",
"meta.global-variable-exists",
),
(
"@mixin m {}\n.a { b: mixin-exists(\"m\"); }\n",
"meta.mixin-exists",
),
(
".a { b: function-exists(\"percentage\"); }\n",
"meta.function-exists",
),
] {
let w = warnings(src, "in.scss");
assert!(
w.iter()
.any(|x| x.contains(&format!("Use {replacement} instead."))),
"{src:?}\n{w:?}"
);
}
for src in [
".a { b: MAP-GET((x: 1), x); }\n",
".a { b: Lighten(#fff, 10%); }\n",
".a { b: PERCENTAGE(0.5); }\n",
] {
assert!(warnings(src, "in.scss").is_empty(), "{src:?}");
}
let seen: std::rc::Rc<std::cell::RefCell<Vec<String>>> =
std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
let sink = std::rc::Rc::clone(&seen);
let opts = Options::default()
.with_syntax(sasso::Syntax::Sass)
.with_url("in.sass")
.with_warn_handler(std::rc::Rc::new(move |ev: &sasso::WarnEvent<'_>| {
sink.borrow_mut().push(ev.formatted.to_string());
}));
let _ = compile(".a\n b: nth(1 2, 1)\n", &opts);
let w = seen.borrow().clone();
assert_eq!(w.len(), 1, "{w:?}");
assert!(w[0].contains("in.sass 2:6"), "{}", w[0]);
}
#[test]
fn a_deprecated_function_reached_indirectly_still_warns() {
let w = warnings(".a { b: call(get-function(\"percentage\"), 0.5); }\n", "in.scss");
assert_eq!(w.len(), 3, "{w:?}");
assert!(w[0].contains("Use meta.get-function instead."), "{}", w[0]);
assert!(w[1].contains("Use meta.call instead."), "{}", w[1]);
assert!(w[2].contains("Use math.percentage instead."), "{}", w[2]);
assert!(
w[1].contains("in.scss 1:9") && w[2].contains("in.scss 1:9"),
"{w:?}"
);
let w = warnings(
"@function foo() { @return 1; }\n.a { b: call(\"foo\"); }\n",
"in.scss",
);
assert_eq!(w.len(), 2, "{w:?}");
assert!(w[0].contains("Use meta.call instead."), "{}", w[0]);
assert!(
w[1].starts_with("DEPRECATION WARNING [call-string]: Passing a string to call() is deprecated and will be illegal in Dart Sass 2.0.0.\n\nRecommendation: call(get-function(\"foo\"))\n"),
"{}",
w[1]
);
let w = warnings(".a { b: feature-exists(\"at-error\"); }\n", "in.scss");
assert_eq!(w.len(), 2, "{w:?}");
assert!(w[1].starts_with("DEPRECATION WARNING [feature-exists]: The feature-exists() function is deprecated.\n\nMore info: https://sass-lang.com/d/feature-exists\n"), "{}", w[1]);
let w = warnings(
"@use \"sass:meta\";\n.a { b: meta.feature-exists(\"at-error\"); }\n",
"in.scss",
);
assert_eq!(w.len(), 1, "{w:?}");
assert!(w[0].contains("[feature-exists]"), "{}", w[0]);
}
#[test]
fn a_deprecation_follows_the_call_that_is_actually_made() {
let dir = std::env::temp_dir().join(format!("sasso_dep_resolve_{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("mkdir");
std::fs::write(dir.join("_fwdmeta.scss"), "@forward \"sass:meta\";\n").expect("write");
let entry = dir.join("in.scss");
let url = entry.to_string_lossy().into_owned();
let imp = sasso::FsImporter::new(Vec::new());
let run = |src: &str| {
std::fs::write(&entry, src).unwrap();
let seen: std::rc::Rc<std::cell::RefCell<Vec<String>>> =
std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
let sink = std::rc::Rc::clone(&seen);
let opts = Options::default()
.with_importer(&imp)
.with_url(&url)
.with_warn_handler(std::rc::Rc::new(move |ev: &sasso::WarnEvent<'_>| {
sink.borrow_mut().push(ev.formatted.to_string());
}));
let _ = compile(src, &opts);
let out = seen.borrow().clone();
out
};
let w = run("@use \"sass:meta\" as m;\n.a { b: m.feature-exists(\"at-error\"); }\n");
assert_eq!(w.len(), 1, "{w:?}");
assert!(w[0].contains("[feature-exists]"), "{}", w[0]);
let w = run("@use \"fwdmeta\" as m;\n.a { b: m.feature-exists(\"at-error\"); }\n");
assert_eq!(w.len(), 1, "{w:?}");
assert!(w[0].contains("[feature-exists]"), "{}", w[0]);
let w = run("@use \"sass:meta\" as *;\n.a { b: feature-exists(\"at-error\"); }\n");
assert_eq!(w.len(), 1, "{w:?}");
assert!(w[0].contains("[feature-exists]"), "{}", w[0]);
assert!(run("@function type-of($x) { @return 1; }\n.a { b: type-of(2); }\n").is_empty());
let w = run("@use \"sass:meta\";\n.a { b: meta.feature-exists(inspect(\"at-error\")); }\n");
assert_eq!(w.len(), 2, "{w:?}");
assert!(w[0].contains("Use meta.inspect instead."), "{}", w[0]);
assert!(w[1].contains("[feature-exists]"), "{}", w[1]);
assert!(run(
"@use \"sass:meta\";\n.a { b: meta.call(meta.get-function(\"percentage\", $css: true), 1); }\n"
)
.is_empty());
let w = run("@function a\\\"b() { @return 1; }\n.a { b: call(\"a\\\\\\\"b\"); }\n");
assert!(
w.iter()
.any(|x| x.contains("Recommendation: call(get-function('a\\\\\"b'))")),
"{w:?}"
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn what_a_call_resolves_to_decides_what_is_deprecated() {
let w = warnings("a { b: map_get((x: 1), x); }\n", "in.scss");
assert_eq!(w.len(), 1, "{w:?}");
assert!(w[0].contains("Use map.get instead."), "{}", w[0]);
assert!(w[0].contains("^^^^^^^^^^^^^^^^^^"), "{}", w[0]);
let w = warnings(
"@use \"sass:meta\";\na { b: meta.feature_exists(\"at-error\"); }\n",
"in.scss",
);
assert_eq!(w.len(), 1, "{w:?}");
assert!(w[0].contains("[feature-exists]"), "{}", w[0]);
let w = warnings(
"@use \"sass:meta\" as *;\na { b: feature_exists(\"at-error\"); }\n",
"in.scss",
);
assert_eq!(w.len(), 1, "{w:?}");
assert!(w[0].contains("[feature-exists]"), "{}", w[0]);
assert!(warnings(
"@use \"sass:meta\"; @use \"sass:math\";\na { b: meta.call(meta.get-function(\"percentage\", $module: \"math\"), 1); }\n",
"in.scss",
)
.is_empty());
let w = warnings(
"@use \"sass:meta\";\na { b: meta.call(meta.get-function(\"feature-exists\", $module: \"meta\"), \"at-error\"); }\n",
"in.scss",
);
assert_eq!(w.len(), 1, "{w:?}");
assert!(w[0].contains("[feature-exists]"), "{}", w[0]);
let w = warnings(
"@use \"sass:meta\";\na { b: meta.call(meta.get-function(\"percentage\"), 1); }\n",
"in.scss",
);
assert_eq!(w.len(), 1, "{w:?}");
assert!(w[0].contains("Use math.percentage instead."), "{}", w[0]);
}
#[test]
fn a_host_function_does_not_exempt_a_global_builtin() {
use std::rc::Rc;
let cb: sasso::HostFunction = Rc::new(|_args: &[u8]| Ok(Vec::new()));
let seen: Rc<std::cell::RefCell<Vec<String>>> = Rc::new(std::cell::RefCell::new(Vec::new()));
let sink = Rc::clone(&seen);
let opts = Options::default()
.with_url("in.scss")
.with_function("type-of($v)", cb)
.with_warn_handler(Rc::new(move |ev: &sasso::WarnEvent<'_>| {
sink.borrow_mut().push(ev.formatted.to_string());
}));
let _ = compile(".a { b: type-of(1); }\n", &opts);
let w = seen.borrow().clone();
assert_eq!(w.len(), 1, "{w:?}");
assert!(w[0].contains("Use meta.type-of instead."), "{}", w[0]);
}
#[test]
fn a_forwarded_builtin_is_reachable_by_call_and_by_reference() {
let dir = std::env::temp_dir().join(format!("sasso_fwd_builtin_{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("mkdir");
std::fs::write(dir.join("_plain.scss"), "@forward \"sass:map\";\n").expect("write");
std::fs::write(dir.join("_pref.scss"), "@forward \"sass:map\" as p-*;\n").expect("write");
std::fs::write(dir.join("_upref.scss"), "@forward \"sass:map\" as p_*;\n").expect("write");
std::fs::write(
dir.join("_shown.scss"),
"@forward \"sass:string\" show to_upper_case;\n",
)
.expect("write");
let entry = dir.join("in.scss");
let url = entry.to_string_lossy().into_owned();
let imp = sasso::FsImporter::new(Vec::new());
let run = |src: &str| -> Result<String, String> {
std::fs::write(&entry, src).unwrap();
let opts = Options::default().with_importer(&imp).with_url(&url);
compile(src, &opts).map_err(|e| e.to_string())
};
for src in [
"@use \"pref\" as p; a { b: p.p-get((x: 1), x); }",
"@use \"pref\" as p; a { b: p.p_get((x: 1), x); }",
"@use \"upref\" as p; a { b: p.p-get((x: 1), x); }",
"@use \"upref\" as p; a { b: p.p_get((x: 1), x); }",
] {
assert_eq!(run(src).as_deref(), Ok("a {\n b: 1;\n}"), "{src}");
}
for src in [
"@use \"shown\" as s; a { b: s.to-upper-case(\"a\"); }",
"@use \"shown\" as s; a { b: s.to_upper_case(\"a\"); }",
] {
assert_eq!(run(src).as_deref(), Ok("a {\n b: \"A\";\n}"), "{src}");
}
assert_eq!(
run("@use \"plain\" as f; @use \"sass:meta\";\na { b: meta.inspect(meta.get-function(\"get\", $module: \"f\")); }")
.as_deref(),
Ok("a {\n b: get-function(\"get\");\n}")
);
assert_eq!(
run("@use \"plain\" as f; @use \"sass:meta\";\na { b: meta.call(meta.get-function(\"get\", $module: \"f\"), (x: 1), x); }")
.as_deref(),
Ok("a {\n b: 1;\n}")
);
assert_eq!(
run("@use \"pref\" as p; @use \"sass:meta\";\na { b: meta.inspect(meta.get-function(\"p-get\", $module: \"p\")); }")
.as_deref(),
Ok("a {\n b: get-function(\"get\");\n}")
);
assert!(run(
"@use \"plain\" as f; @use \"sass:meta\";\na { b: meta.get-function(\"map-get\", $module: \"f\"); }"
)
.unwrap_err()
.contains("Function not found: \"map-get\""));
assert!(run(
"@use \"pref\" as p; @use \"sass:meta\";\na { b: meta.get-function(\"get\", $module: \"p\"); }"
)
.unwrap_err()
.contains("Function not found: \"get\""));
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn a_forward_carries_a_builtin_through_every_rule_it_passes() {
let dir = std::env::temp_dir().join(format!("sasso_fwd_chain_{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("mkdir");
std::fs::write(dir.join("_inner.scss"), "@forward \"sass:map\";\n").expect("write");
std::fs::write(dir.join("_outer.scss"), "@forward \"inner\";\n").expect("write");
std::fs::write(dir.join("_outerq.scss"), "@forward \"inner\" as q-*;\n").expect("write");
std::fs::write(dir.join("_fmeta.scss"), "@forward \"sass:meta\";\n").expect("write");
std::fs::write(
dir.join("_hidepre.scss"),
"@forward \"sass:map\" as p-* hide p-get;\n",
)
.expect("write");
std::fs::write(
dir.join("_hidebare.scss"),
"@forward \"sass:map\" as p-* hide get;\n",
)
.expect("write");
let entry = dir.join("in.scss");
let url = entry.to_string_lossy().into_owned();
let imp = sasso::FsImporter::new(Vec::new());
let run = |src: &str| -> Result<String, String> {
std::fs::write(&entry, src).unwrap();
let opts = Options::default().with_importer(&imp).with_url(&url);
compile(src, &opts).map_err(|e| e.to_string())
};
assert_eq!(
run("@use \"outer\" as o; a { b: o.get((x: 1), x); }").as_deref(),
Ok("a {\n b: 1;\n}")
);
assert_eq!(
run("@use \"outerq\" as o; a { b: o.q-get((x: 1), x); }").as_deref(),
Ok("a {\n b: 1;\n}")
);
assert_eq!(
run("@use \"outer\" as o; @use \"sass:meta\";\na { b: meta.inspect(meta.get-function(\"get\", $module: \"o\")); }")
.as_deref(),
Ok("a {\n b: get-function(\"get\");\n}")
);
assert!(run("@use \"hidepre\" as p; a { b: p.p-get((x: 1), x); }")
.unwrap_err()
.contains("Undefined function."));
assert_eq!(
run("@use \"hidebare\" as p; a { b: p.p-get((x: 1), x); }").as_deref(),
Ok("a {\n b: 1;\n}")
);
assert_eq!(
run("$x: 1;\n@use \"fmeta\" as f; a { b: f.variable-exists(\"x\"); }").as_deref(),
Ok("a {\n b: true;\n}")
);
assert_eq!(
run("@use \"fmeta\" as f; a { b: f.call(f.get-function(\"rgb\"), 1, 2, 3); }").as_deref(),
Ok("a {\n b: rgb(1, 2, 3);\n}")
);
assert_eq!(
run("@use \"inner\" as f; @use \"sass:meta\";\na { b: meta.function-exists(\"get\", $module: \"f\"); }")
.as_deref(),
Ok("a {\n b: true;\n}")
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn a_forwarded_builtin_brings_its_whole_public_face() {
let dir = std::env::temp_dir().join(format!("sasso_fwd_face_{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("mkdir");
std::fs::write(dir.join("_fmath.scss"), "@forward \"sass:math\";\n").expect("write");
std::fs::write(dir.join("_fmap.scss"), "@forward \"sass:map\";\n").expect("write");
std::fs::write(
dir.join("_ishow.scss"),
"@forward \"sass:map\" as p-* show p-get;\n",
)
.expect("write");
std::fs::write(
dir.join("_ihide.scss"),
"@forward \"sass:map\" as p-* hide p-get;\n",
)
.expect("write");
std::fs::write(dir.join("_oshow.scss"), "@forward \"ishow\" as q-*;\n").expect("write");
std::fs::write(dir.join("_ohide.scss"), "@forward \"ihide\" as q-*;\n").expect("write");
std::fs::write(dir.join("_novar.scss"), "@forward \"sass:math\" hide $pi;\n").expect("write");
std::fs::write(dir.join("_onlyvar.scss"), "@forward \"sass:math\" show $pi;\n").expect("write");
let entry = dir.join("in.scss");
let url = entry.to_string_lossy().into_owned();
let imp = sasso::FsImporter::new(Vec::new());
let run = |src: &str| -> Result<String, String> {
std::fs::write(&entry, src).unwrap();
let opts = Options::default().with_importer(&imp).with_url(&url);
compile(src, &opts).map_err(|e| e.to_string())
};
assert_eq!(
run("@use \"fmath\" as f; a { b: f.$pi; }").as_deref(),
Ok("a {\n b: 3.1415926536;\n}")
);
assert_eq!(
run("@use \"fmath\" as *; a { b: $pi; }").as_deref(),
Ok("a {\n b: 3.1415926536;\n}")
);
assert!(run("@use \"novar\" as f; a { b: f.$pi; }")
.unwrap_err()
.contains("Undefined variable."));
assert_eq!(
run("@use \"novar\" as f; a { b: f.floor(1.5); }").as_deref(),
Ok("a {\n b: 1;\n}")
);
assert_eq!(
run("@use \"onlyvar\" as f; a { b: f.$pi; }").as_deref(),
Ok("a {\n b: 3.1415926536;\n}")
);
assert!(run("@use \"onlyvar\" as f; a { b: f.floor(1.5); }")
.unwrap_err()
.contains("Undefined function."));
std::fs::write(dir.join("_fmeta.scss"), "@forward \"sass:meta\";\n").expect("write");
assert!(
run("@use \"fmeta\" as f; a { @include f.load-css(\"nope.css\"); }")
.unwrap_err()
.contains("Can't find stylesheet to import.")
);
assert_eq!(
run("@use \"oshow\" as o; a { b: o.q-p-get((x: 1), x); }").as_deref(),
Ok("a {\n b: 1;\n}")
);
assert!(run("@use \"ohide\" as o; a { b: o.q-p-get((x: 1), x); }")
.unwrap_err()
.contains("Undefined function."));
assert_eq!(
run("@use \"fmap\" as *; a { b: get((x: 1), x); }").as_deref(),
Ok("a {\n b: 1;\n}")
);
assert_eq!(
run("@use \"fmap\" as *; @use \"sass:meta\";\na { b: meta.inspect(meta.get-function(\"get\")); }")
.as_deref(),
Ok("a {\n b: get-function(\"get\");\n}")
);
assert_eq!(
run("@use \"fmap\" as *; @use \"sass:meta\";\na { b: meta.function-exists(\"get\"); }").as_deref(),
Ok("a {\n b: true;\n}")
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn every_kind_of_starred_member_competes_for_the_bare_name() {
let dir = std::env::temp_dir().join(format!("sasso_star_share_{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("mkdir");
std::fs::write(dir.join("_fmath.scss"), "@forward \"sass:math\";\n").expect("write");
std::fs::write(dir.join("_fmap.scss"), "@forward \"sass:map\";\n").expect("write");
std::fs::write(dir.join("_fmeta.scss"), "@forward \"sass:meta\";\n").expect("write");
std::fs::write(dir.join("_fstr.scss"), "@forward \"sass:string\";\n").expect("write");
std::fs::write(dir.join("_upi.scss"), "$pi: USERPI;\n").expect("write");
std::fs::write(
dir.join("_uindex.scss"),
"@function index($a, $b) { @return USER; }\n",
)
.expect("write");
std::fs::write(dir.join("_umix.scss"), "@mixin load-css($x) { u: 1; }\n").expect("write");
let entry = dir.join("in.scss");
let url = entry.to_string_lossy().into_owned();
let imp = sasso::FsImporter::new(Vec::new());
let run = |src: &str| -> Result<String, String> {
std::fs::write(&entry, src).unwrap();
let opts = Options::default().with_importer(&imp).with_url(&url);
compile(src, &opts).map_err(|e| e.to_string())
};
assert_eq!(
run("@use \"fmap\" as *; @use \"sass:map\" as *; a { b: get((x: 1), x); }").as_deref(),
Ok("a {\n b: 1;\n}")
);
assert_eq!(
run("@use \"fmath\" as *; @use \"sass:math\" as *; a { b: $pi; }").as_deref(),
Ok("a {\n b: 3.1415926536;\n}")
);
for (src, what) in [
(
"@use \"uindex\" as *; @use \"sass:string\" as *; a { b: index(\"abc\", \"b\"); }",
"function",
),
(
"@use \"upi\" as *; @use \"sass:math\" as *; a { b: $pi; }",
"variable",
),
(
"@use \"umix\" as *; @use \"sass:meta\" as *; a { @include load-css(\"nope.css\"); }",
"mixin",
),
] {
let err = run(src).unwrap_err();
assert!(
err.contains(&format!("This {what} is available from multiple global modules.")),
"{src}: {err}"
);
}
for (src, want) in [
(
"@use \"sass:math\" as *; @use \"sass:meta\";\na { b: meta.global-variable-exists(\"pi\"); }",
"true",
),
(
"@use \"fmath\" as *; @use \"sass:meta\";\na { b: meta.variable-exists(\"pi\"); }",
"true",
),
(
"@use \"sass:meta\" as *; @use \"sass:meta\" as m;\na { b: m.mixin-exists(\"load-css\"); }",
"true",
),
(
"@use \"fmeta\" as *; @use \"sass:meta\" as m;\na { b: m.mixin-exists(\"load-css\"); }",
"true",
),
] {
assert_eq!(
run(src).as_deref(),
Ok(format!("a {{\n b: {want};\n}}").as_str()),
"{src}"
);
}
assert!(
run("@use \"sass:meta\" as *; a { @include load-css(\"nope.css\"); }")
.unwrap_err()
.contains("Can't find stylesheet to import.")
);
assert_eq!(
run("@use \"sass:meta\" as *; a { b: inspect(get-mixin(\"load-css\")); }").as_deref(),
Ok("a {\n b: get-mixin(\"load-css\");\n}")
);
assert_eq!(
run("@use \"fmeta\" as f; @use \"sass:meta\";\na { b: meta.inspect(meta.get-mixin(\"load-css\", $module: \"f\")); }")
.as_deref(),
Ok("a {\n b: get-mixin(\"load-css\");\n}")
);
assert!(
run("@use \"sass:meta\" as *; a { b: inspect(module-functions(\"meta\")); }")
.unwrap_err()
.contains("There is no module with namespace \"meta\".")
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn a_modules_own_member_shadows_the_builtin_it_forwards() {
let dir = std::env::temp_dir().join(format!("sasso_own_shadow_{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("mkdir");
std::fs::write(
dir.join("_ownfn.scss"),
"@forward \"sass:string\";\n@function index($a, $b) { @return OWN; }\n",
)
.expect("write");
std::fs::write(dir.join("_ownvar.scss"), "@forward \"sass:math\";\n$pi: OWNPI;\n").expect("write");
std::fs::write(
dir.join("_ownmix.scss"),
"@forward \"sass:meta\";\n@mixin load-css($x) { own: 1; }\n",
)
.expect("write");
std::fs::write(dir.join("_umix.scss"), "@mixin load-css($x) { u: 1; }\n").expect("write");
let entry = dir.join("in.scss");
let url = entry.to_string_lossy().into_owned();
let imp = sasso::FsImporter::new(Vec::new());
let run = |src: &str| -> Result<String, String> {
std::fs::write(&entry, src).unwrap();
let opts = Options::default().with_importer(&imp).with_url(&url);
compile(src, &opts).map_err(|e| e.to_string())
};
for (src, want) in [
("@use \"ownfn\" as f; a { b: f.index(\"abc\", \"b\"); }", "OWN"),
("@use \"ownfn\" as *; a { b: index(\"abc\", \"b\"); }", "OWN"),
("@use \"ownvar\" as f; a { b: f.$pi; }", "OWNPI"),
("@use \"ownvar\" as *; a { b: $pi; }", "OWNPI"),
] {
assert_eq!(
run(src).as_deref(),
Ok(format!("a {{\n b: {want};\n}}").as_str()),
"{src}"
);
}
for src in [
"@use \"ownmix\" as f; a { @include f.load-css(\"x\"); }",
"@use \"ownmix\" as *; a { @include load-css(\"x\"); }",
] {
assert_eq!(run(src).as_deref(), Ok("a {\n own: 1;\n}"), "{src}");
}
assert_eq!(
run("@use \"ownfn\" as *; @use \"sass:meta\";\na { b: meta.function-exists(\"index\"); }").as_deref(),
Ok("a {\n b: true;\n}")
);
assert!(
run("@use \"umix\" as *; @use \"sass:meta\" as *;\na { b: inspect(get-mixin(\"load-css\")); }")
.unwrap_err()
.contains("This mixin is available from multiple global modules.")
);
assert_eq!(
run("@use \"umix\" as *; @use \"sass:meta\" as m;\na { b: m.inspect(m.get-mixin(\"load-css\")); }")
.as_deref(),
Ok("a {\n b: get-mixin(\"load-css\");\n}")
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn a_builtin_mixin_reference_can_be_taken_and_invoked() {
let dir = std::env::temp_dir().join(format!("sasso_builtin_mixin_{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("mkdir");
std::fs::write(dir.join("_fmeta.scss"), "@forward \"sass:meta\";\n").expect("write");
std::fs::write(dir.join("real.css"), "x { loaded: 1; }\n").expect("write");
let entry = dir.join("in.scss");
let url = entry.to_string_lossy().into_owned();
let imp = sasso::FsImporter::new(Vec::new());
let run = |src: &str| -> Result<String, String> {
std::fs::write(&entry, src).unwrap();
let opts = Options::default().with_importer(&imp).with_url(&url);
compile(src, &opts).map_err(|e| e.to_string())
};
for src in [
"@use \"sass:meta\" as m; a { b: m.inspect(m.get-mixin(\"load-css\", $module: \"m\")); }",
"@use \"sass:meta\"; a { b: meta.inspect(meta.get-mixin(\"load-css\", $module: \"meta\")); }",
] {
assert_eq!(
run(src).as_deref(),
Ok("a {\n b: get-mixin(\"load-css\");\n}"),
"{src}"
);
}
for src in [
"@use \"sass:meta\";\na { @include meta.apply(meta.get-mixin(\"load-css\", $module: \"meta\"), \"real\"); }",
"@use \"fmeta\" as f;\na { @include f.apply(f.get-mixin(\"load-css\", $module: \"f\"), \"real\"); }",
"@use \"sass:meta\" as *;\na { @include apply(get-mixin(\"load-css\"), \"real\"); }",
] {
assert_eq!(run(src).as_deref(), Ok("a x {\n loaded: 1;\n}"), "{src}");
}
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn a_mixin_reached_through_a_reference_reports_where_it_was_included() {
let dir = std::env::temp_dir().join(format!("sasso_ref_span_{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("mkdir");
std::fs::write(dir.join("_umix.scss"), "@mixin load-css($x) { u: 1; }\n").expect("write");
let entry = dir.join("in.scss");
let url = entry.to_string_lossy().into_owned();
let imp = sasso::FsImporter::new(Vec::new());
let run = |src: &str| -> String {
std::fs::write(&entry, src).unwrap();
let opts = Options::default().with_importer(&imp).with_url(&url);
compile(src, &opts)
.expect_err("expected a compile error")
.to_string()
};
let via_ref = run(
"@use \"sass:meta\";\na {\n @include meta.apply(meta.get-mixin(\"load-css\", $module: \"meta\"), \"nope\");\n}\n",
);
assert!(
via_ref.starts_with("Error: Can't find stylesheet to import."),
"{via_ref}"
);
assert_eq!(caret_line(&via_ref).len(), 72, "{via_ref}");
let direct = run("@use \"sass:meta\";\na {\n @include meta.load-css(\"nope\");\n}\n");
assert_eq!(caret_line(&direct).len(), 30, "{direct}");
let bad_arg = run(
"@use \"sass:meta\";\na {\n @include meta.apply(meta.get-mixin(\"load-css\", $module: \"meta\"), 1);\n}\n",
);
assert!(
bad_arg.starts_with("Error: $url: 1 is not a string."),
"{bad_arg}"
);
assert_eq!(caret_line(&bad_arg).len(), 67, "{bad_arg}");
let ambiguous =
run("@use \"umix\" as *;\n@use \"sass:meta\" as *;\na {\n @include load-css(\"nope\");\n}\n");
assert!(
ambiguous.starts_with("Error: This mixin is available from multiple global modules."),
"{ambiguous}"
);
assert_eq!(caret_line(&ambiguous).len(), 25, "{ambiguous}");
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn a_builtin_mixin_answers_to_its_underscore_spelling() {
let dir = std::env::temp_dir().join(format!("sasso_mixin_under_{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("mkdir");
std::fs::write(dir.join("_fmeta.scss"), "@forward \"sass:meta\";\n").expect("write");
std::fs::write(dir.join("real.css"), "x { loaded: 1; }\n").expect("write");
let entry = dir.join("in.scss");
let url = entry.to_string_lossy().into_owned();
let imp = sasso::FsImporter::new(Vec::new());
let run = |src: &str| -> Result<String, String> {
std::fs::write(&entry, src).unwrap();
let opts = Options::default().with_importer(&imp).with_url(&url);
compile(src, &opts).map_err(|e| e.to_string())
};
for src in [
"@use \"sass:meta\"; a { @include meta.load_css(\"real\"); }",
"@use \"sass:meta\"; a { @include meta.load-css(\"real\"); }",
"@use \"sass:meta\" as m; a { @include m.load_css(\"real\"); }",
"@use \"sass:meta\" as *; a { @include load_css(\"real\"); }",
"@use \"fmeta\" as f; a { @include f.load_css(\"real\"); }",
] {
assert_eq!(run(src).as_deref(), Ok("a x {\n loaded: 1;\n}"), "{src}");
}
assert_eq!(
run("@use \"sass:meta\";\na { b: meta.inspect(meta.get-mixin(\"load_css\", $module: \"meta\")); }")
.as_deref(),
Ok("a {\n b: get-mixin(\"load-css\");\n}")
);
assert_eq!(
run("@use \"sass:meta\";\na { b: meta.get-mixin(\"load_css\", $module: \"meta\") == meta.get-mixin(\"load-css\", $module: \"meta\"); }")
.as_deref(),
Ok("a {\n b: true;\n}")
);
assert_eq!(
run("@use \"sass:meta\";\na { b: meta.mixin-exists(\"load_css\", $module: \"meta\"); }").as_deref(),
Ok("a {\n b: true;\n}")
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn call_by_name_resolves_what_the_name_would_have() {
let dir = std::env::temp_dir().join(format!("sasso_call_name_{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("mkdir");
std::fs::write(dir.join("_fmap.scss"), "@forward \"sass:map\";\n").expect("write");
std::fs::write(dir.join("_own.scss"), "@function own($x) { @return OWN; }\n").expect("write");
std::fs::write(
dir.join("_uindex.scss"),
"@function index($a, $b) { @return USER; }\n",
)
.expect("write");
let entry = dir.join("in.scss");
let url = entry.to_string_lossy().into_owned();
let imp = sasso::FsImporter::new(Vec::new());
let run = |src: &str| -> Result<String, String> {
std::fs::write(&entry, src).unwrap();
let opts = Options::default().with_importer(&imp).with_url(&url);
compile(src, &opts).map_err(|e| e.to_string())
};
for (src, want) in [
(
"@use \"sass:map\" as *; @use \"sass:meta\";\na { b: meta.call(\"get\", (x: 1), x); }",
"1",
),
(
"@use \"fmap\" as *; @use \"sass:meta\";\na { b: meta.call(\"get\", (x: 1), x); }",
"1",
),
(
"@use \"own\" as *; @use \"sass:meta\";\na { b: meta.call(\"own\", 1); }",
"OWN",
),
(
"@use \"sass:string\" as *; @use \"sass:meta\";\na { b: meta.call(\"index\", \"abc\", \"b\"); }",
"2",
),
] {
assert_eq!(
run(src).as_deref(),
Ok(format!("a {{\n b: {want};\n}}").as_str()),
"{src}"
);
}
assert!(run(
"@use \"uindex\" as *; @use \"sass:string\" as *; @use \"sass:meta\";\na { b: meta.call(\"index\", \"abc\", \"b\"); }"
)
.unwrap_err()
.contains("This function is available from multiple global modules."));
assert_eq!(
run("@use \"sass:meta\"; a { b: meta.call(\"nope\", 1); }").as_deref(),
Ok("a {\n b: nope(1);\n}")
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn one_span_can_carry_two_call_string_recommendations() {
let w = warnings(
"@use \"sass:meta\";\n@function pick($n) { @return meta.call($n, 1); }\n@function a($x) { @return A; }\n@function b($x) { @return B; }\n.x { p: pick(\"a\"); q: pick(\"b\"); }\n",
"in.scss",
);
let recs: Vec<&String> = w.iter().filter(|x| x.contains("[call-string]")).collect();
assert_eq!(recs.len(), 2, "{w:?}");
assert!(
recs[0].contains("Recommendation: call(get-function(\"a\"))"),
"{}",
recs[0]
);
assert!(
recs[1].contains("Recommendation: call(get-function(\"b\"))"),
"{}",
recs[1]
);
}
#[test]
fn a_legacy_color_function_suggests_its_replacement() {
let sug = |src: &str| -> Vec<String> {
warnings(src, "in.scss")
.into_iter()
.filter(|w| w.contains("[color-functions]"))
.collect()
};
let one = |src: &str| -> String {
let w = sug(src);
assert_eq!(w.len(), 1, "{src}: {w:?}");
w.into_iter().next().unwrap()
};
for (name, space) in [
("red", "rgb"),
("green", "rgb"),
("blue", "rgb"),
("hue", "hsl"),
("saturation", "hsl"),
("lightness", "hsl"),
] {
let w = one(&format!("a {{ b: {name}(#abcdef); }}\n"));
assert!(w.contains(&format!("{name}() is deprecated. Suggestion:")), "{w}");
assert!(
w.contains(&format!("color.channel($color, \"{name}\", $space: {space})")),
"{w}"
);
}
for src in [
"@use \"sass:color\";\na { b: color.whiteness(#abcdef); }\n",
"@use \"sass:color\" as c;\na { b: c.whiteness(#abcdef); }\n",
"@use \"sass:color\" as *;\na { b: whiteness(#abcdef); }\n",
] {
let w = one(src);
assert!(w.contains("color.whiteness() is deprecated."), "{src}: {w}");
assert!(
w.contains("color.channel($color, \"whiteness\", $space: hwb)"),
"{src}: {w}"
);
}
for (src, scale, adjust) in [
(
"a { b: lighten(#abcdef, 10%); }\n",
Some("51%"),
"$lightness: 10%",
),
(
"a { b: darken(#abcdef, 10%); }\n",
Some("-12.4390243902%"),
"$lightness: -10%",
),
(
"a { b: saturate(#abcdef, 10%); }\n",
Some("31.25%"),
"$saturation: 10%",
),
(
"a { b: desaturate(#abcdef, 10%); }\n",
Some("-14.7058823529%"),
"$saturation: -10%",
),
(
"a { b: opacify(rgba(1, 2, 3, 0.5), 0.1); }\n",
Some("$alpha: 20%"),
"$alpha: 0.1",
),
(
"a { b: transparentize(rgba(1, 2, 3, 0.5), 0.1); }\n",
Some("$alpha: -20%"),
"$alpha: -0.1",
),
("a { b: lighten(#fff, 10%); }\n", Some("100%"), "$lightness: 10%"),
("a { b: darken(#000, 10%); }\n", Some("-100%"), "$lightness: -10%"),
("a { b: lighten(#ccc, 50%); }\n", Some("100%"), "$lightness: 50%"),
("a { b: adjust-hue(#abcdef, 10deg); }\n", None, "$hue: 10deg"),
("a { b: adjust-hue(#abcdef, 0.25turn); }\n", None, "$hue: 90deg"),
("a { b: saturate(#abcdef, 0%); }\n", None, "$saturation: 0%"),
(
"a { b: opacify(hsl(240 100% 50% / none), 0.2); }\n",
Some("$alpha: 20%"),
"$alpha: 0.2",
),
(
"a { b: transparentize(hsl(240 100% 50% / none), 0.2); }\n",
Some("$alpha: -100%"),
"$alpha: -0.2",
),
] {
let w = one(src);
match scale {
Some(s) => {
assert!(w.contains("Suggestions:"), "{src}: {w}");
assert!(w.contains("color.scale($color, "), "{src}: {w}");
assert!(w.contains(s), "{src}: {w}");
}
None => {
assert!(w.contains("Suggestion:"), "{src}: {w}");
assert!(!w.contains("color.scale"), "{src}: {w}");
}
}
assert!(
w.contains(&format!("color.adjust($color, {adjust})")),
"{src}: {w}"
);
}
assert!(one("a { b: lighten(#abcdef, 10); }\n").contains("$lightness: 10%"));
assert!(one("a { b: lighten($color: #abcdef, $amount: 10%); }\n").contains("$lightness: 10%"));
assert!(one("a { b: adjust-hue($color: #abcdef, $degrees: 10deg); }\n")
.contains("color.adjust($color, $hue: 10deg)"));
assert!(sug("a { b: whiteness(#abcdef); }\n").is_empty());
assert!(sug("a { b: blackness(#abcdef); }\n").is_empty());
assert!(sug("@use \"sass:meta\"; a { b: meta.call(\"whiteness\", #abcdef); }\n").is_empty());
assert!(sug("@function lighten($c, $n) { @return MINE; }\na { b: lighten(#abcdef, 10%); }\n").is_empty());
for src in [
"a { b: alpha(rgba(1, 2, 3, 0.5)); }\n",
"a { b: mix(#abcdef, #123456); }\n",
"a { b: grayscale(#abcdef); }\n",
"a { b: complement(#abcdef); }\n",
"a { b: invert(#abcdef); }\n",
] {
assert!(sug(src).is_empty(), "{src}");
}
assert!(sug("a { b: lighten(3, 10%); }\n").is_empty());
assert!(sug("a { b: lighten(#abcdef, 150%); }\n").is_empty());
let w = one("@use \"sass:meta\";\na { b: meta.call(meta.get-function(\"lighten\"), #abcdef, 10%); }\n");
assert!(w.contains("lighten() is deprecated. Suggestions:"), "{w}");
assert!(w.contains("color.scale($color, $lightness: 51%)"), "{w}");
let w = one("a { b: call(\"darken\", #abcdef, 10%); }\n");
assert!(
w.contains("color.scale($color, $lightness: -12.4390243902%)"),
"{w}"
);
let w = one(
"@use \"sass:meta\"; @use \"sass:color\";\na { b: meta.call(meta.get-function(\"whiteness\", $module: \"color\"), #abcdef); }\n",
);
assert!(w.contains("color.whiteness() is deprecated."), "{w}");
assert!(
w.contains("color.channel($color, \"whiteness\", $space: hwb)"),
"{w}"
);
let w = one("a { b: saturate(hsl(20, 99.9999%, 50%), 0.00001%); }\n");
assert!(w.contains("$saturation: 9.9999999997%"), "{w}");
let w = sug(
"@mixin m($c) { b: lighten($c, 10%); }\na { @include m(#abcdef); }\nd { @include m(#123456); }\n",
);
assert_eq!(w.len(), 2, "{w:?}");
assert!(w[0].contains("$lightness: 51%"), "{}", w[0]);
assert!(w[1].contains("$lightness: 12.5615763547%"), "{}", w[1]);
}
#[test]
fn the_legacy_if_suggests_the_modern_syntax() {
let sug = |src: &str| -> Vec<String> { warnings(src, "in.scss") };
let one = |src: &str| -> String {
let w = sug(src);
assert_eq!(w.len(), 1, "{src}: {w:?}");
w.into_iter().next().unwrap()
};
assert!(one("a { b: if(true, 1, 2); }\n").contains("Suggestion: if(sass(true): 1; else: 2)"));
for (src, want) in [
("a { b: if(true,1+2,3); }\n", "if(sass(true): 1 + 2; else: 3)"),
("a { b: if( true , 1 , 2 ); }\n", "if(sass(true): 1; else: 2)"),
("a { b: if(true, 1e3, 2); }\n", "if(sass(true): 1000; else: 2)"),
(
"a { b: if(true, 1px*2, 3); }\n",
"if(sass(true): 1px * 2; else: 3)",
),
(
"a { b: if(true, #ABCDEF, 3); }\n",
"if(sass(true): #ABCDEF; else: 3)",
),
(
"a { b: if(true, \"a\\41 b\", 3); }\n",
"if(sass(true): \"aAb\"; else: 3)",
),
(
"a { b: if(true, 'he said \"hi\"', 3); }\n",
"if(sass(true): 'he said \"hi\"'; else: 3)",
),
(
"$v: 1;\na { b: if(true, \"x#{$v}y\", 3); }\n",
"if(sass(true): \"x#{$v}y\"; else: 3)",
),
("a { b: if(true, (1 2), 3); }\n", "if(sass(true): (1 2); else: 3)"),
(
"a { b: if(true, (a: 1), 2); }\n",
"if(sass(true): (a: 1); else: 2)",
),
("a { b: if(true, [], 2); }\n", "if(sass(true): []; else: 2)"),
("a { b: if(true, (), 2); }\n", "if(sass(true): (); else: 2)"),
("a { b: if(true, (1,), 2); }\n", "if(sass(true): ((1,)); else: 2)"),
("a { b: if(true, [1,], 2); }\n", "if(sass(true): [1]; else: 2)"),
(
"a { b: if(true, (1, 2), 3); }\n",
"if(sass(true): (1, 2); else: 3)",
),
("a { b: if(not true, 1, 2); }\n", "if(sass(not true): 1; else: 2)"),
(
"$v: 1;\na { b: if($v > 1, 1, 2); }\n",
"if(sass($v > 1): 1; else: 2)",
),
("a { b: if(true, 1, null); }\n", "if(sass(true): 1)"),
("$v: 1;\na { b: if($v, null, 2); }\n", "if(not sass($v): 2)"),
("a { b: if(true, null, null); }\n", "if(sass(true): null)"),
] {
let w = one(src);
assert!(w.contains(&format!("Suggestion: {want}")), "{src}: {w}");
}
for src in [
"a { b: if($condition: true, $if-true: 1, $if-false: 2); }\n",
"$a: (true, 1, 2);\na { b: if($a...); }\n",
] {
let w = one(src);
assert!(w.contains("[if-function]"), "{src}: {w}");
assert!(!w.contains("Suggestion:"), "{src}: {w}");
}
let w = sug("a { b: if(true, if(false, 1, 2), 3); }\n");
assert_eq!(w.len(), 2, "{w:?}");
assert!(w[0].contains("if(sass(false): 1; else: 2)"), "{}", w[0]);
assert!(
w[1].contains("if(sass(true): if(false, 1, 2); else: 3)"),
"{}",
w[1]
);
assert_eq!(sug("@mixin m { z: if(true, 1, 2); }\na { q: 1; }\n").len(), 1);
assert_eq!(
sug("@if false { a { z: if(true, 1, 2); } }\nb { q: 1; }\n").len(),
1
);
assert_eq!(
sug("@mixin m($x) { z: if($x, 1, 2); }\na { @include m(true); }\nb { @include m(false); }\n").len(),
1
);
assert_eq!(
sug("@for $i from 1 through 3 { a#{$i} { z: if(true, 1, 2); } }\n").len(),
1
);
let w = one("@mixin m($x) { z: if($x, 1, 2); }\na { @include m(true); }\n");
assert_eq!(w.matches("root stylesheet").count(), 1, "{w}");
assert!(!w.contains("m()"), "{w}");
assert!(one("$x: 1;\na { b: if(true, \"a\\a #{$x}\", 3); }\n")
.contains("Suggestion: if(sass(true): \"a\\a#{$x}\"; else: 3)"));
assert!(one("a { b: if(true, alpha(opacity=80), 3); }\n")
.contains("Suggestion: if(sass(true): alpha(opacity = 80); else: 3)"));
assert_eq!(
sug("@function if($c, $t, $f) { @return 9; }\na { b: if(true, 1, 2); }\n").len(),
1
);
for src in [
"a { z: #{if(true, 1, 2)}; }\n",
"a#{if(true, x, y)} { q: 1; }\n",
"@media (min-width: if(true, 1px, 2px)) { a { q: 1; } }\n",
"@supports (a: if(true, 1, 2)) { b { q: 1; } }\n",
"@each $i in if(true, 1 2, 3) { a { q: $i; } }\n",
"@mixin m($p: if(true, 1, 2)) { q: $p; }\na { @include m; }\n",
"@warn if(true, \"w\", \"v\");\na { q: 1; }\n",
"/* c #{if(true, 1, 2)} */\n",
"@import url(if(true, a, b));\n",
"@mixin m { @content(if(true, 1, 2)); }\na { @include m using ($x) { q: $x; } }\n",
"%p { q: 1; }\na { @extend #{if(true, \"%p\", \"%p\")}; }\n",
"@function --foo() { #{if(true, x, y)}: 1; }\n",
"@keyframes #{if(true, fade, none)} { from { o: 0; } }\n",
] {
let found: Vec<String> = sug(src)
.into_iter()
.filter(|w| w.contains("[if-function]"))
.collect();
assert_eq!(found.len(), 1, "{src}: {found:?}");
}
}
#[test]
fn an_argument_error_shows_the_declaration_it_failed_against() {
let dir = std::env::temp_dir().join(format!("sasso_decl_span_{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("mkdir");
std::fs::write(dir.join("_lib.scss"), "@mixin m($x) { a: $x; }\n").expect("write");
let entry = dir.join("in.scss");
let url = entry.to_string_lossy().into_owned();
let imp = sasso::FsImporter::new(Vec::new());
let run = |src: &str| -> String {
std::fs::write(&entry, src).unwrap();
compile(src, &Options::default().with_importer(&imp).with_url(&url))
.expect_err("expected a compile error")
.to_string()
};
for (src, message) in [
(
"@function f() { @return 1; }\n.a { b: f(1); }\n",
"Only 0 arguments allowed, but 1 was passed.",
),
(
"@function f($x) { @return 1; }\n.a { b: f(1, 2); }\n",
"Only 1 argument allowed, but 2 were passed.",
),
(
"@function f($x, $y) { @return 1; }\n.a { b: f(1, 2, 3); }\n",
"Only 2 arguments allowed, but 3 were passed.",
),
] {
let block = run(src);
assert!(block.starts_with(&format!("Error: {message}\n")), "{block}");
assert!(block.contains(" declaration\n"), "{block}");
assert!(caret_line(&block).ends_with(" invocation"), "{block}");
}
let block = run("@function f($x) { @return 1; }\n.a { b: f(1, $z: 2); }\n");
assert!(block.starts_with("Error: No parameter named $z.\n"), "{block}");
assert!(
block.contains("\u{2501}\u{2501}\u{2501}\u{2501}\u{2501} declaration"),
"{block}"
);
let block = run("@mixin m { a: 1; }\n.a { @include m { b: 2; } }\n");
assert!(
block.starts_with("Error: Mixin doesn't accept a content block.\n"),
"{block}"
);
assert!(block.contains("\u{2501} declaration"), "{block}");
assert_eq!(caret_line(&block), "^^^^^^^^^^ invocation", "{block}");
assert!(!block.contains("m()"), "{block}");
let block = run("@use \"lib\";\n.a { @include lib.m; }\n");
assert!(block.contains("\u{250c}\u{2500}\u{2500}> "), "{block}");
assert!(block.contains("_lib.scss"), "{block}");
assert!(
block.contains("\u{2501}\u{2501}\u{2501}\u{2501}\u{2501} declaration"),
"{block}"
);
std::fs::remove_dir_all(&dir).ok();
}
fn snippet(block: &str) -> String {
let lines: Vec<&str> = block.lines().collect();
let end = lines
.iter()
.rposition(|l| l.trim() == "\u{2575}")
.unwrap_or_else(|| panic!("no closing glyph in:\n{block}"));
lines[..=end].join("\n")
}
#[test]
fn a_span_that_crosses_lines_draws_an_arm_down_to_its_label() {
let run = |src: &str| -> String {
snippet(
&compile(src, &Options::default().with_url("t.scss"))
.expect_err("expected a compile error")
.to_string(),
)
};
assert_eq!(
run("@mixin m($x) { a: $x; }\n.a { @include m(\n); }\n"),
"Error: Missing argument $x.\n \u{2577}\n\
1 \u{2502} @mixin m($x) { a: $x; }\n \u{2502} \u{2501}\u{2501}\u{2501}\u{2501}\u{2501} declaration\n\
2 \u{2502} .a { @include m(\n \u{2502} \u{250c}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}^\n\
3 \u{2502} \u{2502} ); }\n \u{2502} \u{2514}\u{2500}^ invocation\n \u{2575}"
);
assert_eq!(
run("@mixin m($x) { a: $x; }\n@include m(\n)\n;\n"),
"Error: Missing argument $x.\n \u{2577}\n\
1 \u{2502} @mixin m($x) { a: $x; }\n \u{2502} \u{2501}\u{2501}\u{2501}\u{2501}\u{2501} declaration\n\
2 \u{2502} \u{250c} @include m(\n\
3 \u{2502} \u{2502} )\n \u{2502} \u{2514}\u{2500}\u{2500}\u{2500} invocation\n \u{2575}"
);
assert_eq!(
run("@mixin m(\n $x: 1\n) { a: $x; }\n.a { @include m(1, 2); }\n"),
"Error: Only 1 argument allowed, but 2 were passed.\n \u{2577}\n\
1 \u{2502} @mixin m(\n \u{2502} \u{250c}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}^\n\
2 \u{2502} \u{2502} $x: 1\n\
3 \u{2502} \u{2502} ) { a: $x; }\n \u{2502} \u{2514}\u{2500}^ declaration\n\
4 \u{2502} .a { @include m(1, 2); }\n \u{2502} ^^^^^^^^^^^^^^^^ invocation\n \u{2575}"
);
assert_eq!(
run("@mixin m(\n $x: 1\n) { a: $x; }\n.a { @include m(\n 1, 2\n); }\n"),
"Error: Only 1 argument allowed, but 2 were passed.\n \u{2577}\n\
1 \u{2502} @mixin m(\n \u{2502} \u{250c}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}^\n\
2 \u{2502} \u{2502} $x: 1\n\
3 \u{2502} \u{2502} ) { a: $x; }\n \u{2502} \u{2514}\u{2500}^ declaration\n\
4 \u{2502} .a { @include m(\n \u{2502} \u{250c}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}^\n\
5 \u{2502} \u{2502} 1, 2\n\
6 \u{2502} \u{2502} ); }\n \u{2502} \u{2514}\u{2500}^ invocation\n \u{2575}"
);
assert_eq!(
run("@mixin m($x) { a: $x; }\n// pad\n// pad\n.a { @include m(\n); }\n"),
"Error: Missing argument $x.\n \u{2577}\n\
1 \u{2502} @mixin m($x) { a: $x; }\n \u{2502} \u{2501}\u{2501}\u{2501}\u{2501}\u{2501} declaration\n\
... \u{2502}\n\
4 \u{2502} .a { @include m(\n \u{2502} \u{250c}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}^\n\
5 \u{2502} \u{2502} ); }\n \u{2502} \u{2514}\u{2500}^ invocation\n \u{2575}"
);
}
#[test]
fn a_first_class_mixin_reference_carries_the_declaration_too() {
let run = |src: &str| -> String {
compile(src, &Options::default().with_url("t.scss"))
.expect_err("expected a compile error")
.to_string()
};
let block = run(
"@use \"sass:meta\";\n@mixin m { a: 1; }\n.a { @include meta.apply(meta.get-mixin(\"m\")) { b: 2; } }\n",
);
assert_eq!(
snippet(&block),
"Error: Mixin doesn't accept a content block.\n \u{2577}\n\
2 \u{2502} @mixin m { a: 1; }\n \u{2502} \u{2501} declaration\n\
3 \u{2502} .a { @include meta.apply(meta.get-mixin(\"m\")) { b: 2; } }\n \u{2502} \
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ invocation\n \u{2575}"
);
assert!(!block.contains(" m()"), "{block}");
for (src, message) in [
(
"@use \"sass:meta\";\n@mixin m($x) { a: $x; }\n.a { @include meta.apply(meta.get-mixin(\"m\")); }\n",
"Missing argument $x.",
),
(
"@use \"sass:meta\";\n@mixin m() { a: 1; }\n.a { @include meta.apply(meta.get-mixin(\"m\"), 1); }\n",
"Only 0 arguments allowed, but 1 was passed.",
),
(
"@use \"sass:meta\";\n@mixin m($x: 1) { a: $x; }\n.a { @include meta.apply(meta.get-mixin(\"m\"), $z: 1); }\n",
"No parameter named $z.",
),
] {
let block = run(src);
assert!(block.starts_with(&format!("Error: {message}\n")), "{block}");
assert!(block.contains(" declaration\n"), "{block}");
assert!(caret_line(&block).ends_with(" invocation"), "{block}");
}
}
#[test]
fn two_spans_on_one_line_stack_their_rows() {
let run = |src: &str| -> String {
snippet(
&compile(src, &Options::default().with_url("t.scss"))
.expect_err("expected a compile error")
.to_string(),
)
};
assert_eq!(
run("@function f($x) { @return 1; } .a { b: f(1, 2); }\n"),
"Error: Only 1 argument allowed, but 2 were passed.\n \u{2577}\n1 \u{2502} \
@function f($x) { @return 1; } .a { b: f(1, 2); }\n \u{2502} \
^^^^^^^ invocation\n \u{2502} \
\u{2501}\u{2501}\u{2501}\u{2501}\u{2501} declaration\n \u{2575}"
);
assert_eq!(
run("@mixin caller { @include m; } @mixin m($x) { a: $x; } .a { @include caller; }\n"),
"Error: Missing argument $x.\n \u{2577}\n1 \u{2502} \
@mixin caller { @include m; } @mixin m($x) { a: $x; } .a { @include caller; }\n \u{2502} \
^^^^^^^^^^ invocation\n \u{2502} \
\u{2501}\u{2501}\u{2501}\u{2501}\u{2501} declaration\n \u{2575}"
);
}
#[test]
fn a_span_inside_an_arms_range_writes_its_row_under_its_own_line() {
let run = |src: &str| -> String {
snippet(
&compile(src, &Options::default().with_url("t.scss"))
.expect_err("expected a compile error")
.to_string(),
)
};
assert_eq!(
run("@mixin m(\n $x: 1\n) { a: $x; } .b { @include m { c: 1; } }\n"),
"Error: Mixin doesn't accept a content block.\n \u{2577}\n\
1 \u{2502} @mixin m(\n \u{2502} \u{250c}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}^\n\
2 \u{2502} \u{2502} $x: 1\n\
3 \u{2502} \u{2502} ) { a: $x; } .b { @include m { c: 1; } }\n \u{2502} \u{2502} \
^^^^^^^^^^ invocation\n \u{2502} \u{2514}\u{2500}^ declaration\n \u{2575}"
);
assert_eq!(
run("@mixin caller { @include m { c: 1; } } @mixin m(\n $x: 1\n) { a: $x; }\n.b { @include caller; }\n"),
"Error: Mixin doesn't accept a content block.\n \u{2577}\n\
1 \u{2502} @mixin caller { @include m { c: 1; } } @mixin m(\n \u{2502} \
^^^^^^^^^^ invocation\n \u{2502} \u{250c}\
\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}^\n\
2 \u{2502} \u{2502} $x: 1\n\
3 \u{2502} \u{2502} ) { a: $x; }\n \u{2502} \u{2514}\u{2500}^ declaration\n \u{2575}"
);
}