increase code coverage

This commit is contained in:
Connor Skees 2022-12-28 21:42:58 -05:00
parent 6cd208f41d
commit 743ad7a340
22 changed files with 344 additions and 101 deletions

View File

@ -263,7 +263,7 @@ impl Environment {
for name in (*self.scopes.global_variables()).borrow().keys() { for name in (*self.scopes.global_variables()).borrow().keys() {
if (*module).borrow().var_exists(*name) { if (*module).borrow().var_exists(*name) {
return Err(( return Err((
format!("This module and the new module both define a variable named \"{}\".", name = name) format!("This module and the new module both define a variable named \"{name}\".", name = name)
, span).into()); , span).into());
} }
} }

View File

@ -60,7 +60,7 @@ impl<'a, 'c, P: StylesheetParser<'a>> ValueParser<'a, 'c, P> {
} }
if value_parser.inside_bracketed_list { if value_parser.inside_bracketed_list {
let start = parser.toks().cursor(); let bracket_start = parser.toks().cursor();
parser.expect_char('[')?; parser.expect_char('[')?;
parser.whitespace()?; parser.whitespace()?;
@ -71,14 +71,12 @@ impl<'a, 'c, P: StylesheetParser<'a>> ValueParser<'a, 'c, P> {
separator: ListSeparator::Undecided, separator: ListSeparator::Undecided,
brackets: Brackets::Bracketed, brackets: Brackets::Bracketed,
}) })
.span(parser.toks_mut().span_from(start))); .span(parser.toks_mut().span_from(bracket_start)));
} }
Some(start)
} else {
None
}; };
value_parser.start = parser.toks().cursor();
value_parser.single_expression = Some(value_parser.parse_single_expression(parser)?); value_parser.single_expression = Some(value_parser.parse_single_expression(parser)?);
let mut value = value_parser.parse_value(parser)?; let mut value = value_parser.parse_value(parser)?;
@ -392,6 +390,7 @@ impl<'a, 'c, P: StylesheetParser<'a>> ValueParser<'a, 'c, P> {
self.reset_state(parser)?; self.reset_state(parser)?;
continue; continue;
} }
// todo: does this branch ever get hit
} }
if self.single_expression.is_none() { if self.single_expression.is_none() {
@ -400,7 +399,7 @@ impl<'a, 'c, P: StylesheetParser<'a>> ValueParser<'a, 'c, P> {
self.resolve_space_expressions(parser)?; self.resolve_space_expressions(parser)?;
// [resolveSpaceExpressions can modify [singleExpression_], but it // [resolveSpaceExpressions] can modify [singleExpression_], but it
// can't set it to null`. // can't set it to null`.
self.comma_expressions self.comma_expressions
.get_or_insert_with(Default::default) .get_or_insert_with(Default::default)
@ -1368,62 +1367,6 @@ impl<'a, 'c, P: StylesheetParser<'a>> ValueParser<'a, 'c, P> {
.span(span)) .span(span))
} }
fn try_parse_url_contents(
parser: &mut P,
name: Option<String>,
) -> SassResult<Option<Interpolation>> {
let start = parser.toks().cursor();
if !parser.scan_char('(') {
return Ok(None);
}
parser.whitespace_without_comments();
// Match Ruby Sass's behavior: parse a raw URL() if possible, and if not
// backtrack and re-parse as a function expression.
let mut buffer = Interpolation::new();
buffer.add_string(name.unwrap_or_else(|| "url".to_owned()));
buffer.add_char('(');
while let Some(next) = parser.toks().peek() {
match next.kind {
'\\' => {
buffer.add_string(parser.parse_escape(false)?);
}
'!' | '%' | '&' | '*'..='~' | '\u{80}'..=char::MAX => {
parser.toks_mut().next();
buffer.add_char(next.kind);
}
'#' => {
if matches!(parser.toks().peek_n(1), Some(Token { kind: '{', .. })) {
buffer.add_interpolation(parser.parse_single_interpolation()?);
} else {
parser.toks_mut().next();
buffer.add_char(next.kind);
}
}
')' => {
parser.toks_mut().next();
buffer.add_char(next.kind);
return Ok(Some(buffer));
}
' ' | '\t' | '\n' | '\r' => {
parser.whitespace_without_comments();
if !parser.toks().next_char_is(')') {
break;
}
}
_ => break,
}
}
parser.toks_mut().set_cursor(start);
Ok(None)
}
pub(crate) fn try_parse_special_function( pub(crate) fn try_parse_special_function(
parser: &mut P, parser: &mut P,
name: &str, name: &str,
@ -1466,15 +1409,13 @@ impl<'a, 'c, P: StylesheetParser<'a>> ValueParser<'a, 'c, P> {
buffer.add_char('('); buffer.add_char('(');
} }
"url" => { "url" => {
return Ok( return Ok(parser.try_url_contents(None)?.map(|contents| {
ValueParser::try_parse_url_contents(parser, None)?.map(|contents| { AstExpr::String(
AstExpr::String( StringExpr(contents, QuoteKind::None),
StringExpr(contents, QuoteKind::None), parser.toks_mut().span_from(start),
parser.toks_mut().span_from(start), )
) .span(parser.toks_mut().span_from(start))
.span(parser.toks_mut().span_from(start)) }))
}),
)
} }
_ => return Ok(None), _ => return Ok(None),
} }

