diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 1e0053d2a..028b76371 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -1,3 +1,4 @@ +mod constructor_super; mod eq_eq_eq; mod for_direction; mod no_array_constructor; @@ -8,6 +9,7 @@ mod deepscan { pub mod uninvoked_array_callback; } +pub use constructor_super::ConstructorSuper; pub use deepscan::uninvoked_array_callback::UninvokedArrayCallback; pub use eq_eq_eq::EqEqEq; pub use for_direction::ForDirection; @@ -21,6 +23,7 @@ use crate::{context::LintContext, rule::Rule, rule::RuleMeta, AstNode}; lazy_static::lazy_static! { pub static ref RULES: Vec = vec![ RuleEnum::EqEqEq(EqEqEq::default()), + RuleEnum::ConstructorSuper(ConstructorSuper::default()), RuleEnum::NoDebugger(NoDebugger::default()), RuleEnum::NoEmpty(NoEmpty::default()), RuleEnum::NoArrayConstructor(NoArrayConstructor::default()), @@ -34,6 +37,7 @@ lazy_static::lazy_static! { #[allow(clippy::enum_variant_names)] pub enum RuleEnum { EqEqEq(EqEqEq), + ConstructorSuper(ConstructorSuper), NoDebugger(NoDebugger), NoEmpty(NoEmpty), NoArrayConstructor(NoArrayConstructor), @@ -46,6 +50,7 @@ impl RuleEnum { pub const fn name(&self) -> &'static str { match self { Self::EqEqEq(_) => EqEqEq::NAME, + Self::ConstructorSuper(_) => ConstructorSuper::NAME, Self::NoDebugger(_) => NoDebugger::NAME, Self::NoEmpty(_) => NoEmpty::NAME, Self::NoArrayConstructor(_) => NoArrayConstructor::NAME, @@ -60,6 +65,9 @@ impl RuleEnum { Self::EqEqEq(_) => { Self::EqEqEq(maybe_value.map(EqEqEq::from_configuration).unwrap_or_default()) } + Self::ConstructorSuper(_) => Self::ConstructorSuper( + maybe_value.map(ConstructorSuper::from_configuration).unwrap_or_default(), + ), Self::NoDebugger(_) => Self::NoDebugger( maybe_value.map(NoDebugger::from_configuration).unwrap_or_default(), ), @@ -84,6 +92,7 @@ impl RuleEnum { pub fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { match self { Self::EqEqEq(rule) => rule.run(node, ctx), + Self::ConstructorSuper(rule) => rule.run(node, ctx), Self::NoDebugger(rule) => rule.run(node, ctx), Self::NoEmpty(rule) => rule.run(node, ctx), Self::NoArrayConstructor(rule) => rule.run(node, ctx), diff --git a/crates/oxc_linter/src/rules/constructor_super.rs b/crates/oxc_linter/src/rules/constructor_super.rs new file mode 100644 index 000000000..3ec1c6363 --- /dev/null +++ b/crates/oxc_linter/src/rules/constructor_super.rs @@ -0,0 +1,132 @@ +use oxc_ast::{ + ast::{ClassElement, Expression, MethodDefinitionKind, Statement}, + AstKind, Span, +}; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::Error, +}; +use oxc_macros::declare_oxc_lint; + +use crate::{context::LintContext, rule::Rule, AstNode}; + +#[derive(Debug, Error, Diagnostic)] +#[error("eslint(constructor-super): Expected to call 'super()'.")] +#[diagnostic(severity(warning), help("Ensure 'super()' is called from constructor"))] +struct ConstructorSuperDiagnostic(#[label] pub Span); + +#[derive(Debug, Error, Diagnostic)] +#[error("eslint(constructor-super): Unexpected 'super()' because 'super' is not a constructor.")] +#[diagnostic(severity(warning), help("Do not call 'super()' from constructor."))] +struct SuperNotConstructorDiagnostic( + #[label("unexpected 'super()'")] pub Span, + #[label("because this is not a constructor")] pub Span, +); + +#[derive(Debug, Default, Clone)] +pub struct ConstructorSuper; + +declare_oxc_lint!( + /// ### What it does + /// Require 'super()' calls in constructors. + /// + /// ### Why is this bad? + /// + /// + /// ### Example + /// ```javascript + /// class A extends B { + /// constructor() {} + /// } + /// ``` + ConstructorSuper, + nursery +); + +impl Rule for ConstructorSuper { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::Class(class) = node.get().kind() else { return }; + let Some(ctor) = class.body.body.iter().find_map(|el| match el { + ClassElement::MethodDefinition(method_definition) + if method_definition.kind == MethodDefinitionKind::Constructor => + { + Some(method_definition) + } + _ => None, + }) else { return }; + + // In cases where there's no super-class, calling 'super()' inside the constructor + // is handled by the parser. + if let Some(super_class) = &class.super_class { + ctor.value.body.as_ref().map_or_else(|| { + ctx.diagnostic(ConstructorSuperDiagnostic(ctor.span)); + }, |function_body| { + let super_call_expr = function_body.statements.iter().find_map(|stmt| { + let Statement::ExpressionStatement(expr) = stmt else { return None }; + let Expression::CallExpression(call_expr) = &expr.expression else { return None }; + if matches!(call_expr.callee, Expression::Super(_)) { + Some(call_expr.span) + } else { + None + } + }); + + super_call_expr.map_or_else(|| { + ctx.diagnostic(ConstructorSuperDiagnostic(ctor.span)); + }, |span| { + if let Some(super_class_span) = super_class.span() { + ctx.diagnostic(SuperNotConstructorDiagnostic(span, super_class_span)); + } + }); + }); + } + } +} + +trait NonConstructor { + fn span(&self) -> Option; +} + +impl<'a> NonConstructor for Expression<'a> { + fn span(&self) -> Option { + match self { + Self::NullLiteral(lit) => Some(lit.span), + Self::NumberLiteral(lit) => Some(lit.span), + Self::StringLiteral(lit) => Some(lit.span), + _ => None, + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ("class A { }", None), + ("class A { constructor() { } }", None), + ("class A extends null { }", None), + ("class A extends B { constructor() { super(); } }", None), + ("class A extends B { }", None), + ("class A extends B { constructor() { super(); } }", None), + ("class A extends (class B {}) { constructor() { super(); } }", None), + ("class A extends (B = C) { constructor() { super(); } }", None), + ("class A extends (B &&= C) { constructor() { super(); } }", None), + ("class A extends (B ||= C) { constructor() { super(); } }", None), + ("class A extends (B ??= C) { constructor() { super(); } }", None), + ("class A extends (B ||= 5) { constructor() { super(); } }", None), + ("class A extends (B ??= 5) { constructor() { super(); } }", None), + ("class A extends (B || C) { constructor() { super(); } }", None), + ("class A extends (5 && B) { constructor() { super(); } }", None), + ]; + + let fail = vec![ + ("class A extends B { constructor() {} }", None), + ("class A extends null { constructor() { super(); } }", None), + ("class A extends null { constructor() { } }", None), + ("class A extends 100 { constructor() { super(); } }", None), + ("class A extends 'test' { constructor() { super(); } }", None), + ]; + + Tester::new(ConstructorSuper::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/constructor_super.snap b/crates/oxc_linter/src/snapshots/constructor_super.snap new file mode 100644 index 000000000..a440e831d --- /dev/null +++ b/crates/oxc_linter/src/snapshots/constructor_super.snap @@ -0,0 +1,46 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: constructor_super +--- + + ⚠ eslint(constructor-super): Expected to call 'super()'. + ╭─[constructor_super.tsx:1:1] + 1 │ class A extends B { constructor() {} } + · ──────────────── + ╰──── + help: Ensure 'super()' is called from constructor + + ⚠ eslint(constructor-super): Unexpected 'super()' because 'super' is not a constructor. + ╭─[constructor_super.tsx:1:1] + 1 │ class A extends null { constructor() { super(); } } + · ──┬─ ───┬─── + · │ ╰── unexpected 'super()' + · ╰── because this is not a constructor + ╰──── + help: Do not call 'super()' from constructor. + + ⚠ eslint(constructor-super): Expected to call 'super()'. + ╭─[constructor_super.tsx:1:1] + 1 │ class A extends null { constructor() { } } + · ───────────────── + ╰──── + help: Ensure 'super()' is called from constructor + + ⚠ eslint(constructor-super): Unexpected 'super()' because 'super' is not a constructor. + ╭─[constructor_super.tsx:1:1] + 1 │ class A extends 100 { constructor() { super(); } } + · ─┬─ ───┬─── + · │ ╰── unexpected 'super()' + · ╰── because this is not a constructor + ╰──── + help: Do not call 'super()' from constructor. + + ⚠ eslint(constructor-super): Unexpected 'super()' because 'super' is not a constructor. + ╭─[constructor_super.tsx:1:1] + 1 │ class A extends 'test' { constructor() { super(); } } + · ───┬── ───┬─── + · │ ╰── unexpected 'super()' + · ╰── because this is not a constructor + ╰──── + help: Do not call 'super()' from constructor. +