Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(linter): implement class sorting rule (first pass) #1362

Merged
merged 86 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
53e104f
wip
DaniGuardiola Dec 29, 2023
0494556
fixes
DaniGuardiola Dec 29, 2023
da3c54c
add layers and pre-process options
DaniGuardiola Dec 31, 2023
beec56b
class parser!
DaniGuardiola Dec 31, 2023
d50aeab
split into modules
DaniGuardiola Dec 31, 2023
67ccb2c
updated obsolete PoC code and cleaned up
DaniGuardiola Dec 31, 2023
e2672fe
use declare_node_union
DaniGuardiola Dec 31, 2023
737005a
cleanup, docs, unit test TODOs
DaniGuardiola Jan 1, 2024
838edba
Merge remote-tracking branch 'origin/main' into feat/class-sort-rule
ematipico Jan 11, 2024
06cbfb3
resolve merge
ematipico Jan 11, 2024
e58fdb7
chore some code
ematipico Jan 11, 2024
9ddba19
shut down clippy
ematipico Jan 11, 2024
e990013
public docs
DaniGuardiola Jan 11, 2024
9e7d666
Merge remote-tracking branch 'origin/main' into feat/class-sort-rule
ematipico Jan 12, 2024
cff3479
fix: conflict
ematipico Jan 12, 2024
508a93f
address review comments
DaniGuardiola Jan 12, 2024
debd8e6
fix query bug and lexer bug
DaniGuardiola Jan 15, 2024
57d6cbf
fix: test and clippy
ematipico Jan 16, 2024
57043b2
chore: make fix unsafe, and make classes static
ematipico Jan 16, 2024
89295d1
wip integration tests
DaniGuardiola Jan 21, 2024
3f7904d
fix options and tests
DaniGuardiola Jan 22, 2024
71039f5
class info unit tests
DaniGuardiola Jan 22, 2024
9144a65
TODOs
DaniGuardiola Jan 22, 2024
56c4478
improve preset structure
DaniGuardiola Jan 22, 2024
2195b0b
one more class info unit test
DaniGuardiola Jan 22, 2024
627484d
fixes
DaniGuardiola Jan 22, 2024
0afcef5
lexer unit tests
DaniGuardiola Jan 23, 2024
84720be
remove TODO (already covered by integration tests)
DaniGuardiola Jan 23, 2024
0d0b4ed
remove panics
DaniGuardiola Jan 23, 2024
72ee1f1
improve docs
DaniGuardiola Jan 23, 2024
e078cbf
tweak docs
ematipico Jan 24, 2024
6174cee
Merge branch 'feat/class-sort-rule' of github.com:DaniGuardiola/biome…
DaniGuardiola Jan 24, 2024
da104cd
docs(pt-br): translated git hooks page (#1498)
hknsh Jan 12, 2024
841f8cb
feat: HTML grammar (#1544)
ematipico Jan 12, 2024
e78e927
Refactor CSS Parser to rename CssRule to CssQualifiedRule (#1546)
denbezrukov Jan 12, 2024
b20f0a4
fix(js_formatter): fix #1511 (#1547)
Conaclos Jan 12, 2024
34c919a
feat: foudations for migration from prettier (#1545)
ematipico Jan 13, 2024
de9b3e1
fix(config): don't ignore `include` when `ignore` is set (#1548)
Conaclos Jan 13, 2024
801c9ba
refactor(cli): do not emit warnings for protected ignored files (#1552)
Conaclos Jan 13, 2024
e79fca1
chore: move `nhedger` to core contributors (#1557)
nhedger Jan 13, 2024
73d57fc
fix(lint/noUselessFragments): trim trivia to avoid suggesting invalid…
togami2864 Jan 14, 2024
c8d0981
fix(config): apply global include/ignore before tools' include/ignore…
Conaclos Jan 14, 2024
e1119f7
release: 1.5.2 (#1562)
Conaclos Jan 15, 2024
f55f2a7
refactor(parser): Update lexer methods to handle only ASCII identifie…
denbezrukov Jan 15, 2024
4807e98
chore(CHANGELOG): add Unreleased section and fix typo (#1566)
Conaclos Jan 15, 2024
31dc7ac
feat(cli): file system prettier migration (#1567)
ematipico Jan 16, 2024
259a839
feat(linter): show dependency variable name by useExhaustiveDependenc…
mehm8128 Jan 16, 2024
1b5f261
feat(css_formatter): Formatting for `border` property (#1453)
faultyserver Jan 16, 2024
f6b12c4
fix(exhaustiveDeps): perform nested capture check in a correct way (#…
XiNiHa Jan 16, 2024
76fbdb8
fix(js_formatter): fix invalid formatting of nested multiline comment…
ah-yu Jan 17, 2024
a15397c
feat(project): `Deserializable` derive macro (#1564)
arendjr Jan 17, 2024
e39e1b1
fix(lint/noArrayIndexKey): false negative in template literals (#1586)
vasucp1207 Jan 17, 2024
61ce44a
docs(website): fix lefthook pre-push recipe (#1587)
lmauromb Jan 17, 2024
63dbae0
refactor(css_parser): Renamed 'CssSimpleFunction' to 'CssFunction' (#…
denbezrukov Jan 18, 2024
bf0004b
fix(formatter): apply line ending option (#1591)
ematipico Jan 18, 2024
472e702
fix(lsp): register formatter dynamically if possible (#1590)
nhedger Jan 18, 2024
8f79c55
fix(linter): correctly handle multibyte chars in regexes (#1592)
Conaclos Jan 18, 2024
0c03701
docs(website): tables from source rules to Biome rules (#1583)
ematipico Jan 18, 2024
fa3bd5b
fix(lint/useExhaustiveDependencies): do not panic on TS import equal …
Conaclos Jan 18, 2024
300d246
chore: fix links in rules sources page
ematipico Jan 18, 2024
9b25002
chore: fix links in rules sources page /pt2
ematipico Jan 18, 2024
ae46172
chore: update documentation (#1599)
ematipico Jan 19, 2024
f9c92e1
feat(docs): add exclusive rules and fix headings (#1602)
ematipico Jan 19, 2024
3e04fd8
docs(formatter): fix rustdoc example (#1604)
SpanishPear Jan 19, 2024
c063e28
fix(formatter): correctly handle comments at end of export/import lis…
SpanishPear Jan 20, 2024
e03edce
docs: fix pre-commit sample version v0.1.0 (#1612)
9renpoto Jan 20, 2024
d25694f
fix(linter): correctly override linter presets (#1606)
Conaclos Jan 20, 2024
0b6464b
fix(website): git hook shell scripts (#1615)
Conaclos Jan 20, 2024
f8aea9a
fix(config): don't include ignored files known to be jsonc files when…
Conaclos Jan 20, 2024
543248e
fix(website): fix lint-staged glob pattern (#1616)
Conaclos Jan 20, 2024
4bfcfaf
Update no_nodejs_modules.rs (#1621)
huseeiin Jan 21, 2024
c4500f7
fix(js_parser): Allow `const` in TsMethodSignatureTypeMember and TsCa…
magic-akari Jan 21, 2024
8b3ed5f
feat(website): generate open graph images (#1622)
ematipico Jan 21, 2024
c4d72ee
feat(css_parser): CSS parser to improve selector error handling (#1619)
denbezrukov Jan 21, 2024
3214be6
fix(website): rules sources page link (#1632)
Sec-ant Jan 22, 2024
0c12315
release: 1.5.3 (#1629)
Conaclos Jan 22, 2024
74865f0
fix: remove unneeded semicolon in the autofix suggestion (#1638)
togami2864 Jan 22, 2024
20d73ea
Update language-support.mdx (#1639)
millette Jan 22, 2024
db5c6d8
fix(js_formatter): incorrect call chain break (#1646)
kalleep Jan 23, 2024
4176bde
feat(linter): add rule `noSkippedTests` (#1635)
ematipico Jan 23, 2024
997df23
fix(js_parser): correctly parse type arguments in expression (#1645)
ah-yu Jan 24, 2024
e0c1d37
Revert "tweak docs"
ematipico Jan 24, 2024
88357f4
update codegen
ematipico Jan 24, 2024
e23182d
Merge remote-tracking branch 'origin/main' into feat/class-sort-rule
ematipico Jan 24, 2024
f74aec0
codegen
ematipico Jan 24, 2024
a1b156d
test/docs fixes and improvements
DaniGuardiola Jan 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ Read our [guidelines for writing a good changelog entry](https://github.com/biom
it.only("test", () => {});
```

- 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 Expand Up @@ -130,7 +138,7 @@ Read our [guidelines for writing a good changelog entry](https://github.com/biom

Contributed by @magic-akari

- Correctly parse type arguments in expression([#1184](https://github.com/biomejs/biome/issues/1184)).
- Correctly parse type arguments in expression([#1184](https://github.com/biomejs/biome/issues/1184)).

The following code is now correctly parsed in typescript:

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
ematipico marked this conversation as resolved.
Show resolved Hide resolved
/// <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.
ematipico marked this conversation as resolved.
Show resolved Hide resolved
///
/// ### 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",
ematipico marked this conversation as resolved.
Show resolved Hide resolved
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