From ec1e9e8d7ae573c0f0ed01d9519b1bb16180fd46 Mon Sep 17 00:00:00 2001 From: Ishiyama Yuzuki <38999742+Yz4230@users.noreply.github.com> Date: Wed, 18 Dec 2024 23:28:51 +0900 Subject: [PATCH] feat(linter): support class sorting for `tw.div` (#4699) --- CHANGELOG.md | 21 ++++++++++++++++ .../src/lint/nursery/use_sorted_classes.rs | 8 ++++++- .../any_class_string_like.rs | 7 ++++++ .../nursery/use_sorted_classes/options.rs | 14 +++++++++++ .../codeOptionsSorted.options.json | 2 +- .../codeOptionsUnsorted.jsx.snap | 24 +++++++++++++++++++ .../codeOptionsUnsorted.options.json | 2 +- crates/biome_js_syntax/src/jsx_ext.rs | 2 +- 8 files changed, 76 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b4f90afbda6..5c5abaf0769d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -205,6 +205,27 @@ our [guidelines for writing a good changelog entry](https://github.com/biomejs/b Contributed by @lucasweng +- [useSortedClasses](https://biomejs.dev/linter/rules/use-sorted-classes/) now supports wildcards in the `function` option. + + ```json + { + "linter": { + "rules": { + "nursery": { + "useSortedClasses": { + "level": "warn", + "options": { + "functions": ["tw.*"] + } + } + } + } + } + } + ``` + + This allows the rule to handle class sorting for tagged template literals like `` tw.div`...` ``, used in libraries such as [twin.macro](https://github.com/ben-rogerson/twin.macro) and [react-twc](https://github.com/gregberge/twc). + #### Enhancements - `useExportType` and `useImportType` now ignore TypeScript declaration files ([#4416](https://github.com/biomejs/biome/pull/4416)). Contributed by @Conaclos diff --git a/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes.rs b/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes.rs index 2bc14ffc32e2..8b4e7f7fa196 100644 --- a/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes.rs +++ b/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes.rs @@ -75,7 +75,7 @@ declare_lint_rule! { /// { /// "options": { /// "attributes": ["classList"], - /// "functions": ["clsx", "cva", "tw"] + /// "functions": ["clsx", "cva", "tw", "tw.*"] /// } /// } /// ``` @@ -111,6 +111,12 @@ declare_lint_rule! { /// tw`px-2 foo p-4 bar`; /// ``` /// + /// **Since v2.0.0**, tagged template literals like `` tw.div`...` `` are supported by setting `tw.*`: + /// + /// ```js,expect_diagnostic,use_options + /// tw.div`px-2 foo p-4 bar`; + /// ``` + /// /// ### Sort-related /// /// :::caution diff --git a/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/any_class_string_like.rs b/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/any_class_string_like.rs index 92b3e4900de8..fa47006c74d9 100644 --- a/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/any_class_string_like.rs +++ b/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/any_class_string_like.rs @@ -104,6 +104,13 @@ impl AnyClassStringLike { return Some(true); } } + if let Some(AnyJsExpression::JsStaticMemberExpression(tag)) = + template_expression.tag() + { + if options.match_function(tag.text().as_ref()) { + return Some(true); + } + } } else if let Some(jsx_attribute) = JsxAttribute::cast_ref(&ancestor) { let attribute_name = get_attribute_name(&jsx_attribute)?; if options.has_attribute(attribute_name.text()) { diff --git a/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/options.rs b/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/options.rs index 861a1813eae9..25cc804d8cf5 100644 --- a/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/options.rs +++ b/crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/options.rs @@ -33,6 +33,20 @@ impl UtilityClassSortingOptions { false } + pub(crate) fn match_function(&self, name: &str) -> bool { + self.functions.iter().flatten().any(|matcher| { + let mut matcher_parts = matcher.split('.'); + let mut name_parts = name.split('.'); + + let all_parts_match = matcher_parts + .by_ref() + .zip(name_parts.by_ref()) + .all(|(m, p)| m == "*" || m == p); + + all_parts_match && matcher_parts.next().is_none() && name_parts.next().is_none() + }) + } + pub(crate) fn has_attribute(&self, name: &str) -> bool { CLASS_ATTRIBUTES.contains(&name) || self.attributes.iter().flatten().any(|v| v.as_ref() == name) diff --git a/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/codeOptionsSorted.options.json b/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/codeOptionsSorted.options.json index b9e8a6b22d0c..fa79a7b7733a 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/codeOptionsSorted.options.json +++ b/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/codeOptionsSorted.options.json @@ -7,7 +7,7 @@ "level": "error", "options": { "attributes": ["customClassAttribute"], - "functions": ["clsx", "tw"] + "functions": ["clsx", "tw", "tw.*"] } } } diff --git a/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/codeOptionsUnsorted.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/codeOptionsUnsorted.jsx.snap index 4f2519d7a705..789470ac6027 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/codeOptionsUnsorted.jsx.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/codeOptionsUnsorted.jsx.snap @@ -418,6 +418,30 @@ codeOptionsUnsorted.jsx:27:4 lint/nursery/useSortedClasses FIXABLE ━━━ 29 29 │ notClassFunction("px-2 foo p-4 bar"); +``` + +``` +codeOptionsUnsorted.jsx:28:8 lint/nursery/useSortedClasses FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! These CSS classes should be sorted. + + 26 │ clsx("px-2 foo p-4 bar"); + 27 │ tw`px-2 foo p-4 bar`; + > 28 │ tw.div`px-2 foo p-4 bar`; + │ ^^^^^^^^^^^^^^^^ + 29 │ notClassFunction("px-2 foo p-4 bar"); + 30 │ notTemplateFunction`px-2 foo p-4 bar`; + + i Unsafe fix: Sort the classes. + + 26 26 │ clsx("px-2 foo p-4 bar"); + 27 27 │ tw`px-2 foo p-4 bar`; + 28 │ - tw.div`px-2·foo·p-4·bar`; + 28 │ + tw.div`foo·bar·p-4·px-2`; + 29 29 │ notClassFunction("px-2 foo p-4 bar"); + 30 30 │ notTemplateFunction`px-2 foo p-4 bar`; + + ``` ``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/codeOptionsUnsorted.options.json b/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/codeOptionsUnsorted.options.json index b9e8a6b22d0c..fa79a7b7733a 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/codeOptionsUnsorted.options.json +++ b/crates/biome_js_analyze/tests/specs/nursery/useSortedClasses/codeOptionsUnsorted.options.json @@ -7,7 +7,7 @@ "level": "error", "options": { "attributes": ["customClassAttribute"], - "functions": ["clsx", "tw"] + "functions": ["clsx", "tw", "tw.*"] } } } diff --git a/crates/biome_js_syntax/src/jsx_ext.rs b/crates/biome_js_syntax/src/jsx_ext.rs index ac5aa3b35a75..2433ea00b2f7 100644 --- a/crates/biome_js_syntax/src/jsx_ext.rs +++ b/crates/biome_js_syntax/src/jsx_ext.rs @@ -261,7 +261,7 @@ impl JsxSelfClosingElement { impl JsxAttributeList { /// Returns the [JsxAttribute] that match the given `names_to_lookup`. - /// Only attributes with name as [JsxName] can be returned. + /// Only attributes with name as [AnyJsxAttributeName::JsxName] can be returned. /// /// Each name of `names_to_lookup` must be unique. ///