From ce5c35692964af773e208632c8d765270c789a4f Mon Sep 17 00:00:00 2001 From: Sysix Date: Fri, 27 Dec 2024 14:15:57 +0100 Subject: [PATCH 01/11] feat(linter): add `eslint/new-cap` --- crates/oxc_linter/src/ast_util.rs | 42 ++ crates/oxc_linter/src/rules.rs | 2 + .../oxc_linter/src/rules/eslint/func_names.rs | 48 +-- crates/oxc_linter/src/rules/eslint/new_cap.rs | 407 ++++++++++++++++++ 4 files changed, 455 insertions(+), 44 deletions(-) create mode 100644 crates/oxc_linter/src/rules/eslint/new_cap.rs diff --git a/crates/oxc_linter/src/ast_util.rs b/crates/oxc_linter/src/ast_util.rs index 76828fb33d850..97c0853e515e3 100644 --- a/crates/oxc_linter/src/ast_util.rs +++ b/crates/oxc_linter/src/ast_util.rs @@ -6,6 +6,7 @@ use oxc_ecmascript::ToBoolean; use oxc_semantic::{AstNode, IsGlobalReference, NodeId, ReferenceId, Semantic, SymbolId}; use oxc_span::{GetSpan, Span}; use oxc_syntax::operator::{AssignmentOperator, BinaryOperator, LogicalOperator, UnaryOperator}; +use std::borrow::Cow; /// Test if an AST node is a boolean value that never changes. Specifically we /// test for: @@ -469,3 +470,44 @@ pub fn leftmost_identifier_reference<'a, 'b: 'a>( _ => Err(expr), } } + +fn get_property_key_name<'a>(key: &PropertyKey<'a>) -> Option> { + if matches!(key, PropertyKey::NullLiteral(_)) { + return Some("null".into()); + } + + match key { + PropertyKey::RegExpLiteral(regex) => { + Some(Cow::Owned(format!("/{}/{}", regex.regex.pattern, regex.regex.flags))) + } + PropertyKey::BigIntLiteral(bigint) => Some(Cow::Borrowed(bigint.raw.as_str())), + PropertyKey::TemplateLiteral(template) => { + if template.expressions.len() == 0 && template.quasis.len() == 1 { + if let Some(cooked) = &template.quasis[0].value.cooked { + return Some(Cow::Borrowed(cooked.as_str())); + } + } + + None + } + _ => None, + } +} + +pub fn get_static_property_name<'a>(kind: &AstKind<'a>) -> Option> { + let (key, computed) = match kind { + AstKind::PropertyDefinition(definition) => (&definition.key, definition.computed), + AstKind::MethodDefinition(method_definition) => { + (&method_definition.key, method_definition.computed) + } + AstKind::ObjectProperty(property) => (&property.key, property.computed), + // AstKind::MemberExpression(member) => (member., member.is_computed()) + _ => return None, + }; + + if key.is_identifier() && !computed { + return key.name(); + } + + get_property_key_name(key) +} diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 4245090ed683e..a3b517d547062 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; @@ -534,6 +535,7 @@ oxc_macros::declare_all_lint_rules! { eslint::max_classes_per_file, eslint::max_lines, eslint::max_params, + eslint::new_cap, eslint::no_restricted_imports, eslint::no_object_constructor, eslint::no_duplicate_imports, diff --git a/crates/oxc_linter/src/rules/eslint/func_names.rs b/crates/oxc_linter/src/rules/eslint/func_names.rs index ad54b56c06e38..fe3aa5cae1ee4 100644 --- a/crates/oxc_linter/src/rules/eslint/func_names.rs +++ b/crates/oxc_linter/src/rules/eslint/func_names.rs @@ -3,7 +3,7 @@ use std::borrow::Cow; use oxc_ast::{ ast::{ AssignmentTarget, AssignmentTargetProperty, BindingPatternKind, Expression, Function, - FunctionType, MethodDefinitionKind, PropertyKey, PropertyKind, + FunctionType, MethodDefinitionKind, PropertyKind, }, AstKind, }; @@ -14,7 +14,7 @@ use oxc_span::{Atom, GetSpan, Span}; use oxc_syntax::identifier::is_identifier_name; use phf::phf_set; -use crate::{context::LintContext, rule::Rule, AstNode}; +use crate::{ast_util::get_static_property_name, context::LintContext, rule::Rule, AstNode}; fn named_diagnostic(function_name: &str, span: Span) -> OxcDiagnostic { OxcDiagnostic::warn(format!("Unexpected named {function_name}.")) @@ -232,46 +232,6 @@ fn get_function_identifier<'a>(func: &'a Function<'a>) -> Option<&'a Span> { func.id.as_ref().map(|id| &id.span) } -fn get_property_key_name<'a>(key: &PropertyKey<'a>) -> Option> { - if matches!(key, PropertyKey::NullLiteral(_)) { - return Some("null".into()); - } - - match key { - PropertyKey::RegExpLiteral(regex) => { - Some(Cow::Owned(format!("/{}/{}", regex.regex.pattern, regex.regex.flags))) - } - PropertyKey::BigIntLiteral(bigint) => Some(Cow::Borrowed(bigint.raw.as_str())), - PropertyKey::TemplateLiteral(template) => { - if template.expressions.len() == 0 && template.quasis.len() == 1 { - if let Some(cooked) = &template.quasis[0].value.cooked { - return Some(Cow::Borrowed(cooked.as_str())); - } - } - - None - } - _ => None, - } -} - -fn get_static_property_name<'a>(parent_node: &AstNode<'a>) -> Option> { - let (key, computed) = match parent_node.kind() { - AstKind::PropertyDefinition(definition) => (&definition.key, definition.computed), - AstKind::MethodDefinition(method_definition) => { - (&method_definition.key, method_definition.computed) - } - AstKind::ObjectProperty(property) => (&property.key, property.computed), - _ => return None, - }; - - if key.is_identifier() && !computed { - return key.name(); - } - - get_property_key_name(key) -} - /// Gets the name and kind of the given function node. /// @see fn get_function_name_with_kind<'a>(func: &Function<'a>, parent_node: &AstNode<'a>) -> Cow<'a, str> { @@ -335,14 +295,14 @@ fn get_function_name_with_kind<'a>(func: &Function<'a>, parent_node: &AstNode<'a if let Some(name) = definition.key.name() { tokens.push(name); } - } else if let Some(static_name) = get_static_property_name(parent_node) { + } else if let Some(static_name) = get_static_property_name(&parent_node.kind()) { tokens.push(static_name); } else if let Some(name) = func.name() { tokens.push(Cow::Borrowed(name.as_str())); } } _ => { - if let Some(static_name) = get_static_property_name(parent_node) { + if let Some(static_name) = get_static_property_name(&parent_node.kind()) { tokens.push(static_name); } else if let Some(name) = func.name() { tokens.push(Cow::Borrowed(name.as_str())); 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..65e0c1bbcfd43 --- /dev/null +++ b/crates/oxc_linter/src/rules/eslint/new_cap.rs @@ -0,0 +1,407 @@ +use crate::{ast_util::get_static_property_name, context::LintContext, rule::Rule, AstNode}; +use oxc_ast::{ast::Expression, AstKind}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::{CompactStr, Span}; +use regex::Regex; + +fn new_cap_diagnostic(span: Span) -> OxcDiagnostic { + // See for details + OxcDiagnostic::warn("Should be an imperative statement about what is wrong") + .with_help("Should be a command-like statement that tells the user how to fix the issue") + .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 + }; + + let err = format!("eslint/new-cap: expect configuration option '{key}' to be a boolean."); + + value.as_bool().expect(&err) +} + +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 err = format!("eslint/new-cap: expect configuration option '{key}' to be an array."); + let err2 = format!( + "eslint/new-cap: expect array configuration option '{key}' to only contain strings." + ); + + value + .as_array() + .expect(&err) + .iter() + .map(|value| CompactStr::new(value.as_str().expect(&err2))) + .collect::>() +} + +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: None, + cap_is_new_exceptions: vec_str_serde_value(config, "capIsNewExceptions", vec![]), + cap_is_new_exception_pattern: None, + 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 + /// + /// + /// ### Why is this bad? + /// + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```js + /// FIXME: Tests will fail if examples are missing or syntactically incorrect. + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```js + /// FIXME: Tests will fail if examples are missing or syntactically incorrect. + /// ``` + NewCap, + nursery, // TODO: change category to `correctness`, `suspicious`, `pedantic`, `perf`, `restriction`, or `style` + // See for details + + pending // TODO: describe fix capabilities. Remove if no fix can be done, + // keep at 'pending' if you think one could be added but don't know how. + // Options are 'fix', 'fix_dangerous', 'suggestion', and 'conditional_fix_suggestion' +); + +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 Some(short_name) = + extract_name_from_new_expression(&expression.callee, &node.kind()) + else { + return; + }; + + let Some(name) = + &extract_name_deep_from_new_expression(&expression.callee, &node.kind()) + else { + return; + }; + + let capitalization = get_cap(&short_name); + + let allowed = capitalization != GetCapResult::Lower + || is_cap_allowed_expression( + &expression.callee, + name, + &self.new_is_cap_exceptions, + &self.new_is_cap_exception_pattern, + ); + + if !allowed { + ctx.diagnostic(new_cap_diagnostic(expression.span)); + } + } + AstKind::CallExpression(expression) if self.cap_is_new => { + let Some(short_name) = + extract_name_from_new_expression(&expression.callee, &node.kind()) + else { + return; + }; + + let Some(name) = + &extract_name_deep_from_new_expression(&expression.callee, &node.kind()) + 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( + &expression.callee, + name, + &caps_is_new_exceptions, + &self.cap_is_new_exception_pattern, + ); + + if !allowed { + ctx.diagnostic(new_cap_diagnostic(expression.span)); + } + } + _ => (), + } + } +} + +fn extract_name_deep_from_new_expression( + expression: &Expression, + kind: &AstKind, +) -> Option { + if let Some(identifier) = expression.get_identifier_reference() { + return Some(identifier.name.clone().into()); + } + + match expression { + Expression::StaticMemberExpression(expression) => { + let obj_name = extract_name_deep_from_new_expression(&expression.object, kind); + let prop_name = expression.property.name.clone().into_compact_str(); + + if let Some(obj_name) = obj_name { + let new_name = format!("{obj_name}.{prop_name}"); + return Some(CompactStr::new(&new_name)); + } + + Some(prop_name) + } + _ => get_static_property_name(kind).map(std::convert::Into::into), + } +} + +fn extract_name_from_new_expression(expression: &Expression, kind: &AstKind) -> Option { + if let Some(identifier) = expression.get_identifier_reference() { + return Some(identifier.name.clone().into()); + } + + get_static_property_name(kind).map(std::convert::Into::into) +} + +fn is_cap_allowed_expression( + expression: &Expression<'_>, + name: &CompactStr, + exceptions: &Vec, + patterns: &Option, +) -> bool { + if exceptions.contains(name) { + return true; + } + + 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(); +} From 8f7424f0fcb55e0764c85cfee9e2e04d5d3d60fb Mon Sep 17 00:00:00 2001 From: Sysix Date: Fri, 27 Dec 2024 14:39:28 +0100 Subject: [PATCH 02/11] feat(linter): add rule `eslint/new-cap` --- crates/oxc_linter/src/rules/eslint/new_cap.rs | 66 ++++++++++++++----- 1 file changed, 49 insertions(+), 17 deletions(-) diff --git a/crates/oxc_linter/src/rules/eslint/new_cap.rs b/crates/oxc_linter/src/rules/eslint/new_cap.rs index 65e0c1bbcfd43..a25ac093f17bb 100644 --- a/crates/oxc_linter/src/rules/eslint/new_cap.rs +++ b/crates/oxc_linter/src/rules/eslint/new_cap.rs @@ -151,7 +151,7 @@ impl Rule for NewCap { match node.kind() { AstKind::NewExpression(expression) if self.new_is_cap => { let Some(short_name) = - extract_name_from_new_expression(&expression.callee, &node.kind()) + &extract_name_from_new_expression(&expression.callee, &node.kind()) else { return; }; @@ -162,11 +162,12 @@ impl Rule for NewCap { return; }; - let capitalization = get_cap(&short_name); + let capitalization = get_cap(short_name); let allowed = capitalization != GetCapResult::Lower || is_cap_allowed_expression( &expression.callee, + short_name, name, &self.new_is_cap_exceptions, &self.new_is_cap_exception_pattern, @@ -178,7 +179,7 @@ impl Rule for NewCap { } AstKind::CallExpression(expression) if self.cap_is_new => { let Some(short_name) = - extract_name_from_new_expression(&expression.callee, &node.kind()) + &extract_name_from_new_expression(&expression.callee, &node.kind()) else { return; }; @@ -189,7 +190,7 @@ impl Rule for NewCap { return; }; - let capitalization = get_cap(&short_name); + 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()); @@ -197,6 +198,7 @@ impl Rule for NewCap { let allowed = capitalization != GetCapResult::Upper || is_cap_allowed_expression( &expression.callee, + short_name, name, &caps_is_new_exceptions, &self.cap_is_new_exception_pattern, @@ -219,10 +221,13 @@ fn extract_name_deep_from_new_expression( return Some(identifier.name.clone().into()); } - match expression { + match expression.without_parentheses() { Expression::StaticMemberExpression(expression) => { - let obj_name = extract_name_deep_from_new_expression(&expression.object, kind); let prop_name = expression.property.name.clone().into_compact_str(); + let obj_name = extract_name_deep_from_new_expression( + expression.object.without_parentheses(), + kind, + ); if let Some(obj_name) = obj_name { let new_name = format!("{obj_name}.{prop_name}"); @@ -231,6 +236,20 @@ fn extract_name_deep_from_new_expression( Some(prop_name) } + Expression::ComputedMemberExpression(expression) => { + let prop_name = expression.static_property_name()?; + let obj_name = extract_name_deep_from_new_expression( + expression.object.without_parentheses(), + kind, + ); + + if let Some(obj_name) = obj_name { + let new_name = format!("{obj_name}.{prop_name}"); + return Some(CompactStr::new(&new_name)); + } + + Some(prop_name.into_compact_str()) + } _ => get_static_property_name(kind).map(std::convert::Into::into), } } @@ -240,16 +259,29 @@ fn extract_name_from_new_expression(expression: &Expression, kind: &AstKind) -> return Some(identifier.name.clone().into()); } - get_static_property_name(kind).map(std::convert::Into::into) + match expression { + Expression::StaticMemberExpression(expression) => { + Some(expression.property.name.clone().into_compact_str()) + } + Expression::ComputedMemberExpression(expression) => { + expression.static_property_name().map(std::convert::Into::into) + } + _ => get_static_property_name(kind).map(std::convert::Into::into), + } } fn is_cap_allowed_expression( expression: &Expression<'_>, + short_name: &CompactStr, name: &CompactStr, exceptions: &Vec, patterns: &Option, ) -> bool { - if exceptions.contains(name) { + if exceptions.contains(name) || exceptions.contains(short_name) { + return true; + } + + if name == "Date.UTC" { return true; } @@ -297,7 +329,7 @@ fn test() { ("var x = Array(42)", None), ("var x = Boolean(42)", None), ("var x = Date(42)", None), - // ("var x = Date.UTC(2000, 0)", None), + ("var x = Date.UTC(2000, 0)", None), ("var x = Error('error')", None), ("var x = Function('return 0')", None), ("var x = Number(42)", None), @@ -343,17 +375,17 @@ fn test() { // "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 }])), - ), + // ("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 }, + // ("(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 } ]; From 196334fb9fb849670a99ff2f73e18d7c8c038f36 Mon Sep 17 00:00:00 2001 From: Sysix Date: Fri, 27 Dec 2024 15:30:18 +0100 Subject: [PATCH 03/11] feat(linter): add rule eslint/new-cap --- crates/oxc_linter/src/rules/eslint/new_cap.rs | 58 ++++++++++++------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/crates/oxc_linter/src/rules/eslint/new_cap.rs b/crates/oxc_linter/src/rules/eslint/new_cap.rs index a25ac093f17bb..7bdda23547e18 100644 --- a/crates/oxc_linter/src/rules/eslint/new_cap.rs +++ b/crates/oxc_linter/src/rules/eslint/new_cap.rs @@ -1,5 +1,8 @@ use crate::{ast_util::get_static_property_name, context::LintContext, rule::Rule, AstNode}; -use oxc_ast::{ast::Expression, AstKind}; +use oxc_ast::{ + ast::{ComputedMemberExpression, Expression}, + AstKind, +}; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; use oxc_span::{CompactStr, Span}; @@ -150,14 +153,14 @@ impl Rule for NewCap { fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { match node.kind() { AstKind::NewExpression(expression) if self.new_is_cap => { - let Some(short_name) = - &extract_name_from_new_expression(&expression.callee, &node.kind()) + let callee = expression.callee.without_parentheses(); + + let Some(short_name) = &extract_name_from_new_expression(callee, &node.kind()) else { return; }; - let Some(name) = - &extract_name_deep_from_new_expression(&expression.callee, &node.kind()) + let Some(name) = &extract_name_deep_from_new_expression(callee, &node.kind()) else { return; }; @@ -166,7 +169,7 @@ impl Rule for NewCap { let allowed = capitalization != GetCapResult::Lower || is_cap_allowed_expression( - &expression.callee, + callee, short_name, name, &self.new_is_cap_exceptions, @@ -178,14 +181,14 @@ impl Rule for NewCap { } } AstKind::CallExpression(expression) if self.cap_is_new => { - let Some(short_name) = - &extract_name_from_new_expression(&expression.callee, &node.kind()) + let callee = expression.callee.without_parentheses(); + + let Some(short_name) = &extract_name_from_new_expression(callee, &node.kind()) else { return; }; - let Some(name) = - &extract_name_deep_from_new_expression(&expression.callee, &node.kind()) + let Some(name) = &extract_name_deep_from_new_expression(callee, &node.kind()) else { return; }; @@ -197,7 +200,7 @@ impl Rule for NewCap { let allowed = capitalization != GetCapResult::Upper || is_cap_allowed_expression( - &expression.callee, + callee, short_name, name, &caps_is_new_exceptions, @@ -237,7 +240,7 @@ fn extract_name_deep_from_new_expression( Some(prop_name) } Expression::ComputedMemberExpression(expression) => { - let prop_name = expression.static_property_name()?; + let prop_name = get_computed_member_name(expression)?; let obj_name = extract_name_deep_from_new_expression( expression.object.without_parentheses(), kind, @@ -248,24 +251,35 @@ fn extract_name_deep_from_new_expression( return Some(CompactStr::new(&new_name)); } - Some(prop_name.into_compact_str()) + Some(prop_name) } _ => get_static_property_name(kind).map(std::convert::Into::into), } } +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_new_expression(expression: &Expression, kind: &AstKind) -> Option { if let Some(identifier) = expression.get_identifier_reference() { return Some(identifier.name.clone().into()); } - match expression { + match expression.without_parentheses() { Expression::StaticMemberExpression(expression) => { Some(expression.property.name.clone().into_compact_str()) } - Expression::ComputedMemberExpression(expression) => { - expression.static_property_name().map(std::convert::Into::into) - } + Expression::ComputedMemberExpression(expression) => get_computed_member_name(expression), _ => get_static_property_name(kind).map(std::convert::Into::into), } } @@ -415,11 +429,11 @@ fn test() { ("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 a = b[`\\ + // Foo`]();", + // None, + // ), // { "ecmaVersion": 6 }, ("var x = Foo.Bar(42);", Some(serde_json::json!([{ "capIsNewExceptions": ["Foo"] }]))), ( "var x = Bar.Foo(42);", From 2dfbc3fc7bec1cc8bfe5784015aca138013a82b8 Mon Sep 17 00:00:00 2001 From: Sysix Date: Fri, 27 Dec 2024 15:36:30 +0100 Subject: [PATCH 04/11] feat(linter): add rule eslint/new-cap --- crates/oxc_linter/src/rules/eslint/new_cap.rs | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/crates/oxc_linter/src/rules/eslint/new_cap.rs b/crates/oxc_linter/src/rules/eslint/new_cap.rs index 7bdda23547e18..119a1bf99e296 100644 --- a/crates/oxc_linter/src/rules/eslint/new_cap.rs +++ b/crates/oxc_linter/src/rules/eslint/new_cap.rs @@ -174,7 +174,8 @@ impl Rule for NewCap { name, &self.new_is_cap_exceptions, &self.new_is_cap_exception_pattern, - ); + ) + || (!self.properties && short_name != name); if !allowed { ctx.diagnostic(new_cap_diagnostic(expression.span)); @@ -205,7 +206,8 @@ impl Rule for NewCap { name, &caps_is_new_exceptions, &self.cap_is_new_exception_pattern, - ); + ) + || (!self.properties && short_name != name); if !allowed { ctx.diagnostic(new_cap_diagnostic(expression.span)); @@ -389,17 +391,17 @@ fn test() { // "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 }])), - // ), + ("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 }, + ("(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 } ]; From cf7949789222b94ec3eaeafe8d15ce71ae7c6b5c Mon Sep 17 00:00:00 2001 From: Sysix Date: Sat, 28 Dec 2024 14:20:22 +0100 Subject: [PATCH 05/11] feat(linter): add rule eslint/new-cap --- crates/oxc_linter/src/rules/eslint/new_cap.rs | 385 +++++++++++++++++- .../src/snapshots/eslint_new_cap.snap | 141 +++++++ 2 files changed, 506 insertions(+), 20 deletions(-) create mode 100644 crates/oxc_linter/src/snapshots/eslint_new_cap.snap diff --git a/crates/oxc_linter/src/rules/eslint/new_cap.rs b/crates/oxc_linter/src/rules/eslint/new_cap.rs index 119a1bf99e296..cdca0fc6434e3 100644 --- a/crates/oxc_linter/src/rules/eslint/new_cap.rs +++ b/crates/oxc_linter/src/rules/eslint/new_cap.rs @@ -1,18 +1,21 @@ use crate::{ast_util::get_static_property_name, context::LintContext, rule::Rule, AstNode}; use oxc_ast::{ - ast::{ComputedMemberExpression, Expression}, + ast::{ChainElement, ComputedMemberExpression, Expression}, AstKind, }; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; -use oxc_span::{CompactStr, Span}; +use oxc_span::{CompactStr, GetSpan, Span}; use regex::Regex; -fn new_cap_diagnostic(span: Span) -> OxcDiagnostic { - // See for details - OxcDiagnostic::warn("Should be an imperative statement about what is wrong") - .with_help("Should be a command-like statement that tells the user how to fix the issue") - .with_label(span) +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)] @@ -122,15 +125,309 @@ fn caps_allowed_vec() -> Vec { 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. /// /// ### Examples /// /// Examples of **incorrect** code for this rule: /// ```js - /// FIXME: Tests will fail if examples are missing or syntactically incorrect. + /// 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(); /// ``` /// /// Examples of **correct** code for this rule: @@ -138,12 +435,8 @@ declare_oxc_lint!( /// FIXME: Tests will fail if examples are missing or syntactically incorrect. /// ``` NewCap, - nursery, // TODO: change category to `correctness`, `suspicious`, `pedantic`, `perf`, `restriction`, or `style` - // See for details - - pending // TODO: describe fix capabilities. Remove if no fix can be done, - // keep at 'pending' if you think one could be added but don't know how. - // Options are 'fix', 'fix_dangerous', 'suggestion', and 'conditional_fix_suggestion' + style, + pending // TODO: maybe? ); impl Rule for NewCap { @@ -165,9 +458,9 @@ impl Rule for NewCap { return; }; - let capitalization = get_cap(short_name); + let capitalization = &get_cap(short_name); - let allowed = capitalization != GetCapResult::Lower + let allowed = *capitalization != GetCapResult::Lower || is_cap_allowed_expression( callee, short_name, @@ -178,7 +471,7 @@ impl Rule for NewCap { || (!self.properties && short_name != name); if !allowed { - ctx.diagnostic(new_cap_diagnostic(expression.span)); + ctx.diagnostic(new_cap_diagnostic(callee.span(), capitalization)); } } AstKind::CallExpression(expression) if self.cap_is_new => { @@ -194,12 +487,12 @@ impl Rule for NewCap { return; }; - let capitalization = get_cap(short_name); + 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 + let allowed = *capitalization != GetCapResult::Upper || is_cap_allowed_expression( callee, short_name, @@ -210,7 +503,7 @@ impl Rule for NewCap { || (!self.properties && short_name != name); if !allowed { - ctx.diagnostic(new_cap_diagnostic(expression.span)); + ctx.diagnostic(new_cap_diagnostic(callee.span(), capitalization)); } } _ => (), @@ -255,6 +548,43 @@ fn extract_name_deep_from_new_expression( Some(prop_name) } + Expression::ChainExpression(chain) => match &chain.expression { + ChainElement::CallExpression(call) => { + extract_name_deep_from_new_expression(&call.callee, kind) + } + ChainElement::TSNonNullExpression(non_null) => { + extract_name_deep_from_new_expression(&non_null.expression, kind) + } + ChainElement::StaticMemberExpression(expression) => { + let prop_name = expression.property.name.clone().into_compact_str(); + let obj_name = extract_name_deep_from_new_expression( + expression.object.without_parentheses(), + kind, + ); + + 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_new_expression( + expression.object.without_parentheses(), + kind, + ); + + 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, + }, _ => get_static_property_name(kind).map(std::convert::Into::into), } } @@ -282,6 +612,21 @@ fn extract_name_from_new_expression(expression: &Expression, kind: &AstKind) -> 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_new_expression(&call.callee, kind) + } + ChainElement::TSNonNullExpression(non_null) => { + extract_name_from_new_expression(&non_null.expression, kind) + } + ChainElement::StaticMemberExpression(expression) => { + Some(expression.property.name.clone().into_compact_str()) + } + ChainElement::ComputedMemberExpression(expression) => { + get_computed_member_name(expression) + } + ChainElement::PrivateFieldExpression(_) => None, + }, _ => get_static_property_name(kind).map(std::convert::Into::into), } } 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)(); + · ──────── + ╰──── From 35bfb8680d13857d5cfb110aebafc585f76ba8f9 Mon Sep 17 00:00:00 2001 From: Sysix Date: Sat, 28 Dec 2024 14:25:50 +0100 Subject: [PATCH 06/11] feat(linter): add rule eslint/new-cap --- crates/oxc_linter/src/ast_util.rs | 42 ------------- .../oxc_linter/src/rules/eslint/func_names.rs | 48 +++++++++++++-- crates/oxc_linter/src/rules/eslint/new_cap.rs | 61 +++++++------------ 3 files changed, 65 insertions(+), 86 deletions(-) diff --git a/crates/oxc_linter/src/ast_util.rs b/crates/oxc_linter/src/ast_util.rs index 97c0853e515e3..76828fb33d850 100644 --- a/crates/oxc_linter/src/ast_util.rs +++ b/crates/oxc_linter/src/ast_util.rs @@ -6,7 +6,6 @@ use oxc_ecmascript::ToBoolean; use oxc_semantic::{AstNode, IsGlobalReference, NodeId, ReferenceId, Semantic, SymbolId}; use oxc_span::{GetSpan, Span}; use oxc_syntax::operator::{AssignmentOperator, BinaryOperator, LogicalOperator, UnaryOperator}; -use std::borrow::Cow; /// Test if an AST node is a boolean value that never changes. Specifically we /// test for: @@ -470,44 +469,3 @@ pub fn leftmost_identifier_reference<'a, 'b: 'a>( _ => Err(expr), } } - -fn get_property_key_name<'a>(key: &PropertyKey<'a>) -> Option> { - if matches!(key, PropertyKey::NullLiteral(_)) { - return Some("null".into()); - } - - match key { - PropertyKey::RegExpLiteral(regex) => { - Some(Cow::Owned(format!("/{}/{}", regex.regex.pattern, regex.regex.flags))) - } - PropertyKey::BigIntLiteral(bigint) => Some(Cow::Borrowed(bigint.raw.as_str())), - PropertyKey::TemplateLiteral(template) => { - if template.expressions.len() == 0 && template.quasis.len() == 1 { - if let Some(cooked) = &template.quasis[0].value.cooked { - return Some(Cow::Borrowed(cooked.as_str())); - } - } - - None - } - _ => None, - } -} - -pub fn get_static_property_name<'a>(kind: &AstKind<'a>) -> Option> { - let (key, computed) = match kind { - AstKind::PropertyDefinition(definition) => (&definition.key, definition.computed), - AstKind::MethodDefinition(method_definition) => { - (&method_definition.key, method_definition.computed) - } - AstKind::ObjectProperty(property) => (&property.key, property.computed), - // AstKind::MemberExpression(member) => (member., member.is_computed()) - _ => return None, - }; - - if key.is_identifier() && !computed { - return key.name(); - } - - get_property_key_name(key) -} diff --git a/crates/oxc_linter/src/rules/eslint/func_names.rs b/crates/oxc_linter/src/rules/eslint/func_names.rs index fe3aa5cae1ee4..ad54b56c06e38 100644 --- a/crates/oxc_linter/src/rules/eslint/func_names.rs +++ b/crates/oxc_linter/src/rules/eslint/func_names.rs @@ -3,7 +3,7 @@ use std::borrow::Cow; use oxc_ast::{ ast::{ AssignmentTarget, AssignmentTargetProperty, BindingPatternKind, Expression, Function, - FunctionType, MethodDefinitionKind, PropertyKind, + FunctionType, MethodDefinitionKind, PropertyKey, PropertyKind, }, AstKind, }; @@ -14,7 +14,7 @@ use oxc_span::{Atom, GetSpan, Span}; use oxc_syntax::identifier::is_identifier_name; use phf::phf_set; -use crate::{ast_util::get_static_property_name, context::LintContext, rule::Rule, AstNode}; +use crate::{context::LintContext, rule::Rule, AstNode}; fn named_diagnostic(function_name: &str, span: Span) -> OxcDiagnostic { OxcDiagnostic::warn(format!("Unexpected named {function_name}.")) @@ -232,6 +232,46 @@ fn get_function_identifier<'a>(func: &'a Function<'a>) -> Option<&'a Span> { func.id.as_ref().map(|id| &id.span) } +fn get_property_key_name<'a>(key: &PropertyKey<'a>) -> Option> { + if matches!(key, PropertyKey::NullLiteral(_)) { + return Some("null".into()); + } + + match key { + PropertyKey::RegExpLiteral(regex) => { + Some(Cow::Owned(format!("/{}/{}", regex.regex.pattern, regex.regex.flags))) + } + PropertyKey::BigIntLiteral(bigint) => Some(Cow::Borrowed(bigint.raw.as_str())), + PropertyKey::TemplateLiteral(template) => { + if template.expressions.len() == 0 && template.quasis.len() == 1 { + if let Some(cooked) = &template.quasis[0].value.cooked { + return Some(Cow::Borrowed(cooked.as_str())); + } + } + + None + } + _ => None, + } +} + +fn get_static_property_name<'a>(parent_node: &AstNode<'a>) -> Option> { + let (key, computed) = match parent_node.kind() { + AstKind::PropertyDefinition(definition) => (&definition.key, definition.computed), + AstKind::MethodDefinition(method_definition) => { + (&method_definition.key, method_definition.computed) + } + AstKind::ObjectProperty(property) => (&property.key, property.computed), + _ => return None, + }; + + if key.is_identifier() && !computed { + return key.name(); + } + + get_property_key_name(key) +} + /// Gets the name and kind of the given function node. /// @see fn get_function_name_with_kind<'a>(func: &Function<'a>, parent_node: &AstNode<'a>) -> Cow<'a, str> { @@ -295,14 +335,14 @@ fn get_function_name_with_kind<'a>(func: &Function<'a>, parent_node: &AstNode<'a if let Some(name) = definition.key.name() { tokens.push(name); } - } else if let Some(static_name) = get_static_property_name(&parent_node.kind()) { + } else if let Some(static_name) = get_static_property_name(parent_node) { tokens.push(static_name); } else if let Some(name) = func.name() { tokens.push(Cow::Borrowed(name.as_str())); } } _ => { - if let Some(static_name) = get_static_property_name(&parent_node.kind()) { + if let Some(static_name) = get_static_property_name(parent_node) { tokens.push(static_name); } else if let Some(name) = func.name() { tokens.push(Cow::Borrowed(name.as_str())); diff --git a/crates/oxc_linter/src/rules/eslint/new_cap.rs b/crates/oxc_linter/src/rules/eslint/new_cap.rs index cdca0fc6434e3..9455a4c2e351a 100644 --- a/crates/oxc_linter/src/rules/eslint/new_cap.rs +++ b/crates/oxc_linter/src/rules/eslint/new_cap.rs @@ -1,4 +1,4 @@ -use crate::{ast_util::get_static_property_name, context::LintContext, rule::Rule, AstNode}; +use crate::{context::LintContext, rule::Rule, AstNode}; use oxc_ast::{ ast::{ChainElement, ComputedMemberExpression, Expression}, AstKind, @@ -448,13 +448,11 @@ impl Rule for NewCap { AstKind::NewExpression(expression) if self.new_is_cap => { let callee = expression.callee.without_parentheses(); - let Some(short_name) = &extract_name_from_new_expression(callee, &node.kind()) - else { + let Some(short_name) = &extract_name_from_expression(callee) else { return; }; - let Some(name) = &extract_name_deep_from_new_expression(callee, &node.kind()) - else { + let Some(name) = &extract_name_deep_from_expression(callee) else { return; }; @@ -477,13 +475,11 @@ impl Rule for NewCap { AstKind::CallExpression(expression) if self.cap_is_new => { let callee = expression.callee.without_parentheses(); - let Some(short_name) = &extract_name_from_new_expression(callee, &node.kind()) - else { + let Some(short_name) = &extract_name_from_expression(callee) else { return; }; - let Some(name) = &extract_name_deep_from_new_expression(callee, &node.kind()) - else { + let Some(name) = &extract_name_deep_from_expression(callee) else { return; }; @@ -511,10 +507,7 @@ impl Rule for NewCap { } } -fn extract_name_deep_from_new_expression( - expression: &Expression, - kind: &AstKind, -) -> Option { +fn extract_name_deep_from_expression(expression: &Expression) -> Option { if let Some(identifier) = expression.get_identifier_reference() { return Some(identifier.name.clone().into()); } @@ -522,10 +515,8 @@ fn extract_name_deep_from_new_expression( match expression.without_parentheses() { Expression::StaticMemberExpression(expression) => { let prop_name = expression.property.name.clone().into_compact_str(); - let obj_name = extract_name_deep_from_new_expression( - expression.object.without_parentheses(), - kind, - ); + 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}"); @@ -536,10 +527,8 @@ fn extract_name_deep_from_new_expression( } Expression::ComputedMemberExpression(expression) => { let prop_name = get_computed_member_name(expression)?; - let obj_name = extract_name_deep_from_new_expression( - expression.object.without_parentheses(), - kind, - ); + 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}"); @@ -549,18 +538,14 @@ fn extract_name_deep_from_new_expression( Some(prop_name) } Expression::ChainExpression(chain) => match &chain.expression { - ChainElement::CallExpression(call) => { - extract_name_deep_from_new_expression(&call.callee, kind) - } + ChainElement::CallExpression(call) => extract_name_deep_from_expression(&call.callee), ChainElement::TSNonNullExpression(non_null) => { - extract_name_deep_from_new_expression(&non_null.expression, kind) + 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_new_expression( - expression.object.without_parentheses(), - kind, - ); + 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}"); @@ -571,10 +556,8 @@ fn extract_name_deep_from_new_expression( } ChainElement::ComputedMemberExpression(expression) => { let prop_name = get_computed_member_name(expression)?; - let obj_name = extract_name_deep_from_new_expression( - expression.object.without_parentheses(), - kind, - ); + 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}"); @@ -585,7 +568,7 @@ fn extract_name_deep_from_new_expression( } ChainElement::PrivateFieldExpression(_) => None, }, - _ => get_static_property_name(kind).map(std::convert::Into::into), + _ => None, } } @@ -602,7 +585,7 @@ fn get_computed_member_name(computed_member: &ComputedMemberExpression) -> Optio } } -fn extract_name_from_new_expression(expression: &Expression, kind: &AstKind) -> Option { +fn extract_name_from_expression(expression: &Expression) -> Option { if let Some(identifier) = expression.get_identifier_reference() { return Some(identifier.name.clone().into()); } @@ -613,11 +596,9 @@ fn extract_name_from_new_expression(expression: &Expression, kind: &AstKind) -> } Expression::ComputedMemberExpression(expression) => get_computed_member_name(expression), Expression::ChainExpression(chain) => match &chain.expression { - ChainElement::CallExpression(call) => { - extract_name_from_new_expression(&call.callee, kind) - } + ChainElement::CallExpression(call) => extract_name_from_expression(&call.callee), ChainElement::TSNonNullExpression(non_null) => { - extract_name_from_new_expression(&non_null.expression, kind) + extract_name_from_expression(&non_null.expression) } ChainElement::StaticMemberExpression(expression) => { Some(expression.property.name.clone().into_compact_str()) @@ -627,7 +608,7 @@ fn extract_name_from_new_expression(expression: &Expression, kind: &AstKind) -> } ChainElement::PrivateFieldExpression(_) => None, }, - _ => get_static_property_name(kind).map(std::convert::Into::into), + _ => None, } } From 13dfe711bc60296a30cb05b0746e0a656210e840 Mon Sep 17 00:00:00 2001 From: Sysix Date: Sat, 28 Dec 2024 14:55:46 +0100 Subject: [PATCH 07/11] feat(linter): add rule eslint/new-cap --- crates/oxc_linter/src/rules/eslint/new_cap.rs | 52 ++++++++++++------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/crates/oxc_linter/src/rules/eslint/new_cap.rs b/crates/oxc_linter/src/rules/eslint/new_cap.rs index 9455a4c2e351a..34d08f2fb228e 100644 --- a/crates/oxc_linter/src/rules/eslint/new_cap.rs +++ b/crates/oxc_linter/src/rules/eslint/new_cap.rs @@ -71,6 +71,19 @@ fn vec_str_serde_value( .collect::>() } +fn regex_serde_value(map: &serde_json::Map, key: &str) -> Option { + let value = map.get(key)?; + let err = format!("eslint/new-cap: expect configuration option '{key}' to be a regex string."); + + let regex_string = value.as_str().expect(&err); + + 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 { @@ -105,9 +118,9 @@ impl From<&serde_json::Value> for NewCap { "newIsCapExceptions", caps_allowed_vec(), ), - new_is_cap_exception_pattern: None, + 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: None, + cap_is_new_exception_pattern: regex_serde_value(config, "capIsNewExceptionPattern"), properties: bool_serde_value(config, "properties"), })) } @@ -460,11 +473,10 @@ impl Rule for NewCap { let allowed = *capitalization != GetCapResult::Lower || is_cap_allowed_expression( - callee, short_name, name, &self.new_is_cap_exceptions, - &self.new_is_cap_exception_pattern, + self.new_is_cap_exception_pattern.as_ref(), ) || (!self.properties && short_name != name); @@ -490,11 +502,10 @@ impl Rule for NewCap { let allowed = *capitalization != GetCapResult::Upper || is_cap_allowed_expression( - callee, short_name, name, &caps_is_new_exceptions, - &self.cap_is_new_exception_pattern, + self.cap_is_new_exception_pattern.as_ref(), ) || (!self.properties && short_name != name); @@ -613,11 +624,10 @@ fn extract_name_from_expression(expression: &Expression) -> Option { } fn is_cap_allowed_expression( - expression: &Expression<'_>, short_name: &CompactStr, name: &CompactStr, - exceptions: &Vec, - patterns: &Option, + exceptions: &[CompactStr], + patterns: Option<&Regex>, ) -> bool { if exceptions.contains(name) || exceptions.contains(short_name) { return true; @@ -627,6 +637,10 @@ fn is_cap_allowed_expression( return true; } + if let Some(pattern) = &patterns { + return pattern.find(name).is_some(); + }; + false } @@ -695,28 +709,28 @@ fn test() { "var x = Foo(42);", Some(serde_json::json!([{ "capIsNew": true, "capIsNewExceptions": ["Foo"] }])), ), - // ("var x = Foo(42);", Some(serde_json::json!([{ "capIsNewExceptionPattern": "^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 = 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 = 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!([{ "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 }]))), ( From 7b166b31c3d5f465a26c3080f05803abd4a4c5a2 Mon Sep 17 00:00:00 2001 From: Sysix Date: Sat, 28 Dec 2024 15:04:32 +0100 Subject: [PATCH 08/11] feat(linter): add rule eslint/new-cap --- crates/oxc_linter/src/rules/eslint/new_cap.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/oxc_linter/src/rules/eslint/new_cap.rs b/crates/oxc_linter/src/rules/eslint/new_cap.rs index 34d08f2fb228e..26014a4a19ffe 100644 --- a/crates/oxc_linter/src/rules/eslint/new_cap.rs +++ b/crates/oxc_linter/src/rules/eslint/new_cap.rs @@ -151,6 +151,11 @@ declare_oxc_lint!( /// 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: From a081542fad3d9243598f33bf8d071855f9ff058e Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 28 Dec 2024 14:05:52 +0000 Subject: [PATCH 09/11] [autofix.ci] apply automated fixes --- crates/oxc_linter/src/rules/eslint/new_cap.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/oxc_linter/src/rules/eslint/new_cap.rs b/crates/oxc_linter/src/rules/eslint/new_cap.rs index 26014a4a19ffe..fca95e3a35968 100644 --- a/crates/oxc_linter/src/rules/eslint/new_cap.rs +++ b/crates/oxc_linter/src/rules/eslint/new_cap.rs @@ -155,7 +155,7 @@ declare_oxc_lint!( /// 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: From 04f632fd37300ab6bd76318c3ddfc70073a30714 Mon Sep 17 00:00:00 2001 From: Sysix Date: Sat, 28 Dec 2024 18:10:38 +0100 Subject: [PATCH 10/11] feat(linter): add rule eslint/new-cap --- crates/oxc_linter/src/rules/eslint/new_cap.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/crates/oxc_linter/src/rules/eslint/new_cap.rs b/crates/oxc_linter/src/rules/eslint/new_cap.rs index 26014a4a19ffe..847a72acdad06 100644 --- a/crates/oxc_linter/src/rules/eslint/new_cap.rs +++ b/crates/oxc_linter/src/rules/eslint/new_cap.rs @@ -155,7 +155,7 @@ declare_oxc_lint!( /// 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: @@ -447,11 +447,6 @@ declare_oxc_lint!( /// /// var friend = new person.acquaintance(); /// ``` - /// - /// Examples of **correct** code for this rule: - /// ```js - /// FIXME: Tests will fail if examples are missing or syntactically incorrect. - /// ``` NewCap, style, pending // TODO: maybe? From b8a83e517b5d6c848555bbf05aea830cee5c77e3 Mon Sep 17 00:00:00 2001 From: Sysix Date: Mon, 30 Dec 2024 14:01:53 +0100 Subject: [PATCH 11/11] feat(linter): add rule eslint/new-cap --- crates/oxc_linter/src/rules/eslint/new_cap.rs | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/crates/oxc_linter/src/rules/eslint/new_cap.rs b/crates/oxc_linter/src/rules/eslint/new_cap.rs index 847a72acdad06..d1283782c0d32 100644 --- a/crates/oxc_linter/src/rules/eslint/new_cap.rs +++ b/crates/oxc_linter/src/rules/eslint/new_cap.rs @@ -45,9 +45,7 @@ fn bool_serde_value(map: &serde_json::Map, key: &str) return true; // default value }; - let err = format!("eslint/new-cap: expect configuration option '{key}' to be a boolean."); - - value.as_bool().expect(&err) + value.as_bool().unwrap_or(true) } fn vec_str_serde_value( @@ -58,24 +56,20 @@ fn vec_str_serde_value( let Some(value) = map.get(key) else { return default_value; // default value }; - let err = format!("eslint/new-cap: expect configuration option '{key}' to be an array."); - let err2 = format!( - "eslint/new-cap: expect array configuration option '{key}' to only contain strings." - ); - - value - .as_array() - .expect(&err) + + let Some(array_value) = value.as_array() else { + return default_value; // default value + }; + + array_value .iter() - .map(|value| CompactStr::new(value.as_str().expect(&err2))) + .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 err = format!("eslint/new-cap: expect configuration option '{key}' to be a regex string."); - - let regex_string = value.as_str().expect(&err); + let regex_string = value.as_str()?; if let Ok(regex) = Regex::new(regex_string) { return Some(regex);