View File

@ -21,6 +21,6 @@ pub(crate) fn as_hex(c: char) -> u32 {
'0'..='9' => c as u32 - '0' as u32, '0'..='9' => c as u32 - '0' as u32,
'A'..='F' => 10 + c as u32 - 'A' as u32, 'A'..='F' => 10 + c as u32 - 'A' as u32,
'a'..='f' => 10 + c as u32 - 'a' as u32, 'a'..='f' => 10 + c as u32 - 'a' as u32,
_ => panic!(), _ => unreachable!(),
} }
} }

View File

@ -285,7 +285,7 @@ impl<V: fmt::Debug + Clone> MapView for MergedMapView<V> {
} }
} }
panic!("New entries may not be added to MergedMapView") unreachable!("New entries may not be added to MergedMapView")
} }
fn keys(&self) -> Vec<Identifier> { fn keys(&self) -> Vec<Identifier> {

View File

@ -55,26 +55,8 @@ impl PartialEq for Value {
Value::String(s2, ..) => s1 == s2, Value::String(s2, ..) => s1 == s2,
_ => false, _ => false,
}, },
Value::Dimension(SassNumber { Value::Dimension(n1) => match other {
num: n, Value::Dimension(n2) => n1 == n2,
unit,
as_slash: _,
}) => match other {
Value::Dimension(SassNumber {
num: n2,
unit: unit2,
as_slash: _,
}) => {
if !unit.comparable(unit2) {
return false;
}
if (*unit2 == Unit::None || *unit == Unit::None) && unit != unit2 {
return false;
}
*n == n2.convert(unit2, unit)
}
_ => false, _ => false,
}, },
Value::List(list1, sep1, brackets1) => match other { Value::List(list1, sep1, brackets1) => match other {

View File

@ -189,10 +189,20 @@ impl SassNumber {
impl PartialEq for SassNumber { impl PartialEq for SassNumber {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.num == other.num && self.unit == other.unit if !self.unit.comparable(&other.unit) {
return false;
}
if (other.unit == Unit::None || self.unit == Unit::None) && self.unit != other.unit {
return false;
}
self.num == other.num.convert(&other.unit, &self.unit)
} }
} }
impl Eq for SassNumber {}
impl Add<SassNumber> for SassNumber { impl Add<SassNumber> for SassNumber {
type Output = SassNumber; type Output = SassNumber;
fn add(self, rhs: SassNumber) -> Self::Output { fn add(self, rhs: SassNumber) -> Self::Output {
@ -285,5 +295,3 @@ impl Div<SassNumber> for SassNumber {
self.multiply_units(self.num.0 / rhs.num.0, rhs.unit.invert()) self.multiply_units(self.num.0 / rhs.num.0, rhs.unit.invert())
} }
} }
impl Eq for SassNumber {}

View File

@ -262,6 +262,15 @@ test!(
}", }",
"@unknown {\n .bar {\n a: b;\n }\n}\n" "@unknown {\n .bar {\n a: b;\n }\n}\n"
); );
test!(
query_begins_with_interpolation,
"a {
@at-root (#{wi}th: rule) {
color: red;
}
}",
"a {\n color: red;\n}\n"
);
error!( error!(
missing_closing_curly_brace, missing_closing_curly_brace,
"@at-root {", "Error: expected \"}\"." "@at-root {", "Error: expected \"}\"."

View File

@ -635,3 +635,26 @@ error!(
single_arg_saturate_expects_number, single_arg_saturate_expects_number,
"a {\n color: saturate(red);\n}\n", "Error: $amount: red is not a number." "a {\n color: saturate(red);\n}\n", "Error: $amount: red is not a number."
); );
error!(
hex_color_starts_with_number_non_hex_digit_at_position_2,
"a {\n color: #0zz;\n}\n", "Error: Expected hex digit."
);
error!(
hex_color_starts_with_number_non_hex_digit_at_position_3,
"a {\n color: #00z;\n}\n", "Error: Expected hex digit."
);
test!(
hex_color_starts_with_number_non_hex_digit_at_position_4,
"a {\n color: #000z;\n}\n",
"a {\n color: #000 z;\n}\n"
);
test!(
#[ignore = "we don't emit 4 character hex colors correctly"]
hex_color_starts_with_number_non_hex_digit_at_position_5,
"a {\n color: #0000z;\n}\n",
"a {\n color: rgba(0, 0, 0, 0) z;\n}\n"
);
error!(
hex_color_starts_with_number_non_hex_digit_at_position_6,
"a {\n color: #00000z;\n}\n", "Error: Expected hex digit."
);

View File

@ -170,3 +170,4 @@ test!(
"a {/**/}", "a {/**/}",
"a { /**/ }\n" "a { /**/ }\n"
); );
test!(silent_comment_as_child, "a {\n// silent\n}\n", "");

