diff --git a/.changeset/negative-margin-support.md b/.changeset/negative-margin-support.md new file mode 100644 index 000000000000..204aaa79c307 --- /dev/null +++ b/.changeset/negative-margin-support.md @@ -0,0 +1,12 @@ +--- +"@biomejs/biome": patch +--- + +Added support for negative value utilities in [`useSortedClasses`](https://biomejs.dev/linter/rules/use-sorted-classes/). Negative value utilities such as `-ml-2` or `-top-4` are now recognized and sorted correctly alongside their positive counterparts. + +```jsx +// Now detected as unsorted: +
+// Suggested fix: + +``` diff --git a/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/class_info.rs b/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/class_info.rs index b943b76f0d7a..5817025229ba 100644 --- a/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/class_info.rs +++ b/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/class_info.rs @@ -38,6 +38,11 @@ enum UtilityMatch { impl From<(&str, &str)> for UtilityMatch { /// Checks if a utility matches a target, and returns the result. fn from((target, utility_text): (&str, &str)) -> Self { + // Support negative value utilities (e.g., `-ml-2` should match `ml-` or `-ml-`). + // Strip the leading `-` from both target and utility for matching purposes. + let target = target.strip_prefix('-').unwrap_or(target); + let utility_text = utility_text.strip_prefix('-').unwrap_or(utility_text); + // If the target ends with `$`, then it's an exact target. if target.ends_with('$') { // Check if the utility matches the target (without the final `$`) exactly. @@ -62,8 +67,8 @@ mod utility_match_tests { #[test] fn test_exact_match() { assert_eq!(UtilityMatch::from(("px-2$", "px-2")), UtilityMatch::Exact); - // TODO: support negative values - // assert_eq!(UtilityMatch::from(("px-2$", "-px-2")), UtilityMatch::Exact); + // Negative values should also match + assert_eq!(UtilityMatch::from(("px-2$", "-px-2")), UtilityMatch::Exact); assert_eq!( UtilityMatch::from(("px-2$", "not-px-2")), UtilityMatch::None @@ -80,8 +85,8 @@ mod utility_match_tests { #[test] fn test_partial_match() { assert_eq!(UtilityMatch::from(("px-", "px-2")), UtilityMatch::Partial); - // TODO: support negative values - // assert_eq!(UtilityMatch::from(("px-", "-px-2")), UtilityMatch::Partial); + // Negative values should also match + assert_eq!(UtilityMatch::from(("px-", "-px-2")), UtilityMatch::Partial); assert_eq!(UtilityMatch::from(("px-", "px-2.5")), UtilityMatch::Partial); assert_eq!( UtilityMatch::from(("px-", "px-anything")), @@ -92,10 +97,68 @@ mod utility_match_tests { UtilityMatch::Partial ); assert_eq!(UtilityMatch::from(("px-", "px-")), UtilityMatch::None); - // TODO: support negative values - // assert_eq!(UtilityMatch::from(("px-", "-px-")), UtilityMatch::None); + // Negative prefix without value should also not match + assert_eq!(UtilityMatch::from(("px-", "-px-")), UtilityMatch::None); assert_eq!(UtilityMatch::from(("px-", "not-px-2")), UtilityMatch::None); } + + #[test] + fn test_negative_margin_utilities() { + // Test negative margin utilities like -ml-2, -mr-4, etc. + assert_eq!(UtilityMatch::from(("ml-", "-ml-2")), UtilityMatch::Partial); + assert_eq!(UtilityMatch::from(("mr-", "-mr-4")), UtilityMatch::Partial); + assert_eq!(UtilityMatch::from(("mt-", "-mt-1")), UtilityMatch::Partial); + assert_eq!(UtilityMatch::from(("mb-", "-mb-3")), UtilityMatch::Partial); + assert_eq!(UtilityMatch::from(("m-", "-m-2")), UtilityMatch::Partial); + assert_eq!(UtilityMatch::from(("mx-", "-mx-4")), UtilityMatch::Partial); + assert_eq!(UtilityMatch::from(("my-", "-my-6")), UtilityMatch::Partial); + // Negative spacing utilities + assert_eq!( + UtilityMatch::from(("space-x-", "-space-x-2")), + UtilityMatch::Partial + ); + assert_eq!( + UtilityMatch::from(("space-y-", "-space-y-4")), + UtilityMatch::Partial + ); + // Negative positioning utilities + assert_eq!( + UtilityMatch::from(("top-", "-top-2")), + UtilityMatch::Partial + ); + assert_eq!( + UtilityMatch::from(("right-", "-right-4")), + UtilityMatch::Partial + ); + assert_eq!( + UtilityMatch::from(("bottom-", "-bottom-1")), + UtilityMatch::Partial + ); + assert_eq!( + UtilityMatch::from(("left-", "-left-3")), + UtilityMatch::Partial + ); + assert_eq!( + UtilityMatch::from(("inset-", "-inset-2")), + UtilityMatch::Partial + ); + } + + #[test] + fn test_negative_target_utilities() { + // Test that targets with leading `-` also work (for custom utilities defined with `-` prefix) + // Exact match with negative target + assert_eq!(UtilityMatch::from(("-test$", "-test")), UtilityMatch::Exact); + assert_eq!(UtilityMatch::from(("-test$", "test")), UtilityMatch::Exact); + // Partial match with negative target + assert_eq!(UtilityMatch::from(("-ml-", "-ml-2")), UtilityMatch::Partial); + assert_eq!(UtilityMatch::from(("-ml-", "ml-2")), UtilityMatch::Partial); + // Both negative target and utility + assert_eq!( + UtilityMatch::from(("-custom-", "-custom-value")), + UtilityMatch::Partial + ); + } } /// Sort-related information about a utility. @@ -669,4 +732,102 @@ mod get_class_info_tests { ); assert_eq!(get_class_info("unknown", &sort_config), None); } + + #[test] + fn test_get_class_info_negative_values() { + const UTILITIES_CONFIG: [UtilityLayer; 2] = [ + UtilityLayer { + name: "layer0", + classes: &["m-", "mx-", "my-", "mt-", "mr-", "mb-", "ml-"], + }, + UtilityLayer { + name: "layer1", + classes: &["top-", "right-", "bottom-", "left-", "inset-"], + }, + ]; + let variants: &'static [&'static str; 2] = &["hover", "focus"]; + + let sort_config = SortConfig::new(&ConfigPreset { + utilities: &UTILITIES_CONFIG, + variants, + }); + + // Test negative margin classes + assert_eq!( + get_class_info("-ml-2", &sort_config), + Some(ClassInfo { + text: "-ml-2".into(), + variant_weight: None, + layer_index: 0, + utility_index: 6, // ml- is at index 6 + arbitrary_variants: None + }) + ); + assert_eq!( + get_class_info("-m-4", &sort_config), + Some(ClassInfo { + text: "-m-4".into(), + variant_weight: None, + layer_index: 0, + utility_index: 0, // m- is at index 0 + arbitrary_variants: None + }) + ); + assert_eq!( + get_class_info("-mx-2", &sort_config), + Some(ClassInfo { + text: "-mx-2".into(), + variant_weight: None, + layer_index: 0, + utility_index: 1, // mx- is at index 1 + arbitrary_variants: None + }) + ); + + // Test negative positioning classes + assert_eq!( + get_class_info("-top-4", &sort_config), + Some(ClassInfo { + text: "-top-4".into(), + variant_weight: None, + layer_index: 1, + utility_index: 0, // top- is at index 0 + arbitrary_variants: None + }) + ); + assert_eq!( + get_class_info("-left-2", &sort_config), + Some(ClassInfo { + text: "-left-2".into(), + variant_weight: None, + layer_index: 1, + utility_index: 3, // left- is at index 3 + arbitrary_variants: None + }) + ); + + // Test negative with variants + assert_eq!( + get_class_info("hover:-ml-2", &sort_config), + Some(ClassInfo { + text: "hover:-ml-2".into(), + variant_weight: Some(bitvec![u8, Lsb0; 1]), + layer_index: 0, + utility_index: 6, // ml- is at index 6 + arbitrary_variants: None + }) + ); + + // Positive values should still work + assert_eq!( + get_class_info("ml-2", &sort_config), + Some(ClassInfo { + text: "ml-2".into(), + variant_weight: None, + layer_index: 0, + utility_index: 6, // ml- is at index 6 + arbitrary_variants: None + }) + ); + } } diff --git a/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/unsorted.jsx b/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/unsorted.jsx index af26da713498..add55a3daee4 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/unsorted.jsx +++ b/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/unsorted.jsx @@ -30,6 +30,15 @@ + {/* negative value utilities */} + {/* SHOULD emit diagnostics (negative values like -ml-2 should be detected) */} + + + + + + + >; // functions diff --git a/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/unsorted.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/unsorted.jsx.snap index a2bea952c3c6..addc21bf860f 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/unsorted.jsx.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/unsorted.jsx.snap @@ -1,7 +1,6 @@ --- source: crates/biome_js_analyze/tests/spec_tests.rs expression: unsorted.jsx -snapshot_kind: text --- # Input ```jsx @@ -37,6 +36,15 @@ snapshot_kind: text + {/* negative value utilities */} + {/* SHOULD emit diagnostics (negative values like -ml-2 should be detected) */} + + + + + + + >; // functions @@ -540,7 +548,7 @@ unsorted.jsx:31:13 lint/nursery/useSortedClasses FIXABLE ━━━━━━━ > 31 │ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 32 │ - 33 │ >; + 33 │ {/* negative value utilities */} i Unsafe fix: Sort the classes. @@ -549,7 +557,7 @@ unsorted.jsx:31:13 lint/nursery/useSortedClasses FIXABLE ━━━━━━━ 31 │ - → 31 │ + → 32 32 │ - 33 33 │ >; + 33 33 │ {/* negative value utilities */} ``` @@ -563,8 +571,8 @@ unsorted.jsx:32:13 lint/nursery/useSortedClasses FIXABLE ━━━━━━━ 31 │ > 32 │ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - 33 │ >; - 34 │ + 33 │ {/* negative value utilities */} + 34 │ {/* SHOULD emit diagnostics (negative values like -ml-2 should be detected) */} i Unsafe fix: Sort the classes. @@ -572,224 +580,392 @@ unsorted.jsx:32:13 lint/nursery/useSortedClasses FIXABLE ━━━━━━━ 31 31 │ 32 │ - → 32 │ + → - 33 33 │ >; - 34 34 │ + 33 33 │ {/* negative value utilities */} + 34 34 │ {/* SHOULD emit diagnostics (negative values like -ml-2 should be detected) */} ``` ``` -unsorted.jsx:46:13 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +unsorted.jsx:35:13 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ i These CSS classes should be sorted. - 44 │ // nested values - 45 │ /* SHOULD emit diagnostics (class attribute supported by default) */ - > 46 │ ; + 33 │ {/* negative value utilities */} + 34 │ {/* SHOULD emit diagnostics (negative values like -ml-2 should be detected) */} + > 35 │ + │ ^^^^^^^^^^^^^^^^^ + 36 │ + 37 │ + + i Unsafe fix: Sort the classes. + + 33 33 │ {/* negative value utilities */} + 34 34 │ {/* SHOULD emit diagnostics (negative values like -ml-2 should be detected) */} + 35 │ - → + 35 │ + → + 36 36 │ + 37 37 │ + + +``` + +``` +unsorted.jsx:36:13 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i These CSS classes should be sorted. + + 34 │ {/* SHOULD emit diagnostics (negative values like -ml-2 should be detected) */} + 35 │ + > 36 │ + │ ^^^^^^^^^^^^^^^^^ + 37 │ + 38 │ + + i Unsafe fix: Sort the classes. + + 34 34 │ {/* SHOULD emit diagnostics (negative values like -ml-2 should be detected) */} + 35 35 │ + 36 │ - → + 36 │ + → + 37 37 │ + 38 38 │ + + +``` + +``` +unsorted.jsx:37:13 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i These CSS classes should be sorted. + + 35 │ + 36 │ + > 37 │ + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 38 │ + 39 │ + + i Unsafe fix: Sort the classes. + + 35 35 │ + 36 36 │ + 37 │ - → + 37 │ + → + 38 38 │ + 39 39 │ + + +``` + +``` +unsorted.jsx:38:13 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i These CSS classes should be sorted. + + 36 │ + 37 │ + > 38 │ + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 39 │ + 40 │ + + i Unsafe fix: Sort the classes. + + 36 36 │ + 37 37 │ + 38 │ - → + 38 │ + → + 39 39 │ + 40 40 │ + + +``` + +``` +unsorted.jsx:39:13 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i These CSS classes should be sorted. + + 37 │ + 38 │ + > 39 │ + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 40 │ + 41 │ + + i Unsafe fix: Sort the classes. + + 37 37 │ + 38 38 │ + 39 │ - → + 39 │ + → + 40 40 │ + 41 41 │ + + +``` + +``` +unsorted.jsx:40:13 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i These CSS classes should be sorted. + + 38 │ + 39 │ + > 40 │ + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 41 │ + 42 │ >; + + i Unsafe fix: Sort the classes. + + 38 38 │ + 39 39 │ + 40 │ - → + 40 │ + → + 41 41 │ + 42 42 │ >; + + +``` + +``` +unsorted.jsx:41:13 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i These CSS classes should be sorted. + + 39 │ + 40 │ + > 41 │ + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^ + 42 │ >; + 43 │ + + i Unsafe fix: Sort the classes. + + 39 39 │ + 40 40 │ + 41 │ - → + 41 │ + → + 42 42 │ >; + 43 43 │ + + +``` + +``` +unsorted.jsx:55:13 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i These CSS classes should be sorted. + + 53 │ // nested values + 54 │ /* SHOULD emit diagnostics (class attribute supported by default) */ + > 55 │ ; │ ^^^^^^^^^^^^^^^^^^ - 47 │ ; - 48 │ ; + 56 │ ; + 57 │ ; i Unsafe fix: Sort the classes. - 44 44 │ // nested values - 45 45 │ /* SHOULD emit diagnostics (class attribute supported by default) */ - 46 │ - ; - 46 │ + ; - 47 47 │ ; - 48 48 │ ; + 53 53 │ // nested values + 54 54 │ /* SHOULD emit diagnostics (class attribute supported by default) */ + 55 │ - ; + 55 │ + ; + 56 56 │ ; + 57 57 │ ; ``` ``` -unsorted.jsx:47:14 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +unsorted.jsx:56:14 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ i These CSS classes should be sorted. - 45 │ /* SHOULD emit diagnostics (class attribute supported by default) */ - 46 │ ; - > 47 │ ; + 54 │ /* SHOULD emit diagnostics (class attribute supported by default) */ + 55 │ ; + > 56 │ ; │ ^^^^^^^^^^^^^^^^ - 48 │ ; - 49 │ ; + 57 │ ; + 58 │ ; i Unsafe fix: Sort the classes. - 45 45 │ /* SHOULD emit diagnostics (class attribute supported by default) */ - 46 46 │ ; - 47 │ - ; - 47 │ + ; - 48 48 │ ; - 49 49 │ ; + 54 54 │ /* SHOULD emit diagnostics (class attribute supported by default) */ + 55 55 │ ; + 56 │ - ; + 56 │ + ; + 57 57 │ ; + 58 58 │ ; ``` ``` -unsorted.jsx:48:14 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +unsorted.jsx:57:14 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ i These CSS classes should be sorted. - 46 │ ; - 47 │ ; - > 48 │ ; + 55 │ ; + 56 │ ; + > 57 │ ; │ ^^^^^^^^^^^^^^^^^^ - 49 │ ; - 50 │ ; + 59 │ ; - 47 47 │ ; - 48 │ - ; - 48 │ + ; - 49 49 │ ; - 50 50 │ ; + 56 56 │ ; + 57 │ - ; + 57 │ + ; + 58 58 │ ; + 59 59 │ ; - 48 │ ; - > 49 │ ; + 56 │ ; + 57 │ ; + > 58 │ ; │ ^^^^^^^^^^^^^^^^ - 50 │ ; - 48 48 │ ; - 49 │ - ; - 49 │ + ; - 50 50 │ ; + 57 57 │ ; + 58 │ - ; + 58 │ + ; + 59 59 │