diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ada9b949977..e4bb9e096280 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,11 @@ our [guidelines for writing a good changelog entry](https://github.com/biomejs/b ### Linter +#### New features + +- Add [noTemplateCurlyInString](https://biomejs.dev/linter/rules/no-template-curly-in-string/). Contributed by @fireairforce +- Add [NoOctalEscape](https://biomejs.dev/linter/rules/no-octal-escape/). Contributed by @fireairforce + #### Bug fixes - [noUselessStringConcat](https://biomejs.dev/linter/rules/no-useless-string-concat/) no longer panics when it encounters malformed code. Contributed by @Conaclos @@ -72,6 +77,8 @@ our [guidelines for writing a good changelog entry](https://github.com/biomejs/b - [useSemanticElements](https://biomejs.dev/linter/rules/use-semantic-elements/) now ignores `alert` and `alertdialog` roles ([#3858](https://github.com/biomejs/biome/issues/3858)). Contributed by @Conaclos +- [noUselessFragments](https://biomejs.dev/linter/rules/no-useless-fragments/) don't create invaild JSX code when Fragments children contains JSX Expression and in a LogicalExpression. Contributed by @fireairforce + ### Parser ## v1.9.2 (2024-09-19) diff --git a/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs b/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs index 1502eae7abc9..16318d7cd703 100644 --- a/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs +++ b/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs @@ -1107,6 +1107,16 @@ pub(crate) fn migrate_eslint_any_rule( let rule = group.no_sparse_array.get_or_insert(Default::default()); rule.set_level(rule_severity.into()); } + "no-template-curly-in-string" => { + if !options.include_nursery { + return false; + } + let group = rules.nursery.get_or_insert_with(Default::default); + let rule = group + .no_template_curly_in_string + .get_or_insert(Default::default()); + rule.set_level(rule_severity.into()); + } "no-this-before-super" => { let group = rules.correctness.get_or_insert_with(Default::default); let rule = group.no_unreachable_super.get_or_insert(Default::default()); diff --git a/crates/biome_configuration/src/analyzer/linter/rules.rs b/crates/biome_configuration/src/analyzer/linter/rules.rs index a1eb9bb0c6fd..7f9d335f686c 100644 --- a/crates/biome_configuration/src/analyzer/linter/rules.rs +++ b/crates/biome_configuration/src/analyzer/linter/rules.rs @@ -3332,6 +3332,10 @@ pub struct Nursery { #[doc = "Enforce the use of String.slice() over String.substr() and String.substring()."] #[serde(skip_serializing_if = "Option::is_none")] pub no_substr: Option>, + #[doc = "Disallow template literal placeholder syntax in regular strings."] + #[serde(skip_serializing_if = "Option::is_none")] + pub no_template_curly_in_string: + Option>, #[doc = "Disallow unknown pseudo-class selectors."] #[serde(skip_serializing_if = "Option::is_none")] pub no_unknown_pseudo_class: @@ -3429,6 +3433,7 @@ impl Nursery { "noSecrets", "noStaticElementInteractions", "noSubstr", + "noTemplateCurlyInString", "noUnknownPseudoClass", "noUnknownPseudoElement", "noUselessEscapeInRegex", @@ -3464,13 +3469,13 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[3]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31]), ]; const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), @@ -3506,6 +3511,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33]), ]; #[doc = r" Retrieves the recommended rules"] pub(crate) fn is_recommended_true(&self) -> bool { @@ -3607,86 +3613,91 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16])); } } - if let Some(rule) = self.no_unknown_pseudo_class.as_ref() { + if let Some(rule) = self.no_template_curly_in_string.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } } - if let Some(rule) = self.no_unknown_pseudo_element.as_ref() { + if let Some(rule) = self.no_unknown_pseudo_class.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); } } - if let Some(rule) = self.no_useless_escape_in_regex.as_ref() { + if let Some(rule) = self.no_unknown_pseudo_element.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19])); } } - if let Some(rule) = self.no_value_at_rule.as_ref() { + if let Some(rule) = self.no_useless_escape_in_regex.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20])); } } - 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[21])); } } - 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[22])); } } - if let Some(rule) = self.use_component_export_only_modules.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[23])); } } - 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[24])); } } - 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[25])); } } - 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[26])); } } - if let Some(rule) = self.use_explicit_function_return_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[27])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.use_explicit_function_return_type.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28])); } } - 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[29])); } } - 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[30])); } } - 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[31])); } } - 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[32])); } } + 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[33])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> FxHashSet> { @@ -3776,86 +3787,91 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16])); } } - if let Some(rule) = self.no_unknown_pseudo_class.as_ref() { + if let Some(rule) = self.no_template_curly_in_string.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } } - if let Some(rule) = self.no_unknown_pseudo_element.as_ref() { + if let Some(rule) = self.no_unknown_pseudo_class.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); } } - if let Some(rule) = self.no_useless_escape_in_regex.as_ref() { + if let Some(rule) = self.no_unknown_pseudo_element.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19])); } } - if let Some(rule) = self.no_value_at_rule.as_ref() { + if let Some(rule) = self.no_useless_escape_in_regex.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20])); } } - 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[21])); } } - 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[22])); } } - if let Some(rule) = self.use_component_export_only_modules.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[23])); } } - 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[24])); } } - 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[25])); } } - 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[26])); } } - if let Some(rule) = self.use_explicit_function_return_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[27])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.use_explicit_function_return_type.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28])); } } - 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[29])); } } - 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[30])); } } - 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[31])); } } - 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[32])); } } + 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[33])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -3960,6 +3976,10 @@ impl Nursery { .no_substr .as_ref() .map(|conf| (conf.level(), conf.get_options())), + "noTemplateCurlyInString" => self + .no_template_curly_in_string + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), "noUnknownPseudoClass" => self .no_unknown_pseudo_class .as_ref() diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index b1301407ee2e..d39031092aa9 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -161,6 +161,7 @@ define_categories! { "lint/nursery/noShorthandPropertyOverrides": "https://biomejs.dev/linter/rules/no-shorthand-property-overrides", "lint/nursery/noStaticElementInteractions": "https://biomejs.dev/linter/rules/no-static-element-interactions", "lint/nursery/noSubstr": "https://biomejs.dev/linter/rules/no-substr", + "lint/nursery/noTemplateCurlyInString": "https://biomejs.dev/linter/rules/no-template-curly-in-string", "lint/nursery/noUndeclaredDependencies": "https://biomejs.dev/linter/rules/no-undeclared-dependencies", "lint/nursery/noUnknownFunction": "https://biomejs.dev/linter/rules/no-unknown-function", "lint/nursery/noUnknownMediaFeatureName": "https://biomejs.dev/linter/rules/no-unknown-media-feature-name", diff --git a/crates/biome_js_analyze/src/lint/nursery.rs b/crates/biome_js_analyze/src/lint/nursery.rs index 7969f987f502..c808f8122d78 100644 --- a/crates/biome_js_analyze/src/lint/nursery.rs +++ b/crates/biome_js_analyze/src/lint/nursery.rs @@ -16,6 +16,7 @@ pub mod no_restricted_types; pub mod no_secrets; 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 use_adjacent_overload_signatures; pub mod use_aria_props_supported_by_role; @@ -47,6 +48,7 @@ declare_lint_group! { self :: no_secrets :: NoSecrets , self :: no_static_element_interactions :: NoStaticElementInteractions , self :: no_substr :: NoSubstr , + self :: no_template_curly_in_string :: NoTemplateCurlyInString , self :: no_useless_escape_in_regex :: NoUselessEscapeInRegex , self :: use_adjacent_overload_signatures :: UseAdjacentOverloadSignatures , self :: use_aria_props_supported_by_role :: UseAriaPropsSupportedByRole , diff --git a/crates/biome_js_analyze/src/lint/nursery/no_template_curly_in_string.rs b/crates/biome_js_analyze/src/lint/nursery/no_template_curly_in_string.rs new file mode 100644 index 000000000000..f141da3b4c7a --- /dev/null +++ b/crates/biome_js_analyze/src/lint/nursery/no_template_curly_in_string.rs @@ -0,0 +1,95 @@ +use biome_analyze::{ + context::RuleContext, declare_lint_rule, Ast, Rule, RuleDiagnostic, RuleSource, +}; +use biome_console::markup; +use biome_js_syntax::JsStringLiteralExpression; +use biome_rowan::{TextRange, TextSize}; + +declare_lint_rule! { + /// Disallow template literal placeholder syntax in regular strings. + /// + /// ECMAScript 6 allows programmers to create strings containing variable or expressions using template literals, + /// instead of string concatenation, by writing expressions like ${variable} between two backtick quotes (`). + /// It can be easy to use the wrong quotes when wanting to use template literals, by writing "${variable}", + /// and end up with the literal value "${variable}" instead of a string containing the value of the injected expressions. + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```js,expect_diagnostic + /// const a = "Hello ${name}!"; + /// ``` + /// + /// ```js,expect_diagnostic + /// const a = 'Hello ${name}!'; + /// ``` + /// + /// ```js,expect_diagnostic + /// const a = "Time: ${12 * 60 * 60 * 1000}"; + /// ``` + /// + /// ### Valid + /// + /// ```js + /// const a = `Hello ${name}!`; + /// const a = `Time: ${12 * 60 * 60 * 1000}`; + /// + /// const a = templateFunction`Hello ${name}`; + /// ``` + /// + pub NoTemplateCurlyInString { + version: "next", + name: "noTemplateCurlyInString", + language: "js", + sources: &[RuleSource::Eslint("no-template-curly-in-string")], + recommended: false, + } +} + +impl Rule for NoTemplateCurlyInString { + type Query = Ast; + type State = (u32, u32); + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Self::Signals { + let node = ctx.query(); + let token = node.value_token().ok()?; + let text = token.text(); + + let mut byte_iter = text.bytes().enumerate(); + while let Some((i, byte)) = byte_iter.next() { + if byte == b'$' { + if let Some((_, b'{')) = byte_iter.next() { + for (j, inner_byte) in byte_iter.by_ref() { + if inner_byte == b'}' { + return Some((i as u32, (j + 1) as u32)); + } + } + } + } + } + None + } + + fn diagnostic(ctx: &RuleContext, range: &Self::State) -> Option { + let value_token = ctx.query().value_token().ok()?; + let value_token_range = value_token.text_trimmed_range(); + Some( + RuleDiagnostic::new( + rule_category!(), + TextRange::new( + value_token_range.start() + TextSize::from(range.0), + value_token_range.start() + TextSize::from(range.1), + ), + markup! { + "Unexpected template string placeholder." + }, + ) + .note(markup! { + "Turn the string into a template string." + }), + ) + } +} diff --git a/crates/biome_js_analyze/src/options.rs b/crates/biome_js_analyze/src/options.rs index 3f02fe6b57db..8babc8375238 100644 --- a/crates/biome_js_analyze/src/options.rs +++ b/crates/biome_js_analyze/src/options.rs @@ -208,6 +208,7 @@ pub type NoSuspiciousSemicolonInJsx = < lint :: suspicious :: no_suspicious_semi pub type NoSvgWithoutTitle = ::Options; pub type NoSwitchDeclarations = < lint :: correctness :: no_switch_declarations :: NoSwitchDeclarations as biome_analyze :: Rule > :: Options ; +pub type NoTemplateCurlyInString = < lint :: nursery :: no_template_curly_in_string :: NoTemplateCurlyInString as biome_analyze :: Rule > :: Options ; pub type NoThenProperty = ::Options; pub type NoThisInStatic = diff --git a/crates/biome_js_analyze/tests/specs/nursery/noTemplateCurlyInString/invalid.js b/crates/biome_js_analyze/tests/specs/nursery/noTemplateCurlyInString/invalid.js new file mode 100644 index 000000000000..c6516d02fc94 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noTemplateCurlyInString/invalid.js @@ -0,0 +1,7 @@ +let a = 'Hello, ${name}'; +let a = "Hello, ${name}"; +let a = '${greeting}, ${name}'; +let a = 'Hello, ${index + 1}'; +let a = 'Hello, ${name + " foo"}'; +let a = 'Hello, ${name || "foo"}'; +let a = 'Hello, ${{foo: "bar"}.foo}'; \ No newline at end of file diff --git a/crates/biome_js_analyze/tests/specs/nursery/noTemplateCurlyInString/invalid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noTemplateCurlyInString/invalid.js.snap new file mode 100644 index 000000000000..bb44ca7260de --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noTemplateCurlyInString/invalid.js.snap @@ -0,0 +1,128 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid.js +--- +# Input +```jsx +let a = 'Hello, ${name}'; +let a = "Hello, ${name}"; +let a = '${greeting}, ${name}'; +let a = 'Hello, ${index + 1}'; +let a = 'Hello, ${name + " foo"}'; +let a = 'Hello, ${name || "foo"}'; +let a = 'Hello, ${{foo: "bar"}.foo}'; +``` + +# Diagnostics +``` +invalid.js:1:17 lint/nursery/noTemplateCurlyInString ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected template string placeholder. + + > 1 │ let a = 'Hello, ${name}'; + │ ^^^^^^^ + 2 │ let a = "Hello, ${name}"; + 3 │ let a = '${greeting}, ${name}'; + + i Turn the string into a template string. + + +``` + +``` +invalid.js:2:17 lint/nursery/noTemplateCurlyInString ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected template string placeholder. + + 1 │ let a = 'Hello, ${name}'; + > 2 │ let a = "Hello, ${name}"; + │ ^^^^^^^ + 3 │ let a = '${greeting}, ${name}'; + 4 │ let a = 'Hello, ${index + 1}'; + + i Turn the string into a template string. + + +``` + +``` +invalid.js:3:10 lint/nursery/noTemplateCurlyInString ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected template string placeholder. + + 1 │ let a = 'Hello, ${name}'; + 2 │ let a = "Hello, ${name}"; + > 3 │ let a = '${greeting}, ${name}'; + │ ^^^^^^^^^^^ + 4 │ let a = 'Hello, ${index + 1}'; + 5 │ let a = 'Hello, ${name + " foo"}'; + + i Turn the string into a template string. + + +``` + +``` +invalid.js:4:17 lint/nursery/noTemplateCurlyInString ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected template string placeholder. + + 2 │ let a = "Hello, ${name}"; + 3 │ let a = '${greeting}, ${name}'; + > 4 │ let a = 'Hello, ${index + 1}'; + │ ^^^^^^^^^^^^ + 5 │ let a = 'Hello, ${name + " foo"}'; + 6 │ let a = 'Hello, ${name || "foo"}'; + + i Turn the string into a template string. + + +``` + +``` +invalid.js:5:17 lint/nursery/noTemplateCurlyInString ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected template string placeholder. + + 3 │ let a = '${greeting}, ${name}'; + 4 │ let a = 'Hello, ${index + 1}'; + > 5 │ let a = 'Hello, ${name + " foo"}'; + │ ^^^^^^^^^^^^^^^^ + 6 │ let a = 'Hello, ${name || "foo"}'; + 7 │ let a = 'Hello, ${{foo: "bar"}.foo}'; + + i Turn the string into a template string. + + +``` + +``` +invalid.js:6:17 lint/nursery/noTemplateCurlyInString ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected template string placeholder. + + 4 │ let a = 'Hello, ${index + 1}'; + 5 │ let a = 'Hello, ${name + " foo"}'; + > 6 │ let a = 'Hello, ${name || "foo"}'; + │ ^^^^^^^^^^^^^^^^ + 7 │ let a = 'Hello, ${{foo: "bar"}.foo}'; + + i Turn the string into a template string. + + +``` + +``` +invalid.js:7:17 lint/nursery/noTemplateCurlyInString ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected template string placeholder. + + 5 │ let a = 'Hello, ${name + " foo"}'; + 6 │ let a = 'Hello, ${name || "foo"}'; + > 7 │ let a = 'Hello, ${{foo: "bar"}.foo}'; + │ ^^^^^^^^^^^^^^ + + i Turn the string into a template string. + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noTemplateCurlyInString/valid.js b/crates/biome_js_analyze/tests/specs/nursery/noTemplateCurlyInString/valid.js new file mode 100644 index 000000000000..e10f3038e812 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noTemplateCurlyInString/valid.js @@ -0,0 +1,14 @@ +let a = `Hello, ${name}`; +let a = templateFunction`Hello, ${name}`; +let a = `Hello, name`; +let a = 'Hello, name'; +let a = 'Hello, ' + name; +let a = `Hello, ${index + 1}`; +let a = `Hello, ${name + " foo"}`; +let a = `Hello, ${name || "foo"}`; +let a = `Hello, ${{foo: "bar"}.foo}`; +let a = '$2'; +let a = '${'; +let a = '$}'; +let a = '{foo}'; +let a = '{foo: "bar"}'; \ No newline at end of file diff --git a/crates/biome_js_analyze/tests/specs/nursery/noTemplateCurlyInString/valid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noTemplateCurlyInString/valid.js.snap new file mode 100644 index 000000000000..9e72393b401c --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noTemplateCurlyInString/valid.js.snap @@ -0,0 +1,21 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: valid.js +--- +# Input +```jsx +let a = `Hello, ${name}`; +let a = templateFunction`Hello, ${name}`; +let a = `Hello, name`; +let a = 'Hello, name'; +let a = 'Hello, ' + name; +let a = `Hello, ${index + 1}`; +let a = `Hello, ${name + " foo"}`; +let a = `Hello, ${name || "foo"}`; +let a = `Hello, ${{foo: "bar"}.foo}`; +let a = '$2'; +let a = '${'; +let a = '$}'; +let a = '{foo}'; +let a = '{foo: "bar"}'; +``` diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index d1d37f06a061..81fa5a018fc5 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -1286,6 +1286,10 @@ export interface Nursery { * Enforce the use of String.slice() over String.substr() and String.substring(). */ noSubstr?: RuleFixConfiguration_for_Null; + /** + * Disallow template literal placeholder syntax in regular strings. + */ + noTemplateCurlyInString?: RuleConfiguration_for_Null; /** * Disallow unknown pseudo-class selectors. */ @@ -2854,6 +2858,7 @@ export type Category = | "lint/nursery/noShorthandPropertyOverrides" | "lint/nursery/noStaticElementInteractions" | "lint/nursery/noSubstr" + | "lint/nursery/noTemplateCurlyInString" | "lint/nursery/noUndeclaredDependencies" | "lint/nursery/noUnknownFunction" | "lint/nursery/noUnknownMediaFeatureName" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 93f122967de7..c2efd0fe4d50 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -2195,6 +2195,13 @@ { "type": "null" } ] }, + "noTemplateCurlyInString": { + "description": "Disallow template literal placeholder syntax in regular strings.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "noUnknownPseudoClass": { "description": "Disallow unknown pseudo-class selectors.", "anyOf": [