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(configuration): add the fix rule option #2892

Merged
merged 2 commits into from
May 16, 2024
Merged
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
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,47 @@ our [guidelines for writing a good changelog entry](https://github.com/biomejs/b

### Configuration

#### New features

- Add an rule option `fix` to override the code fix kind of a rule ([#2882](https://github.com/biomejs/biome/issues/2882)).

A rule can provide a safe or an **unsafe** code **action**.
You can now tune the kind of code actions thanks to the `fix` option.
This rule option takes a value among:

- `none`: the rule no longer emits code actions.
- `safe`: the rule emits safe code action.
- `unsafe`: the rule emits unsafe code action.

The following configuration disables the code actions of `noUnusedVariables`, makes the emitted code actions of `style/useConst` and `style/useTemplate` unsafe and safe respectively.

```json
{
"linter": {
"rules": {
"correctness": {
"noUnusedVariables": {
"level": "error",
"fix": "none"
},
"style": {
"useConst": {
"level": "warn",
"fix": "unsafe"
},
"useTemplate": {
"level": "warn",
"fix": "safe"
}
}
}
}
}
}
```

Contributed by @Conaclos

#### Enhancements

- The `javascript.formatter.trailingComma` option is deprecated and renamed to `javascript.formatter.trailingCommas`. The corresponding CLI option `--trailing-comma` is also deprecated and renamed to `--trailing-commas`. Details can be checked in [#2492](https://github.com/biomejs/biome/pull/2492). Contributed by @Sec-ant
Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

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

20 changes: 11 additions & 9 deletions crates/biome_analyze/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,20 @@ version = "0.5.7"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
biome_console = { workspace = true }
biome_diagnostics = { workspace = true }
biome_rowan = { workspace = true }
bitflags = { workspace = true }
rustc-hash = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true, features = ["derive"] }
tracing = { workspace = true }
biome_console = { workspace = true }
biome_deserialize = { workspace = true, optional = true }
biome_deserialize_macros = { workspace = true, optional = true }
biome_diagnostics = { workspace = true }
biome_rowan = { workspace = true }
bitflags = { workspace = true }
rustc-hash = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true, features = ["derive"], optional = true }
tracing = { workspace = true }


[features]
serde = ["schemars"]
serde = ["dep:serde", "dep:schemars", "dep:biome_deserialize", "dep:biome_deserialize_macros"]

[lints]
workspace = true
30 changes: 24 additions & 6 deletions crates/biome_analyze/src/options.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
use rustc_hash::FxHashMap;

use crate::{Rule, RuleKey};
use crate::{FixKind, Rule, RuleKey};
use std::any::{Any, TypeId};
use std::fmt::Debug;
use std::path::PathBuf;

/// A convenient new type data structure to store the options that belong to a rule
#[derive(Debug)]
pub struct RuleOptions((TypeId, Box<dyn Any>));
pub struct RuleOptions(TypeId, Box<dyn Any>, Option<FixKind>);

impl RuleOptions {
/// Creates a new [RuleOptions]
pub fn new<O: 'static>(options: O, fix_kind: Option<FixKind>) -> Self {
Self(TypeId::of::<O>(), Box::new(options), fix_kind)
}

/// It returns the deserialized rule option
pub fn value<O: 'static>(&self) -> &O {
let (type_id, value) = &self.0;
let RuleOptions(type_id, value, _) = &self;
let current_id = TypeId::of::<O>();
debug_assert_eq!(type_id, &current_id);
// SAFETY: the code should fail when asserting the types.
Expand All @@ -21,9 +26,8 @@ impl RuleOptions {
value.downcast_ref::<O>().unwrap()
}

/// Creates a new [RuleOptions]
pub fn new<O: 'static>(options: O) -> Self {
Self((TypeId::of::<O>(), Box::new(options)))
pub fn fix_kind(&self) -> Option<FixKind> {
self.2
}
}

Expand All @@ -41,6 +45,10 @@ impl AnalyzerRules {
pub fn get_rule_options<O: 'static>(&self, rule_key: &RuleKey) -> Option<&O> {
self.0.get(rule_key).map(|o| o.value::<O>())
}

pub fn get_rule_fix_kind(&self, rule_key: &RuleKey) -> Option<FixKind> {
self.0.get(rule_key).and_then(|options| options.fix_kind())
}
}