View File

@ -267,3 +267,10 @@ test!(
}", }",
"a {\n color: true;\n}\n" "a {\n color: true;\n}\n"
); );
test!(
calculation_equality_converts_units,
"a {
color: calc(1in + 1rem) == calc(2.54cm + 1rem);
}",
"a {\n color: true;\n}\n"
);

View File

@ -273,3 +273,7 @@ error!(
nothing_after_dot_in_value_preceded_by_minus_sign, nothing_after_dot_in_value_preceded_by_minus_sign,
"a { color: -.", "Error: Expected digit." "a { color: -.", "Error: Expected digit."
); );
error!(
nothing_after_bang_in_space_separated_list,
"a { color: a !", r#"Error: Expected "important"."#
);

View File

@ -409,6 +409,13 @@ test!(
}", }",
"a {\n color: before;\n}\n" "a {\n color: before;\n}\n"
); );
test!(
can_parse_module_variable_declaration,
"@function foo() {
foo.$bar: red;
}",
""
);
error!( error!(
function_no_return, function_no_return,
"@function foo() {} "@function foo() {}

View File

@ -523,6 +523,72 @@ test!(
); );
error!(unclosed_single_quote, r#"@import '"#, "Error: Expected '."); error!(unclosed_single_quote, r#"@import '"#, "Error: Expected '.");
error!(unclosed_double_quote, r#"@import ""#, "Error: Expected \"."); error!(unclosed_double_quote, r#"@import ""#, "Error: Expected \".");
error!(
dynamic_disallowed_inside_if,
r#"@if true {
@import "foo";
}"#,
"Error: This at-rule is not allowed here."
);
error!(
dynamic_disallowed_inside_while,
r#"@while true {
@import "foo";
}"#,
"Error: This at-rule is not allowed here."
);
error!(
dynamic_disallowed_inside_for,
r#"@for $i from 0 through 1 {
@import "foo";
}"#,
"Error: This at-rule is not allowed here."
);
error!(
dynamic_disallowed_inside_each,
r#"@each $i in a {
@import "foo";
}"#,
"Error: This at-rule is not allowed here."
);
test!(
static_allowed_inside_if,
r#"@if true {
@import "foo.css";
}"#,
"@import \"foo.css\";\n"
);
test!(
static_allowed_inside_while,
r#"
$a: 0;
@while $a == 0 {
@import "foo.css";
$a: 1;
}"#,
"@import \"foo.css\";\n"
);
test!(
static_allowed_inside_for,
r#"@for $i from 0 to 1 {
@import "foo.css";
}"#,
"@import \"foo.css\";\n"
);
test!(
static_allowed_inside_each,
r#"@each $i in a {
@import "foo.css";
}"#,
"@import \"foo.css\";\n"
);
error!(
dynamic_disallowed_inside_mixin,
r#"@mixin foo {
@import "foo";
}"#,
"Error: This at-rule is not allowed here."
);
// todo: edge case tests for plain css imports moved to top // todo: edge case tests for plain css imports moved to top
// todo: test for calling paths, e.g. `grass b\index.scss` // todo: test for calling paths, e.g. `grass b\index.scss`

