diff --git a/crates/oxc_linter/src/generated/rule_runner_impls.rs b/crates/oxc_linter/src/generated/rule_runner_impls.rs index 177cdf1aae391..f1b7ae230d6d5 100644 --- a/crates/oxc_linter/src/generated/rule_runner_impls.rs +++ b/crates/oxc_linter/src/generated/rule_runner_impls.rs @@ -2736,6 +2736,12 @@ impl RuleRunner for crate::rules::unicorn::consistent_function_scoping::Consiste const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run; } +impl RuleRunner for crate::rules::unicorn::custom_error_definition::CustomErrorDefinition { + const NODE_TYPES: Option<&AstTypesBitset> = + Some(&AstTypesBitset::from_types(&[AstType::Class])); + const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run; +} + impl RuleRunner for crate::rules::unicorn::empty_brace_spaces::EmptyBraceSpaces { const NODE_TYPES: Option<&AstTypesBitset> = Some(&AstTypesBitset::from_types(&[ AstType::BlockStatement, diff --git a/crates/oxc_linter/src/generated/rules_enum.rs b/crates/oxc_linter/src/generated/rules_enum.rs index 3119531d64a27..e6e562e7f1db8 100644 --- a/crates/oxc_linter/src/generated/rules_enum.rs +++ b/crates/oxc_linter/src/generated/rules_enum.rs @@ -555,6 +555,7 @@ pub use crate::rules::unicorn::consistent_date_clone::ConsistentDateClone as Uni pub use crate::rules::unicorn::consistent_empty_array_spread::ConsistentEmptyArraySpread as UnicornConsistentEmptyArraySpread; pub use crate::rules::unicorn::consistent_existence_index_check::ConsistentExistenceIndexCheck as UnicornConsistentExistenceIndexCheck; pub use crate::rules::unicorn::consistent_function_scoping::ConsistentFunctionScoping as UnicornConsistentFunctionScoping; +pub use crate::rules::unicorn::custom_error_definition::CustomErrorDefinition as UnicornCustomErrorDefinition; pub use crate::rules::unicorn::empty_brace_spaces::EmptyBraceSpaces as UnicornEmptyBraceSpaces; pub use crate::rules::unicorn::error_message::ErrorMessage as UnicornErrorMessage; pub use crate::rules::unicorn::escape_case::EscapeCase as UnicornEscapeCase; @@ -1147,6 +1148,7 @@ pub enum RuleEnum { UnicornConsistentEmptyArraySpread(UnicornConsistentEmptyArraySpread), UnicornConsistentExistenceIndexCheck(UnicornConsistentExistenceIndexCheck), UnicornConsistentFunctionScoping(UnicornConsistentFunctionScoping), + UnicornCustomErrorDefinition(UnicornCustomErrorDefinition), UnicornEmptyBraceSpaces(UnicornEmptyBraceSpaces), UnicornErrorMessage(UnicornErrorMessage), UnicornEscapeCase(UnicornEscapeCase), @@ -1900,7 +1902,8 @@ const UNICORN_CONSISTENT_EXISTENCE_INDEX_CHECK_ID: usize = UNICORN_CONSISTENT_EMPTY_ARRAY_SPREAD_ID + 1usize; const UNICORN_CONSISTENT_FUNCTION_SCOPING_ID: usize = UNICORN_CONSISTENT_EXISTENCE_INDEX_CHECK_ID + 1usize; -const UNICORN_EMPTY_BRACE_SPACES_ID: usize = UNICORN_CONSISTENT_FUNCTION_SCOPING_ID + 1usize; +const UNICORN_CUSTOM_ERROR_DEFINITION_ID: usize = UNICORN_CONSISTENT_FUNCTION_SCOPING_ID + 1usize; +const UNICORN_EMPTY_BRACE_SPACES_ID: usize = UNICORN_CUSTOM_ERROR_DEFINITION_ID + 1usize; const UNICORN_ERROR_MESSAGE_ID: usize = UNICORN_EMPTY_BRACE_SPACES_ID + 1usize; const UNICORN_ESCAPE_CASE_ID: usize = UNICORN_ERROR_MESSAGE_ID + 1usize; const UNICORN_EXPLICIT_LENGTH_CHECK_ID: usize = UNICORN_ESCAPE_CASE_ID + 1usize; @@ -2710,6 +2713,7 @@ impl RuleEnum { UNICORN_CONSISTENT_EXISTENCE_INDEX_CHECK_ID } Self::UnicornConsistentFunctionScoping(_) => UNICORN_CONSISTENT_FUNCTION_SCOPING_ID, + Self::UnicornCustomErrorDefinition(_) => UNICORN_CUSTOM_ERROR_DEFINITION_ID, Self::UnicornEmptyBraceSpaces(_) => UNICORN_EMPTY_BRACE_SPACES_ID, Self::UnicornErrorMessage(_) => UNICORN_ERROR_MESSAGE_ID, Self::UnicornEscapeCase(_) => UNICORN_ESCAPE_CASE_ID, @@ -3515,6 +3519,7 @@ impl RuleEnum { UnicornConsistentExistenceIndexCheck::NAME } Self::UnicornConsistentFunctionScoping(_) => UnicornConsistentFunctionScoping::NAME, + Self::UnicornCustomErrorDefinition(_) => UnicornCustomErrorDefinition::NAME, Self::UnicornEmptyBraceSpaces(_) => UnicornEmptyBraceSpaces::NAME, Self::UnicornErrorMessage(_) => UnicornErrorMessage::NAME, Self::UnicornEscapeCase(_) => UnicornEscapeCase::NAME, @@ -4342,6 +4347,7 @@ impl RuleEnum { UnicornConsistentExistenceIndexCheck::CATEGORY } Self::UnicornConsistentFunctionScoping(_) => UnicornConsistentFunctionScoping::CATEGORY, + Self::UnicornCustomErrorDefinition(_) => UnicornCustomErrorDefinition::CATEGORY, Self::UnicornEmptyBraceSpaces(_) => UnicornEmptyBraceSpaces::CATEGORY, Self::UnicornErrorMessage(_) => UnicornErrorMessage::CATEGORY, Self::UnicornEscapeCase(_) => UnicornEscapeCase::CATEGORY, @@ -5160,6 +5166,7 @@ impl RuleEnum { UnicornConsistentExistenceIndexCheck::FIX } Self::UnicornConsistentFunctionScoping(_) => UnicornConsistentFunctionScoping::FIX, + Self::UnicornCustomErrorDefinition(_) => UnicornCustomErrorDefinition::FIX, Self::UnicornEmptyBraceSpaces(_) => UnicornEmptyBraceSpaces::FIX, Self::UnicornErrorMessage(_) => UnicornErrorMessage::FIX, Self::UnicornEscapeCase(_) => UnicornEscapeCase::FIX, @@ -6068,6 +6075,7 @@ impl RuleEnum { Self::UnicornConsistentFunctionScoping(_) => { UnicornConsistentFunctionScoping::documentation() } + Self::UnicornCustomErrorDefinition(_) => UnicornCustomErrorDefinition::documentation(), Self::UnicornEmptyBraceSpaces(_) => UnicornEmptyBraceSpaces::documentation(), Self::UnicornErrorMessage(_) => UnicornErrorMessage::documentation(), Self::UnicornEscapeCase(_) => UnicornEscapeCase::documentation(), @@ -7665,6 +7673,10 @@ impl RuleEnum { UnicornConsistentFunctionScoping::config_schema(generator) .or_else(|| UnicornConsistentFunctionScoping::schema(generator)) } + Self::UnicornCustomErrorDefinition(_) => { + UnicornCustomErrorDefinition::config_schema(generator) + .or_else(|| UnicornCustomErrorDefinition::schema(generator)) + } Self::UnicornEmptyBraceSpaces(_) => UnicornEmptyBraceSpaces::config_schema(generator) .or_else(|| UnicornEmptyBraceSpaces::schema(generator)), Self::UnicornErrorMessage(_) => UnicornErrorMessage::config_schema(generator) @@ -8874,6 +8886,7 @@ impl RuleEnum { Self::UnicornConsistentEmptyArraySpread(_) => "unicorn", Self::UnicornConsistentExistenceIndexCheck(_) => "unicorn", Self::UnicornConsistentFunctionScoping(_) => "unicorn", + Self::UnicornCustomErrorDefinition(_) => "unicorn", Self::UnicornEmptyBraceSpaces(_) => "unicorn", Self::UnicornErrorMessage(_) => "unicorn", Self::UnicornEscapeCase(_) => "unicorn", @@ -10532,6 +10545,9 @@ impl RuleEnum { UnicornConsistentFunctionScoping::from_configuration(value)?, )) } + Self::UnicornCustomErrorDefinition(_) => Ok(Self::UnicornCustomErrorDefinition( + UnicornCustomErrorDefinition::from_configuration(value)?, + )), Self::UnicornEmptyBraceSpaces(_) => Ok(Self::UnicornEmptyBraceSpaces( UnicornEmptyBraceSpaces::from_configuration(value)?, )), @@ -11827,6 +11843,7 @@ impl RuleEnum { Self::UnicornConsistentEmptyArraySpread(rule) => rule.to_configuration(), Self::UnicornConsistentExistenceIndexCheck(rule) => rule.to_configuration(), Self::UnicornConsistentFunctionScoping(rule) => rule.to_configuration(), + Self::UnicornCustomErrorDefinition(rule) => rule.to_configuration(), Self::UnicornEmptyBraceSpaces(rule) => rule.to_configuration(), Self::UnicornErrorMessage(rule) => rule.to_configuration(), Self::UnicornEscapeCase(rule) => rule.to_configuration(), @@ -12532,6 +12549,7 @@ impl RuleEnum { Self::UnicornConsistentEmptyArraySpread(rule) => rule.run(node, ctx), Self::UnicornConsistentExistenceIndexCheck(rule) => rule.run(node, ctx), Self::UnicornConsistentFunctionScoping(rule) => rule.run(node, ctx), + Self::UnicornCustomErrorDefinition(rule) => rule.run(node, ctx), Self::UnicornEmptyBraceSpaces(rule) => rule.run(node, ctx), Self::UnicornErrorMessage(rule) => rule.run(node, ctx), Self::UnicornEscapeCase(rule) => rule.run(node, ctx), @@ -13235,6 +13253,7 @@ impl RuleEnum { Self::UnicornConsistentEmptyArraySpread(rule) => rule.run_once(ctx), Self::UnicornConsistentExistenceIndexCheck(rule) => rule.run_once(ctx), Self::UnicornConsistentFunctionScoping(rule) => rule.run_once(ctx), + Self::UnicornCustomErrorDefinition(rule) => rule.run_once(ctx), Self::UnicornEmptyBraceSpaces(rule) => rule.run_once(ctx), Self::UnicornErrorMessage(rule) => rule.run_once(ctx), Self::UnicornEscapeCase(rule) => rule.run_once(ctx), @@ -14012,6 +14031,7 @@ impl RuleEnum { rule.run_on_jest_node(jest_node, ctx) } Self::UnicornConsistentFunctionScoping(rule) => rule.run_on_jest_node(jest_node, ctx), + Self::UnicornCustomErrorDefinition(rule) => rule.run_on_jest_node(jest_node, ctx), Self::UnicornEmptyBraceSpaces(rule) => rule.run_on_jest_node(jest_node, ctx), Self::UnicornErrorMessage(rule) => rule.run_on_jest_node(jest_node, ctx), Self::UnicornEscapeCase(rule) => rule.run_on_jest_node(jest_node, ctx), @@ -14741,6 +14761,7 @@ impl RuleEnum { Self::UnicornConsistentEmptyArraySpread(rule) => rule.should_run(ctx), Self::UnicornConsistentExistenceIndexCheck(rule) => rule.should_run(ctx), Self::UnicornConsistentFunctionScoping(rule) => rule.should_run(ctx), + Self::UnicornCustomErrorDefinition(rule) => rule.should_run(ctx), Self::UnicornEmptyBraceSpaces(rule) => rule.should_run(ctx), Self::UnicornErrorMessage(rule) => rule.should_run(ctx), Self::UnicornEscapeCase(rule) => rule.should_run(ctx), @@ -15622,6 +15643,7 @@ impl RuleEnum { Self::UnicornConsistentFunctionScoping(_) => { UnicornConsistentFunctionScoping::IS_TSGOLINT_RULE } + Self::UnicornCustomErrorDefinition(_) => UnicornCustomErrorDefinition::IS_TSGOLINT_RULE, Self::UnicornEmptyBraceSpaces(_) => UnicornEmptyBraceSpaces::IS_TSGOLINT_RULE, Self::UnicornErrorMessage(_) => UnicornErrorMessage::IS_TSGOLINT_RULE, Self::UnicornEscapeCase(_) => UnicornEscapeCase::IS_TSGOLINT_RULE, @@ -16556,6 +16578,7 @@ impl RuleEnum { Self::UnicornConsistentFunctionScoping(_) => { UnicornConsistentFunctionScoping::HAS_CONFIG } + Self::UnicornCustomErrorDefinition(_) => UnicornCustomErrorDefinition::HAS_CONFIG, Self::UnicornEmptyBraceSpaces(_) => UnicornEmptyBraceSpaces::HAS_CONFIG, Self::UnicornErrorMessage(_) => UnicornErrorMessage::HAS_CONFIG, Self::UnicornEscapeCase(_) => UnicornEscapeCase::HAS_CONFIG, @@ -17313,6 +17336,7 @@ impl RuleEnum { Self::UnicornConsistentEmptyArraySpread(rule) => rule.types_info(), Self::UnicornConsistentExistenceIndexCheck(rule) => rule.types_info(), Self::UnicornConsistentFunctionScoping(rule) => rule.types_info(), + Self::UnicornCustomErrorDefinition(rule) => rule.types_info(), Self::UnicornEmptyBraceSpaces(rule) => rule.types_info(), Self::UnicornErrorMessage(rule) => rule.types_info(), Self::UnicornEscapeCase(rule) => rule.types_info(), @@ -18016,6 +18040,7 @@ impl RuleEnum { Self::UnicornConsistentEmptyArraySpread(rule) => rule.run_info(), Self::UnicornConsistentExistenceIndexCheck(rule) => rule.run_info(), Self::UnicornConsistentFunctionScoping(rule) => rule.run_info(), + Self::UnicornCustomErrorDefinition(rule) => rule.run_info(), Self::UnicornEmptyBraceSpaces(rule) => rule.run_info(), Self::UnicornErrorMessage(rule) => rule.run_info(), Self::UnicornEscapeCase(rule) => rule.run_info(), @@ -18811,6 +18836,7 @@ pub static RULES: std::sync::LazyLock> = std::sync::LazyLock::new( UnicornConsistentExistenceIndexCheck::default(), ), RuleEnum::UnicornConsistentFunctionScoping(UnicornConsistentFunctionScoping::default()), + RuleEnum::UnicornCustomErrorDefinition(UnicornCustomErrorDefinition::default()), RuleEnum::UnicornEmptyBraceSpaces(UnicornEmptyBraceSpaces::default()), RuleEnum::UnicornErrorMessage(UnicornErrorMessage::default()), RuleEnum::UnicornEscapeCase(UnicornEscapeCase::default()), diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 0299d8631cda9..07cbccadf226d 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -452,6 +452,7 @@ pub(crate) mod unicorn { pub mod consistent_empty_array_spread; pub mod consistent_existence_index_check; pub mod consistent_function_scoping; + pub mod custom_error_definition; pub mod empty_brace_spaces; pub mod error_message; pub mod escape_case; diff --git a/crates/oxc_linter/src/rules/unicorn/custom_error_definition.rs b/crates/oxc_linter/src/rules/unicorn/custom_error_definition.rs new file mode 100644 index 0000000000000..5c6056d99d624 --- /dev/null +++ b/crates/oxc_linter/src/rules/unicorn/custom_error_definition.rs @@ -0,0 +1,675 @@ +use oxc_ast::{ + AstKind, + ast::{ + AssignmentTarget, Class, ClassElement, Expression, MethodDefinitionKind, + PropertyDefinition, Statement, + }, + match_member_expression, +}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::{GetSpan, Span}; + +use crate::{AstNode, ast_util, context::LintContext, rule::Rule}; + +fn invalid_class_name_diagnostic(span: Span, expected: &str) -> OxcDiagnostic { + OxcDiagnostic::warn(format!("Invalid class name, use `{expected}`.")).with_label(span) +} + +fn missing_super_call_diagnostic(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("Missing call to `super()` in constructor.").with_label(span) +} + +fn invalid_name_property_diagnostic(span: Span, name: &str) -> OxcDiagnostic { + OxcDiagnostic::warn(format!("The `name` property should be set to `{name}`.")).with_label(span) +} + +fn pass_message_to_super_diagnostic(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("Pass the error message to `super()` instead of setting `this.message`.") + .with_label(span) +} + +fn invalid_export_diagnostic(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("Exported error name should match error class").with_label(span) +} + +#[derive(Debug, Default, Clone)] +pub struct CustomErrorDefinition; + +declare_oxc_lint!( + /// ### What it does + /// + /// Enforces the only valid way of Error subclassing. It works with any super class that ends in Error. + /// + /// ### Why is this bad? + /// + /// Incorrectly defined custom errors can lead to unexpected behavior when + /// catching and identifying errors. Missing `super()` calls, wrong `name` + /// property values, or non-standard class names make error handling unreliable. + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```js + /// class CustomError extends Error { + /// constructor(message) { + /// super(message); + /// // The `this.message` assignment is useless as it's already set via the `super()` call. + /// this.message = message; + /// this.name = 'CustomError'; + /// } + /// } + /// + /// class CustomError extends Error { + /// constructor(message) { + /// super(); + /// // Pass the error message to `super()` instead of setting `this.message`. + /// this.message = message; + /// this.name = 'CustomError'; + /// } + /// } + /// + /// class CustomError extends Error { + /// constructor(message) { + /// super(message); + /// // No `name` property set. The name property is needed so the + /// // error shows up as `[CustomError: foo]` and not `[Error: foo]`. + /// } + /// } + /// + /// class CustomError extends Error { + /// constructor(message) { + /// super(message); + /// // Use a string literal to set the `name` property as it will not change after minifying. + /// this.name = this.constructor.name; + /// } + /// } + /// + /// class CustomError extends Error { + /// constructor(message) { + /// super(message); + /// // The `name` property should be set to the class name. + /// this.name = 'MyError'; + /// } + /// } + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```js + /// class CustomError extends Error { + /// constructor(message) { + /// super(message); + /// this.name = 'CustomError'; + /// } + /// } + /// + /// class CustomError extends Error { + /// constructor() { + /// super('My custom error'); + /// this.name = 'CustomError'; + /// } + /// } + /// + /// class CustomError extends TypeError { + /// constructor() { + /// super(); + /// this.name = 'CustomError'; + /// } + /// } + /// + /// class CustomError extends Error { + /// name = 'CustomError'; + /// } + /// ``` + CustomErrorDefinition, + unicorn, + style, + pending, +); + +impl Rule for CustomErrorDefinition { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + if let AstKind::Class(class) = node.kind() { + check_class(class, node, ctx); + } + } +} + +fn check_export_assignment( + class: &Class, + exports_property_span: Span, + exports_property_name: &str, + ctx: &LintContext, +) { + if !has_valid_super_class(class) { + return; + } + + let Some(id) = &class.id else { + return; + }; + + let error_name = id.name.as_str(); + if exports_property_name != error_name { + ctx.diagnostic(invalid_export_diagnostic(exports_property_span)); + } +} + +fn check_class<'a>(class: &Class<'a>, node: &AstNode<'a>, ctx: &LintContext<'a>) { + if !has_valid_super_class(class) { + return; + } + + if let Some((exports_property_span, exports_property_name)) = + get_export_assignment_info(node, ctx) + { + check_export_assignment(class, exports_property_span, exports_property_name, ctx); + } + + let Some(id) = &class.id else { + return; + }; + + let name = id.name.as_str(); + let expected_class_name = get_class_name(name); + + if name != expected_class_name { + ctx.diagnostic(invalid_class_name_diagnostic(id.span, &expected_class_name)); + } + + let constructor = class.body.body.iter().find_map(|el| match el { + ClassElement::MethodDefinition(method) + if method.kind == MethodDefinitionKind::Constructor && method.value.body.is_some() => + { + Some(method) + } + _ => None, + }); + let has_constructor_signature = class.body.body.iter().any(|el| { + matches!(el, ClassElement::MethodDefinition(method) if method.kind == MethodDefinitionKind::Constructor) + }); + + let name_property = class.body.body.iter().find_map(|el| { + if let ClassElement::PropertyDefinition(prop) = el + && is_name_property_definition(prop) + { + Some(prop.as_ref()) + } else { + None + } + }); + + let Some(constructor_method) = constructor else { + if has_constructor_signature { + return; + } + + if is_valid_name_property(name_property, name) { + return; + } + + if let Some(prop) = name_property { + let span = prop.value.as_ref().map_or(prop.span, oxc_span::GetSpan::span); + ctx.diagnostic(invalid_name_property_diagnostic(span, name)); + } else { + ctx.diagnostic(invalid_name_property_diagnostic(class.span, name)); + } + return; + }; + + let Some(body) = &constructor_method.value.body else { + return; + }; + + let statements = &body.statements; + + let has_super = statements.iter().any(|s| is_super_call(s)); + + let message_assignment_idx = statements.iter().position(|s| is_this_assignment(s, "message")); + + if !has_super { + ctx.diagnostic(missing_super_call_diagnostic(body.span)); + } else if message_assignment_idx.is_some() { + let Some(super_stmt) = statements.iter().find(|s| is_super_call(s)) else { + return; + }; + + let Statement::ExpressionStatement(super_expr_stmt) = super_stmt else { + return; + }; + + ctx.diagnostic(pass_message_to_super_diagnostic(super_expr_stmt.span)); + } + + let name_assignment = statements.iter().find(|s| is_this_assignment(s, "name")); + + let invalid_name_span = if let Some(name_stmt) = name_assignment { + let Statement::ExpressionStatement(expr_stmt) = name_stmt else { + return; + }; + let Expression::AssignmentExpression(assign) = &expr_stmt.expression else { + return; + }; + + (!is_expected_string_literal(&assign.right, name)).then_some(assign.right.span()) + } else if let Some(prop) = name_property { + let span = prop.value.as_ref().map_or(prop.span, oxc_span::GetSpan::span); + match &prop.value { + Some(value) => (!is_expected_string_literal(value, name)).then_some(span), + None => Some(span), + } + } else { + Some(body.span) + }; + + if let Some(span) = invalid_name_span { + ctx.diagnostic(invalid_name_property_diagnostic(span, name)); + } +} + +fn get_export_assignment_info<'a>( + node: &AstNode<'a>, + ctx: &LintContext<'a>, +) -> Option<(Span, &'a str)> { + let AstKind::AssignmentExpression(assign) = + ast_util::iter_outer_expressions(ctx.nodes(), node.id()).next()? + else { + return None; + }; + + let AssignmentTarget::StaticMemberExpression(member) = &assign.left else { + return None; + }; + + let Expression::Identifier(obj_ident) = &member.object else { + return None; + }; + + (obj_ident.name.as_str() == "exports") + .then_some((member.property.span, member.property.name.as_str())) +} + +fn get_class_name(name: &str) -> String { + let uppered = upper_first(name); + + if let Some(stripped) = strip_suffix_case_insensitive(&uppered, "error") { + format!("{stripped}Error") + } else { + format!("{uppered}Error") + } +} + +fn strip_suffix_case_insensitive<'a>(value: &'a str, suffix: &str) -> Option<&'a str> { + let start = value.len().checked_sub(suffix.len())?; + value[start..].eq_ignore_ascii_case(suffix).then_some(&value[..start]) +} + +fn is_name_property_definition(prop: &PropertyDefinition) -> bool { + !prop.r#static && !prop.computed && prop.key.is_specific_static_name("name") +} + +fn is_valid_super_class_name(name: &str) -> bool { + let Some(prefix) = name.strip_suffix("Error") else { + return false; + }; + + let bytes = prefix.as_bytes(); + let mut index = 0; + + while index < bytes.len() { + if !bytes[index].is_ascii_uppercase() { + return false; + } + + index += 1; + + while index < bytes.len() + && (bytes[index].is_ascii_lowercase() || bytes[index].is_ascii_digit()) + { + index += 1; + } + } + + true +} + +fn has_valid_super_class(class: &Class) -> bool { + let Some(super_class) = &class.super_class else { + return false; + }; + let name = match super_class.get_inner_expression() { + Expression::Identifier(ident) => Some(ident.name.as_str()), + e @ match_member_expression!(Expression) => e.to_member_expression().static_property_name(), + _ => None, + }; + name.is_some_and(is_valid_super_class_name) +} + +fn upper_first(s: &str) -> String { + let mut chars = s.chars(); + match chars.next() { + None => String::new(), + Some(first) => first.to_uppercase().chain(chars).collect(), + } +} + +fn is_super_call(stmt: &Statement) -> bool { + let Statement::ExpressionStatement(expr_stmt) = stmt else { return false }; + let Expression::CallExpression(call) = &expr_stmt.expression else { return false }; + matches!(&call.callee, Expression::Super(_)) +} + +fn is_this_assignment(stmt: &Statement, prop_name: &str) -> bool { + let Statement::ExpressionStatement(expr_stmt) = stmt else { return false }; + let Expression::AssignmentExpression(assign) = &expr_stmt.expression else { return false }; + + if let AssignmentTarget::StaticMemberExpression(member) = &assign.left { + matches!(&member.object, Expression::ThisExpression(_)) + && member.property.name.as_str() == prop_name + } else { + false + } +} + +fn is_valid_name_property(name_property: Option<&PropertyDefinition>, class_name: &str) -> bool { + if let Some(prop) = name_property + && let Some(Expression::StringLiteral(lit)) = &prop.value + { + return lit.value.as_str() == class_name; + } + + false +} + +fn is_expected_string_literal(expr: &Expression, expected: &str) -> bool { + matches!(expr, Expression::StringLiteral(lit) if lit.value.as_str() == expected) +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + "class Foo { }", + "class Foo extends Bar { }", + "class Foo extends Bar() { }", + "const Foo = class { }", + r"const FooError = class extends Error { + constructor(message) { + super(message); + this.name = 'FooError'; + } + }", + r"class FooError extends Http.ProtocolError { + constructor(message) { + super(message); + this.name = 'FooError'; + } + }", + r"class FooError extends Error { + constructor(message) { + super(message); + this.name = 'FooError'; + } + }", + r"class FooError extends Error { + constructor() { + super('My super awesome Foo Error'); + this.name = 'FooError'; + } + }", + r"class FooError extends TypeError { + constructor() { + super(); + this.name = 'FooError'; + } + }", + r"class FooError extends Error { + name = 'FooError'; + }", + r"export class FooError extends TypeError { + constructor() { + super(); + this.name = 'FooError'; + } + };", + r"export default class FooError extends TypeError { + constructor() { + super(); + this.name = 'FooError'; + } + };", + r"module.exports = class FooError extends TypeError { + constructor() { + super(); + this.name = 'FooError'; + } + };", + r"exports.FooError = class FooError extends TypeError { + constructor() { + super(); + this.name = 'FooError'; + } + };", + r"exports.FooError = (class FooError extends TypeError { + constructor() { + super(); + this.name = 'FooError'; + } + });", + r"exports.FooError = class extends Error { + constructor(error) { + super(error); + } + };", + r"exports.fooError = class extends Error { + constructor(error) { + super(error); + this.name = 'fooError'; + } + };", + "exports.whatever = class Whatever {};", + r"class FooError extends Error { + constructor(error) { + super(error); + this.name = 'FooError'; + } + }; + exports.fooError = FooError;", + r"class FooError extends Error { + constructor() { + super(); + this.name = 'FooError'; + someThingNotThis.message = 'My custom message'; + } + }", + r"export class ValidationError extends Error { + name = 'ValidationError'; + constructor(message) { + super(message); + } + }", + r"class CustomError extends Error { + constructor(type: string, text: string, reply?: any); + }", + ]; + + let fail = vec![ + r"class FooError extends Error {}", + r"class FooError extends Error { + name = 'BadError'; + }", + r"class FooError extends Error { + static name = 'FooError'; + }", + r"class fooError extends Error { + constructor(message) { + super(message); + this.name = 'FooError'; + } + }", + r"class fooError extends Error { + constructor(message) { + super(message); + this.name = 'fooError'; + } + }", + r"class Foo extends Error { + constructor(message) { + super(message); + this.name = 'Foo'; + } + }", + r"class FooERROR extends Error { + constructor(message) { + super(message); + this.name = 'FooERROR'; + } + }", + r"class fooerror extends Error { + constructor(message) { + super(message); + this.name = 'fooerror'; + } + }", + r"class FooError extends Error { + constructor() { } + }", + r"class FooError extends Error { + constructor() { + super(); + this.message = 'My custom message'; + } + }", + r"class FooError extends Error { + constructor() { + super(); + } + }", + r"class FooError extends Error { + constructor(message: string); + constructor(message: string) { + this.name = 'FooError'; + } + }", + r"class FooError extends Error { + constructor() { + super('My awesome Foo Error'); + this.name = this.constructor.name; + } + }", + r"class FooError extends Error { + constructor(message) { + super(message); + this.message = message; + this.name = 'FooError'; + } + }", + r"class FooError extends Error { + constructor(message) { + super(); + this.message = message; + this.name = 'FooError'; + } + }", + r"class FooError extends Error { + constructor(message) { + super(); + this.message = message; + } + }", + r"class FooError extends Error { + constructor(message) { + super(); + this.name = 'FooError'; + this.message = message; + } + }", + r"class FooError extends Http.ProtocolError { + constructor() { + super(); + this.name = 'foo'; + } + }", + r"module.exports = class FooError extends TypeError { + constructor() { + super(); + this.name = 'foo'; + } + };", + r"exports.fooError = class FooError extends Error { + constructor(error) { + super(error); + this.name = 'FooError'; + } + };", + r"exports.fooError = (class FooError extends Error { + constructor(error) { + super(error); + this.name = 'FooError'; + } + });", + r"exports.FooError = class FooError extends TypeError { + constructor() { + super(); + this.name = 'foo'; + } + };", + r"export class FooError extends TypeError { + constructor() { + super(); + this.name = 'foo'; + } + };", + r"export default class FooError extends TypeError { + constructor() { + super(); + this.name = 'foo'; + } + };", + r"class AbortError extends Error { + constructor(message) { + if (message instanceof Error) { + this.originalError = message; + message = message.message; + } + + super(); + this.name = 'AbortError'; + this.message = message; + } + }", + r"class FooError extends Error { + constructor() { + super(); + this.message = foo.error; + this.name = 'FooError'; + } + }", + r"export class ValidationError extends Error { + name = 'FOO'; + constructor(message) { + super(message); + } + }", + r"const name = 'computed-name'; + class FooError extends Error { + [name] = 'FooError'; + constructor(message) { + super(message); + } + }", + r"export class ValidationError extends Error { + 'name': SomeType; + constructor(message) { + super(message); + } + }", + r"class FooError extends Error { + name: string; + }", + ]; + + Tester::new(CustomErrorDefinition::NAME, CustomErrorDefinition::PLUGIN, pass, fail) + .test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/unicorn_custom_error_definition.snap b/crates/oxc_linter/src/snapshots/unicorn_custom_error_definition.snap new file mode 100644 index 0000000000000..c0655b13bcf0f --- /dev/null +++ b/crates/oxc_linter/src/snapshots/unicorn_custom_error_definition.snap @@ -0,0 +1,272 @@ +--- +source: crates/oxc_linter/src/tester.rs +--- + + ⚠ eslint-plugin-unicorn(custom-error-definition): The `name` property should be set to `FooError`. + ╭─[custom_error_definition.tsx:1:1] + 1 │ class FooError extends Error {} + · ─────────────────────────────── + ╰──── + + ⚠ eslint-plugin-unicorn(custom-error-definition): The `name` property should be set to `FooError`. + ╭─[custom_error_definition.tsx:2:20] + 1 │ class FooError extends Error { + 2 │ name = 'BadError'; + · ────────── + 3 │ } + ╰──── + + ⚠ eslint-plugin-unicorn(custom-error-definition): The `name` property should be set to `FooError`. + ╭─[custom_error_definition.tsx:1:1] + 1 │ ╭─▶ class FooError extends Error { + 2 │ │ static name = 'FooError'; + 3 │ ╰─▶ } + ╰──── + + ⚠ eslint-plugin-unicorn(custom-error-definition): Invalid class name, use `FooError`. + ╭─[custom_error_definition.tsx:1:7] + 1 │ class fooError extends Error { + · ──────── + 2 │ constructor(message) { + ╰──── + + ⚠ eslint-plugin-unicorn(custom-error-definition): The `name` property should be set to `fooError`. + ╭─[custom_error_definition.tsx:4:29] + 3 │ super(message); + 4 │ this.name = 'FooError'; + · ────────── + 5 │ } + ╰──── + + ⚠ eslint-plugin-unicorn(custom-error-definition): Invalid class name, use `FooError`. + ╭─[custom_error_definition.tsx:1:7] + 1 │ class fooError extends Error { + · ──────── + 2 │ constructor(message) { + ╰──── + + ⚠ eslint-plugin-unicorn(custom-error-definition): Invalid class name, use `FooError`. + ╭─[custom_error_definition.tsx:1:7] + 1 │ class Foo extends Error { + · ─── + 2 │ constructor(message) { + ╰──── + + ⚠ eslint-plugin-unicorn(custom-error-definition): Invalid class name, use `FooError`. + ╭─[custom_error_definition.tsx:1:7] + 1 │ class FooERROR extends Error { + · ──────── + 2 │ constructor(message) { + ╰──── + + ⚠ eslint-plugin-unicorn(custom-error-definition): Invalid class name, use `FooError`. + ╭─[custom_error_definition.tsx:1:7] + 1 │ class fooerror extends Error { + · ──────── + 2 │ constructor(message) { + ╰──── + + ⚠ eslint-plugin-unicorn(custom-error-definition): Missing call to `super()` in constructor. + ╭─[custom_error_definition.tsx:2:27] + 1 │ class FooError extends Error { + 2 │ constructor() { } + · ─── + 3 │ } + ╰──── + + ⚠ eslint-plugin-unicorn(custom-error-definition): The `name` property should be set to `FooError`. + ╭─[custom_error_definition.tsx:2:27] + 1 │ class FooError extends Error { + 2 │ constructor() { } + · ─── + 3 │ } + ╰──── + + ⚠ eslint-plugin-unicorn(custom-error-definition): Pass the error message to `super()` instead of setting `this.message`. + ╭─[custom_error_definition.tsx:3:17] + 2 │ constructor() { + 3 │ super(); + · ──────── + 4 │ this.message = 'My custom message'; + ╰──── + + ⚠ eslint-plugin-unicorn(custom-error-definition): The `name` property should be set to `FooError`. + ╭─[custom_error_definition.tsx:2:27] + 1 │ class FooError extends Error { + 2 │ ╭─▶ constructor() { + 3 │ │ super(); + 4 │ │ this.message = 'My custom message'; + 5 │ ╰─▶ } + 6 │ } + ╰──── + + ⚠ eslint-plugin-unicorn(custom-error-definition): The `name` property should be set to `FooError`. + ╭─[custom_error_definition.tsx:2:27] + 1 │ class FooError extends Error { + 2 │ ╭─▶ constructor() { + 3 │ │ super(); + 4 │ ╰─▶ } + 5 │ } + ╰──── + + ⚠ eslint-plugin-unicorn(custom-error-definition): Missing call to `super()` in constructor. + ╭─[custom_error_definition.tsx:3:42] + 2 │ constructor(message: string); + 3 │ ╭─▶ constructor(message: string) { + 4 │ │ this.name = 'FooError'; + 5 │ ╰─▶ } + 6 │ } + ╰──── + + ⚠ eslint-plugin-unicorn(custom-error-definition): The `name` property should be set to `FooError`. + ╭─[custom_error_definition.tsx:4:29] + 3 │ super('My awesome Foo Error'); + 4 │ this.name = this.constructor.name; + · ───────────────────── + 5 │ } + ╰──── + + ⚠ eslint-plugin-unicorn(custom-error-definition): Pass the error message to `super()` instead of setting `this.message`. + ╭─[custom_error_definition.tsx:3:17] + 2 │ constructor(message) { + 3 │ super(message); + · ─────────────── + 4 │ this.message = message; + ╰──── + + ⚠ eslint-plugin-unicorn(custom-error-definition): Pass the error message to `super()` instead of setting `this.message`. + ╭─[custom_error_definition.tsx:3:17] + 2 │ constructor(message) { + 3 │ super(); + · ──────── + 4 │ this.message = message; + ╰──── + + ⚠ eslint-plugin-unicorn(custom-error-definition): Pass the error message to `super()` instead of setting `this.message`. + ╭─[custom_error_definition.tsx:3:17] + 2 │ constructor(message) { + 3 │ super(); + · ──────── + 4 │ this.message = message; + ╰──── + + ⚠ eslint-plugin-unicorn(custom-error-definition): The `name` property should be set to `FooError`. + ╭─[custom_error_definition.tsx:2:34] + 1 │ class FooError extends Error { + 2 │ ╭─▶ constructor(message) { + 3 │ │ super(); + 4 │ │ this.message = message; + 5 │ ╰─▶ } + 6 │ } + ╰──── + + ⚠ eslint-plugin-unicorn(custom-error-definition): Pass the error message to `super()` instead of setting `this.message`. + ╭─[custom_error_definition.tsx:3:17] + 2 │ constructor(message) { + 3 │ super(); + · ──────── + 4 │ this.name = 'FooError'; + ╰──── + + ⚠ eslint-plugin-unicorn(custom-error-definition): The `name` property should be set to `FooError`. + ╭─[custom_error_definition.tsx:4:29] + 3 │ super(); + 4 │ this.name = 'foo'; + · ───── + 5 │ } + ╰──── + + ⚠ eslint-plugin-unicorn(custom-error-definition): The `name` property should be set to `FooError`. + ╭─[custom_error_definition.tsx:4:29] + 3 │ super(); + 4 │ this.name = 'foo'; + · ───── + 5 │ } + ╰──── + + ⚠ eslint-plugin-unicorn(custom-error-definition): Exported error name should match error class + ╭─[custom_error_definition.tsx:1:9] + 1 │ exports.fooError = class FooError extends Error { + · ──────── + 2 │ constructor(error) { + ╰──── + + ⚠ eslint-plugin-unicorn(custom-error-definition): Exported error name should match error class + ╭─[custom_error_definition.tsx:1:9] + 1 │ exports.fooError = (class FooError extends Error { + · ──────── + 2 │ constructor(error) { + ╰──── + + ⚠ eslint-plugin-unicorn(custom-error-definition): The `name` property should be set to `FooError`. + ╭─[custom_error_definition.tsx:4:29] + 3 │ super(); + 4 │ this.name = 'foo'; + · ───── + 5 │ } + ╰──── + + ⚠ eslint-plugin-unicorn(custom-error-definition): The `name` property should be set to `FooError`. + ╭─[custom_error_definition.tsx:4:29] + 3 │ super(); + 4 │ this.name = 'foo'; + · ───── + 5 │ } + ╰──── + + ⚠ eslint-plugin-unicorn(custom-error-definition): The `name` property should be set to `FooError`. + ╭─[custom_error_definition.tsx:4:29] + 3 │ super(); + 4 │ this.name = 'foo'; + · ───── + 5 │ } + ╰──── + + ⚠ eslint-plugin-unicorn(custom-error-definition): Pass the error message to `super()` instead of setting `this.message`. + ╭─[custom_error_definition.tsx:8:17] + 7 │ + 8 │ super(); + · ──────── + 9 │ this.name = 'AbortError'; + ╰──── + + ⚠ eslint-plugin-unicorn(custom-error-definition): Pass the error message to `super()` instead of setting `this.message`. + ╭─[custom_error_definition.tsx:3:17] + 2 │ constructor() { + 3 │ super(); + · ──────── + 4 │ this.message = foo.error; + ╰──── + + ⚠ eslint-plugin-unicorn(custom-error-definition): The `name` property should be set to `ValidationError`. + ╭─[custom_error_definition.tsx:2:20] + 1 │ export class ValidationError extends Error { + 2 │ name = 'FOO'; + · ───── + 3 │ constructor(message) { + ╰──── + + ⚠ eslint-plugin-unicorn(custom-error-definition): The `name` property should be set to `FooError`. + ╭─[custom_error_definition.tsx:4:34] + 3 │ [name] = 'FooError'; + 4 │ ╭─▶ constructor(message) { + 5 │ │ super(message); + 6 │ ╰─▶ } + 7 │ } + ╰──── + + ⚠ eslint-plugin-unicorn(custom-error-definition): The `name` property should be set to `ValidationError`. + ╭─[custom_error_definition.tsx:2:13] + 1 │ export class ValidationError extends Error { + 2 │ 'name': SomeType; + · ───────────────── + 3 │ constructor(message) { + ╰──── + + ⚠ eslint-plugin-unicorn(custom-error-definition): The `name` property should be set to `FooError`. + ╭─[custom_error_definition.tsx:2:13] + 1 │ class FooError extends Error { + 2 │ name: string; + · ───────────── + 3 │ } + ╰────