Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
30 changes: 30 additions & 0 deletions .changeset/use-consistent-boolean-props.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
"@biomejs/biome": patch
---

Added the [`useConsistentBooleanProps`](https://biomejs.dev/linter/rules/use-consistent-boolean-props/) rule.
This rule enforces consistent usage of boolean props in JSX based on the configured mode (`implicit` or `explicit`).

**Invalid (implicit):**

```jsx
<input disabled={true} />;
```

**Valid (implicit):**

```jsx
<input disabled />;
```

**Invalid (explicit):**

```jsx
<input disabled />;
```

**Valid (explicit):**

```jsx
<input disabled={true} />;
```
109 changes: 65 additions & 44 deletions crates/biome_configuration/src/analyzer/linter/rules.rs

Large diffs are not rendered by default.

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 @@ -199,6 +199,7 @@ define_categories! {
"lint/nursery/useArraySortCompare": "https://biomejs.dev/linter/rules/use-array-sort-compare",
"lint/nursery/useBiomeSuppressionComment": "https://biomejs.dev/linter/rules/use-biome-suppression-comment",
"lint/nursery/useConsistentArrowReturn": "https://biomejs.dev/linter/rules/use-consistent-arrow-return",
"lint/nursery/useConsistentBooleanProps": "https://biomejs.dev/linter/rules/use-consistent-boolean-props",
"lint/nursery/useConsistentGraphqlDescriptions": "https://biomejs.dev/linter/rules/use-consistent-graphql-descriptions",
"lint/nursery/useConsistentObjectDefinition": "https://biomejs.dev/linter/rules/use-consistent-object-definition",
"lint/nursery/useDeprecatedDate": "https://biomejs.dev/linter/rules/use-deprecated-date",
Expand Down
3 changes: 2 additions & 1 deletion crates/biome_js_analyze/src/lint/nursery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ pub mod no_vue_reserved_keys;
pub mod no_vue_reserved_props;
pub mod use_array_sort_compare;
pub mod use_consistent_arrow_return;
pub mod use_consistent_boolean_props;
pub mod use_exhaustive_switch_cases;
pub mod use_explicit_type;
pub mod use_find;
Expand All @@ -39,4 +40,4 @@ pub mod use_sorted_classes;
pub mod use_spread;
pub mod use_vue_define_macros_order;
pub mod use_vue_multi_word_component_names;
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_empty_source :: NoEmptySource , self :: no_floating_promises :: NoFloatingPromises , self :: no_for_in :: NoForIn , self :: no_import_cycles :: NoImportCycles , self :: no_increment_decrement :: NoIncrementDecrement , self :: no_jsx_literals :: NoJsxLiterals , self :: no_misused_promises :: NoMisusedPromises , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursion , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_shadow :: NoShadow , self :: no_sync_scripts :: NoSyncScripts , self :: no_unknown_attribute :: NoUnknownAttribute , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unused_expressions :: NoUnusedExpressions , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_duplicate_keys :: NoVueDuplicateKeys , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: use_array_sort_compare :: UseArraySortCompare , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_find :: UseFind , self :: use_max_params :: UseMaxParams , self :: use_qwik_method_usage :: UseQwikMethodUsage , self :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScope , self :: use_sorted_classes :: UseSortedClasses , self :: use_spread :: UseSpread , self :: use_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_empty_source :: NoEmptySource , self :: no_floating_promises :: NoFloatingPromises , self :: no_for_in :: NoForIn , self :: no_import_cycles :: NoImportCycles , self :: no_increment_decrement :: NoIncrementDecrement , self :: no_jsx_literals :: NoJsxLiterals , self :: no_misused_promises :: NoMisusedPromises , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursion , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_shadow :: NoShadow , self :: no_sync_scripts :: NoSyncScripts , self :: no_unknown_attribute :: NoUnknownAttribute , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unused_expressions :: NoUnusedExpressions , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_duplicate_keys :: NoVueDuplicateKeys , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: use_array_sort_compare :: UseArraySortCompare , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_consistent_boolean_props :: UseConsistentBooleanProps , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_find :: UseFind , self :: use_max_params :: UseMaxParams , self :: use_qwik_method_usage :: UseQwikMethodUsage , self :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScope , self :: use_sorted_classes :: UseSortedClasses , self :: use_spread :: UseSpread , self :: use_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
use crate::JsRuleAction;
use biome_analyze::{
Ast, FixKind, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule,
};
use biome_console::markup;
use biome_diagnostics::Severity;
use biome_js_factory::make;
use biome_js_syntax::{
AnyJsLiteralExpression, AnyJsxAttributeValue, JsSyntaxKind, JsxAttribute,
JsxAttributeInitializerClause, T,
};
use biome_rowan::{AstNode, BatchMutationExt};
use biome_rule_options::use_consistent_boolean_props::{
BooleanPropMode, UseConsistentBooleanPropsOptions,
};

declare_lint_rule! {
/// Enforces consistent usage of boolean props in JSX attributes.
///
/// ## Options
///
/// The following option is available
///
/// ### `mode`
///
/// Controls whether boolean props should be implicit or explicit.
/// Supports two options - `Implicit` and `Explicit`, where as `Implicit` is the default.
///
/// ## Example
///
/// ### Invalid (Implicit mode)
///
/// ```jsx,expect_diagnostic
/// <input disabled={true} />
/// ```
///
/// ### Valid (Implicit mode)
///
/// ```jsx
/// <input disabled />
/// ```
///
/// ### Invalid (Explicit mode)
///
/// ```json,options
/// {
/// "options": {
/// "mode": "explicit"
/// }
/// }
/// ```
/// ```jsx,use_options,expect_diagnostic
/// <input disabled />
/// ```
///
/// ### Valid (Explicit mode)
///
/// ```jsx,use_options
/// <input disabled={true} />
/// ```
///
/// ```jsx,use_options
/// <input disabled={false} />
/// ```
pub UseConsistentBooleanProps {
version: "next",
name: "useConsistentBooleanProps",
language: "jsx",
sources: &[RuleSource::EslintReact("jsx-boolean-value").inspired()],
recommended: false,
severity: Severity::Information,
fix_kind: FixKind::Safe,
}
}

impl Rule for UseConsistentBooleanProps {
type Query = Ast<JsxAttribute>;
type State = ();
type Signals = Option<Self::State>;
type Options = UseConsistentBooleanPropsOptions;

fn run(ctx: &RuleContext<Self>) -> Option<Self::State> {
let jsx_attribute = ctx.query();
let options = ctx.options();
let mode = options.mode.clone().unwrap_or_default();

match (mode, jsx_attribute.initializer()) {
(BooleanPropMode::Implicit, Some(jsx_attribute_initializer_clause)) => {
if is_true_literal(&jsx_attribute_initializer_clause) {
return Some(());
}
None
}
(BooleanPropMode::Explicit, None) => Some(()),
_ => None,
}
}

fn diagnostic(ctx: &RuleContext<Self>, _: &Self::State) -> Option<RuleDiagnostic> {
let jsx_attribute = ctx.query();
let mode = ctx.options().mode.clone().unwrap_or_default();

let prefix = "Boolean JSX prop with value `true` should be ";
// Determine the proper message based on mode
let message = match mode {
BooleanPropMode::Implicit => {
markup! { {prefix} "implicit (omit "<Emphasis>"={true}"</Emphasis>")." }
}
BooleanPropMode::Explicit => {
markup! { {prefix} "explicit ("<Emphasis>"={true}"</Emphasis>" must be present)." }
}
};

Some(RuleDiagnostic::new(
rule_category!(),
jsx_attribute.range(),
message.to_owned(),
))
}

fn action(ctx: &RuleContext<Self>, _: &Self::State) -> Option<JsRuleAction> {
let jsx_attribute = ctx.query();
let mode = ctx.options().mode.clone().unwrap_or_default();

let mut mutation = ctx.root().begin();
let mut mutation_has_changes = false;

match mode {
BooleanPropMode::Explicit => {
// Explicit mode: add `={true}` if missing
if jsx_attribute.initializer().is_none() {
let attr_value = make::jsx_expression_attribute_value(
make::token(JsSyntaxKind::L_CURLY),
biome_js_syntax::AnyJsExpression::AnyJsLiteralExpression(
AnyJsLiteralExpression::JsBooleanLiteralExpression(
make::js_boolean_literal_expression(make::token(T![true])),
),
),
make::token(JsSyntaxKind::R_CURLY),
);

let initializer = make::jsx_attribute_initializer_clause(
make::token(T![=]),
AnyJsxAttributeValue::JsxExpressionAttributeValue(attr_value),
);

let next_attr = jsx_attribute.clone().with_initializer(Some(initializer));
mutation_has_changes = true;
mutation.replace_node(jsx_attribute.clone(), next_attr);
}
}
BooleanPropMode::Implicit => {
// Implicit mode: remove initializer if it is `true`
if let Some(init) = jsx_attribute.initializer()
&& is_true_literal(&init)
{
// Remove initializer
let next_attr = make::jsx_attribute(jsx_attribute.name().ok()?).build();
mutation_has_changes = true;
mutation.replace_node(jsx_attribute.clone(), next_attr);
}
}
}

if mutation_has_changes {
Some(JsRuleAction::new(
ctx.metadata().action_category(ctx.category(), ctx.group()),
ctx.metadata().applicability(),
match mode {
BooleanPropMode::Explicit => markup! {
"This attribute requires an explicit `true` literal. "
"Add `= {true}` to the attribute."
}
.to_owned(),
BooleanPropMode::Implicit => markup! {
"This attribute uses an explicit `{true}` value. "
"Remove the value and leave the attribute empty."
}
.to_owned(),
},
mutation,
))
} else {
None
}
}
}

/// Checks whether the given JSX attribute initializer clause represents a `true` literal. e.g. disabled={true},
/// and NOT disabled="true" or disabled={false} etc.
fn is_true_literal(init: &JsxAttributeInitializerClause) -> bool {
if let Ok(expr) = init.value()
&& let Some(jsx_expr) = expr.as_jsx_expression_attribute_value()
&& let Ok(inner) = jsx_expr.expression()
&& let Some(lit) = inner.as_any_js_literal_expression()
&& let Some(boolean) = lit.as_js_boolean_literal_expression()
{
return boolean
.value_token()
.is_ok_and(|token| token.text_trimmed() == "true");
}

false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<input disabled />;
<input accept /** some comment */ />;
<input /** some comment */ accept />;
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
assertion_line: 151
expression: invalidExplicit.jsx
---
# Input
```jsx
<input disabled />;
<input accept /** some comment */ />;
<input /** some comment */ accept />;

```

# Diagnostics
```
invalidExplicit.jsx:1:8 lint/nursery/useConsistentBooleanProps FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━

i Boolean JSX prop with value `true` should be explicit (={true} must be present).

> 1 │ <input disabled />;
│ ^^^^^^^^
2 │ <input accept /** some comment */ />;
3 │ <input /** some comment */ accept />;

i Safe fix: This attribute requires an explicit `true` literal. Add `= {true}` to the attribute.

1 │ <input·disabled·={true}·/>;
│ ++++++++

```

```
invalidExplicit.jsx:2:8 lint/nursery/useConsistentBooleanProps FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━

i Boolean JSX prop with value `true` should be explicit (={true} must be present).

1 │ <input disabled />;
> 2 │ <input accept /** some comment */ />;
│ ^^^^^^
3 │ <input /** some comment */ accept />;
4 │

i Safe fix: This attribute requires an explicit `true` literal. Add `= {true}` to the attribute.

2 │ <input·accept·/**·some·comment·*/·={true}·/**·some·comment·*/·/>;
│ ++++++++++++++++++++++++++++

```

```
invalidExplicit.jsx:3:28 lint/nursery/useConsistentBooleanProps FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━

i Boolean JSX prop with value `true` should be explicit (={true} must be present).

1 │ <input disabled />;
2 │ <input accept /** some comment */ />;
> 3 │ <input /** some comment */ accept />;
│ ^^^^^^
4 │

i Safe fix: This attribute requires an explicit `true` literal. Add `= {true}` to the attribute.

3 │ <input·/**·some·comment·*/·accept·={true}·/>;
│ ++++++++

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json",
"linter": {
"rules": {
"nursery": {
"useConsistentBooleanProps": {
"level": "error",
"options": {
"mode": "explicit"
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<input disabled={true} />;
<input accept={true} /** some comment */ />;
<input /** some comment */ accept={true} />;
<input /** some comment */ accept={ true } />;
Loading