Skip to content

Commit

Permalink
feat(linter): implement noUselessStringRaw (#4263)
Browse files Browse the repository at this point in the history
  • Loading branch information
fireairforce authored Oct 14, 2024
1 parent d254b97 commit 561b54c
Show file tree
Hide file tree
Showing 12 changed files with 262 additions and 33 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ our [guidelines for writing a good changelog entry](https://github.com/biomejs/b
- Add [noHeadImportInDocument](https://biomejs.dev/linter/rules/no-head-import-in-document/). Contributed by @kaioduarte
- Add [noImgElement](https://biomejs.dev/linter/rules/no-img-element/). Contributed by @kaioduarte
- Add [guardForIn](https://biomejs.dev/linter/rules/guard-for-in/). Contributed by @fireairforce
- Add [noUselessStringRaw](https://github.com/biomejs/biome/pull/4263). Contributed by @fireairforce

#### Bug Fixes

Expand Down
86 changes: 53 additions & 33 deletions crates/biome_configuration/src/analyzer/linter/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3377,6 +3377,10 @@ pub struct Nursery {
#[serde(skip_serializing_if = "Option::is_none")]
pub no_useless_escape_in_regex:
Option<RuleFixConfiguration<biome_js_analyze::options::NoUselessEscapeInRegex>>,
#[doc = "Disallow unnecessary String.raw function in template string literals without any escape sequence."]
#[serde(skip_serializing_if = "Option::is_none")]
pub no_useless_string_raw:
Option<RuleConfiguration<biome_js_analyze::options::NoUselessStringRaw>>,
#[doc = "Disallow use of @value rule in css modules."]
#[serde(skip_serializing_if = "Option::is_none")]
pub no_value_at_rule: Option<RuleConfiguration<biome_css_analyze::options::NoValueAtRule>>,
Expand Down Expand Up @@ -3479,6 +3483,7 @@ impl Nursery {
"noUnknownPseudoElement",
"noUnknownTypeSelector",
"noUselessEscapeInRegex",
"noUselessStringRaw",
"noValueAtRule",
"useAdjacentOverloadSignatures",
"useAriaPropsSupportedByRole",
Expand Down Expand Up @@ -3522,10 +3527,10 @@ impl Nursery {
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26]),
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27]),
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28]),
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31]),
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35]),
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32]),
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36]),
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41]),
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37]),
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42]),
];
const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]),
Expand Down Expand Up @@ -3572,6 +3577,7 @@ impl Nursery {
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41]),
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42]),
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43]),
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[44]),
];
#[doc = r" Retrieves the recommended rules"]
pub(crate) fn is_recommended_true(&self) -> bool {
Expand Down Expand Up @@ -3733,81 +3739,86 @@ impl Nursery {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28]));
}
}
if let Some(rule) = self.no_value_at_rule.as_ref() {
if let Some(rule) = self.no_useless_string_raw.as_ref() {
if rule.is_enabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29]));
}
}
if let Some(rule) = self.use_adjacent_overload_signatures.as_ref() {
if let Some(rule) = self.no_value_at_rule.as_ref() {
if rule.is_enabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30]));
}
}
if let Some(rule) = self.use_aria_props_supported_by_role.as_ref() {
if let Some(rule) = self.use_adjacent_overload_signatures.as_ref() {
if rule.is_enabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31]));
}
}
if let Some(rule) = self.use_at_index.as_ref() {
if let Some(rule) = self.use_aria_props_supported_by_role.as_ref() {
if rule.is_enabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32]));
}
}
if let Some(rule) = self.use_component_export_only_modules.as_ref() {
if let Some(rule) = self.use_at_index.as_ref() {
if rule.is_enabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33]));
}
}
if let Some(rule) = self.use_consistent_curly_braces.as_ref() {
if let Some(rule) = self.use_component_export_only_modules.as_ref() {
if rule.is_enabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34]));
}
}
if let Some(rule) = self.use_consistent_member_accessibility.as_ref() {
if let Some(rule) = self.use_consistent_curly_braces.as_ref() {
if rule.is_enabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35]));
}
}
if let Some(rule) = self.use_deprecated_reason.as_ref() {
if let Some(rule) = self.use_consistent_member_accessibility.as_ref() {
if rule.is_enabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36]));
}
}
if let Some(rule) = self.use_explicit_type.as_ref() {
if let Some(rule) = self.use_deprecated_reason.as_ref() {
if rule.is_enabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37]));
}
}
if let Some(rule) = self.use_guard_for_in.as_ref() {
if let Some(rule) = self.use_explicit_type.as_ref() {
if rule.is_enabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38]));
}
}
if let Some(rule) = self.use_import_restrictions.as_ref() {
if let Some(rule) = self.use_guard_for_in.as_ref() {
if rule.is_enabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39]));
}
}
if let Some(rule) = self.use_sorted_classes.as_ref() {
if let Some(rule) = self.use_import_restrictions.as_ref() {
if rule.is_enabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40]));
}
}
if let Some(rule) = self.use_strict_mode.as_ref() {
if let Some(rule) = self.use_sorted_classes.as_ref() {
if rule.is_enabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41]));
}
}
if let Some(rule) = self.use_trim_start_end.as_ref() {
if let Some(rule) = self.use_strict_mode.as_ref() {
if rule.is_enabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42]));
}
}
if let Some(rule) = self.use_valid_autocomplete.as_ref() {
if let Some(rule) = self.use_trim_start_end.as_ref() {
if rule.is_enabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43]));
}
}
if let Some(rule) = self.use_valid_autocomplete.as_ref() {
if rule.is_enabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[44]));
}
}
index_set
}
pub(crate) fn get_disabled_rules(&self) -> FxHashSet<RuleFilter<'static>> {
Expand Down Expand Up @@ -3957,81 +3968,86 @@ impl Nursery {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28]));
}
}
if let Some(rule) = self.no_value_at_rule.as_ref() {
if let Some(rule) = self.no_useless_string_raw.as_ref() {
if rule.is_disabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29]));
}
}
if let Some(rule) = self.use_adjacent_overload_signatures.as_ref() {
if let Some(rule) = self.no_value_at_rule.as_ref() {
if rule.is_disabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30]));
}
}
if let Some(rule) = self.use_aria_props_supported_by_role.as_ref() {
if let Some(rule) = self.use_adjacent_overload_signatures.as_ref() {
if rule.is_disabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31]));
}
}
if let Some(rule) = self.use_at_index.as_ref() {
if let Some(rule) = self.use_aria_props_supported_by_role.as_ref() {
if rule.is_disabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32]));
}
}
if let Some(rule) = self.use_component_export_only_modules.as_ref() {
if let Some(rule) = self.use_at_index.as_ref() {
if rule.is_disabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33]));
}
}
if let Some(rule) = self.use_consistent_curly_braces.as_ref() {
if let Some(rule) = self.use_component_export_only_modules.as_ref() {
if rule.is_disabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34]));
}
}
if let Some(rule) = self.use_consistent_member_accessibility.as_ref() {
if let Some(rule) = self.use_consistent_curly_braces.as_ref() {
if rule.is_disabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35]));
}
}
if let Some(rule) = self.use_deprecated_reason.as_ref() {
if let Some(rule) = self.use_consistent_member_accessibility.as_ref() {
if rule.is_disabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36]));
}
}
if let Some(rule) = self.use_explicit_type.as_ref() {
if let Some(rule) = self.use_deprecated_reason.as_ref() {
if rule.is_disabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37]));
}
}
if let Some(rule) = self.use_guard_for_in.as_ref() {
if let Some(rule) = self.use_explicit_type.as_ref() {
if rule.is_disabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38]));
}
}
if let Some(rule) = self.use_import_restrictions.as_ref() {
if let Some(rule) = self.use_guard_for_in.as_ref() {
if rule.is_disabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39]));
}
}
if let Some(rule) = self.use_sorted_classes.as_ref() {
if let Some(rule) = self.use_import_restrictions.as_ref() {
if rule.is_disabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40]));
}
}
if let Some(rule) = self.use_strict_mode.as_ref() {
if let Some(rule) = self.use_sorted_classes.as_ref() {
if rule.is_disabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41]));
}
}
if let Some(rule) = self.use_trim_start_end.as_ref() {
if let Some(rule) = self.use_strict_mode.as_ref() {
if rule.is_disabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42]));
}
}
if let Some(rule) = self.use_valid_autocomplete.as_ref() {
if let Some(rule) = self.use_trim_start_end.as_ref() {
if rule.is_disabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43]));
}
}
if let Some(rule) = self.use_valid_autocomplete.as_ref() {
if rule.is_disabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[44]));
}
}
index_set
}
#[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"]
Expand Down Expand Up @@ -4184,6 +4200,10 @@ impl Nursery {
.no_useless_escape_in_regex
.as_ref()
.map(|conf| (conf.level(), conf.get_options())),
"noUselessStringRaw" => self
.no_useless_string_raw
.as_ref()
.map(|conf| (conf.level(), conf.get_options())),
"noValueAtRule" => self
.no_value_at_rule
.as_ref()
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 @@ -182,6 +182,7 @@ define_categories! {
"lint/nursery/noUnmatchableAnbSelector": "https://biomejs.dev/linter/rules/no-unmatchable-anb-selector",
"lint/nursery/noUnusedFunctionParameters": "https://biomejs.dev/linter/rules/no-unused-function-parameters",
"lint/nursery/noUselessEscapeInRegex": "https://biomejs.dev/linter/rules/no-useless-escape-in-regex",
"lint/nursery/noUselessStringRaw": "https://biomejs.dev/linter/rules/no-useless-string-raw",
"lint/nursery/noValueAtRule": "https://biomejs.dev/linter/rules/no-value-at-rule",
"lint/nursery/useAdjacentOverloadSignatures": "https://biomejs.dev/linter/rules/use-adjacent-overload-signatures",
"lint/nursery/useAriaPropsSupportedByRole": "https://biomejs.dev/linter/rules/use-aria-props-supported-by-role",
Expand Down
2 changes: 2 additions & 0 deletions crates/biome_js_analyze/src/lint/nursery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub mod no_static_element_interactions;
pub mod no_substr;
pub mod no_template_curly_in_string;
pub mod no_useless_escape_in_regex;
pub mod no_useless_string_raw;
pub mod use_adjacent_overload_signatures;
pub mod use_aria_props_supported_by_role;
pub mod use_at_index;
Expand Down Expand Up @@ -62,6 +63,7 @@ declare_lint_group! {
self :: no_substr :: NoSubstr ,
self :: no_template_curly_in_string :: NoTemplateCurlyInString ,
self :: no_useless_escape_in_regex :: NoUselessEscapeInRegex ,
self :: no_useless_string_raw :: NoUselessStringRaw ,
self :: use_adjacent_overload_signatures :: UseAdjacentOverloadSignatures ,
self :: use_aria_props_supported_by_role :: UseAriaPropsSupportedByRole ,
self :: use_at_index :: UseAtIndex ,
Expand Down
103 changes: 103 additions & 0 deletions crates/biome_js_analyze/src/lint/nursery/no_useless_string_raw.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
use biome_analyze::{context::RuleContext, declare_lint_rule, Ast, Rule, RuleDiagnostic};
use biome_console::markup;
use biome_js_syntax::{AnyJsTemplateElement, JsTemplateExpression};
use biome_rowan::{AstNode, AstNodeList};

declare_lint_rule! {
/// Disallow unnecessary `String.raw` function in template string literals without any escape sequence.
///
/// `String.raw` is useless when contains a raw string without any escape-like sequence.
///
/// ## Examples
///
/// ### Invalid
///
/// ```js,expect_diagnostic
/// String.raw`a`;
/// ```
///
/// ```js,expect_diagnostic
/// String.raw`a ${v}`;
/// ```
///
/// ### Valid
///
/// ```js
/// String.raw`\n ${a}`;
/// ```
///
/// ```js
/// String.raw`\n`;
/// ```
pub NoUselessStringRaw {
version: "next",
name: "noUselessStringRaw",
language: "js",
recommended: false,
}
}

impl Rule for NoUselessStringRaw {
type Query = Ast<JsTemplateExpression>;
type State = ();
type Signals = Option<Self::State>;
type Options = ();

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let node = ctx.query();
let tag = node.tag()?;
let tag = tag.as_js_static_member_expression()?;

let object = tag.object().ok()?;
let object_expr = object.as_js_identifier_expression()?;
let object_name = object_expr.name().ok()?.value_token().ok()?;
let object_name = object_name.text_trimmed();

let member = tag.member().ok()?;
let member_name = member.as_js_name()?.value_token().ok()?;
let member_name = member_name.text_trimmed();

if object_name != "String" || member_name != "raw" {
return None;
}

if can_remove_string_raw(node) {
Some(())
} else {
None
}
}

fn diagnostic(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<RuleDiagnostic> {
let node = ctx.query();
Some(
RuleDiagnostic::new(
rule_category!(),
node.range(),
markup! {
"String.raw is useless when the raw string doesn't contain any escape sequence."
},
)
.note(markup! {
"Remove the String.raw call beacause it's useless here, String.raw can deal with string which contains escape sequence like \\n, \\t, \\r, \\\\, \\\", \\\'."
}),
)
}
}

fn can_remove_string_raw(node: &JsTemplateExpression) -> bool {
!node.elements().iter().any(|element| {
match element {
AnyJsTemplateElement::JsTemplateElement(_) => false,
AnyJsTemplateElement::JsTemplateChunkElement(chunk) => {
match chunk.template_chunk_token() {
Ok(token) => token.text().contains('\\'),
Err(_) => {
// if found an error, return `true` means `String.raw` can't remove
true
}
}
}
}
})
}
Loading

0 comments on commit 561b54c

Please sign in to comment.