From fec262f1593c53e4d6c46f6934e3e2ebc2144edc Mon Sep 17 00:00:00 2001 From: Justinas Delinda <8914032+minht11@users.noreply.github.com> Date: Sat, 25 May 2024 21:33:43 +0300 Subject: [PATCH] feat(linter): implement useErrorMessage (#2978) --- CHANGELOG.md | 1 + .../migrate/eslint_any_rule_to_biome.rs | 8 + .../biome_configuration/src/linter/rules.rs | 67 ++- .../src/categories.rs | 5 +- crates/biome_js_analyze/src/lint/nursery.rs | 2 + .../use_consistent_builtin_instantiation.rs | 17 +- .../src/lint/nursery/use_error_message.rs | 167 ++++++ crates/biome_js_analyze/src/options.rs | 2 + .../specs/nursery/useErrorMessage/invalid.js | 34 ++ .../nursery/useErrorMessage/invalid.js.snap | 511 ++++++++++++++++++ .../specs/nursery/useErrorMessage/valid.js | 37 ++ .../nursery/useErrorMessage/valid.js.snap | 45 ++ crates/biome_js_syntax/src/expr_ext.rs | 16 + .../@biomejs/backend-jsonrpc/src/workspace.ts | 9 +- .../@biomejs/biome/configuration_schema.json | 7 + 15 files changed, 889 insertions(+), 39 deletions(-) create mode 100644 crates/biome_js_analyze/src/lint/nursery/use_error_message.rs create mode 100644 crates/biome_js_analyze/tests/specs/nursery/useErrorMessage/invalid.js create mode 100644 crates/biome_js_analyze/tests/specs/nursery/useErrorMessage/invalid.js.snap create mode 100644 crates/biome_js_analyze/tests/specs/nursery/useErrorMessage/valid.js create mode 100644 crates/biome_js_analyze/tests/specs/nursery/useErrorMessage/valid.js.snap diff --git a/CHANGELOG.md b/CHANGELOG.md index ed0907adbb68..482f77b42139 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -228,6 +228,7 @@ our [guidelines for writing a good changelog entry](https://github.com/biomejs/b #### New features +- Add [nursery/useErrorMessage](https://biomejs.dev/linter/rules/use_error_message/). Contributed by @minht11 - Add [nursery/useThrowOnlyError](https://biomejs.dev/linter/rules/use_throw_only_error/). Contributed by @minht11 - Add [nursery/useImportExtensions](https://biomejs.dev/linter/rules/use-import-extensions/). Contributed by @minht11 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 c8f6973c455a..e271a4de5ffc 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 @@ -1334,6 +1334,14 @@ pub(crate) fn migrate_eslint_any_rule( .get_or_insert(Default::default()); rule.set_level(rule_severity.into()); } + "unicorn/error-message" => { + if !options.include_nursery { + return false; + } + let group = rules.nursery.get_or_insert_with(Default::default); + let rule = group.use_error_message.get_or_insert(Default::default()); + rule.set_level(rule_severity.into()); + } "unicorn/explicit-length-check" => { if !options.include_inspired { results.has_inspired_rules = true; diff --git a/crates/biome_configuration/src/linter/rules.rs b/crates/biome_configuration/src/linter/rules.rs index b51b3b4c7375..b91195962af6 100644 --- a/crates/biome_configuration/src/linter/rules.rs +++ b/crates/biome_configuration/src/linter/rules.rs @@ -2830,6 +2830,9 @@ pub struct Nursery { #[doc = "Require the default clause in switch statements."] #[serde(skip_serializing_if = "Option::is_none")] pub use_default_switch_clause: Option>, + #[doc = "Enforce passing a message value when creating a built-in error."] + #[serde(skip_serializing_if = "Option::is_none")] + pub use_error_message: Option>, #[doc = "Enforce explicitly comparing the length, size, byteLength or byteOffset property of a value."] #[serde(skip_serializing_if = "Option::is_none")] pub use_explicit_length_check: Option>, @@ -2913,6 +2916,7 @@ impl Nursery { "useArrayLiterals", "useConsistentBuiltinInstantiation", "useDefaultSwitchClause", + "useErrorMessage", "useExplicitLengthCheck", "useFocusableInteractive", "useGenericFontNames", @@ -2961,9 +2965,9 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38]), ]; const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), @@ -3008,6 +3012,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42]), ]; #[doc = r" Retrieves the recommended rules"] pub(crate) fn is_recommended_true(&self) -> bool { @@ -3179,61 +3184,66 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30])); } } - if let Some(rule) = self.use_explicit_length_check.as_ref() { + if let Some(rule) = self.use_error_message.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31])); } } - if let Some(rule) = self.use_focusable_interactive.as_ref() { + if let Some(rule) = self.use_explicit_length_check.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32])); } } - if let Some(rule) = self.use_generic_font_names.as_ref() { + if let Some(rule) = self.use_focusable_interactive.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33])); } } - if let Some(rule) = self.use_import_extensions.as_ref() { + if let Some(rule) = self.use_generic_font_names.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.use_import_extensions.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35])); } } - if let Some(rule) = self.use_number_to_fixed_digits_argument.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[36])); } } - if let Some(rule) = self.use_semantic_elements.as_ref() { + if let Some(rule) = self.use_number_to_fixed_digits_argument.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37])); } } - if let Some(rule) = self.use_sorted_classes.as_ref() { + if let Some(rule) = self.use_semantic_elements.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38])); } } - if let Some(rule) = self.use_throw_new_error.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[39])); } } - if let Some(rule) = self.use_throw_only_error.as_ref() { + if let Some(rule) = self.use_throw_new_error.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40])); } } - if let Some(rule) = self.use_top_level_regex.as_ref() { + if let Some(rule) = self.use_throw_only_error.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41])); } } + if let Some(rule) = self.use_top_level_regex.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> FxHashSet> { @@ -3393,61 +3403,66 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30])); } } - if let Some(rule) = self.use_explicit_length_check.as_ref() { + if let Some(rule) = self.use_error_message.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31])); } } - if let Some(rule) = self.use_focusable_interactive.as_ref() { + if let Some(rule) = self.use_explicit_length_check.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32])); } } - if let Some(rule) = self.use_generic_font_names.as_ref() { + if let Some(rule) = self.use_focusable_interactive.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33])); } } - if let Some(rule) = self.use_import_extensions.as_ref() { + if let Some(rule) = self.use_generic_font_names.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.use_import_extensions.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35])); } } - if let Some(rule) = self.use_number_to_fixed_digits_argument.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[36])); } } - if let Some(rule) = self.use_semantic_elements.as_ref() { + if let Some(rule) = self.use_number_to_fixed_digits_argument.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37])); } } - if let Some(rule) = self.use_sorted_classes.as_ref() { + if let Some(rule) = self.use_semantic_elements.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38])); } } - if let Some(rule) = self.use_throw_new_error.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[39])); } } - if let Some(rule) = self.use_throw_only_error.as_ref() { + if let Some(rule) = self.use_throw_new_error.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40])); } } - if let Some(rule) = self.use_top_level_regex.as_ref() { + if let Some(rule) = self.use_throw_only_error.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41])); } } + if let Some(rule) = self.use_top_level_regex.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -3608,6 +3623,10 @@ impl Nursery { .use_default_switch_clause .as_ref() .map(|conf| (conf.level(), conf.get_options())), + "useErrorMessage" => self + .use_error_message + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), "useExplicitLengthCheck" => self .use_explicit_length_check .as_ref() diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index 35ab0c58ae21..d2b65a69d4a7 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -110,17 +110,16 @@ define_categories! { "lint/correctness/useValidForDirection": "https://biomejs.dev/linter/rules/use-valid-for-direction", "lint/correctness/useYield": "https://biomejs.dev/linter/rules/use-yield", "lint/nursery/colorNoInvalidHex": "https://biomejs.dev/linter/rules/color-no-invalid-hex", - "lint/nursery/useAdjacentOverloadSignatures": "https://biomejs.dev/linter/rules/use-adjacent-overload-signatures", "lint/nursery/noColorInvalidHex": "https://biomejs.dev/linter/rules/no-color-invalid-hex", "lint/nursery/noConsole": "https://biomejs.dev/linter/rules/no-console", "lint/nursery/noConstantMathMinMaxClamp": "https://biomejs.dev/linter/rules/no-constant-math-min-max-clamp", - "lint/nursery/noEmptyBlock": "https://biomejs.dev/linter/rules/no-empty-block", "lint/nursery/noDoneCallback": "https://biomejs.dev/linter/rules/no-done-callback", "lint/nursery/noDuplicateAtImportRules": "https://biomejs.dev/linter/rules/no-duplicate-at-import-rules", "lint/nursery/noDuplicateElseIf": "https://biomejs.dev/linter/rules/no-duplicate-else-if", "lint/nursery/noDuplicateFontNames": "https://biomejs.dev/linter/rules/no-font-family-duplicate-names", "lint/nursery/noDuplicateJsonKeys": "https://biomejs.dev/linter/rules/no-duplicate-json-keys", "lint/nursery/noDuplicateSelectorsKeyframeBlock": "https://biomejs.dev/linter/rules/no-duplicate-selectors-keyframe-block", + "lint/nursery/noEmptyBlock": "https://biomejs.dev/linter/rules/no-empty-block", "lint/nursery/noEvolvingAny": "https://biomejs.dev/linter/rules/no-evolving-any", "lint/nursery/noFlatMapIdentity": "https://biomejs.dev/linter/rules/no-flat-map-identity", "lint/nursery/noImportantInKeyframe": "https://biomejs.dev/linter/rules/no-important-in-keyframe", @@ -141,10 +140,12 @@ define_categories! { "lint/nursery/noUselessStringConcat": "https://biomejs.dev/linter/rules/no-useless-string-concat", "lint/nursery/noUselessUndefinedInitialization": "https://biomejs.dev/linter/rules/no-useless-undefined-initialization", "lint/nursery/noYodaExpression": "https://biomejs.dev/linter/rules/no-yoda-expression", + "lint/nursery/useAdjacentOverloadSignatures": "https://biomejs.dev/linter/rules/use-adjacent-overload-signatures", "lint/nursery/useArrayLiterals": "https://biomejs.dev/linter/rules/use-array-literals", "lint/nursery/useBiomeSuppressionComment": "https://biomejs.dev/linter/rules/use-biome-suppression-comment", "lint/nursery/useConsistentBuiltinInstantiation": "https://biomejs.dev/linter/rules/use-consistent-new-builtin", "lint/nursery/useDefaultSwitchClause": "https://biomejs.dev/linter/rules/use-default-switch-clause", + "lint/nursery/useErrorMessage": "https://biomejs.dev/linter/rules/use-error-message", "lint/nursery/useExplicitLengthCheck": "https://biomejs.dev/linter/rules/use-explicit-length-check", "lint/nursery/useFocusableInteractive": "https://biomejs.dev/linter/rules/use-focusable-interactive", "lint/nursery/useGenericFontNames": "https://biomejs.dev/linter/rules/use-generic-font-names", diff --git a/crates/biome_js_analyze/src/lint/nursery.rs b/crates/biome_js_analyze/src/lint/nursery.rs index bac3db613b9c..1b4d3475a3f6 100644 --- a/crates/biome_js_analyze/src/lint/nursery.rs +++ b/crates/biome_js_analyze/src/lint/nursery.rs @@ -20,6 +20,7 @@ pub mod use_adjacent_overload_signatures; pub mod use_array_literals; pub mod use_consistent_builtin_instantiation; pub mod use_default_switch_clause; +pub mod use_error_message; pub mod use_explicit_length_check; pub mod use_focusable_interactive; pub mod use_import_extensions; @@ -53,6 +54,7 @@ declare_group! { self :: use_array_literals :: UseArrayLiterals , self :: use_consistent_builtin_instantiation :: UseConsistentBuiltinInstantiation , self :: use_default_switch_clause :: UseDefaultSwitchClause , + self :: use_error_message :: UseErrorMessage , self :: use_explicit_length_check :: UseExplicitLengthCheck , self :: use_focusable_interactive :: UseFocusableInteractive , self :: use_import_extensions :: UseImportExtensions , diff --git a/crates/biome_js_analyze/src/lint/nursery/use_consistent_builtin_instantiation.rs b/crates/biome_js_analyze/src/lint/nursery/use_consistent_builtin_instantiation.rs index 24c6b054023a..203ae2a9bd2c 100644 --- a/crates/biome_js_analyze/src/lint/nursery/use_consistent_builtin_instantiation.rs +++ b/crates/biome_js_analyze/src/lint/nursery/use_consistent_builtin_instantiation.rs @@ -259,18 +259,13 @@ pub struct UseConsistentBuiltinInstantiationState { fn extract_callee_and_rule( node: &JsNewOrCallExpression, ) -> Option<(AnyJsExpression, BuiltinCreationRule)> { - match node { - JsNewOrCallExpression::JsNewExpression(node) => { - let callee = node.callee().ok()?; + let rule = match node { + JsNewOrCallExpression::JsNewExpression(_) => BuiltinCreationRule::MustNotUseNew, + JsNewOrCallExpression::JsCallExpression(_) => BuiltinCreationRule::MustUseNew, + }; + let callee = node.callee().ok()?; - Some((callee, BuiltinCreationRule::MustNotUseNew)) - } - JsNewOrCallExpression::JsCallExpression(node) => { - let callee: AnyJsExpression = node.callee().ok()?; - - Some((callee, BuiltinCreationRule::MustUseNew)) - } - } + Some((callee, rule)) } fn convert_new_expression_to_call_expression(expr: &JsNewExpression) -> Option { diff --git a/crates/biome_js_analyze/src/lint/nursery/use_error_message.rs b/crates/biome_js_analyze/src/lint/nursery/use_error_message.rs new file mode 100644 index 000000000000..8a730fbd164b --- /dev/null +++ b/crates/biome_js_analyze/src/lint/nursery/use_error_message.rs @@ -0,0 +1,167 @@ +use biome_analyze::{context::RuleContext, declare_rule, Rule, RuleDiagnostic, RuleSource}; +use biome_console::markup; +use biome_js_syntax::{global_identifier, AnyJsExpression, JsNewOrCallExpression}; +use biome_rowan::AstNode; + +use crate::services::semantic::Semantic; + +declare_rule! { + /// Enforce passing a message value when creating a built-in error. + /// + /// This rule enforces a message value to be passed in when creating an instance of a built-in `Error` object, + /// which leads to more readable and debuggable code. + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```js,expect_diagnostic + /// throw Error(); + /// ``` + /// ```js,expect_diagnostic + /// throw Error(''); + /// ``` + /// ```js,expect_diagnostic + /// throw new TypeError(); + /// ``` + /// ```js,expect_diagnostic + /// const error = new AggregateError(errors); + /// ``` + /// + /// ### Valid + /// + /// ```js + /// throw Error('Unexpected property.'); + /// ``` + /// ```js + /// throw new TypeError('Array expected.'); + /// ``` + /// ```js + /// const error = new AggregateError(errors, 'Promises rejected.'); + /// ``` + pub UseErrorMessage { + version: "next", + name: "useErrorMessage", + language: "js", + sources: &[RuleSource::EslintUnicorn("error-message")], + recommended: false, + } +} + +impl Rule for UseErrorMessage { + type Query = Semantic; + type State = UseErrorMessageRule; + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Self::Signals { + let node = ctx.query(); + let callee = node.callee().ok()?; + + let (reference, name) = global_identifier(&callee.omit_parentheses())?; + let name_text = name.text(); + + if BUILTIN_ERRORS.binary_search(&name_text).is_err() + || ctx.model().binding(&reference).is_some() + { + return None; + } + + let argument_position = if name_text == "AggregateError" { 1 } else { 0 }; + let arguments = node.arguments()?; + + let has_spread = arguments + .args() + .into_iter() + .take(argument_position + 1) + .map_while(|arg| arg.ok()) + .any(|arg| arg.as_js_spread().is_some()); + + if has_spread { + return None; + } + + let Some(arg) = arguments + .args() + .into_iter() + .nth(argument_position) + .and_then(|a| a.ok()) + else { + return Some(UseErrorMessageRule::MissingMessage); + }; + + match arg.as_any_js_expression()? { + AnyJsExpression::AnyJsLiteralExpression(literal) => { + let Some(string_literal) = literal.as_js_string_literal_expression() else { + return Some(UseErrorMessageRule::NotString); + }; + + let text = string_literal.inner_string_text().ok()?; + if text.trim().is_empty() { + return Some(UseErrorMessageRule::EmptyString); + } + + None + } + AnyJsExpression::JsTemplateExpression(template) => { + if template.elements().into_iter().count() == 0 { + return Some(UseErrorMessageRule::EmptyString); + } + + None + } + AnyJsExpression::JsArrayExpression(_) | AnyJsExpression::JsObjectExpression(_) => { + Some(UseErrorMessageRule::NotString) + } + _ => None, + } + } + + fn diagnostic(ctx: &RuleContext, state: &Self::State) -> Option { + let node = ctx.query().arguments()?; + + let message = match state { + UseErrorMessageRule::MissingMessage => "Provide an error message for the error.", + UseErrorMessageRule::EmptyString => "Error message should not be an empty string.", + UseErrorMessageRule::NotString => "Error message should be a string.", + }; + + Some( + RuleDiagnostic::new( + rule_category!(), + node.range(), + markup! { {message} }, + ) + .note(markup! { + "Providing meaningful error messages leads to more readable and debuggable code." + }), + ) + } +} + +/// Sorted array of builtins errors requiring an error message. +/// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error +const BUILTIN_ERRORS: &[&str] = &[ + "AggregateError", + "Error", + "EvalError", + "InternalError", + "RangeError", + "ReferenceError", + "SyntaxError", + "TypeError", + "URIError", +]; + +pub enum UseErrorMessageRule { + MissingMessage, + EmptyString, + NotString, +} + +#[test] +fn test_order() { + for items in BUILTIN_ERRORS.windows(2) { + assert!(items[0] < items[1], "{} < {}", items[0], items[1]); + } +} diff --git a/crates/biome_js_analyze/src/options.rs b/crates/biome_js_analyze/src/options.rs index 756c2e337aa1..2d6261f42a6b 100644 --- a/crates/biome_js_analyze/src/options.rs +++ b/crates/biome_js_analyze/src/options.rs @@ -271,6 +271,8 @@ pub type UseDefaultSwitchClause = < lint :: nursery :: use_default_switch_clause pub type UseDefaultSwitchClauseLast = < lint :: suspicious :: use_default_switch_clause_last :: UseDefaultSwitchClauseLast as biome_analyze :: Rule > :: Options ; pub type UseEnumInitializers = ::Options; +pub type UseErrorMessage = + ::Options; pub type UseExhaustiveDependencies = < lint :: correctness :: use_exhaustive_dependencies :: UseExhaustiveDependencies as biome_analyze :: Rule > :: Options ; pub type UseExplicitLengthCheck = < lint :: nursery :: use_explicit_length_check :: UseExplicitLengthCheck as biome_analyze :: Rule > :: Options ; pub type UseExponentiationOperator = < lint :: style :: use_exponentiation_operator :: UseExponentiationOperator as biome_analyze :: Rule > :: Options ; diff --git a/crates/biome_js_analyze/tests/specs/nursery/useErrorMessage/invalid.js b/crates/biome_js_analyze/tests/specs/nursery/useErrorMessage/invalid.js new file mode 100644 index 000000000000..f8db71c0547f --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useErrorMessage/invalid.js @@ -0,0 +1,34 @@ +throw new Error(); +throw Error(); +throw new Error(""); +throw new Error(``); + +new AggregateError() +new EvalError() +new InternalError() +new RangeError() +new ReferenceError() +new SyntaxError() +new TypeError() +new URIError() + +throw new Error([]); +throw new Error([foo]); +throw new Error({}); +throw new Error({ foo }); +throw new Error(1); +throw new Error(undefined); +throw new Error(null); +throw new Error(true); + +new AggregateError(errors); +new AggregateError(errors); +new AggregateError(errors, ""); +new AggregateError(errors, ``); +new AggregateError(errors, "", extraArgument); + +new AggregateError(errors, []); +new AggregateError(errors, [foo]); +new AggregateError(errors, [0][0]); +new AggregateError(errors, {}); +new AggregateError(errors, { foo }); diff --git a/crates/biome_js_analyze/tests/specs/nursery/useErrorMessage/invalid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/useErrorMessage/invalid.js.snap new file mode 100644 index 000000000000..03d5ae37f28e --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useErrorMessage/invalid.js.snap @@ -0,0 +1,511 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid.js +--- +# Input +```jsx +throw new Error(); +throw Error(); +throw new Error(""); +throw new Error(``); + +new AggregateError() +new EvalError() +new InternalError() +new RangeError() +new ReferenceError() +new SyntaxError() +new TypeError() +new URIError() + +throw new Error([]); +throw new Error([foo]); +throw new Error({}); +throw new Error({ foo }); +throw new Error(1); +throw new Error(undefined); +throw new Error(null); +throw new Error(true); + +new AggregateError(errors); +new AggregateError(errors); +new AggregateError(errors, ""); +new AggregateError(errors, ``); +new AggregateError(errors, "", extraArgument); + +new AggregateError(errors, []); +new AggregateError(errors, [foo]); +new AggregateError(errors, [0][0]); +new AggregateError(errors, {}); +new AggregateError(errors, { foo }); + +``` + +# Diagnostics +``` +invalid.js:1:16 lint/nursery/useErrorMessage ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Provide an error message for the error. + + > 1 │ throw new Error(); + │ ^^ + 2 │ throw Error(); + 3 │ throw new Error(""); + + i Providing meaningful error messages leads to more readable and debuggable code. + + +``` + +``` +invalid.js:2:12 lint/nursery/useErrorMessage ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Provide an error message for the error. + + 1 │ throw new Error(); + > 2 │ throw Error(); + │ ^^ + 3 │ throw new Error(""); + 4 │ throw new Error(``); + + i Providing meaningful error messages leads to more readable and debuggable code. + + +``` + +``` +invalid.js:3:16 lint/nursery/useErrorMessage ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Error message should not be an empty string. + + 1 │ throw new Error(); + 2 │ throw Error(); + > 3 │ throw new Error(""); + │ ^^^^ + 4 │ throw new Error(``); + 5 │ + + i Providing meaningful error messages leads to more readable and debuggable code. + + +``` + +``` +invalid.js:4:16 lint/nursery/useErrorMessage ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Error message should not be an empty string. + + 2 │ throw Error(); + 3 │ throw new Error(""); + > 4 │ throw new Error(``); + │ ^^^^ + 5 │ + 6 │ new AggregateError() + + i Providing meaningful error messages leads to more readable and debuggable code. + + +``` + +``` +invalid.js:6:19 lint/nursery/useErrorMessage ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Provide an error message for the error. + + 4 │ throw new Error(``); + 5 │ + > 6 │ new AggregateError() + │ ^^ + 7 │ new EvalError() + 8 │ new InternalError() + + i Providing meaningful error messages leads to more readable and debuggable code. + + +``` + +``` +invalid.js:7:14 lint/nursery/useErrorMessage ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Provide an error message for the error. + + 6 │ new AggregateError() + > 7 │ new EvalError() + │ ^^ + 8 │ new InternalError() + 9 │ new RangeError() + + i Providing meaningful error messages leads to more readable and debuggable code. + + +``` + +``` +invalid.js:8:18 lint/nursery/useErrorMessage ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Provide an error message for the error. + + 6 │ new AggregateError() + 7 │ new EvalError() + > 8 │ new InternalError() + │ ^^ + 9 │ new RangeError() + 10 │ new ReferenceError() + + i Providing meaningful error messages leads to more readable and debuggable code. + + +``` + +``` +invalid.js:9:15 lint/nursery/useErrorMessage ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Provide an error message for the error. + + 7 │ new EvalError() + 8 │ new InternalError() + > 9 │ new RangeError() + │ ^^ + 10 │ new ReferenceError() + 11 │ new SyntaxError() + + i Providing meaningful error messages leads to more readable and debuggable code. + + +``` + +``` +invalid.js:10:19 lint/nursery/useErrorMessage ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Provide an error message for the error. + + 8 │ new InternalError() + 9 │ new RangeError() + > 10 │ new ReferenceError() + │ ^^ + 11 │ new SyntaxError() + 12 │ new TypeError() + + i Providing meaningful error messages leads to more readable and debuggable code. + + +``` + +``` +invalid.js:11:16 lint/nursery/useErrorMessage ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Provide an error message for the error. + + 9 │ new RangeError() + 10 │ new ReferenceError() + > 11 │ new SyntaxError() + │ ^^ + 12 │ new TypeError() + 13 │ new URIError() + + i Providing meaningful error messages leads to more readable and debuggable code. + + +``` + +``` +invalid.js:12:14 lint/nursery/useErrorMessage ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Provide an error message for the error. + + 10 │ new ReferenceError() + 11 │ new SyntaxError() + > 12 │ new TypeError() + │ ^^ + 13 │ new URIError() + 14 │ + + i Providing meaningful error messages leads to more readable and debuggable code. + + +``` + +``` +invalid.js:13:13 lint/nursery/useErrorMessage ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Provide an error message for the error. + + 11 │ new SyntaxError() + 12 │ new TypeError() + > 13 │ new URIError() + │ ^^ + 14 │ + 15 │ throw new Error([]); + + i Providing meaningful error messages leads to more readable and debuggable code. + + +``` + +``` +invalid.js:15:16 lint/nursery/useErrorMessage ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Error message should be a string. + + 13 │ new URIError() + 14 │ + > 15 │ throw new Error([]); + │ ^^^^ + 16 │ throw new Error([foo]); + 17 │ throw new Error({}); + + i Providing meaningful error messages leads to more readable and debuggable code. + + +``` + +``` +invalid.js:16:16 lint/nursery/useErrorMessage ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Error message should be a string. + + 15 │ throw new Error([]); + > 16 │ throw new Error([foo]); + │ ^^^^^^^ + 17 │ throw new Error({}); + 18 │ throw new Error({ foo }); + + i Providing meaningful error messages leads to more readable and debuggable code. + + +``` + +``` +invalid.js:17:16 lint/nursery/useErrorMessage ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Error message should be a string. + + 15 │ throw new Error([]); + 16 │ throw new Error([foo]); + > 17 │ throw new Error({}); + │ ^^^^ + 18 │ throw new Error({ foo }); + 19 │ throw new Error(1); + + i Providing meaningful error messages leads to more readable and debuggable code. + + +``` + +``` +invalid.js:18:16 lint/nursery/useErrorMessage ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Error message should be a string. + + 16 │ throw new Error([foo]); + 17 │ throw new Error({}); + > 18 │ throw new Error({ foo }); + │ ^^^^^^^^^ + 19 │ throw new Error(1); + 20 │ throw new Error(undefined); + + i Providing meaningful error messages leads to more readable and debuggable code. + + +``` + +``` +invalid.js:19:16 lint/nursery/useErrorMessage ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Error message should be a string. + + 17 │ throw new Error({}); + 18 │ throw new Error({ foo }); + > 19 │ throw new Error(1); + │ ^^^ + 20 │ throw new Error(undefined); + 21 │ throw new Error(null); + + i Providing meaningful error messages leads to more readable and debuggable code. + + +``` + +``` +invalid.js:21:16 lint/nursery/useErrorMessage ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Error message should be a string. + + 19 │ throw new Error(1); + 20 │ throw new Error(undefined); + > 21 │ throw new Error(null); + │ ^^^^^^ + 22 │ throw new Error(true); + 23 │ + + i Providing meaningful error messages leads to more readable and debuggable code. + + +``` + +``` +invalid.js:22:16 lint/nursery/useErrorMessage ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Error message should be a string. + + 20 │ throw new Error(undefined); + 21 │ throw new Error(null); + > 22 │ throw new Error(true); + │ ^^^^^^ + 23 │ + 24 │ new AggregateError(errors); + + i Providing meaningful error messages leads to more readable and debuggable code. + + +``` + +``` +invalid.js:24:19 lint/nursery/useErrorMessage ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Provide an error message for the error. + + 22 │ throw new Error(true); + 23 │ + > 24 │ new AggregateError(errors); + │ ^^^^^^^^ + 25 │ new AggregateError(errors); + 26 │ new AggregateError(errors, ""); + + i Providing meaningful error messages leads to more readable and debuggable code. + + +``` + +``` +invalid.js:25:19 lint/nursery/useErrorMessage ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Provide an error message for the error. + + 24 │ new AggregateError(errors); + > 25 │ new AggregateError(errors); + │ ^^^^^^^^ + 26 │ new AggregateError(errors, ""); + 27 │ new AggregateError(errors, ``); + + i Providing meaningful error messages leads to more readable and debuggable code. + + +``` + +``` +invalid.js:26:19 lint/nursery/useErrorMessage ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Error message should not be an empty string. + + 24 │ new AggregateError(errors); + 25 │ new AggregateError(errors); + > 26 │ new AggregateError(errors, ""); + │ ^^^^^^^^^^^^ + 27 │ new AggregateError(errors, ``); + 28 │ new AggregateError(errors, "", extraArgument); + + i Providing meaningful error messages leads to more readable and debuggable code. + + +``` + +``` +invalid.js:27:19 lint/nursery/useErrorMessage ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Error message should not be an empty string. + + 25 │ new AggregateError(errors); + 26 │ new AggregateError(errors, ""); + > 27 │ new AggregateError(errors, ``); + │ ^^^^^^^^^^^^ + 28 │ new AggregateError(errors, "", extraArgument); + 29 │ + + i Providing meaningful error messages leads to more readable and debuggable code. + + +``` + +``` +invalid.js:28:19 lint/nursery/useErrorMessage ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Error message should not be an empty string. + + 26 │ new AggregateError(errors, ""); + 27 │ new AggregateError(errors, ``); + > 28 │ new AggregateError(errors, "", extraArgument); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 29 │ + 30 │ new AggregateError(errors, []); + + i Providing meaningful error messages leads to more readable and debuggable code. + + +``` + +``` +invalid.js:30:19 lint/nursery/useErrorMessage ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Error message should be a string. + + 28 │ new AggregateError(errors, "", extraArgument); + 29 │ + > 30 │ new AggregateError(errors, []); + │ ^^^^^^^^^^^^ + 31 │ new AggregateError(errors, [foo]); + 32 │ new AggregateError(errors, [0][0]); + + i Providing meaningful error messages leads to more readable and debuggable code. + + +``` + +``` +invalid.js:31:19 lint/nursery/useErrorMessage ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Error message should be a string. + + 30 │ new AggregateError(errors, []); + > 31 │ new AggregateError(errors, [foo]); + │ ^^^^^^^^^^^^^^^ + 32 │ new AggregateError(errors, [0][0]); + 33 │ new AggregateError(errors, {}); + + i Providing meaningful error messages leads to more readable and debuggable code. + + +``` + +``` +invalid.js:33:19 lint/nursery/useErrorMessage ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Error message should be a string. + + 31 │ new AggregateError(errors, [foo]); + 32 │ new AggregateError(errors, [0][0]); + > 33 │ new AggregateError(errors, {}); + │ ^^^^^^^^^^^^ + 34 │ new AggregateError(errors, { foo }); + 35 │ + + i Providing meaningful error messages leads to more readable and debuggable code. + + +``` + +``` +invalid.js:34:19 lint/nursery/useErrorMessage ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Error message should be a string. + + 32 │ new AggregateError(errors, [0][0]); + 33 │ new AggregateError(errors, {}); + > 34 │ new AggregateError(errors, { foo }); + │ ^^^^^^^^^^^^^^^^^ + 35 │ + + i Providing meaningful error messages leads to more readable and debuggable code. + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useErrorMessage/valid.js b/crates/biome_js_analyze/tests/specs/nursery/useErrorMessage/valid.js new file mode 100644 index 000000000000..87680ed1f040 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useErrorMessage/valid.js @@ -0,0 +1,37 @@ +throw new Error("error"); +throw new TypeError("error"); +throw new MyCustomError("error"); +throw new MyCustomError(); +throw generateError(); +throw foo(); +throw err; +throw 1; +const err = TypeError("error"); +throw err; +// Should not check other argument +new Error("message", 0, 0); +// We don't know the value +new Error(foo); +new Error(...foo); + +new AggregateError(errors, "message"); +new NotAggregateError(errors); +new AggregateError(...foo); +new AggregateError(...foo, ""); +new AggregateError(errors, ...foo); +new AggregateError(errors, message, ""); +new AggregateError("", message, ""); + +// Invalid but we don't know the value +const errorMessage1 = Object.freeze({ errorMessage: 1 }).errorMessage; +throw new Error(errorMessage); +throw new Error((lineNumber = 2)); +throw new Error({ foo: 0 }.foo); + +{ + // Do not fail if Error is shadowed + const Error = function () {}; + const err1 = new Error({ + name: "Unauthorized", + }); +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/useErrorMessage/valid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/useErrorMessage/valid.js.snap new file mode 100644 index 000000000000..647e2abe51bb --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useErrorMessage/valid.js.snap @@ -0,0 +1,45 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: valid.js +--- +# Input +```jsx +throw new Error("error"); +throw new TypeError("error"); +throw new MyCustomError("error"); +throw new MyCustomError(); +throw generateError(); +throw foo(); +throw err; +throw 1; +const err = TypeError("error"); +throw err; +// Should not check other argument +new Error("message", 0, 0); +// We don't know the value +new Error(foo); +new Error(...foo); + +new AggregateError(errors, "message"); +new NotAggregateError(errors); +new AggregateError(...foo); +new AggregateError(...foo, ""); +new AggregateError(errors, ...foo); +new AggregateError(errors, message, ""); +new AggregateError("", message, ""); + +// Invalid but we don't know the value +const errorMessage1 = Object.freeze({ errorMessage: 1 }).errorMessage; +throw new Error(errorMessage); +throw new Error((lineNumber = 2)); +throw new Error({ foo: 0 }.foo); + +{ + // Do not fail if Error is shadowed + const Error = function () {}; + const err1 = new Error({ + name: "Unauthorized", + }); +} + +``` diff --git a/crates/biome_js_syntax/src/expr_ext.rs b/crates/biome_js_syntax/src/expr_ext.rs index c5e1613eba09..4d0425ea4a4f 100644 --- a/crates/biome_js_syntax/src/expr_ext.rs +++ b/crates/biome_js_syntax/src/expr_ext.rs @@ -29,6 +29,22 @@ declare_node_union! { pub JsNewOrCallExpression = JsNewExpression | JsCallExpression } +impl JsNewOrCallExpression { + pub fn callee(&self) -> SyntaxResult { + match self { + JsNewOrCallExpression::JsNewExpression(node) => node.callee(), + JsNewOrCallExpression::JsCallExpression(node) => node.callee(), + } + } + + pub fn arguments(&self) -> Option { + match self { + JsNewOrCallExpression::JsNewExpression(node) => node.arguments(), + JsNewOrCallExpression::JsCallExpression(node) => node.arguments().ok(), + } + } +} + impl JsReferenceIdentifier { /// Returns `true` if this identifier refers to the `undefined` symbol. /// diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 4629d9e27239..1f045cc55454 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -1081,6 +1081,10 @@ export interface Nursery { * Require the default clause in switch statements. */ useDefaultSwitchClause?: RuleConfiguration_for_Null; + /** + * Enforce passing a message value when creating a built-in error. + */ + useErrorMessage?: RuleConfiguration_for_Null; /** * Enforce explicitly comparing the length, size, byteLength or byteOffset property of a value. */ @@ -2271,17 +2275,16 @@ export type Category = | "lint/correctness/useValidForDirection" | "lint/correctness/useYield" | "lint/nursery/colorNoInvalidHex" - | "lint/nursery/useAdjacentOverloadSignatures" | "lint/nursery/noColorInvalidHex" | "lint/nursery/noConsole" | "lint/nursery/noConstantMathMinMaxClamp" - | "lint/nursery/noEmptyBlock" | "lint/nursery/noDoneCallback" | "lint/nursery/noDuplicateAtImportRules" | "lint/nursery/noDuplicateElseIf" | "lint/nursery/noDuplicateFontNames" | "lint/nursery/noDuplicateJsonKeys" | "lint/nursery/noDuplicateSelectorsKeyframeBlock" + | "lint/nursery/noEmptyBlock" | "lint/nursery/noEvolvingAny" | "lint/nursery/noFlatMapIdentity" | "lint/nursery/noImportantInKeyframe" @@ -2302,10 +2305,12 @@ export type Category = | "lint/nursery/noUselessStringConcat" | "lint/nursery/noUselessUndefinedInitialization" | "lint/nursery/noYodaExpression" + | "lint/nursery/useAdjacentOverloadSignatures" | "lint/nursery/useArrayLiterals" | "lint/nursery/useBiomeSuppressionComment" | "lint/nursery/useConsistentBuiltinInstantiation" | "lint/nursery/useDefaultSwitchClause" + | "lint/nursery/useErrorMessage" | "lint/nursery/useExplicitLengthCheck" | "lint/nursery/useFocusableInteractive" | "lint/nursery/useGenericFontNames" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 76f24bb9c9e4..9134d738d172 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -1828,6 +1828,13 @@ { "type": "null" } ] }, + "useErrorMessage": { + "description": "Enforce passing a message value when creating a built-in error.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "useExplicitLengthCheck": { "description": "Enforce explicitly comparing the length, size, byteLength or byteOffset property of a value.", "anyOf": [