View File

@ -412,6 +412,26 @@ test!(
"a {\n color: [null];\n}\n", "a {\n color: [null];\n}\n",
"a {\n color: [];\n}\n" "a {\n color: [];\n}\n"
); );
test!(
space_separated_bracketed_list_in_parens,
"a {\n color: ([a b]);\n}\n",
"a {\n color: [a b];\n}\n"
);
test!(
does_not_eval_division_inside_space_separated_bracketed_list_in_parens,
"a {\n color: ([1/2 1/2]);\n}\n",
"a {\n color: [1/2 1/2];\n}\n"
);
test!(
comma_separated_bracketed_list_in_parens,
"a {\n color: ([a, b]);\n}\n",
"a {\n color: [a, b];\n}\n"
);
test!(
does_not_eval_division_inside_comma_separated_bracketed_list_in_parens,
"a {\n color: ([1/2, 1/2]);\n}\n",
"a {\n color: [1/2, 1/2];\n}\n"
);
test!( test!(
comma_separated_list_has_element_beginning_with_capital_A, comma_separated_list_has_element_beginning_with_capital_A,
"a {\n color: a, A, \"Noto Color Emoji\";\n}\n", "a {\n color: a, A, \"Noto Color Emoji\";\n}\n",

View File

@ -280,6 +280,27 @@ test!(
"a {\n color: (a: b)==(a: c);\n}\n", "a {\n color: (a: b)==(a: c);\n}\n",
"a {\n color: false;\n}\n" "a {\n color: false;\n}\n"
); );
test!(
important_as_key,
"a {\n color: inspect((a: b, !important: c));\n}\n",
"a {\n color: (a: b, !important: c);\n}\n"
);
error!(
bang_identifier_not_important_as_key,
"a {\n color: inspect((a: b, !a: c));\n}\n", r#"Error: expected ")"."#
);
error!(
bang_identifier_not_important_but_starts_with_i_as_key,
"a {\n color: inspect((a: b, !i: c));\n}\n", r#"Error: Expected "important"."#
);
error!(
bang_identifier_not_important_ascii_whitespace_as_key,
"a {\n color: inspect((a: b, ! : c));\n}\n", r#"Error: Expected "important"."#
);
error!(
bang_identifier_not_important_loud_comment_as_key,
"a {\n color: inspect((a: b, !/**/: c));\n}\n", r#"Error: expected ")"."#
);
test!( test!(
empty_with_single_line_comments, empty_with_single_line_comments,
"$foo: (\n \n // :/a.b\n \n ); "$foo: (\n \n // :/a.b\n \n );
@ -314,3 +335,7 @@ error!(
denies_comma_separated_list_without_parens_as_key, denies_comma_separated_list_without_parens_as_key,
"$map: (a: 1, b, c, d: e);", "Error: expected \":\"." "$map: (a: 1, b, c, d: e);", "Error: expected \":\"."
); );
error!(
nothing_after_first_comma,
"$map: (a: b,", "Error: expected \")\"."
);

View File

@ -570,6 +570,17 @@ test!(
}"#, }"#,
"@media (min-width: \\0 ) {\n a {\n color: red;\n }\n}\n" "@media (min-width: \\0 ) {\n a {\n color: red;\n }\n}\n"
); );
test!(
simple_unmergeable,
"a {
@media a {
@media b {
color: red;
}
}
}",
""
);
error!( error!(
media_query_has_quoted_closing_paren, media_query_has_quoted_closing_paren,
r#"@media ('a)'w) { r#"@media ('a)'w) {

View File

@ -35,6 +35,32 @@ test!(
"a {\n color: 1 or 2;\n}\n", "a {\n color: 1 or 2;\n}\n",
grass::Options::default().input_syntax(InputSyntax::Css) grass::Options::default().input_syntax(InputSyntax::Css)
); );
test!(
simple_calculation,
"a {
color: calc(1 + 1);
}",
"a {\n color: 2;\n}\n",
grass::Options::default().input_syntax(InputSyntax::Css)
);
test!(
simple_url_import,
r#"@import url("foo");"#,
"@import url(\"foo\");\n",
grass::Options::default().input_syntax(InputSyntax::Css)
);
test!(
import_no_file_extension,
r#"@import "foo";"#,
"@import \"foo\";\n",
grass::Options::default().input_syntax(InputSyntax::Css)
);
test!(
import_with_condition,
r#"@import "foo" screen and (foo: bar);"#,
"@import \"foo\" screen and (foo: bar);\n",
grass::Options::default().input_syntax(InputSyntax::Css)
);
test!( test!(
does_not_evaluate_not, does_not_evaluate_not,
"a { "a {
@ -155,6 +181,14 @@ error!(
"Error: Operators aren't allowed in plain CSS.", "Error: Operators aren't allowed in plain CSS.",
grass::Options::default().input_syntax(InputSyntax::Css) grass::Options::default().input_syntax(InputSyntax::Css)
); );
error!(
disallows_interpolation,
"a {
color: a#{b}c;
}",
"Error: Interpolation isn't allowed in plain CSS.",
grass::Options::default().input_syntax(InputSyntax::Css)
);
test!( test!(
allows_rgb_function, allows_rgb_function,
"a { "a {
@ -163,3 +197,13 @@ test!(
"a {\n color: rgb(true, a, b);\n}\n", "a {\n color: rgb(true, a, b);\n}\n",
grass::Options::default().input_syntax(InputSyntax::Css) grass::Options::default().input_syntax(InputSyntax::Css)
); );
test!(
simple_supports,
"@supports (foo) {
a {
color: red;
}
}",
"@supports (foo) {\n a {\n color: red;\n }\n}\n",
grass::Options::default().input_syntax(InputSyntax::Css)
);

View File

@ -81,6 +81,18 @@ test!(
"/* loud */\n", "/* loud */\n",
grass::Options::default().input_syntax(InputSyntax::Sass) grass::Options::default().input_syntax(InputSyntax::Sass)
); );
test!(
special_mixin_and_include_characters,
r#"
=foo
color: red
a
+foo
"#,
"a {\n color: red;\n}\n",
grass::Options::default().input_syntax(InputSyntax::Sass)
);
error!( error!(
multiline_comment_in_value_position, multiline_comment_in_value_position,
r#" r#"
@ -90,3 +102,15 @@ loud */ red
"Error: expected */.", "Error: expected */.",
grass::Options::default().input_syntax(InputSyntax::Sass) grass::Options::default().input_syntax(InputSyntax::Sass)
); );
error!(
document_starts_with_spaces,
r#" "#,
"Error: Indenting at the beginning of the document is illegal.",
grass::Options::default().input_syntax(InputSyntax::Sass)
);
error!(
document_starts_with_tab,
"\t",
"Error: Indenting at the beginning of the document is illegal.",
grass::Options::default().input_syntax(InputSyntax::Sass)
);

View File

@ -927,6 +927,10 @@ error!(
denies_optional_in_selector, denies_optional_in_selector,
"a !optional {}", "Error: expected \"{\"." "a !optional {}", "Error: expected \"{\"."
); );
error!(
child_selector_starts_with_forward_slash,
"a { /b { } }", "Error: expected selector."
);
// todo: // todo:
// [attr=url] { // [attr=url] {

