diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 5daf1dc6da84a..9fae5f8eaeb23 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -44,6 +44,7 @@ mod eslint { pub mod max_classes_per_file; pub mod max_lines; pub mod max_params; + pub mod new_cap; pub mod no_alert; pub mod no_array_constructor; pub mod no_async_promise_executor; @@ -541,6 +542,7 @@ oxc_macros::declare_all_lint_rules! { eslint::max_classes_per_file, eslint::max_lines, eslint::max_params, + eslint::new_cap, eslint::no_extra_label, eslint::no_multi_assign, eslint::no_nested_ternary, diff --git a/crates/oxc_linter/src/rules/eslint/new_cap.rs b/crates/oxc_linter/src/rules/eslint/new_cap.rs new file mode 100644 index 0000000000000..d1283782c0d32 --- /dev/null +++ b/crates/oxc_linter/src/rules/eslint/new_cap.rs @@ -0,0 +1,789 @@ +use crate::{context::LintContext, rule::Rule, AstNode}; +use oxc_ast::{ + ast::{ChainElement, ComputedMemberExpression, Expression}, + AstKind, +}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::{CompactStr, GetSpan, Span}; +use regex::Regex; + +fn new_cap_diagnostic(span: Span, cap: &GetCapResult) -> OxcDiagnostic { + let msg = if *cap == GetCapResult::Lower { + "A constructor name should not start with a lowercase letter." + } else { + "A function with a name starting with an uppercase letter should only be used as a constructor." + }; + + OxcDiagnostic::warn(msg).with_label(span) +} + +#[derive(Debug, Default, Clone)] +pub struct NewCap(Box); + +#[derive(Debug, Default, Clone)] +pub struct NewCapConfig { + new_is_cap: bool, + cap_is_new: bool, + new_is_cap_exceptions: Vec, + new_is_cap_exception_pattern: Option, + cap_is_new_exceptions: Vec, + cap_is_new_exception_pattern: Option, + properties: bool, +} + +impl std::ops::Deref for NewCap { + type Target = NewCapConfig; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +fn bool_serde_value(map: &serde_json::Map, key: &str) -> bool { + let Some(value) = map.get(key) else { + return true; // default value + }; + + value.as_bool().unwrap_or(true) +} + +fn vec_str_serde_value( + map: &serde_json::Map, + key: &str, + default_value: Vec, +) -> Vec { + let Some(value) = map.get(key) else { + return default_value; // default value + }; + + let Some(array_value) = value.as_array() else { + return default_value; // default value + }; + + array_value + .iter() + .map(|value| CompactStr::new(value.as_str().unwrap_or_default())) + .collect::>() +} + +fn regex_serde_value(map: &serde_json::Map, key: &str) -> Option { + let value = map.get(key)?; + let regex_string = value.as_str()?; + + if let Ok(regex) = Regex::new(regex_string) { + return Some(regex); + } + + None +} + +impl From<&serde_json::Value> for NewCap { + fn from(raw: &serde_json::Value) -> Self { + let Some(config_entry) = raw.get(0) else { + return Self(Box::new(NewCapConfig { + new_is_cap: true, + cap_is_new: true, + new_is_cap_exceptions: caps_allowed_vec(), + new_is_cap_exception_pattern: None, + cap_is_new_exceptions: vec![], + cap_is_new_exception_pattern: None, + properties: true, + })); + }; + + let config = config_entry + .as_object() + .map_or_else( + || { + Err(OxcDiagnostic::warn( + "eslint/new-cap: invalid configuration, expected object.", + )) + }, + Ok, + ) + .unwrap(); + + Self(Box::new(NewCapConfig { + new_is_cap: bool_serde_value(config, "newIsCap"), + cap_is_new: bool_serde_value(config, "capIsNew"), + new_is_cap_exceptions: vec_str_serde_value( + config, + "newIsCapExceptions", + caps_allowed_vec(), + ), + new_is_cap_exception_pattern: regex_serde_value(config, "newIsCapExceptionPattern"), + cap_is_new_exceptions: vec_str_serde_value(config, "capIsNewExceptions", vec![]), + cap_is_new_exception_pattern: regex_serde_value(config, "capIsNewExceptionPattern"), + properties: bool_serde_value(config, "properties"), + })) + } +} + +const CAPS_ALLOWED: [&str; 11] = [ + "Array", "Boolean", "Date", "Error", "Function", "Number", "Object", "RegExp", "String", + "Symbol", "BigInt", +]; + +fn caps_allowed_vec() -> Vec { + CAPS_ALLOWED.iter().map(|x| CompactStr::new(x)).collect::>() +} + +declare_oxc_lint!( + /// ### What it does + /// + /// This rule requires constructor names to begin with a capital letter. + /// + /// ### Why is this bad? + /// + /// The new operator in JavaScript creates a new instance of a particular type of object. + /// That type of object is represented by a constructor function. + /// Since constructor functions are just regular functions, the only defining characteristic + /// is that new is being used as part of the call. + /// Native JavaScript functions begin with an uppercase letter to distinguish those functions + /// that are to be used as constructors from functions that are not. + /// Many style guides recommend following this pattern + /// to more easily determine which functions are to be used as constructors. + /// + /// **Warning**: + /// The option `newIsCapExceptionPattern` and `capIsNewExceptionPattern` are implemented with + /// the [rust regex syntax](https://docs.rs/regex/latest/regex/). Many JavaScript features + /// are not supported (Lookahead, Lookbehinds, ...). + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```js + /// function foo(arg) { + /// return Boolean(arg); + /// } + /// ``` + /// + /// Examples of **incorrect** code for this rule with the default `{ "newIsCap": true }` option: + /// + /// ```js + /// /*eslint new-cap: ["error", { "newIsCap": true }]*/ + /// + /// var friend = new person(); + /// ``` + /// + /// Examples of **correct** code for this rule with the default `{ "newIsCap": true }` option: + /// + /// ```js + /// /*eslint new-cap: ["error", { "newIsCap": true }]*/ + /// + /// var friend = new Person(); + /// ``` + /// + /// Examples of **correct** code for this rule with the `{ "newIsCap": false }` option: + /// + /// ```js + /// /*eslint new-cap: ["error", { "newIsCap": false }]*/ + /// + /// var friend = new person(); + /// ``` + /// + /// Examples of **incorrect** code for this rule with the default `{ "capIsNew": true }` option: + /// + /// ```js + /// /*eslint new-cap: ["error", { "capIsNew": true }]*/ + /// + /// var colleague = Person(); + /// ``` + /// + /// Examples of **correct** code for this rule with the default `{ "capIsNew": true }` option: + /// + /// ```js + /// /*eslint new-cap: ["error", { "capIsNew": true }]*/ + /// + /// var colleague = new Person(); + /// ``` + /// + /// Examples of **correct** code for this rule with the `{ "capIsNew": false }` option: + /// + /// ```js + /// /*eslint new-cap: ["error", { "capIsNew": false }]*/ + /// + /// var colleague = Person(); + /// ``` + /// + /// Examples of additional **correct** code for this rule with the `{ "newIsCapExceptions": ["events"] }` option: + /// + /// ```js + /// /*eslint new-cap: ["error", { "newIsCapExceptions": ["events"] }]*/ + /// + /// var events = require('events'); + /// + /// var emitter = new events(); + /// ``` + /// + /// Examples of additional **correct** code for this rule with the `{ "newIsCapExceptionPattern": "^person\\.." }` option: + /// + /// ```js + /// /*eslint new-cap: ["error", { "newIsCapExceptionPattern": "^person\\.." }]*/ + /// + /// var friend = new person.acquaintance(); + /// + /// var bestFriend = new person.friend(); + /// ``` + /// + /// Examples of additional **correct** code for this rule with the `{ "newIsCapExceptionPattern": "\\.bar$" }` option: + /// + /// ```js + /// /*eslint new-cap: ["error", { "newIsCapExceptionPattern": "\\.bar$" }]*/ + /// + /// var friend = new person.bar(); + /// ``` + /// + /// Examples of additional **correct** code for this rule with the `{ "capIsNewExceptions": ["Person"] }` option: + /// + /// ```js + /// /*eslint new-cap: ["error", { "capIsNewExceptions": ["Person"] }]*/ + /// + /// function foo(arg) { + /// return Person(arg); + /// } + /// ``` + /// + /// Examples of additional **correct** code for this rule with the `{ "capIsNewExceptionPattern": "^person\\.." }` option: + /// + /// ```js + /// /*eslint new-cap: ["error", { "capIsNewExceptionPattern": "^person\\.." }]*/ + /// + /// var friend = person.Acquaintance(); + /// var bestFriend = person.Friend(); + /// ``` + /// + /// Examples of additional **correct** code for this rule with the `{ "capIsNewExceptionPattern": "\\.Bar$" }` option: + /// + /// ```js + /// /*eslint new-cap: ["error", { "capIsNewExceptionPattern": "\\.Bar$" }]*/ + /// + /// foo.Bar(); + /// ``` + /// + /// Examples of additional **correct** code for this rule with the `{ "capIsNewExceptionPattern": "^Foo" }` option: + /// + /// ```js + /// /*eslint new-cap: ["error", { "capIsNewExceptionPattern": "^Foo" }]*/ + /// + /// var x = Foo(42); + /// + /// var y = Foobar(42); + /// + /// var z = Foo.Bar(42); + /// ``` + /// + /// ### properties + /// + /// Examples of **incorrect** code for this rule with the default `{ "properties": true }` option: + /// + /// ```js + /// /*eslint new-cap: ["error", { "properties": true }]*/ + /// + /// var friend = new person.acquaintance(); + /// ``` + /// + /// Examples of **correct** code for this rule with the default `{ "properties": true }` option: + /// + /// ```js + /// /*eslint new-cap: ["error", { "properties": true }]*/ + /// + /// var friend = new person.Acquaintance(); + /// ``` + /// + /// Examples of **correct** code for this rule with the `{ "properties": false }` option: + /// + /// ```js + /// /*eslint new-cap: ["error", { "properties": false }]*/ + /// + /// var friend = new person.acquaintance(); + /// ``` + /// + /// Examples of **incorrect** code for this rule with the default `{ "newIsCap": true }` option: + /// + /// ```js + /// /*eslint new-cap: ["error", { "newIsCap": true }]*/ + /// + /// var friend = new person(); + /// ``` + /// + /// Examples of **correct** code for this rule with the default `{ "newIsCap": true }` option: + /// + /// ```js + /// /*eslint new-cap: ["error", { "newIsCap": true }]*/ + /// + /// var friend = new Person(); + /// ``` + /// + /// Examples of **correct** code for this rule with the `{ "newIsCap": false }` option: + /// + /// ```js + /// /*eslint new-cap: ["error", { "newIsCap": false }]*/ + /// + /// var friend = new person(); + /// ``` + /// + /// Examples of **incorrect** code for this rule with the default `{ "capIsNew": true }` option: + /// + /// ```js + /// /*eslint new-cap: ["error", { "capIsNew": true }]*/ + /// + /// var colleague = Person(); + /// ``` + /// + /// Examples of **correct** code for this rule with the default `{ "capIsNew": true }` option: + /// + /// ```js + /// /*eslint new-cap: ["error", { "capIsNew": true }]*/ + /// + /// var colleague = new Person(); + /// ``` + /// + /// Examples of **correct** code for this rule with the `{ "capIsNew": false }` option: + /// + /// ```js + /// /*eslint new-cap: ["error", { "capIsNew": false }]*/ + /// + /// var colleague = Person(); + /// ``` + /// + /// Examples of additional **correct** code for this rule with the `{ "newIsCapExceptions": ["events"] }` option: + /// + /// ```js + /// /*eslint new-cap: ["error", { "newIsCapExceptions": ["events"] }]*/ + /// + /// var events = require('events'); + /// + /// var emitter = new events(); + /// ``` + /// + /// Examples of additional **correct** code for this rule with the `{ "newIsCapExceptionPattern": "^person\\.." }` option: + /// + /// ```js + /// /*eslint new-cap: ["error", { "newIsCapExceptionPattern": "^person\\.." }]*/ + /// + /// var friend = new person.acquaintance(); + /// + /// var bestFriend = new person.friend(); + /// ``` + /// + /// Examples of additional **correct** code for this rule with the `{ "newIsCapExceptionPattern": "\\.bar$" }` option: + /// + /// ```js + /// /*eslint new-cap: ["error", { "newIsCapExceptionPattern": "\\.bar$" }]*/ + /// + /// var friend = new person.bar(); + /// ``` + /// + /// Examples of additional **correct** code for this rule with the `{ "capIsNewExceptions": ["Person"] }` option: + /// + /// ::: correct + /// + /// ```js + /// /*eslint new-cap: ["error", { "capIsNewExceptions": ["Person"] }]*/ + /// + /// function foo(arg) { + /// return Person(arg); + /// } + /// ``` + /// + /// Examples of additional **correct** code for this rule with the `{ "capIsNewExceptionPattern": "^person\\.." }` option: + /// + /// ```js + /// /*eslint new-cap: ["error", { "capIsNewExceptionPattern": "^person\\.." }]*/ + /// + /// var friend = person.Acquaintance(); + /// var bestFriend = person.Friend(); + /// ``` + /// + /// Examples of additional **correct** code for this rule with the `{ "capIsNewExceptionPattern": "\\.Bar$" }` option: + /// + /// ```js + /// /*eslint new-cap: ["error", { "capIsNewExceptionPattern": "\\.Bar$" }]*/ + /// + /// foo.Bar(); + /// ``` + /// + /// Examples of additional **correct** code for this rule with the `{ "capIsNewExceptionPattern": "^Foo" }` option: + /// + /// ```js + /// /*eslint new-cap: ["error", { "capIsNewExceptionPattern": "^Foo" }]*/ + /// + /// var x = Foo(42); + /// + /// var y = Foobar(42); + /// + /// var z = Foo.Bar(42); + /// ``` + /// + /// Examples of **incorrect** code for this rule with the default `{ "properties": true }` option: + /// + /// ```js + /// /*eslint new-cap: ["error", { "properties": true }]*/ + /// + /// var friend = new person.acquaintance(); + /// ``` + /// + /// + /// Examples of **correct** code for this rule with the default `{ "properties": true }` option: + /// + /// ```js + /// /*eslint new-cap: ["error", { "properties": true }]*/ + /// + /// var friend = new person.Acquaintance(); + /// ``` + /// + /// Examples of **correct** code for this rule with the `{ "properties": false }` option: + /// + /// ```js + /// /*eslint new-cap: ["error", { "properties": false }]*/ + /// + /// var friend = new person.acquaintance(); + /// ``` + NewCap, + style, + pending // TODO: maybe? +); + +impl Rule for NewCap { + fn from_configuration(value: serde_json::Value) -> Self { + NewCap::from(&value) + } + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + match node.kind() { + AstKind::NewExpression(expression) if self.new_is_cap => { + let callee = expression.callee.without_parentheses(); + + let Some(short_name) = &extract_name_from_expression(callee) else { + return; + }; + + let Some(name) = &extract_name_deep_from_expression(callee) else { + return; + }; + + let capitalization = &get_cap(short_name); + + let allowed = *capitalization != GetCapResult::Lower + || is_cap_allowed_expression( + short_name, + name, + &self.new_is_cap_exceptions, + self.new_is_cap_exception_pattern.as_ref(), + ) + || (!self.properties && short_name != name); + + if !allowed { + ctx.diagnostic(new_cap_diagnostic(callee.span(), capitalization)); + } + } + AstKind::CallExpression(expression) if self.cap_is_new => { + let callee = expression.callee.without_parentheses(); + + let Some(short_name) = &extract_name_from_expression(callee) else { + return; + }; + + let Some(name) = &extract_name_deep_from_expression(callee) else { + return; + }; + + let capitalization = &get_cap(short_name); + + let mut caps_is_new_exceptions = self.cap_is_new_exceptions.clone(); + caps_is_new_exceptions.append(&mut caps_allowed_vec()); + + let allowed = *capitalization != GetCapResult::Upper + || is_cap_allowed_expression( + short_name, + name, + &caps_is_new_exceptions, + self.cap_is_new_exception_pattern.as_ref(), + ) + || (!self.properties && short_name != name); + + if !allowed { + ctx.diagnostic(new_cap_diagnostic(callee.span(), capitalization)); + } + } + _ => (), + } + } +} + +fn extract_name_deep_from_expression(expression: &Expression) -> Option { + if let Some(identifier) = expression.get_identifier_reference() { + return Some(identifier.name.clone().into()); + } + + match expression.without_parentheses() { + Expression::StaticMemberExpression(expression) => { + let prop_name = expression.property.name.clone().into_compact_str(); + let obj_name = + extract_name_deep_from_expression(expression.object.without_parentheses()); + + if let Some(obj_name) = obj_name { + let new_name = format!("{obj_name}.{prop_name}"); + return Some(CompactStr::new(&new_name)); + } + + Some(prop_name) + } + Expression::ComputedMemberExpression(expression) => { + let prop_name = get_computed_member_name(expression)?; + let obj_name = + extract_name_deep_from_expression(expression.object.without_parentheses()); + + if let Some(obj_name) = obj_name { + let new_name = format!("{obj_name}.{prop_name}"); + return Some(CompactStr::new(&new_name)); + } + + Some(prop_name) + } + Expression::ChainExpression(chain) => match &chain.expression { + ChainElement::CallExpression(call) => extract_name_deep_from_expression(&call.callee), + ChainElement::TSNonNullExpression(non_null) => { + extract_name_deep_from_expression(&non_null.expression) + } + ChainElement::StaticMemberExpression(expression) => { + let prop_name = expression.property.name.clone().into_compact_str(); + let obj_name = + extract_name_deep_from_expression(expression.object.without_parentheses()); + + if let Some(obj_name) = obj_name { + let new_name = format!("{obj_name}.{prop_name}"); + return Some(CompactStr::new(&new_name)); + } + + Some(prop_name) + } + ChainElement::ComputedMemberExpression(expression) => { + let prop_name = get_computed_member_name(expression)?; + let obj_name = + extract_name_deep_from_expression(expression.object.without_parentheses()); + + if let Some(obj_name) = obj_name { + let new_name = format!("{obj_name}.{prop_name}"); + return Some(CompactStr::new(&new_name)); + } + + Some(prop_name) + } + ChainElement::PrivateFieldExpression(_) => None, + }, + _ => None, + } +} + +fn get_computed_member_name(computed_member: &ComputedMemberExpression) -> Option { + let expression = computed_member.expression.without_parentheses(); + + match &expression { + Expression::StringLiteral(lit) => Some(lit.value.as_ref().into()), + Expression::TemplateLiteral(lit) if lit.expressions.is_empty() && lit.quasis.len() == 1 => { + Some(lit.quasis[0].value.raw.as_ref().into()) + } + Expression::RegExpLiteral(lit) => lit.raw.as_ref().map(|x| x.clone().into_compact_str()), + _ => None, + } +} + +fn extract_name_from_expression(expression: &Expression) -> Option { + if let Some(identifier) = expression.get_identifier_reference() { + return Some(identifier.name.clone().into()); + } + + match expression.without_parentheses() { + Expression::StaticMemberExpression(expression) => { + Some(expression.property.name.clone().into_compact_str()) + } + Expression::ComputedMemberExpression(expression) => get_computed_member_name(expression), + Expression::ChainExpression(chain) => match &chain.expression { + ChainElement::CallExpression(call) => extract_name_from_expression(&call.callee), + ChainElement::TSNonNullExpression(non_null) => { + extract_name_from_expression(&non_null.expression) + } + ChainElement::StaticMemberExpression(expression) => { + Some(expression.property.name.clone().into_compact_str()) + } + ChainElement::ComputedMemberExpression(expression) => { + get_computed_member_name(expression) + } + ChainElement::PrivateFieldExpression(_) => None, + }, + _ => None, + } +} + +fn is_cap_allowed_expression( + short_name: &CompactStr, + name: &CompactStr, + exceptions: &[CompactStr], + patterns: Option<&Regex>, +) -> bool { + if exceptions.contains(name) || exceptions.contains(short_name) { + return true; + } + + if name == "Date.UTC" { + return true; + } + + if let Some(pattern) = &patterns { + return pattern.find(name).is_some(); + }; + + false +} + +#[derive(PartialEq, Debug)] +enum GetCapResult { + Upper, + Lower, + NonAlpha, +} + +fn get_cap(string: &CompactStr) -> GetCapResult { + let first_char = string.chars().next().unwrap(); + + if !first_char.is_alphabetic() { + return GetCapResult::NonAlpha; + } + + if first_char.is_lowercase() { + return GetCapResult::Lower; + } + + GetCapResult::Upper +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ("var x = new Constructor();", None), + ("var x = new a.b.Constructor();", None), + ("var x = new a.b['Constructor']();", None), + ("var x = new a.b[Constructor]();", None), + ("var x = new a.b[constructor]();", None), + ("var x = new function(){};", None), + ("var x = new _;", None), + ("var x = new $;", None), + ("var x = new Σ;", None), + ("var x = new _x;", None), + ("var x = new $x;", None), + ("var x = new this;", None), + ("var x = Array(42)", None), + ("var x = Boolean(42)", None), + ("var x = Date(42)", None), + ("var x = Date.UTC(2000, 0)", None), + ("var x = Error('error')", None), + ("var x = Function('return 0')", None), + ("var x = Number(42)", None), + ("var x = Object(null)", None), + ("var x = RegExp(42)", None), + ("var x = String(42)", None), + ("var x = Symbol('symbol')", None), + ("var x = BigInt('1n')", None), + ("var x = _();", None), + ("var x = $();", None), + ("var x = Foo(42)", Some(serde_json::json!([{ "capIsNew": false }]))), + ("var x = bar.Foo(42)", Some(serde_json::json!([{ "capIsNew": false }]))), + ("var x = Foo.bar(42)", Some(serde_json::json!([{ "capIsNew": false }]))), + ("var x = bar[Foo](42)", None), + ("var x = bar['Foo'](42)", Some(serde_json::json!([{ "capIsNew": false }]))), + ("var x = Foo.bar(42)", None), + ("var x = new foo(42)", Some(serde_json::json!([{ "newIsCap": false }]))), + ("var o = { 1: function() {} }; o[1]();", None), + ("var o = { 1: function() {} }; new o[1]();", None), + ( + "var x = Foo(42);", + Some(serde_json::json!([{ "capIsNew": true, "capIsNewExceptions": ["Foo"] }])), + ), + ("var x = Foo(42);", Some(serde_json::json!([{ "capIsNewExceptionPattern": "^Foo" }]))), + ( + "var x = new foo(42);", + Some(serde_json::json!([{ "newIsCap": true, "newIsCapExceptions": ["foo"] }])), + ), + ("var x = new foo(42);", Some(serde_json::json!([{ "newIsCapExceptionPattern": "^foo" }]))), + ("var x = Object(42);", Some(serde_json::json!([{ "capIsNewExceptions": ["Foo"] }]))), + ("var x = Foo.Bar(42);", Some(serde_json::json!([{ "capIsNewExceptions": ["Bar"] }]))), + ("var x = Foo.Bar(42);", Some(serde_json::json!([{ "capIsNewExceptions": ["Foo.Bar"] }]))), + ( + "var x = Foo.Bar(42);", + Some(serde_json::json!([{ "capIsNewExceptionPattern": "^Foo\\.." }])), + ), + ("var x = new foo.bar(42);", Some(serde_json::json!([{ "newIsCapExceptions": ["bar"] }]))), + ( + "var x = new foo.bar(42);", + Some(serde_json::json!([{ "newIsCapExceptions": ["foo.bar"] }])), + ), + ( + "var x = new foo.bar(42);", + Some(serde_json::json!([{ "newIsCapExceptionPattern": "^foo\\.." }])), + ), + ("var x = new foo.bar(42);", Some(serde_json::json!([{ "properties": false }]))), + ("var x = Foo.bar(42);", Some(serde_json::json!([{ "properties": false }]))), + ( + "var x = foo.Bar(42);", + Some(serde_json::json!([{ "capIsNew": false, "properties": false }])), + ), + ("foo?.bar();", None), // { "ecmaVersion": 2020 }, + ("(foo?.bar)();", None), // { "ecmaVersion": 2020 }, + ("new (foo?.Bar)();", None), // { "ecmaVersion": 2020 }, + ("(foo?.Bar)();", Some(serde_json::json!([{ "properties": false }]))), // { "ecmaVersion": 2020 }, + ("new (foo?.bar)();", Some(serde_json::json!([{ "properties": false }]))), // { "ecmaVersion": 2020 }, + ("Date?.UTC();", None), // { "ecmaVersion": 2020 }, + ("(Date?.UTC)();", None), // { "ecmaVersion": 2020 } + ]; + + let fail = vec![ + ("var x = new c();", None), + ("var x = new φ;", None), + ("var x = new a.b.c;", None), + ("var x = new a.b['c'];", None), + ("var b = Foo();", None), + ("var b = a.Foo();", None), + ("var b = a['Foo']();", None), + ("var b = a.Date.UTC();", None), + ("var b = UTC();", None), + ("var a = B.C();", None), + ( + "var a = B + .C();", + None, + ), + ("var a = new B.c();", None), + ( + "var a = new B. + c();", + None, + ), + ("var a = new c();", None), + ("var a = new b[ ( 'foo' ) ]();", None), // { "ecmaVersion": 6 }, + ("var a = new b[`foo`];", None), // { "ecmaVersion": 6 }, + // ( + // "var a = b[`\\ + // Foo`]();", + // None, + // ), // { "ecmaVersion": 6 }, + ("var x = Foo.Bar(42);", Some(serde_json::json!([{ "capIsNewExceptions": ["Foo"] }]))), + ( + "var x = Bar.Foo(42);", + Some(serde_json::json!([{ "capIsNewExceptionPattern": "^Foo\\.." }])), + ), + ("var x = new foo.bar(42);", Some(serde_json::json!([{ "newIsCapExceptions": ["foo"] }]))), + ( + "var x = new bar.foo(42);", + Some(serde_json::json!([{ "newIsCapExceptionPattern": "^foo\\.." }])), + ), + ("new (foo?.bar)();", None), // { "ecmaVersion": 2020 }, + ("foo?.Bar();", None), // { "ecmaVersion": 2020 }, + ("(foo?.Bar)();", None), // { "ecmaVersion": 2020 } + ]; + + Tester::new(NewCap::NAME, NewCap::CATEGORY, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/eslint_new_cap.snap b/crates/oxc_linter/src/snapshots/eslint_new_cap.snap new file mode 100644 index 0000000000000..b9c33210e5c95 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/eslint_new_cap.snap @@ -0,0 +1,141 @@ +--- +source: crates/oxc_linter/src/tester.rs +snapshot_kind: text +--- + ⚠ eslint(new-cap): A constructor name should not start with a lowercase letter. + ╭─[new_cap.tsx:1:13] + 1 │ var x = new c(); + · ─ + ╰──── + + ⚠ eslint(new-cap): A constructor name should not start with a lowercase letter. + ╭─[new_cap.tsx:1:13] + 1 │ var x = new φ; + · ─ + ╰──── + + ⚠ eslint(new-cap): A constructor name should not start with a lowercase letter. + ╭─[new_cap.tsx:1:13] + 1 │ var x = new a.b.c; + · ───── + ╰──── + + ⚠ eslint(new-cap): A constructor name should not start with a lowercase letter. + ╭─[new_cap.tsx:1:13] + 1 │ var x = new a.b['c']; + · ──────── + ╰──── + + ⚠ eslint(new-cap): A function with a name starting with an uppercase letter should only be used as a constructor. + ╭─[new_cap.tsx:1:9] + 1 │ var b = Foo(); + · ─── + ╰──── + + ⚠ eslint(new-cap): A function with a name starting with an uppercase letter should only be used as a constructor. + ╭─[new_cap.tsx:1:9] + 1 │ var b = a.Foo(); + · ───── + ╰──── + + ⚠ eslint(new-cap): A function with a name starting with an uppercase letter should only be used as a constructor. + ╭─[new_cap.tsx:1:9] + 1 │ var b = a['Foo'](); + · ──────── + ╰──── + + ⚠ eslint(new-cap): A function with a name starting with an uppercase letter should only be used as a constructor. + ╭─[new_cap.tsx:1:9] + 1 │ var b = a.Date.UTC(); + · ────────── + ╰──── + + ⚠ eslint(new-cap): A function with a name starting with an uppercase letter should only be used as a constructor. + ╭─[new_cap.tsx:1:9] + 1 │ var b = UTC(); + · ─── + ╰──── + + ⚠ eslint(new-cap): A function with a name starting with an uppercase letter should only be used as a constructor. + ╭─[new_cap.tsx:1:9] + 1 │ var a = B.C(); + · ─── + ╰──── + + ⚠ eslint(new-cap): A function with a name starting with an uppercase letter should only be used as a constructor. + ╭─[new_cap.tsx:1:9] + 1 │ ╭─▶ var a = B + 2 │ ╰─▶ .C(); + ╰──── + + ⚠ eslint(new-cap): A constructor name should not start with a lowercase letter. + ╭─[new_cap.tsx:1:13] + 1 │ var a = new B.c(); + · ─── + ╰──── + + ⚠ eslint(new-cap): A constructor name should not start with a lowercase letter. + ╭─[new_cap.tsx:1:13] + 1 │ ╭─▶ var a = new B. + 2 │ ╰─▶ c(); + ╰──── + + ⚠ eslint(new-cap): A constructor name should not start with a lowercase letter. + ╭─[new_cap.tsx:1:13] + 1 │ var a = new c(); + · ─ + ╰──── + + ⚠ eslint(new-cap): A constructor name should not start with a lowercase letter. + ╭─[new_cap.tsx:1:13] + 1 │ var a = new b[ ( 'foo' ) ](); + · ────────────── + ╰──── + + ⚠ eslint(new-cap): A constructor name should not start with a lowercase letter. + ╭─[new_cap.tsx:1:13] + 1 │ var a = new b[`foo`]; + · ──────── + ╰──── + + ⚠ eslint(new-cap): A function with a name starting with an uppercase letter should only be used as a constructor. + ╭─[new_cap.tsx:1:9] + 1 │ var x = Foo.Bar(42); + · ─────── + ╰──── + + ⚠ eslint(new-cap): A function with a name starting with an uppercase letter should only be used as a constructor. + ╭─[new_cap.tsx:1:9] + 1 │ var x = Bar.Foo(42); + · ─────── + ╰──── + + ⚠ eslint(new-cap): A constructor name should not start with a lowercase letter. + ╭─[new_cap.tsx:1:13] + 1 │ var x = new foo.bar(42); + · ─────── + ╰──── + + ⚠ eslint(new-cap): A constructor name should not start with a lowercase letter. + ╭─[new_cap.tsx:1:13] + 1 │ var x = new bar.foo(42); + · ─────── + ╰──── + + ⚠ eslint(new-cap): A constructor name should not start with a lowercase letter. + ╭─[new_cap.tsx:1:6] + 1 │ new (foo?.bar)(); + · ──────── + ╰──── + + ⚠ eslint(new-cap): A function with a name starting with an uppercase letter should only be used as a constructor. + ╭─[new_cap.tsx:1:1] + 1 │ foo?.Bar(); + · ──────── + ╰──── + + ⚠ eslint(new-cap): A function with a name starting with an uppercase letter should only be used as a constructor. + ╭─[new_cap.tsx:1:2] + 1 │ (foo?.Bar)(); + · ──────── + ╰────