/// A data structured derived from the `biome.json` file
Expand Down Expand Up @@ -95,6 +103,16 @@ impl AnalyzerOptions {
.cloned()
}

pub fn rule_fix_kind<R>(&self) -> Option<FixKind>
where
R: Rule + 'static,
R::Options: Clone,
{
self.configuration
.rules
.get_rule_fix_kind(&RuleKey::rule::<R>())
}

pub fn preferred_quote(&self) -> &PreferredQuote {
&self.configuration.preferred_quote
}
Expand Down
54 changes: 35 additions & 19 deletions crates/biome_analyze/src/rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ use biome_diagnostics::{
Visit,
};
use biome_rowan::{AstNode, BatchMutation, BatchMutationExt, Language, TextRange};
use serde::Serialize;
use std::cmp::Ordering;
use std::fmt::Debug;

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
/// Static metadata containing information about a rule
pub struct RuleMetadata {
/// It marks if a rule is deprecated, and if so a reason has to be provided.
Expand All @@ -35,17 +35,29 @@ pub struct RuleMetadata {
/// Whether a rule is recommended or not
pub recommended: bool,
/// The kind of fix
pub fix_kind: Option<FixKind>,
pub fix_kind: FixKind,
/// The source URL of the rule
pub sources: &'static [RuleSource],
/// The source kind of the rule
pub source_kind: Option<RuleSourceKind>,
}

#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[cfg_attr(
feature = "serde",
derive(
biome_deserialize_macros::Deserializable,
schemars::JsonSchema,
serde::Deserialize,
serde::Serialize
)
)]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
/// Used to identify the kind of code action emitted by a rule
pub enum FixKind {
/// The rule doesn't emit code actions.
#[default]
None,
/// The rule emits a code action that is safe to apply. Usually these fixes don't change the semantic of the program.
Safe,
/// The rule emits a code action that is _unsafe_ to apply. Usually these fixes remove comments, or change
Expand All @@ -56,23 +68,27 @@ pub enum FixKind {
impl Display for FixKind {
fn fmt(&self, fmt: &mut biome_console::fmt::Formatter) -> std::io::Result<()> {
match self {
FixKind::None => fmt.write_str("None"),
FixKind::Safe => fmt.write_str("Safe"),
FixKind::Unsafe => fmt.write_str("Unsafe"),
}
}
}

impl From<&FixKind> for Applicability {
fn from(kind: &FixKind) -> Self {
match kind {
FixKind::Safe => Applicability::Always,
FixKind::Unsafe => Applicability::MaybeIncorrect,
impl TryFrom<FixKind> for Applicability {
type Error = &'static str;
fn try_from(value: FixKind) -> Result<Self, Self::Error> {
match value {
FixKind::None => Err("The fix kind is None"),
FixKind::Safe => Ok(Applicability::Always),
FixKind::Unsafe => Ok(Applicability::MaybeIncorrect),
}
}
}

#[derive(Debug, Clone, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
#[derive(Debug, Clone, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub enum RuleSource {
/// Rules from [Rust Clippy](https://rust-lang.github.io/rust-clippy/master/index.html)
Clippy(&'static str),
Expand Down Expand Up @@ -241,8 +257,9 @@ impl RuleSource {
}
}

#[derive(Debug, Default, Clone, Copy, Serialize)]
#[serde(rename_all = "camelCase")]
#[derive(Debug, Default, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub enum RuleSourceKind {
/// The rule implements the same logic of the source
#[default]
Expand Down Expand Up @@ -271,7 +288,7 @@ impl RuleMetadata {
docs,
language,
recommended: false,
fix_kind: None,
fix_kind: FixKind::None,
sources: &[],
source_kind: None,
}
Expand All @@ -288,7 +305,7 @@ impl RuleMetadata {
}

pub const fn fix_kind(mut self, kind: FixKind) -> Self {
self.fix_kind = Some(kind);
self.fix_kind = kind;
self
}

Expand All @@ -312,9 +329,8 @@ impl RuleMetadata {

pub fn to_applicability(&self) -> Applicability {
self.fix_kind
.as_ref()
.try_into()
.expect("Fix kind is not set in the rule metadata")
.into()
}
}

Expand Down
14 changes: 13 additions & 1 deletion crates/biome_analyze/src/signals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,18 @@ where
fn actions(&self) -> AnalyzerActionIter<RuleLanguage<R>> {
let globals = self.options.globals();

let configured_applicability = if let Some(fix_kind) = self.options.rule_fix_kind::<R>() {
match fix_kind {
crate::FixKind::None => {
// The action is disabled
return AnalyzerActionIter::new(vec![]);
}
crate::FixKind::Safe => Some(Applicability::Always),
crate::FixKind::Unsafe => Some(Applicability::MaybeIncorrect),
}
} else {
None
};
let options = self.options.rule_options::<R>().unwrap_or_default();
let ctx = RuleContext::new(
&self.query_result,
Expand All @@ -384,7 +396,7 @@ where
if let Some(action) = R::action(&ctx, &self.state) {
actions.push(AnalyzerAction {
rule_name: Some((<R::Group as RuleGroup>::NAME, R::METADATA.name)),
applicability: action.applicability(),
applicability: configured_applicability.unwrap_or(action.applicability()),
category: action.category,
mutation: action.mutation,
message: action.message,
Expand Down
21 changes: 12 additions & 9 deletions crates/biome_cli/src/commands/explain.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use biome_analyze::RuleMetadata;
use biome_analyze::{FixKind, RuleMetadata};
use biome_console::{markup, ConsoleExt};
use biome_service::documentation::Doc;

Expand All @@ -10,14 +10,17 @@ fn print_rule(session: CliSession, metadata: &RuleMetadata) {
"# "{metadata.name}"\n"
});

if let Some(kind) = &metadata.fix_kind {
session.app.console.log(markup! {
"Fix is "{kind}".\n"
});
} else {
session.app.console.log(markup! {
"No fix available.\n"
});
match metadata.fix_kind {
FixKind::None => {
session.app.console.log(markup! {
"No fix available.\n"
});
}
kind => {
session.app.console.log(markup! {
"Fix is "{kind}".\n"
});
}
}

let docs = metadata
Expand Down
5 changes: 5 additions & 0 deletions crates/biome_cli/src/execute/migrate/eslint_to_biome.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ fn migrate_eslint_rule(
group.no_restricted_globals = Some(biome_config::RuleConfiguration::WithOptions(
biome_config::RuleWithOptions {
level: severity.into(),
fix: None,
options: Box::new(no_restricted_globals::RestrictedGlobalsOptions {
denied_globals: globals.collect(),
}),
Expand All @@ -225,6 +226,7 @@ fn migrate_eslint_rule(
group.use_valid_aria_role = Some(biome_config::RuleConfiguration::WithOptions(
biome_config::RuleWithOptions {
level: severity.into(),
fix: None,
options: Box::new((*rule_options).into()),
},
));
Expand All @@ -239,6 +241,7 @@ fn migrate_eslint_rule(
Some(biome_config::RuleConfiguration::WithOptions(
biome_config::RuleWithOptions {
level: severity.into(),
fix: None,
options: rule_options.into(),
},
));
Expand All @@ -255,6 +258,7 @@ fn migrate_eslint_rule(
group.use_naming_convention = Some(biome_config::RuleConfiguration::WithOptions(
biome_config::RuleWithOptions {
level: severity.into(),
fix: None,
options: options.into(),
},
));
Expand All @@ -266,6 +270,7 @@ fn migrate_eslint_rule(
group.use_filenaming_convention = Some(
biome_config::RuleConfiguration::WithOptions(biome_config::RuleWithOptions {
level: conf.severity().into(),
fix: None,
options: Box::new(conf.option_or_default().into()),
}),
);
Expand Down
Loading