View File

@ -276,6 +276,30 @@ test!(
"a {\n color: calc(1dpi + 1dppx);\n}\n", "a {\n color: calc(1dpi + 1dppx);\n}\n",
"a {\n color: 97dpi;\n}\n" "a {\n color: 97dpi;\n}\n"
); );
test!(
ternary_inside_calc,
"a {\n color: calc(if(true, 1, unit(foo)));\n}\n",
"a {\n color: 1;\n}\n"
);
test!(
retains_parens_around_var_in_calc,
"a {\n color: calc((var(--a)) + 1rem);\n}\n",
"a {\n color: calc((var(--a)) + 1rem);\n}\n"
);
test!(
removes_superfluous_parens_around_function_call_in_calc,
"a {\n color: calc((foo(--a)) + 1rem);\n}\n",
"a {\n color: calc(foo(--a) + 1rem);\n}\n"
);
test!(
calculation_inside_calc,
"a {\n color: calc(calc(1px + 1rem) * calc(2px - 2in));\n}\n",
"a {\n color: calc((1px + 1rem) * -190px);\n}\n"
);
error!(
escaped_close_paren_inside_calc,
"a {\n color: calc(\\));\n}\n", r#"Error: Expected "(" or "."."#
);
error!( error!(
nothing_after_last_arg, nothing_after_last_arg,
"a { color: calc(1 + 1", r#"Error: expected "+", "-", "*", "/", or ")"."# "a { color: calc(1 + 1", r#"Error: expected "+", "-", "*", "/", or ")"."#

View File

@ -21,6 +21,31 @@ test!(
"a {\n color: u+27a;\n}\n", "a {\n color: u+27a;\n}\n",
"a {\n color: u+27a;\n}\n" "a {\n color: u+27a;\n}\n"
); );
test!(
second_element_in_list,
"a {\n color: a u+55;\n}\n",
"a {\n color: a u+55;\n}\n"
);
test!(
escaped_lowercase_u,
"a {\n color: \\75+55;\n}\n",
"a {\n color: u55;\n}\n"
);
test!(
escaped_uppercase_u,
"a {\n color: \\55+55;\n}\n",
"a {\n color: U55;\n}\n"
);
test!(
escaped_lowercase_u_with_space_after_escape,
"a {\n color: \\75 +55;\n}\n",
"a {\n color: u55;\n}\n"
);
test!(
escaped_uppercase_u_with_space_after_escape,
"a {\n color: \\55 +55;\n}\n",
"a {\n color: U55;\n}\n"
);
error!( error!(
interpolated_range, interpolated_range,
"a {\n color: U+2A#{70}C;\n}\n", "Error: Expected end of identifier." "a {\n color: U+2A#{70}C;\n}\n", "Error: Expected end of identifier."
@ -37,6 +62,19 @@ error!(
length_of_6_with_question_mark, length_of_6_with_question_mark,
"a {\n color: U+123456?;\n}\n", "Error: Expected at most 6 digits." "a {\n color: U+123456?;\n}\n", "Error: Expected at most 6 digits."
); );
error!(
// todo: escaped u at start \75 and \55 nothing_after_plus_lowercase,
// with and without space "a {\n color: u+;\n}\n", r#"Error: Expected hex digit or "?"."#
);
error!(
nothing_after_plus_uppercase,
"a {\n color: U+;\n}\n", r#"Error: Expected hex digit or "?"."#
);
error!(
second_part_of_range_is_empty,
"a {\n color: u+55-;\n}\n", r#"Error: Expected hex digit."#
);
error!(
second_part_of_range_is_more_than_6_chars,
"a {\n color: u+55-1234567;\n}\n", r#"Error: Expected at most 6 digits."#
);

View File

@ -171,6 +171,11 @@ test!(
"a {\n color: url(#);\n}\n", "a {\n color: url(#);\n}\n",
"a {\n color: url(#);\n}\n" "a {\n color: url(#);\n}\n"
); );
test!(
escaped_close_paren,
"a {\n color: url(\\));\n}\n",
"a {\n color: url(\\));\n}\n"
);
error!( error!(
url_nothing_after_forward_slash_in_interpolation, url_nothing_after_forward_slash_in_interpolation,
"a { color: url(#{/", "Error: Expected expression." "a { color: url(#{/", "Error: Expected expression."