Skip to content

Commit

Permalink
feat(linter): implement class sorting rule (first pass) (#1362)
Browse files Browse the repository at this point in the history
Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>
Co-authored-by: Cookie <34422996+CookieDasora@users.noreply.github.com>
Co-authored-by: Victor <78874691+victor-teles@users.noreply.github.com>
Co-authored-by: Denis Bezrukov <6227442+denbezrukov@users.noreply.github.com>
Co-authored-by: Victorien Elvinger <victorien@elvinger.fr>
Co-authored-by: Nicolas Hedger <649677+nhedger@users.noreply.github.com>
Co-authored-by: ty <62130798+togami2864@users.noreply.github.com>
Co-authored-by: mehm8128 <83744975+mehm8128@users.noreply.github.com>
Co-authored-by: Jon <jonegeland@gmail.com>
Co-authored-by: Cosmo Shin (신의하) <me@xiniha.dev>
Co-authored-by: Zheyu Zhang <zheyuzhang03@gmail.com>
Co-authored-by: Arend van Beelen jr <arendjr@gmail.com>
Co-authored-by: Vasu Singh <vasucp1207@gmail.com>
Co-authored-by: Luis Mauro <1216941+lmauromb@users.noreply.github.com>
Co-authored-by: Shrey Sudhir <shrey.somaiya@gmail.com>
Co-authored-by: Keisuke Umeno <9renpoto@gmail.com>
Co-authored-by: huseeiin <122984423+huseeiin@users.noreply.github.com>
Co-authored-by: magic-akari <akari.ccino@gmail.com>
Co-authored-by: Ze-Zheng Wu <zezhengwu@proton.me>
Co-authored-by: Robin Millette <robin@millette.info>
Co-authored-by: Karl Persson <kalle.persson@grafana.com>
  • Loading branch information
1 parent 27bbf3d commit 24fcf19
Show file tree
Hide file tree
Showing 32 changed files with 3,780 additions and 13 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ Read our [guidelines for writing a good changelog entry](https://github.com/biom
```
Contributed by @ematipico

- Add rule [noSortedClasses](https://biomejs.dev/linter/rules/use-sorted-classes), to sort CSS utility classes:

```diff
- <div class="px-2 foo p-4 bar" />
+ <div class="foo·bar·p-4·px-2" />
```
Contributed by @DaniGuardiola

### Parser

## 1.5.3 (2024-01-22)
Expand Down
1 change: 1 addition & 0 deletions crates/biome_diagnostics_categories/src/categories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ define_categories! {
"lint/nursery/useNodejsImportProtocol": "https://biomejs.dev/linter/rules/use-nodejs-import-protocol",
"lint/nursery/useNumberNamespace": "https://biomejs.dev/linter/rules/use-number-namespace",
"lint/nursery/useShorthandFunctionType": "https://biomejs.dev/linter/rules/use-shorthand-function-type",
"lint/nursery/useSortedClasses": "https://biomejs.dev/linter/rules/use-sorted-classes",
"lint/performance/noAccumulatingSpread": "https://biomejs.dev/linter/rules/no-accumulating-spread",
"lint/performance/noDelete": "https://biomejs.dev/linter/rules/no-delete",
"lint/security/noDangerouslySetInnerHtml": "https://biomejs.dev/linter/rules/no-dangerously-set-inner-html",
Expand Down
12 changes: 12 additions & 0 deletions crates/biome_js_analyze/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::analyzers::nursery::use_consistent_array_type::ConsistentArrayTypeOpt
use crate::analyzers::nursery::use_filenaming_convention::FilenamingConventionOptions;
use crate::semantic_analyzers::correctness::use_exhaustive_dependencies::HooksOptions;
use crate::semantic_analyzers::correctness::use_hook_at_top_level::DeprecatedHooksOptions;
use crate::semantic_analyzers::nursery::use_sorted_classes::UtilityClassSortingOptions;
use crate::semantic_analyzers::style::no_restricted_globals::RestrictedGlobalsOptions;
use crate::semantic_analyzers::style::use_naming_convention::NamingConventionOptions;
use crate::{
Expand Down Expand Up @@ -38,6 +39,8 @@ pub enum PossibleOptions {
RestrictedGlobals(RestrictedGlobalsOptions),
/// Options for `useValidAriaRole` rule
ValidAriaRole(ValidAriaRoleOptions),
/// Options for `useSortedClasses` rule
UtilityClassSorting(UtilityClassSortingOptions),
}

impl Default for PossibleOptions {
Expand Down Expand Up @@ -105,6 +108,13 @@ impl PossibleOptions {
};
RuleOptions::new(options)
}
"useSortedClasses" => {
let options = match self {
PossibleOptions::UtilityClassSorting(options) => options.clone(),
_ => UtilityClassSortingOptions::default(),
};
RuleOptions::new(options)
}
// TODO: review error
_ => panic!("This rule {:?} doesn't have options", rule_key),
}
Expand Down Expand Up @@ -137,6 +147,8 @@ impl Deserializable for PossibleOptions {
"useValidAriaRole" => {
Deserializable::deserialize(value, "options", diagnostics).map(Self::ValidAriaRole)
}
"useSortedClasses" => Deserializable::deserialize(value, "options", diagnostics)
.map(Self::UtilityClassSorting),
_ => {
diagnostics.push(
DeserializationDiagnostic::new(markup! {
Expand Down
2 changes: 2 additions & 0 deletions crates/biome_js_analyze/src/semantic_analyzers/nursery.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
mod any_class_string_like;
mod class_info;
mod class_lexer;
mod options;
mod presets;
mod sort;
mod sort_config;

use biome_analyze::{
context::RuleContext, declare_rule, ActionCategory, FixKind, Rule, RuleDiagnostic,
};
use biome_console::markup;
use biome_diagnostics::Applicability;
use biome_js_factory::make::{js_string_literal, js_string_literal_expression, jsx_string};
use biome_rowan::{AstNode, BatchMutationExt};

use crate::JsRuleAction;

pub use self::options::UtilityClassSortingOptions;
use self::{
any_class_string_like::AnyClassStringLike,
presets::{get_utilities_preset, UseSortedClassesPreset},
sort::sort_class_name,
sort_config::SortConfig,
};

declare_rule! {
/// Enforce the sorting of CSS utility classes.
///
/// This rule implements the same sorting algorithm as [Tailwind CSS](https://tailwindcss.com/blog/automatic-class-sorting-with-prettier#how-classes-are-sorted), but supports any utility class framework including [UnoCSS](https://unocss.dev/).
///
/// It is analogous to [`prettier-plugin-tailwindcss`](https://github.com/tailwindlabs/prettier-plugin-tailwindcss).
///
///
/// :::caution
/// ## Important notes
///
/// This rule is a work in progress, and is only partially implemented. Progress is being tracked in the following GitHub issue: https://github.com/biomejs/biome/issues/1274
///
/// Currently, utility class sorting is **not part of the formatter**, and is implemented as a linter rule instead, with an automatic fix. The fix is, at this stage, classified as unsafe. This means that **it won't be applied automatically** as part of IDE actions such as "fix on save".
///
/// We appreciate any feedback on this rule, and encourage you to try it out and report any issues you find.
///
/// **Please read this entire documentation page before reporting an issue.**
///
/// Notably, keep in mind that the following features are not supported yet:
///
/// - Variant sorting.
/// - Custom utilitites and variants (such as ones introduced by Tailwind CSS plugins). Only the default Tailwind CSS configuration is supported.
/// - Options such as `prefix` and `separator`.
/// - Tagged template literals.
/// - Object properties (e.g. in `clsx` calls).
///
/// Please don't report issues about these features.
/// :::
///
/// ## Examples
///
/// ### Invalid
///
/// ```jsx,expect_diagnostic
/// <div class="px-2 foo p-4 bar" />;
/// ```
///
/// ## Options
///
/// ### Code-related
///
/// ```json
/// {
/// "options": {
/// "attributes": ["classList"],
/// "functions": ["clsx", "cva", "tw"]
/// }
/// }
/// ```
///
/// #### attributes
///
/// Classes in the `class` and `className` JSX attributes are always sorted. Use this option to add more attributes that should be sorted.
///
/// #### functions
///
/// If specified, strings in the indicated functions will be sorted. This is useful when working with libraries like [`clsx`](https://github.com/lukeed/clsx) or [`cva`](https://cva.style/).
///
/// ```js,ignore
/// clsx("px-2 foo p-4 bar", {
/// "block mx-4": condition,
/// });
/// ```
///
/// Tagged template literals are also supported, for example:
///
/// ```js,ignore
/// tw`px-2`;
/// tw.div`px-2`;
/// ```
///
/// :::caution
/// Tagged template literal support has not been implemented yet.
/// :::
///
/// ### Sort-related
///
/// :::caution
/// At the moment, this rule does not support customizing the sort options. Instead, the default Tailwind CSS configuration is hard-coded.
/// :::
///
/// ## Differences with [Prettier](https://github.com/tailwindlabs/prettier-plugin-tailwindcss)
///
/// The main key difference is that Tailwind CSS and its Prettier plugin read and execute the `tailwind.config.js` JavaScript file, which Biome can't do. Instead, Biome implements a simpler version of the configuration. The trade-offs are explained below.
///
/// ### Values are not known
///
/// The rule has no knowledge of values such as colors, font sizes, or spacing values, which are normally defined in a configuration file like `tailwind.config.js`. Instead, the rule matches utilities that support values in a simpler way: if they start with a known utility prefix, such as `px-` or `text-`, they're considered valid.
///
/// This has two implications:
///
/// - False positives: classes can be wrongly recognized as utilities even though their values are incorrect. For example, if there's a `px-` utility defined in the configuration, it will match all of the following classes: `px-2`, `px-1337`, `px-[not-actually-valid]`, `px-literally-anything`.
/// - No distinction between different utilities that share the same prefix: for example, `text-red-500` and `text-lg` are both interpreted as the same type of utility by this rule, even though the former refers to a color and the latter to a font size. This results in all utilities that share the same prefix being sorted together, regardless of their actual values.
///
/// ### Custom additions must be specified
///
/// The built-in Tailwind CSS preset (enabled by default) contains the set of utilities and variants that are available with the default configuration. More utilities and variants can be added through Tailwind CSS plugins. In Biome, these need to be manually specified in the Biome configuration file in order to "extend" the preset.
///
/// ### Presets can't be modified
///
/// In Tailwind CSS, core plugins (which provide the default utilities and variants) can be disabled. In Biome, however, there is no way to disable parts of a preset: it's all or nothing. A work-around is to, instead of using a preset, manually specify all utilities and variants in the Biome configuration file.
///
/// ### Whitespace is collapsed
///
/// The Tailwind CSS Prettier plugin preserves all original whitespace. This rule, however, collapses all whitespace (including newlines) into single spaces.
///
/// This is a deliberate decision. We're unsure about this behavior, and would appreciate feedback on it. If this is a problem for you, please share a detailed explanation of your use case in [the GitHub issue](https://github.com/biomejs/biome/issues/1274).
///
pub(crate) UseSortedClasses {
version: "next",
name: "useSortedClasses",
recommended: false,
fix_kind: FixKind::Unsafe,
}
}

impl Rule for UseSortedClasses {
type Query = AnyClassStringLike;
type State = String;
type Signals = Option<Self::State>;
type Options = UtilityClassSortingOptions;

fn run(ctx: &RuleContext<Self>) -> Option<Self::State> {
// TODO: unsure if options are needed here. The sort config should ideally be created once
// from the options and then reused for all queries.
// let options = &ctx.options();
// TODO: the sort config should already exist at this point, and be generated from the options,
// including the preset and extended options as well.
let sort_config = SortConfig::new(
get_utilities_preset(&UseSortedClassesPreset::default()),
Vec::new(),
);

let value = ctx.query().value()?;
let sorted_value = sort_class_name(&value, &sort_config);
if value.text() != sorted_value {
Some(sorted_value)
} else {
None
}
}

fn diagnostic(ctx: &RuleContext<Self>, _: &Self::State) -> Option<RuleDiagnostic> {
Some(RuleDiagnostic::new(
rule_category!(),
ctx.query().range(),
"These CSS classes should be sorted.",
))
}

fn action(ctx: &RuleContext<Self>, state: &Self::State) -> Option<JsRuleAction> {
let mut mutation = ctx.root().begin();
match ctx.query() {
AnyClassStringLike::JsStringLiteralExpression(string_literal) => {
let replacement = js_string_literal_expression(js_string_literal(state));
mutation.replace_node(string_literal.clone(), replacement);
}
AnyClassStringLike::JsxString(jsx_string_node) => {
let replacement = jsx_string(js_string_literal(state));
mutation.replace_node(jsx_string_node.clone(), replacement);
}
AnyClassStringLike::JsTemplateChunkElement(_) => return None,
};

Some(JsRuleAction {
category: ActionCategory::QuickFix,
applicability: Applicability::MaybeIncorrect,
message: markup! {
"Sort the classes."
}
.to_owned(),
mutation,
})
}
}
Loading

0 comments on commit 24fcf19

Please sign in to comment.